1 Punkte von GN⁺ 19 시간 전 | 1 Kommentare | Auf WhatsApp teilen
  • Die Regeln der C-Sprache können selbst scheinbar einfachen Code mit Zeigervergleichen, Aliasing, Nullzeigern oder nicht initialisierten Werten zu undefiniertem Verhalten machen
  • Bei Integer-Konstanten, sizeof, Zeichenkonstanten und uint8_t-Arithmetik können die Ergebnisse wegen Typauswahl und Integer-Promotions je nach Plattform, Schreibweise und Ort der Zwischenzuweisung unterschiedlich ausfallen
  • Bei Funktionsdeklarationen führen foo() und foo(void), fehlende Prototypen, Default Argument Promotions und Funktionen ohne Rückgabewert in C und C++ zu unterschiedlicher Zulässigkeit oder unterschiedlichem Verhalten
  • Arrays sind keine Zeiger; Array-Parameter werden zu Zeigern angepasst, und a, &a und &a[0] haben trotz gleicher Adresse unterschiedliche Typen, sodass sie nicht austauschbar sind
  • Operatorpräzedenz und Auswertungsreihenfolge sind getrennte Dinge, und sogar Formulierungen im Standard bestimmen zusammen mit der Struktur von switch-Blöcken und der Lebensdauer temporärer Objekte das tatsächliche Laufzeitverhalten

Undefiniertes Verhalten und Zeigerregeln

  • Zeigervergleich und strikte Aliasing-Regeln

    • Selbst wenn gleich typisierte Zeiger p und q auf dieselbe Adresse zeigen, kann der Vergleich p == q undefiniertes Verhalten sein, wenn sie aus unterschiedlichen Objekten stammen und nicht Teil desselben aggregate- oder union-Objekts sind
    • Dass Zeiger abstrakter sind als bloße numerische Adressen, wird im zugehörigen Artikel weiter erläutert
    • Wenn auf ein int-Objekt über ein short-lvalue zugegriffen wird, ist das nach der Regel des strict aliasing undefiniertes Verhalten
    • Ein unsigned char-Zeiger darf ausnahmsweise jedes Objekt aliasen; daher ist der Zugriff auf ein int-Objekt über ein unsigned char-lvalue zulässig
    • Für unsigned char ist garantiert, dass es weder Padding-Bits noch trap representations gibt; seit C11 ist auch für signed char garantiert, dass es keine Padding-Bits gibt
    • Type-based alias analysis wird im zugehörigen Artikel behandelt
  • Nullzeiger und Zeigerdarstellung

    • Die Bitdarstellung eines Nullzeigers muss nicht notwendigerweise aus lauter Nullbits bestehen
    • Der C-Standard definiert eine null pointer constant, aber nicht die Laufzeitdarstellung eines Nullzeigers oder die Darstellung gewöhnlicher Zeiger
    • Die Symbolics Lisp Machine 3600 verwendet statt numerischer Zeiger Tupel der Form <array-object, index>; die Nullzeigerdarstellung ist dort <nil, 0>
    • Weitere Beispiele finden sich in clc FAQ 5.17
    • Die Konstante 0 ist je nach Kontext entweder ein Integer oder ein Nullzeiger, und (void *)0 wird als Nullzeiger ausgewertet
    • Selbst wenn ein Ausdruck e zu 0 ausgewertet wird, ist nicht garantiert, dass (void *)e ein Nullzeiger wird
    • Nur wenn eine null pointer constant in einen Zeigertyp konvertiert wird, ist garantiert, dass sie einem Nullzeiger entspricht
    • Arithmetik mit einem Nullzeiger ist undefiniertes Verhalten; daher ist selbst dann, wenn e ein Nullzeiger ist, nicht garantiert, dass e + 0 ebenfalls ein Nullzeiger ist
  • Nicht initialisierte Werte

    • Wird ein Objekt mit automatischer Speicherdauer gelesen, das nicht initialisiert wurde, und könnte dieses Objekt die Speicherklasse register haben, wobei seine Adresse nie genommen wurde, ist das nach C11 § 6.3.2.1 ¶ 2 undefiniertes Verhalten
    • Diese Regel steht im Zusammenhang mit der Intel-Itanium-Architektur, die in DR338 behandelt wird
    • Die allgemeinen Integer-Register von Itanium besitzen 64 Bit plus ein trap bit; dieses trap bit ist das NaT (not-a-thing), das anzeigt, ob das Register initialisiert wurde
    • Wenn die Adresse der Variablen genommen wird, entfällt diese Bedingung, aber der Wert bleibt indeterminate und kann eine trap representation oder ein unspecified value sein
    • Das Lesen einer trap representation ist nach C11 § 6.2.6.1 ¶ 5 undefiniertes Verhalten
    • Handelt es sich um ein unspecified value, kann selbst das Ergebnis von x != x entweder true oder false sein, und wenn int x unspecified ist, ist auch nach x *= 0 nicht garantiert, dass x den Wert 0 hat
    • Indeterminate und unspecified value werden in DR260, DR451, N1793, N1818, N2012, N2013, N2221 diskutiert
  • unsigned char und memcpy

    • Der Typ unsigned char hat nach C11 § 6.2.6.1 ¶ 3 keine trap representations, daher ist sein Anfangswert unspecified
    • In einer Antwort eines C-Komiteemitglieds auf StackOverflow wird argumentiert, dass der Wert von x nach einem Aufruf der Standardbibliotheksfunktion memcpy specified sein müsse; in dieser Interpretation würde x != x zu false werden
    • Eine klare Grundlage dafür im C-Standard ist nicht erkennbar, und die Antwort des Komitees in DR451 steht im Widerspruch dazu, da dort die Verwendung einer Bibliotheksfunktion mit einem indeterminate value als undefiniertes Verhalten bezeichnet wird
    • Diese Frage bleibt offen; weitere Diskussionen finden sich in Uninitialized Reads

