Quiz zur Programmiersprache C
(stefansf.de)- 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 unduint8_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()undfoo(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,&aund&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
pundqauf dieselbe Adresse zeigen, kann der Vergleichp == qundefiniertes 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 einshort-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 einint-Objekt über einunsigned char-lvalue zulässig - Für
unsigned charist garantiert, dass es weder Padding-Bits noch trap representations gibt; seit C11 ist auch fürsigned chargarantiert, dass es keine Padding-Bits gibt - Type-based alias analysis wird im zugehörigen Artikel behandelt
- Selbst wenn gleich typisierte Zeiger
-
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
0ist je nach Kontext entweder ein Integer oder ein Nullzeiger, und(void *)0wird als Nullzeiger ausgewertet - Selbst wenn ein Ausdruck
ezu0ausgewertet wird, ist nicht garantiert, dass(void *)eein 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
eein Nullzeiger ist, nicht garantiert, dasse + 0ebenfalls ein Nullzeiger ist
-
Nicht initialisierte Werte
- Wird ein Objekt mit automatischer Speicherdauer gelesen, das nicht initialisiert wurde, und könnte dieses Objekt die Speicherklasse
registerhaben, 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 != xentwedertrueoderfalsesein, und wennint xunspecified ist, ist auch nachx *= 0nicht garantiert, dassxden Wert 0 hat - Indeterminate und unspecified value werden in DR260, DR451, N1793, N1818, N2012, N2013, N2221 diskutiert
- Wird ein Objekt mit automatischer Speicherdauer gelesen, das nicht initialisiert wurde, und könnte dieses Objekt die Speicherklasse
-
unsigned charundmemcpy- Der Typ
unsigned charhat 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
xnach einem Aufruf der Standardbibliotheksfunktionmemcpyspecified sein müsse; in dieser Interpretation würdex != xzufalsewerden - 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
- Der Typ
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 Konstantenint,unsigned int,long int,unsigned long int,long long int,unsigned long long int - Konstanten zwischen
INT_MAX+1undUINT_MAXkö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
intundlongmit 32 Bit in einem Register übergeben,long longdagegen mit 64 Bit in zwei Registern - Auf Plattformen, auf denen
int32 Bit breit ist, wird-1 < 0x8000zutrue; auf Plattformen mit 16-Bit-intwird es zufalse, 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
sizeofliefert einen unsigned integer vom Typsize_tzurü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
-1wird bei der Umwandlung in unsigned zum maximalen unsigned Integer des entsprechenden Rangs - Daher wird
sizeof(int) > -1immer zufalseausgewertet
- Der Operator
-
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')immertrueist; garantiert ist nursizeof(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
- In C haben Zeichenkonstanten nach C11 § 6.4.4.4 ¶ 10 den Typ
-
uint8_t-Arithmetik und Division- Auch wenn
a,bundcvor dem Lesen initialisiert sind, können sich die Werte vonxundzwegen Integer-Promotion und der Position der Zwischenspeicherung unterscheiden - Die Werte aller Variablen werden zunächst auf die Größe von
intpromotet; 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=2fürxder Wert((255 + 1) / 2) % 256 = 128 - Die Zwischenvariable
ywird zu(255 + 1) % 256 = 0, danach wirdzzu(0 / 2) % 256 = 0, also128 != 0 - unsigned integer overflow ist wohldefiniertes Verhalten
- Da sich die Modulo-Operation über die Addition verteilt, sind
xundzimmer gleich, wenn man die Division durch eine Addition ersetzt - Ändert man die erste Zuweisung zu
uint8_t x = ((uint8_t)(a + b)) / c;, sindxundzebenfalls immer gleich
- Auch wenn
-
const-Variablen und variable length arrays- Auch wenn man die mit
constqualifizierten Variablennundmals 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,_Alignofund 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
xnicht kompilieren - Im Block Scope sind variable length arrays erlaubt; daher kann die Compilation Unit mit dem lokalen Array
ykompiliert 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
yals normales Array mit 42 Elementen kompiliert
- Auch wenn man die mit
Funktionsdeklarationen, Rückgabewerte, Linkage
-
foo()undfoo(void)- Eine Funktionsdeklaration der Form
foo()deklariert eine Funktion, deren Anzahl und Typen der Argumente unbekannt sind, währendfoo(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
floatzudoublehochgestuft 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 und42alsintdargestellt; istbaralso für einen RückgabetypTnicht mitT (*)(int)kompatibel, führt das zu undefiniertem Verhalten
- Eine Funktionsdeklaration der Form
-
Funktionen mit Rückgabewert, die keinen Wert zurückgeben
- Auch wenn eine Funktion den Rückgabetyp
inthat 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äßigintangenommen; deshalb sind Funktionen ohne Rückgabewert historisch mit der Regel des implizitenintverknüpft - Die Regel des impliziten
intwurde 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
returnohne 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
- Auch wenn eine Funktion den Rückgabetyp
-
Linkage und
extern- Wird in einem Gültigkeitsbereich, in dem eine frühere Deklaration sichtbar ist, derselbe Bezeichner erneut mit
externdeklariert, 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
- Wird in einem Gültigkeitsbereich, in dem eine frühere Deklaration sichtbar ist, derselbe Bezeichner erneut mit
Qualifizierer und unvollständige Typen
-
constbei Funktionsparametern- Wenn in einer Funktionsdeklaration der Parameter
xmitconstqualifiziert ist, in der Funktionsdefinition aber nicht, und im Funktionsrumpf ein Wert inxgeschrieben 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
- Wenn in einer Funktionsdeklaration der Parameter
-
constim Funktionsrückgabetyp- Wenn nur der Rückgabetyp der Funktionsdefinition mit
constqualifiziert 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 Funktionsdeklaratoren 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
Tist; 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
- Wenn nur der Rückgabetyp der Funktionsdefinition mit
-
Unvollständige Structs und globale Variablen
- Wenn
struct foozum 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
- Wenn
-
External Object vom Typ
void- Eine Variablendeklaration vom Typ
voidmit interner Linkage ist nicht zulässig, aber eine Variablendeklaration vom Typvoidmit externer Linkage ist grammatikalisch zulässig und wird im C11-Standard nirgendwo ausdrücklich verboten - Nach C11 § 6.2.5 ¶ 19 ist der Typ
voidein 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 Objektsfoovom Typvoidkein 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
foozu bilden, wenn der Typ inconst voidgeändert wird; das wirkt eher wie ein Versehen als wie ein beabsichtigtes Feature
- Eine Variablendeklaration vom Typ
-
Pointer-
const-Konvertierung- Wenn
Tein abgeleiteter Objekttyp ist, ist die Zuweisung ancpzulässig; ob die Zuweisung ancppzulässig ist, lässt sich jedoch nicht kurz beantworten - Dieses Thema wird in einem Beitrag zur impliziten Pointer-to-const-Konvertierung behandelt
- Wenn
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];werdena,&aund&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
- Bei
-
Array-Parameter und lokale Arrays
- Ist der Typ eines Funktionsparameters ein „Array von
T“, wird er zu „Pointer aufT“ angepasst - Auch wenn der Parameter
xwieint[42]aussieht, wird er tatsächlich alsint *behandelt - Ist die lokale Variable
yeinint[42], dann istsizeof(y)gleich42 * sizeof(int) - Da die Größe eines Objekt-Pointers im Allgemeinen nicht der Größe von 42 Ganzzahlen entspricht, ist
sizeof(x) == sizeof(y)normalerweisefalse - Details dazu finden sich im entsprechenden Beitrag
- Ist der Typ eines Funktionsparameters ein „Array von
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+++ywird als Kombination bestehender Operatoren interpretiert und ist äquivalent zu(x++) + y- Auch
--*--pist kein neuer Operator, sondern eine Kombination bestehender Operatoren --*--pist äquivalent zu--(*(--p)); im Beispiel wird es zu-1ausgewertet und weist als Seiteneffekt-1anx[0]zu
- In C können anders als in C++ keine neuen Operatoren definiert werden, daher gibt es keinen neuen Operator wie
-
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 vonxnicht festgelegt ist, also ob er1oder2ist, handelt es sich um undefiniertes Verhalten - Mit den Optionen
-std=c11 -O2wertet GCC 8.2.1 den Beispielausdruck zu4aus, Clang 7.0.0 dagegen zu3
-
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=1ausgewertet und ergibttrue; anschließend wirdx=2ausgewertet und ergibt ebenfallstrue, daher ist der gesamte Ausdrucktrue
- Bei den logischen Operatoren
-
Die freie Struktur des
switch-Rumpfs- Der Rumpf einer
switch-Anweisung kann aus beliebigen Statements bestehen, daher kann auch eine Struktur mit gemischten loop- undif-Konstruktionen gültig sein - Selbst der
true-Zweig innerhalb einerif-Anweisung, deren Steuerungsausdruck immerfalseist, wird mit einemcase-Label zu live code;printf("1");ist also kein dead code - Bei einem Sprung zu
case 2werden clause-1 und der Steuerungsausdruck der loop möglicherweise nicht ausgeführt; daher muss die Variableivorher initialisiert sein - Auch wenn bei
case 1ohnebreakein fall through auftritt, kann beicase 1imtrue-Zweig desifundcase 2imfalse-Zweigcase 2übersprungen und mitcase 3fortgefahren werden - Nach den drei Aufrufen
foo(0); foo(1); foo(2);lautet die Konsolenausgabe02313223 - Ein bekanntes reales Beispiel für die Mischung aus loop und switch ist Duff's device
- Der Rumpf einer
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.
clang-std=<language-standard>-pedantic -Wall -Wextraverwendet 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/
clangsind heutzutage ziemlich gut, und als <language-standard> kann man c89, c99, c11 oder c23 verwenden.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.
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.
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.