Jeder C-Programmierer trifft auf dieselbe kurze Liste von Fehlern, meist in der ersten Woche und gelegentlich ein Jahrzehnt später. Was sie katalogisierenswert macht, ist, dass die meisten Fehlermeldungen erzeugen, die sie nicht beschreiben - oder, schlimmer, gar keine Meldung.
Jeder Eintrag unten besteht aus einem Symptom, der Ursache dahinter und der Lösung.
Fehlendes Semikolon und die Lawine
Symptom: eine Wand aus Fehlern, alle in Zeilen gemeldet, die korrekt aussehen.
program.c:6:5: error: expected ';' before 'printf'
program.c:7:5: error: expected declaration specifiers before 'return'
Ursache: C beendet Anweisungen mit ;. Lass eines weg, und der Compiler klebt die nächste Zeile an die aktuelle und meldet dann die Verwirrung an der Stelle, an der der zusammengefügte Text keinen Sinn mehr ergibt - typischerweise in der folgenden Zeile.
int main(void) {
int x = 5 /* fehlendes Semikolon */
printf("%d\n", x);
return 0;
}
Lösung: Wenn ein Schwung Fehler auftaucht, behebe nur den ersten und kompiliere neu. Alles danach kann Folgeschaden sein. Und prüfe immer die Zeile vor der, die der Compiler nennt.
Dieselbe Lawine entsteht durch eine nicht geschlossene Klammer oder einen nicht beendeten /*-Kommentar, wobei die Fehler Dutzende Zeilen entfernt landen können.
Eine Zusatzbemerkung: kein Semikolon nach dem Kopf eines if, for oder while und keines nach der schließenden Klammer einer Funktionsdefinition. if (x > 0); kompiliert problemlos und tut nichts - das ; ist der ganze Rumpf.
Zuweisung statt Vergleich
Symptom: eine Bedingung, die immer wahr ist, oder eine Variable, die sich rätselhaft ändert.
int x = 5;
if (x = 10) { /* weist 10 zu, testet dann 10 -> wahr */
printf("x ist zehn\n"); /* wird immer ausgegeben; x ist jetzt 10 */
}
Ursache: = weist zu, == vergleicht. Der Wert der Zuweisung ist der zugewiesene Wert, if (x = 10) testet also 10, was ungleich null und damit wahr ist. Der Compiler akzeptiert es, weil es gelegentlich gemeint ist.
Lösung: Benutze == in jeder Bedingung und schalte -Wall ein, damit der Compiler warnt:
warning: suggest parentheses around assignment used as truth value
Wenn du eine Zuweisung in einer Bedingung wirklich meinst - üblich bei while ((c = getchar()) != EOF) -, sagen die zusätzlichen Klammern das und bringen die Warnung zum Schweigen.
Implizite Deklaration einer Funktion
Symptom:
warning: implicit declaration of function 'printf'
warning: implicit declaration of function 'malloc'
... gefolgt manchmal von seltsamen Laufzeitergebnissen oder einem Linkfehler.
Ursache: Der Compiler traf auf einen Aufruf einer Funktion, für die er keine Deklaration hat. In Dialekten vor C99 nahm er an, die Funktion liefere int, und machte weiter; auf 64-Bit-Systemen schneidet diese Annahme einen zurückgegebenen Zeiger auf 32 Bit ab - und so macht ein fehlendes <stdlib.h> aus malloc einen Absturz.
Lösung: Binde den richtigen Header ein.
| Funktion | Header |
|---|---|
printf, scanf, fopen | <stdio.h> |
malloc, free, atoi, rand, exit | <stdlib.h> |
strlen, strcpy, strcmp | <string.h> |
sqrt, pow, fabs | <math.h> |
isdigit, toupper | <ctype.h> |
time | <time.h> |
Bei deinen eigenen Funktionen bedeutet dieselbe Warnung, dass du eine vor ihrer Definition aufgerufen hast. Setze einen Prototyp über main:
scanf ohne das &
Symptom: Das Programm stürzt bei der Eingabe ab oder liest nichts und lässt die Variable unverändert.
int age;
scanf("%d", age); /* fehlendes & : übergibt den Wert, nicht die Adresse */
Ursache: scanf schreibt in deine Variable, es braucht also deren Adresse. age zu übergeben reicht den Müll weiter, der darin stand, und scanf behandelt das als Ort zum Schreiben, was meist ein Segfault ist.
Lösung: ein & vor der Variablen - bei jedem Typ außer einem Array, das bereits eine Adresse ist:
Zwei benachbarte Fallen derselben Familie: Benutze %lf für ein double in scanf (%f bedeutet dort float, und die Bytes eines float in ein double zu schreiben lässt es falsch zurück), und begrenze ein %s immer mit einer Breite - %49s für einen 50-Byte-Puffer -, sonst läuft ein langes Wort darüber hinaus.
Noch besser: Lies mit fgets eine ganze Zeile und werte sie aus, was nicht überlaufen kann und keine Reste im Eingabepuffer lässt. Mehr zu beidem unter scanf.
Zeichenketten mit == vergleichen
Symptom: Zwei Zeichenketten, die offensichtlich übereinstimmen, gelten als verschieden.
char a[] = "hallo";
char b[] = "hallo";
if (a == b) { /* vergleicht zwei Adressen: falsch */
printf("gleich\n");
}
Ursache: Eine C-Zeichenkette ist kein Wert, sie ist ein Zeiger auf das erste Zeichen. == vergleicht die Zeiger. Zwei Arrays mit identischem Text liegen an verschiedenen Adressen, der Test ist also falsch. (Verwirrenderweise liefert der Vergleich zweier identischer Literale manchmal wahr, weil der Compiler nur eine Kopie ablegen darf - was den Bug sprunghaft macht.)
Lösung: strcmp, und denk daran, dass es bei Gleichheit 0 liefert:
Auch die umgekehrte Lesart erwischt viele: if (strcmp(a, b)) ist wahr, wenn die Zeichenketten sich unterscheiden, denn ein Ergebnis ungleich null bedeutet „nicht gleich". Schreibe das == 0 immer ausdrücklich hin.
Ganzzahldivision
Symptom: ein Durchschnitt von 0, ein Prozentsatz, der immer 0 oder 100 ist, ein Verhältnis, das seinen Bruchteil verloren hat.
int correct = 7, total = 10;
double score = correct / total; /* 0.0, nicht 0.7 */
Ursache: Beide Operanden sind int, C macht also eine Ganzzahldivision und schneidet ab, bevor das Ergebnis einem double zugewiesen wird. 7 / 10 ist 0; 0 in double umzuwandeln ergibt 0.0.
Lösung: Mach einen Operanden vor der Division zu einem Gleitkommawert:
Einen Operanden zu casten befördert den anderen automatisch mit. Das Ergebnis zu casten ist zu spät - das Abschneiden ist bereits passiert. Siehe Typumwandlung.
Dieselbe Falle versteckt sich in Ausdrücken wie (a + b) / 2 für einen Mittelpunkt und in 1 / 2 * x, was unabhängig von x immer 0 ist.
Fehlendes oder falsches return
Symptom: Eine Funktion liefert eine plausibel aussehende falsche Zahl, bei jedem Lauf oder jedem Build eine andere.
int add(int a, int b) {
int sum = a + b;
/* keine return-Anweisung */
}
Ursache: Das Ende einer Nicht-void-Funktion ohne Rückgabe zu erreichen liefert einen unspezifizierten Wert - in der Praxis das, was zufällig im Rückgaberegister stand. Es ist undefiniertes Verhalten, wenn der Aufrufer ihn benutzt.
Die heimtückischere Form kehrt auf manchen Pfaden zurück und auf anderen nicht:
int classify(int n) {
if (n > 0) return 1;
if (n < 0) return -1;
/* n == 0 fällt hinten heraus */
}
Lösung: Gib auf jedem Pfad etwas zurück und kompiliere mit -Wall - GCCs „control reaches end of non-void function" fängt beide Varianten ab.
Uninitialisierte Variablen
Symptom: Müllausgabe oder Ergebnisse, die sich zwischen Läufen und zwischen Optimierungsstufen ändern.
int total; /* enthält, was auf dem Stack lag */
for (int i = 1; i <= 5; i++) {
total += i; /* addiert zu Müll */
}
printf("%d\n", total); /* irgendeine riesige Zahl */
Ursache: Lokale Variablen werden nicht genullt. Eine globale oder static-Variable wird automatisch auf null gesetzt; eine lokale beginnt mit den Bytes, die an dieser Stackadresse schon lagen.
Lösung: Initialisiere an der Stelle der Deklaration. Es kostet nichts und beseitigt die ganze Fehlerklasse:
-Wall -Wextra warnt bei vielen davon („may be used uninitialized"), und -fsanitize=memory oder valgrind fangen den Rest ab. Dieser Fehler ist besonders fies, weil ein uninitialisierter Zeiger direkt zu einem Segmentierungsfehler führt.
Off-by-one
Symptom: Das letzte Element wird verpasst, oder ein Element zu viel wird angefasst und das Programm verhält sich später falsch.
int arr[5];
for (int i = 0; i <= 5; i++) { /* fasst arr[5] an, das nicht existiert */
arr[i] = i;
}
Ursache: Ein Array mit n Elementen hat die Indizes 0 bis n - 1. <= führt einen zusätzlichen Durchlauf aus.
Lösung: das Muster i < n, und n aus dem Array berechnen, statt die Zahl zweimal zu schreiben:
Die Zeichenketten-Variante - den Platz für '\0' zu vergessen - ist derselbe Fehler mit anderem Hut, und char word[5] mit "hallo" darin ist ein Pufferüberlauf.
Zwei kleinere, die man kennen sollte
Semikolon nach einem Schleifenkopf. for (int i = 0; i < 10; i++); gefolgt von einem geklammerten Block lässt die Schleife zehnmal nichts tun und führt den Block dann einmal aus. Es kompiliert sauber.
sizeof auf einem Zeiger. Innerhalb einer Funktion ist ein Array-Parameter ein Zeiger, sizeof(arr) ist also die Größe des Zeigers (8 Bytes), nicht die des Arrays. Übergib die Länge als separates Argument:
Die Gewohnheit, die das meiste davon verhindert
Kompiliere mit eingeschalteten Warnungen, vom allerersten Programm an:
gcc -Wall -Wextra -g program.c -o program
-Wall -Wextra fängt die Zuweisung-in-der-Bedingung, das fehlende return, den uninitialisierten Lesezugriff, die unbenutzte Variable und die printf-Formatangabe ab, die nicht zu ihrem Argument passt. -fsanitize=address,undefined während der Entwicklung hinzuzufügen fängt fast alles Übrige in dem Moment ab, in dem es passiert.
Behandle jede Warnung als einen Fehler, den du nur noch nicht getroffen hast. Ein C-Programm, das ohne Warnungen kompiliert, ist nicht garantiert korrekt - aber fast jedes C-Programm, das abstürzt, hat vorher vor etwas gewarnt.
Häufig gestellte Fragen
Was bedeutet „implicit declaration of function" in C?
Der Compiler traf auf einen Aufruf einer Funktion, deren Deklaration er nie gesehen hat. Fast immer hast du ein #include vergessen - printf braucht <stdio.h>, malloc braucht <stdlib.h>, strlen braucht <string.h>. Es kann auch bedeuten, dass du eine eigene Funktion vor ihrer Definition aufgerufen hast, was ein Prototyp über main behebt.
Warum meldet mein C-Programm einen Fehler in einer Zeile, die in Ordnung aussieht?
Meist, weil der eigentliche Fehler in der Zeile davor steckt. Ein fehlendes Semikolon, eine nicht geschlossene Klammer oder ein nicht beendeter Kommentar lässt den Compiler deine zwei Zeilen als eine lesen, er meldet die Verwirrung also dort, wo es endgültig unlesbar wird. Prüfe immer zuerst die Zeile über der gemeldeten.
Warum kann ich Zeichenketten in C nicht mit == vergleichen?
Weil eine C-Zeichenkette ein char * ist - ein Zeiger - und == daher zwei Adressen vergleicht, nicht die Zeichen, auf die sie zeigen. Zwei identische Zeichenketten an verschiedenen Orten ergeben false. Benutze strcmp(a, b) == 0 aus <string.h>, das 0 liefert, wenn die Inhalte übereinstimmen.
Warum ergibt 5 / 2 in C den Wert 2?
Weil beide Operanden Ganzzahlen sind, führt C eine Ganzzahldivision durch und verwirft den Bruchteil. Mach eine Seite zu einem Gleitkommawert, um 2.5 zu erhalten: 5.0 / 2 oder (double)a / b bei Variablen. Das Ergebnis zu casten ist zu spät - (double)(5 / 2) ist 2.0.