Ganzzahlkonstanten, Integer-Promotions, sizeof

  • Schreibweise und Typen von Ganzzahlkonstanten

    • Dezimale Ganzzahlkonstanten ohne Suffix werden immer aus der Liste der signed-Typen gewählt, während oktale und hexadezimale Konstanten signed oder unsigned sein können
    • Nach C17 § 6.4.4.1 wird der Typ einer Ganzzahlkonstante als der erste Typ aus der Liste bestimmt, der den betreffenden Wert darstellen kann
    • Ohne Suffix ist die Reihenfolge bei Dezimalkonstanten int, long int, long long int; bei oktalen und hexadezimalen Konstanten int, unsigned int, long int, unsigned long int, long long int, unsigned long long int
    • Konstanten zwischen INT_MAX+1 und UINT_MAX können je nachdem, ob sie dezimal oder hexadezimal geschrieben sind, unterschiedliche Typen haben; das kann in ABI-sensitivem Code wie Aufrufen variadischer Funktionen einen Unterschied machen
    • In der Arm 32-bit architecture ABI werden int und long mit 32 Bit in einem Register übergeben, long long dagegen mit 64 Bit in zwei Registern
    • Auf Plattformen, auf denen int 32 Bit breit ist, wird -1 < 0x8000 zu true; auf Plattformen mit 16-Bit-int wird es zu false, was Portabilitätsprobleme verursachen kann
    • Auch bei generic selection, C++-Überladungsfunktionen und Ausdrücken wie sizeof(0x80000000) == sizeof(2147483648) kann der unterschiedliche Konstantentyp das Ergebnis verändern
  • sizeof(int) > -1

    • Der Operator sizeof liefert einen unsigned integer vom Typ size_t zurück
    • Nach den usual arithmetic conversions in C11 § 6.3.1.8 wird ein signed-Operand in einen unsigned-Typ gleichen Rangs umgewandelt, wenn er einen niedrigeren Rang als der unsigned-Operand hat
    • Ein signed Integer mit dem Wert -1 wird bei der Umwandlung in unsigned zum maximalen unsigned Integer des entsprechenden Rangs
    • Daher wird sizeof(int) > -1 immer zu false ausgewertet
  • Typ von Zeichenkonstanten

    • In C haben Zeichenkonstanten nach C11 § 6.4.4.4 ¶ 10 den Typ int
    • Daher ist nicht garantiert, dass sizeof(char) == sizeof('x') immer true ist; garantiert ist nur sizeof(int) == sizeof('x')
    • Eine integer character constant kann aus einer oder mehreren Multibyte-Zeichenfolgen bestehen, daher ist auch 'abc' gültig; ihre Darstellung ist implementierungsdefiniert
    • Der Wert einer integer character constant mit genau einem Zeichen ist gleich der Integer-Darstellung eines Objekts vom Typ char, das dasselbe einzelne Zeichen repräsentiert
  • uint8_t-Arithmetik und Division

    • Auch wenn a, b und c vor dem Lesen initialisiert sind, können sich die Werte von x und z wegen Integer-Promotion und der Position der Zwischenspeicherung unterscheiden
    • Die Werte aller Variablen werden zunächst auf die Größe von int promotet; danach werden Addition und Division ausgeführt, und jedes Zuweisungsergebnis wird abgeschnitten und im jeweiligen Variablentyp gespeichert
    • Zum Beispiel ergibt sich bei a=255, b=1, c=2 für x der Wert ((255 + 1) / 2) % 256 = 128
    • Die Zwischenvariable y wird zu (255 + 1) % 256 = 0, danach wird z zu (0 / 2) % 256 = 0, also 128 != 0
    • unsigned integer overflow ist wohldefiniertes Verhalten
    • Da sich die Modulo-Operation über die Addition verteilt, sind x und z immer gleich, wenn man die Division durch eine Addition ersetzt
    • Ändert man die erste Zuweisung zu uint8_t x = ((uint8_t)(a + b)) / c;, sind x und z ebenfalls immer gleich
  • const-Variablen und variable length arrays

    • Auch wenn man die mit const qualifizierten Variablen n und m als Array-Größe verwendet, sind sie in C keine integer constant expressions
    • In C11 § 6.6 ¶ 6 sind integer constant expressions auf integer constants, enumeration constants, character constants, sizeof-Ergebnisse mit Integerwert, _Alignof und Gleitkommakonstanten als unmittelbare Operanden von Casts beschränkt, deren Ergebnis ein Integer-Konstantenausdruck ist
    • Ist der Ausdruck für die Array-Größe keine integer constant expression, wird das Array nach C11 § 6.7.6.2 ¶ 4 zu einem variable length array
    • variable length arrays sind auf File Scope nicht erlaubt; daher lässt sich die Compilation Unit mit dem globalen Array x nicht kompilieren
    • Im Block Scope sind variable length arrays erlaubt; daher kann die Compilation Unit mit dem lokalen Array y kompiliert werden
    • variable length arrays sind jedoch ein conditional feature, das von einer Implementierung nicht unterstützt werden muss; bei Compilern ohne Unterstützung kann daher auch das Beispiel im Block Scope nicht kompiliert werden
    • In C++ werden beide Compilation Units kompiliert; da C++ das Konzept variable length array nicht kennt, wird y als normales Array mit 42 Elementen kompiliert

