Każdy programista C trafia na tę samą krótką listę pomyłek, zwykle w pierwszym tygodniu, a czasem dekadę później. Warto je skatalogować, bo większość z nich daje komunikaty o błędach, które ich nie opisują, albo, co gorsza, nie daje żadnego komunikatu.
Każdy wpis poniżej to objaw, przyczyna i rozwiązanie.
Brakujący średnik i lawina błędów
Objaw: ściana błędów zgłoszonych w liniach, które wyglądają poprawnie.
program.c:6:5: error: expected ';' before 'printf'
program.c:7:5: error: expected declaration specifiers before 'return'
Przyczyna: C kończy instrukcje znakiem ;. Pomiń go, a kompilator skleja następną linię z bieżącą i zgłasza zamieszanie w miejscu, w którym połączony tekst przestaje mieć sens, zwykle w linii następnej.
int main(void) {
int x = 5 /* brakuje średnika */
printf("%d\n", x);
return 0;
}
Rozwiązanie: gdy pojawi się seria błędów, napraw tylko pierwszy i skompiluj ponownie. Wszystko, co po nim, może być skutkiem ubocznym. I zawsze sprawdzaj linię przed tą, którą wskazuje kompilator.
Ta sama lawina bierze się z niezamkniętego nawiasu klamrowego albo niezakończonego komentarza /*, a wtedy błędy mogą wylądować dziesiątki linii dalej.
Jeszcze jedna uwaga: średnika nie stawia się po nagłówku if, for czy while ani po nawiasie zamykającym definicję funkcji. if (x > 0); kompiluje się bez problemu i nic nie robi: ; to całe ciało.
Przypisanie zamiast porównania
Objaw: warunek, który zawsze jest prawdziwy, albo zmienna, która tajemniczo się zmienia.
int x = 5;
if (x = 10) { /* przypisuje 10, potem sprawdza 10 -> prawda */
printf("x to dziesiec\n"); /* wypisuje zawsze; x ma teraz wartość 10 */
}
Przyczyna: = przypisuje, == porównuje. Wartością przypisania jest przypisana wartość, więc if (x = 10) sprawdza 10, które jest niezerowe, czyli prawdziwe. Kompilator to przyjmuje, bo czasem właśnie o to chodzi.
Rozwiązanie: używaj == w każdym warunku i włącz -Wall, żeby kompilator ostrzegał:
warning: suggest parentheses around assignment used as truth value
Jeśli naprawdę chcesz przypisania w warunku (co jest częste w while ((c = getchar()) != EOF)), dodatkowe nawiasy mówią o tym wprost i wyciszają ostrzeżenie.
Niejawna deklaracja funkcji
Objaw:
warning: implicit declaration of function 'printf'
warning: implicit declaration of function 'malloc'
...po czym czasem pojawiają się dziwne wyniki w czasie działania albo błąd linkowania.
Przyczyna: kompilator natrafił na wywołanie funkcji, dla której nie ma deklaracji. W dialektach sprzed C99 zakładał, że funkcja zwraca int, i jechał dalej; w systemach 64-bitowych to założenie ucina zwrócony wskaźnik do 32 bitów i w ten sposób brakujący <stdlib.h> zamienia malloc w awarię.
Rozwiązanie: dołącz właściwy nagłówek.
| Funkcja | Nagłówek |
|---|---|
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> |
W przypadku własnych funkcji to samo ostrzeżenie oznacza, że funkcja jest wywoływana przed jej zdefiniowaniem. Umieść prototyp nad main:
scanf bez &
Objaw: program wysypuje się przy wczytywaniu albo nic nie wczytuje i zostawia zmienną bez zmian.
int age;
scanf("%d", age); /* brak & : przekazuje wartość, a nie adres */
Przyczyna: scanf zapisuje do twojej zmiennej, więc potrzebuje jej adresu. Przekazanie age oddaje śmieci, które w niej były, a scanf traktuje je jako miejsce do zapisu, co zwykle kończy się segfaultem.
Rozwiązanie: & przed zmienną, dla każdego typu poza tablicą, która już jest adresem:
Dwie sąsiednie pułapki z tej samej rodziny: w scanf używaj %lf dla double (%f oznacza tam float, a zapisanie bajtów floata do double zostawia w nim błędną wartość) i zawsze ograniczaj %s szerokością, na przykład %49s dla bufora 50-bajtowego, bo inaczej długie słowo go przepełni.
Jeszcze lepiej: wczytaj całą linię przez fgets i ją sparsuj; tak nie da się przepełnić bufora i nie zostają w nim resztki wejścia. Więcej o obu w artykule o scanf.
Porównywanie stringów przez ==
Objaw: dwa stringi, które ewidentnie są takie same, okazują się różne.
char a[] = "czesc";
char b[] = "czesc";
if (a == b) { /* porównuje dwa adresy: fałsz */
printf("takie same\n");
}
Przyczyna: string w C nie jest wartością, tylko wskaźnikiem na pierwszy znak. == porównuje wskaźniki. Dwie tablice z identycznym tekstem leżą pod różnymi adresami, więc test daje fałsz. (Co gorsza, porównanie dwóch identycznych literałów czasem daje prawdę, bo kompilator może przechowywać jedną kopię, przez co błąd pojawia się tylko czasami.)
Rozwiązanie: strcmp i pamiętaj, że zwraca 0 dla równych:
Odwrócona logika też łapie wiele osób: if (strcmp(a, b)) jest prawdziwe, gdy stringi się różnią, bo niezerowy wynik oznacza "nie są równe". Zawsze pisz == 0 jawnie.
Dzielenie całkowite
Objaw: średnia równa 0, procent, który zawsze wynosi 0 albo 100, stosunek, który zgubił część ułamkową.
int correct = 7, total = 10;
double score = correct / total; /* 0.0, a nie 0.7 */
Przyczyna: oba argumenty są typu int, więc C wykonuje dzielenie całkowite i obcina wynik zanim zostanie on przypisany do double. 7 / 10 to 0; zamiana 0 na double daje 0.0.
Rozwiązanie: zamień jeden argument na liczbę zmiennoprzecinkową przed dzieleniem:
Rzutowanie jednego argumentu automatycznie promuje drugi. Rzutowanie wyniku jest spóźnione: obcięcie już nastąpiło. Zobacz rzutowanie typów.
Ta sama pułapka kryje się w wyrażeniach takich jak (a + b) / 2 przy liczeniu środka i 1 / 2 * x, które zawsze daje 0, niezależnie od x.
Brakujący albo błędny return
Objaw: funkcja zwraca wiarygodnie wyglądającą, ale błędną liczbę, inną przy każdym uruchomieniu albo każdej kompilacji.
int add(int a, int b) {
int sum = a + b;
/* brak instrukcji return */
}
Przyczyna: dojście do końca funkcji, która nie jest void, bez zwrócenia wartości daje wartość nieokreśloną: w praktyce to, co akurat było w rejestrze zwracanej wartości. Jeśli wywołujący jej użyje, to niezdefiniowane zachowanie.
Bardziej podstępna wersja zwraca wartość na niektórych ścieżkach, a na innych nie:
int classify(int n) {
if (n > 0) return 1;
if (n < 0) return -1;
/* dla n == 0 wykonanie spada z końca funkcji */
}
Rozwiązanie: zwracaj wartość na każdej ścieżce i kompiluj z -Wall: komunikat GCC "control reaches end of non-void function" wyłapuje obie wersje.
Niezainicjalizowane zmienne
Objaw: śmieci na wyjściu albo wyniki, które zmieniają się między uruchomieniami i między poziomami optymalizacji.
int total; /* zawiera to, co było na stosie */
for (int i = 1; i <= 5; i++) {
total += i; /* dodawanie do śmieci */
}
printf("%d\n", total); /* jakaś ogromna liczba */
Przyczyna: zmienne lokalne nie są zerowane. Zmienna globalna albo static jest automatycznie ustawiana na zero; lokalna zaczyna od bajtów, które już leżały pod tym adresem na stosie.
Rozwiązanie: inicjalizuj w miejscu deklaracji. Nic to nie kosztuje, a usuwa całą klasę błędów:
-Wall -Wextra ostrzega przed wieloma takimi przypadkami ("may be used uninitialized"), a -fsanitize=memory albo valgrind wyłapują resztę. Ten błąd jest szczególnie paskudny, bo niezainicjalizowany wskaźnik prowadzi prosto do segmentation fault.
Błąd o jeden
Objaw: ostatni element zostaje pominięty albo program dotyka o jeden element za dużo i później zachowuje się dziwnie.
int arr[5];
for (int i = 0; i <= 5; i++) { /* dotyka arr[5], które nie istnieje */
arr[i] = i;
}
Przyczyna: tablica n-elementowa ma indeksy od 0 do n - 1. <= wykonuje jeden obieg za dużo.
Rozwiązanie: wzorzec i < n i liczenie n z tablicy zamiast wpisywania tej samej liczby dwa razy:
Wersja ze stringami, czyli zapomnienie o miejscu na '\0', to ten sam błąd w innym przebraniu, a char word[5] z "hello" w środku to przepełnienie bufora.
Dwa mniejsze, które warto znać
Średnik po nagłówku pętli. for (int i = 0; i < 10; i++);, po którym następuje blok w klamrach, wykonuje pętlę dziesięć razy, nic nie robiąc, a potem wykonuje blok jeden raz. Kompiluje się bez zastrzeżeń.
sizeof na wskaźniku. Wewnątrz funkcji parametr tablicowy jest wskaźnikiem, więc sizeof(arr) to rozmiar wskaźnika (8 bajtów), a nie tablicy. Przekazuj długość jako osobny argument:
Nawyk, który zapobiega większości z tego
Kompiluj z włączonymi ostrzeżeniami, od pierwszego programu:
gcc -Wall -Wextra -g program.c -o program
-Wall -Wextra wyłapuje przypisanie w warunku, brakujący return, odczyt niezainicjalizowanej zmiennej, nieużywaną zmienną i specyfikator printf, który nie pasuje do argumentu. Dodanie -fsanitize=address,undefined podczas pracy nad kodem wyłapuje prawie całą resztę w chwili, gdy się zdarza.
Traktuj każde ostrzeżenie jak błąd, który jeszcze się nie ujawnił. Program w C, który kompiluje się bez ostrzeżeń, nie ma gwarancji poprawności, ale prawie każdy program w C, który się wysypuje, wcześniej przed czymś ostrzegał.
Najczęściej zadawane pytania
Co oznacza 'implicit declaration of function' w C?
Kompilator natrafił na wywołanie funkcji, której deklaracji nigdy nie widział. Prawie zawsze brakuje #include: printf wymaga <stdio.h>, malloc wymaga <stdlib.h>, strlen wymaga <string.h>. Może to też oznaczać, że wywołujesz własną funkcję, zanim ją zdefiniujesz, co naprawia prototyp nad main.
Dlaczego program w C zgłasza błąd w linii, która wygląda dobrze?
Zwykle dlatego, że prawdziwa pomyłka jest w linii wcześniej. Brakujący średnik, niezamknięty nawias klamrowy albo niezakończony komentarz sprawiają, że kompilator czyta dwie linie jako jedną, więc zgłasza zamieszanie tam, gdzie tekst w końcu przestaje dać się sparsować. Zawsze najpierw sprawdź linię nad tą zgłoszoną.
Dlaczego w C nie można porównywać stringów przez ==?
Bo string w C to char *, czyli wskaźnik, więc == porównuje dwa adresy, a nie znaki, na które wskazują. Dwa identyczne stringi zapisane w różnych miejscach dają false. Używaj strcmp(a, b) == 0 z <string.h>, które zwraca 0, gdy zawartość się zgadza.
Dlaczego 5 / 2 daje w C wynik 2?
Bo oba argumenty są liczbami całkowitymi, więc C wykonuje dzielenie całkowite i odrzuca część ułamkową. Żeby dostać 2.5, niech jedna strona będzie liczbą zmiennoprzecinkową: 5.0 / 2 albo (double)a / b przy zmiennych. Rzutowanie wyniku jest spóźnione: (double)(5 / 2) to 2.0.