Funktionsdeklarationen, Rückgabewerte, Linkage

  • foo() und foo(void)

    • Eine Funktionsdeklaration der Form foo() deklariert eine Funktion, deren Anzahl und Typen der Argumente unbekannt sind, während foo(void) eine nullary function ohne Argumente deklariert
    • Dieser Unterschied wird in einem Beitrag zu Funktionsdeklarationen, -definitionen und Prototypen behandelt
    • Eine Deklaration ohne Argumentliste führt nur den Funktionsnamen ein und legt Anzahl und Typen der Argumente nicht fest; daher kann sie in Verbindung mit der späteren Funktionsdefinition zulässig sein
    • Wird eine Funktion ohne Prototyp aufgerufen, werden die default argument promotions angewendet, sodass float zu double hochgestuft wird
    • Ist der Funktionstyp nach der Promotion nicht mit dem Typ der tatsächlichen Funktionsdefinition kompatibel, ist die Kombination aus Deklaration und Definition nicht gültig
    • Ein Funktionsaufruf ohne Deklaration kann in C kompiliert werden, weil implizite Funktionen erlaubt waren; in C++ ist das ein Compilerfehler
    • Erfolgt ein Aufruf wie bar(42) ohne Deklaration, werden Integer-Argument-Promotions angewendet und 42 als int dargestellt; ist bar also für einen Rückgabetyp T nicht mit T (*)(int) kompatibel, führt das zu undefiniertem Verhalten
  • Funktionen mit Rückgabewert, die keinen Wert zurückgeben

    • Auch wenn eine Funktion den Rückgabetyp int hat und keinen Wert zurückgibt, kann das in C zulässig sein, solange das Ergebnis des Aufrufs nicht verwendet wird
    • In K&R-C gab es keinen Typ void, und wenn ein Typ weggelassen wurde, wurde standardmäßig int angenommen; deshalb sind Funktionen ohne Rückgabewert historisch mit der Regel des impliziten int verknüpft
    • Die Regel des impliziten int wurde in C99 abgeschafft; einschlägige Diskussionen finden sich in N661 und der C99 rationale
    • C17 § 6.9.1 ¶ 12 legt fest, dass es undefiniertes Verhalten ist, wenn das Ende der Funktion bei } erreicht wird und der Aufrufer den Rückgabewert des Funktionsaufrufs verwendet
    • In C++98 § 6.6.3 ¶ 2 ist das bloße Herauslaufen aus dem Ende einer Funktion mit Rückgabewert selbst äquivalent zu einem return ohne Wert und führt bei einer Funktion mit Rückgabewert zu undefiniertem Verhalten
    • C++-Compiler können im Allgemeinen nicht beweisen, in welchem Zweig abort_program() tatsächlich beendet wird, daher können sie in solchen Fällen meist nur eine Diagnose statt eines Fehlers ausgeben
  • Linkage und extern

    • Wird in einem Gültigkeitsbereich, in dem eine frühere Deklaration sichtbar ist, derselbe Bezeichner erneut mit extern deklariert, hat die spätere Deklaration dieselbe Linkage wie die frühere
    • C17 § 6.2.2 ¶ 4 legt fest, dass eine spätere extern-Deklaration dieselbe Linkage hat, wenn eine frühere Deklaration interne oder externe Linkage festgelegt hat
    • Ist die frühere Deklaration nicht sichtbar oder hatte sie keine Linkage, hat der extern-Bezeichner externe Linkage
    • Deklarationskombinationen in umgekehrter Reihenfolge können zu undefiniertem Verhalten führen; GCC und Clang erkennen das

Qualifizierer und unvollständige Typen

  • const bei Funktionsparametern

    • Wenn in einer Funktionsdeklaration der Parameter x mit const qualifiziert ist, in der Funktionsdefinition aber nicht, und im Funktionsrumpf ein Wert in x geschrieben wird, ist das dennoch zulässig
    • Nach C11 § 6.7.6.3 ¶ 15 wird bei der Beurteilung der Typkompatibilität von Funktionsparametern und des composite type jeder als qualified type deklarierte Parameter als unqualified version behandelt
    • Dasselbe Thema wird auch in DR040 behandelt
  • const im Funktionsrückgabetyp

    • Wenn nur der Rückgabetyp der Funktionsdefinition mit const qualifiziert ist, die Deklaration aber nicht, lässt sich die richtige Antwort nicht einfach als richtig oder falsch einstufen
    • Der allgemeine Konsens ist, dass Qualifizierer von rvalues ignoriert werden sollten; die Standardformulierung bis einschließlich C11 behandelte das jedoch nicht ausdrücklich
    • In C17 wurde klargestellt, dass rvalue-Qualifizierer bei Casts, lvalue conversion und Funktionsdeklara­toren ignoriert werden müssen
    • C17 § 6.7.6.3 ¶ 5 stellt ausdrücklich fest, dass der von einer Funktion zurückgegebene Typ die unqualified version von T ist; diese Formulierung wurde in C17 hinzugefügt
    • Auch bei unterschiedlicher const-Qualifikation des Rückgabetyps kann eine Zuweisung von Funktionstypen zulässig sein
    • Weitere Diskussionen finden sich in DR423 und DR481
  • Unvollständige Structs und globale Variablen

    • Wenn struct foo zum Zeitpunkt der Deklaration einer globalen Variablen ein unvollständiger Typ ist und seine Größe daher unbekannt ist, kann das dennoch in bestimmten Situationen erlaubt sein, sofern der Typ später in derselben translation unit vervollständigt wird
    • Eine ähnliche Logik gilt auch für globale Variablen oder Arrays unvollständiger Typen
    • Dieses Thema wird auch in DR016 behandelt
  • External Object vom Typ void

    • Eine Variablendeklaration vom Typ void mit interner Linkage ist nicht zulässig, aber eine Variablendeklaration vom Typ void mit externer Linkage ist grammatikalisch zulässig und wird im C11-Standard nirgendwo ausdrücklich verboten
    • Nach C11 § 6.2.5 ¶ 19 ist der Typ void ein nicht vervollständigbarer unvollständiger Objekttyp, der aus der leeren Menge von Werten besteht
    • C11 § 6.3.2.1 ¶ 1 definiert ein lvalue als Ausdruck eines Objekttyps außer void; daher ist der Name eines Objekts foo vom Typ void kein gültiges lvalue
    • Nach C11 ist es schwer, sich für ein externes void-Objekt sinnvolle und standardkonforme Operationen vorzustellen
    • DR012 behandelt, dass es zulässig wird, die Adresse des Objekts foo zu bilden, wenn der Typ in const void geändert wird; das wirkt eher wie ein Versehen als wie ein beabsichtigtes Feature
  • Pointer-const-Konvertierung

Arrays, String-Literale und Pointer-Anpassung

  • Arrays sind keine Pointer

    • Array-Initialisierung und Pointer-Initialisierung sind nicht gleichwertig
    • Die erste Form initialisiert ein veränderbares Array mit automatischer oder statischer Speicherdauer
    • Die zweite Form initialisiert einen Pointer auf ein Array mit statischer Speicherdauer; dieses Array ist nicht notwendigerweise veränderbar
    • Arrays sind keine Pointer; Details dazu finden sich im entsprechenden Beitrag
  • a, &a, &a[0]

    • Bei int a[42]; werden a, &a und &a[0] alle zur Adresse des ersten Elements des Arrays ausgewertet
    • Die Typen der drei Ausdrücke sind jedoch verschieden, daher sind sie nicht austauschbar
    • Details dazu finden sich im entsprechenden Beitrag
  • Array-Parameter und lokale Arrays

    • Ist der Typ eines Funktionsparameters ein „Array von T“, wird er zu „Pointer auf T“ angepasst
    • Auch wenn der Parameter x wie int[42] aussieht, wird er tatsächlich als int * behandelt
    • Ist die lokale Variable y ein int[42], dann ist sizeof(y) gleich 42 * sizeof(int)
    • Da die Größe eines Objekt-Pointers im Allgemeinen nicht der Größe von 42 Ganzzahlen entspricht, ist sizeof(x) == sizeof(y) normalerweise false
    • Details dazu finden sich im entsprechenden Beitrag

Operatoren, Auswertungsreihenfolge und Kontrollfluss

  • x+++y

    • In C können anders als in C++ keine neuen Operatoren definiert werden, daher gibt es keinen neuen Operator wie +++
    • x+++y wird als Kombination bestehender Operatoren interpretiert und ist äquivalent zu (x++) + y
    • Auch --*--p ist kein neuer Operator, sondern eine Kombination bestehender Operatoren
    • --*--p ist äquivalent zu --(*(--p)); im Beispiel wird es zu -1 ausgewertet und weist als Seiteneffekt -1 an x[0] zu
  • Auswertungsreihenfolge arithmetischer Operanden

    • Die Operatorpriorität ist klar definiert, aber die Auswertungsreihenfolge arithmetischer Operanden ist nicht definiert
    • Bei (x=1) + (x=2) ist die Reihenfolge der beiden Zuweisungen nicht definiert; da daher der Endwert von x nicht festgelegt ist, also ob er 1 oder 2 ist, handelt es sich um undefiniertes Verhalten
    • Mit den Optionen -std=c11 -O2 wertet GCC 8.2.1 den Beispielausdruck zu 4 aus, Clang 7.0.0 dagegen zu 3
  • Auswertungsreihenfolge logischer Operatoren

    • Bei den logischen Operatoren && und || ist auch die Auswertungsreihenfolge der Operanden klar definiert
    • In der Terminologie des C-Standards existiert zwischen der Auswertung des ersten und der des zweiten Operanden ein sequence point
    • Im Beispiel wird zuerst x=1 ausgewertet und ergibt true; anschließend wird x=2 ausgewertet und ergibt ebenfalls true, daher ist der gesamte Ausdruck true
  • Die freie Struktur des switch-Rumpfs

    • Der Rumpf einer switch-Anweisung kann aus beliebigen Statements bestehen, daher kann auch eine Struktur mit gemischten loop- und if-Konstruktionen gültig sein
    • Selbst der true-Zweig innerhalb einer if-Anweisung, deren Steuerungsausdruck immer false ist, wird mit einem case-Label zu live code; printf("1"); ist also kein dead code
    • Bei einem Sprung zu case 2 werden clause-1 und der Steuerungsausdruck der loop möglicherweise nicht ausgeführt; daher muss die Variable i vorher initialisiert sein
    • Auch wenn bei case 1 ohne break ein fall through auftritt, kann bei case 1 im true-Zweig des if und case 2 im false-Zweig case 2 übersprungen und mit case 3 fortgefahren werden
    • Nach den drei Aufrufen foo(0); foo(1); foo(2); lautet die Konsolenausgabe 02313223
    • Ein bekanntes reales Beispiel für die Mischung aus loop und switch ist Duff's device

Lebensdauer temporärer Objekte und Unterschiede zwischen C-Standardversionen

  • Ein bestimmter Codeausschnitt ist in C11 undefiniertes Verhalten, muss es in C99 aber nicht sein
  • In C11 ist die Lebensdauer eines bestimmten Objekts verkürzt, sodass ein von einem Funktionsaufruf zurückgegebenes Objekt nur bis zur Auswertung des rechten Operanden lebt
  • In C99 lebt dasselbe Objekt bis zum Ende des enclosing block
  • Wird auf ein Objekt nach dem Ende seiner Lebensdauer zugegriffen, ist das gemäß C11 § 6.2.4 ¶ 2 undefiniertes Verhalten
  • Auch in C99 ist die Lebensdauer eines Objekts mit automatic storage duration an den nächstgelegenen enclosing block gebunden; wird außerhalb dieses Blocks auf das Objekt zugegriffen, ist das undefiniertes Verhalten
  • C11 § 6.2.4 ¶ 8 legt fest, dass eine non-lvalue expression vom Struktur- oder Union-Typ, die ein Array-Member enthält, auf ein Objekt mit automatic storage duration und temporärer Lebensdauer verweist
  • Die Lebensdauer dieses temporären Objekts beginnt mit der Auswertung des Ausdrucks und endet mit dem Abschluss der Auswertung des einschließenden full expression oder full declarator
  • Jeder Versuch, ein Objekt mit temporärer Lebensdauer zu verändern, ist undefiniertes Verhalten
  • Das Beispiel stammt aus N1285; dort findet sich auch weitere Diskussion dazu

1 Kommentare

 
Lobste.rs-Kommentare
  • Frage 4 ist in C23 nicht gültig, vorher aber schon.
    Frage 10 ist weder richtig noch falsch, deshalb etwas unerquicklich für eine Multiple-Choice-Frage.
    Frage 15 ist technisch falsch, besonders im Zusammenhang mit Frage 13, und bei Frage 20 heißt es „nicht spezifiziert“, also ebenfalls keine der Antworten.
    Frage 30 ist je nach Lesart mehrdeutig.
    Trotzdem habe ich 27 von 31 richtig, und Compiler-Entwickler zu sein hilft wohl ein bisschen.

  • Nach etwa vier Fragen war das letzte Gefühl verschwunden, dass C einfach genug ist, um es mal für ein Side-Project zu verwenden.

    • Wenn man bei GCC oder clang -std=<language-standard> -pedantic -Wall -Wextra verwendet und Warnungen jedes Mal tatsächlich behebt und Pointer-Casts sowie Pointer-Manipulationen so weit wie möglich vermeidet, scheint man die großen Fallstricke vermeiden zu können.
      Die Warnungen von GCC/clang sind heutzutage ziemlich gut, und als <language-standard> kann man c89, c99, c11 oder c23 verwenden.
    • C ist einfach, aber die Akrobatik rund um undefiniertes Verhalten ist es nicht.
      Wenn man einen Compiler wie tcc verwendet, der keine seltsamen Optimierungen macht, erlebt man weniger bizarre Überraschungen.
  • Ich habe einfach danach ausgewählt: „Was wäre hier das absurdeste Verhalten?“ und damit 21 von 32 richtig gehabt.
    Die meisten Fehler kamen daher, dass ich nicht tief genug darüber nachgedacht habe, wie absurd es tatsächlich sein könnte.
    Ich habe C zuletzt vor über 15 Jahren ein bisschen angefasst, aber dieses Quiz macht mir keine Lust, es noch einmal zu versuchen.

    • Nebenbei: ChatGPT hatte 22 von 32 richtig, ohne die zusätzlichen Erklärungen hinter jeder Antwort gesehen zu haben.
  • Nach C23-Maßstab ist die Antwort auf Frage 4 nicht gültig.

  • Interessanterweise habe ich 27 von 32 richtig, obwohl ich schon eine ganze Weile kein C mehr benutzt habe.
    Genau deshalb habe ich mich immer auf statische Analyzer und Linter verlassen.

  • Schon Frage 1 fühlte sich fragwürdig an.
    Es wurde nicht berücksichtigt, woher diese Pointer überhaupt kommen könnten, und damit der dort beschriebene Fall überhaupt eintreten kann, braucht es sehr spezielle Bedingungen.
    In den meisten Fällen ist schon der Versuch, den Pointer zu erzeugen, undefiniertes Verhalten, aber fair kann man es wohl trotzdem nennen.
    Frage 3 hat mich wirklich überrascht und war wieder so eine typische C-Falle.
    Dass C-Integer-Literale überhaupt einen festen Typ haben, ist von Grund auf ziemlich nervig.
    Die Regeln zur Integer-Promotion gleichen manches aus, sind aber auch eine Fehlerquelle.
    Moderne Sprachen sollten die meisten oder gleich alle impliziten numerischen Casts verbieten, den Typ von Literalen möglichst aus dem Kontext ableiten und, wenn das nicht geht, einen expliziten Cast verlangen.
    Nach Frage 6 habe ich aufgegeben, dem Test zu vertrauen.
    Zuerst lag das daran, dass die Antwort auf Frage 5 offenbar so gestaltet war, dass man Frage 6 praktisch falsch beantworten musste, aber beim erneuten Hinsehen scheint Frage 6 selbst falsch zu sein.
    In der Erklärung heißt es, der Funktionsaufruf sei undefiniertes Verhalten, aber gefragt wurde, ob die Funktionsdefinition legal ist, und vermutlich war sie legal.

    • Wenn zwei Arrays im Speicher direkt aneinandergrenzen und eines auf das erste Element des einen und das andere direkt hinter das letzte Element des anderen zeigt, entsteht genau so eine Situation.
      Und das scheint gar nicht so selten zu sein.
  • Die switch()-Frage war wirklich gut.
    Knifflig, aber der Prozess, sie im Kopf durchzugehen, hat großen Spaß gemacht.