Kompilator czyta od góry do dołu
Kompilator C przetwarza plik źródłowy w jednym przebiegu, od pierwszej linii do ostatniej. Gdy dochodzi do wywołania funkcji, musi już znać trzy rzeczy: co funkcja zwraca, ile przyjmuje argumentów i jakie mają typy. Bez tego nie wygeneruje poprawnego kodu ani nie sprawdzi wywołania.
Dlatego to jest problem:
#include <stdio.h>
int main(void) {
printf("%d\n", add(2, 3)); /* add nie było jeszcze widziane */
return 0;
}
int add(int a, int b) {
return a + b;
}
Rozwiązaniem jest prototyp: sygnatura funkcji zapisana przed wywołaniem, ze średnikiem w miejscu ciała.
Teraz main może stać na początku i tak zorganizowana jest większość plików w C: prototypy u góry mówią, co plik oferuje, main czyta się jak zarys, a szczegóły są niżej.
Deklaracja a definicja
Dwa słowa, które łatwo pomylić, a warto je rozróżniać:
- Deklaracja mówi, że funkcja istnieje, i podaje jej typ. Kończy się średnikiem i nie ma ciała. Możesz ją napisać dowolnie wiele razy.
- Definicja dostarcza ciało. Musi wystąpić dokładnie raz w całym programie: dwie definicje tej samej funkcji to błąd linkera ("multiple definition of").
int add(int a, int b); /* deklaracja (prototyp) */
int add(int a, int b) { return a + b; } /* definicja, a zarazem deklaracja */
Nazwy parametrów są w prototypie opcjonalne; dla kompilatora liczą się tylko typy:
int add(int, int); /* poprawne i równoważne */
int add(int a, int b); /* lepiej: nazwy dokumentują kolejność */
Używaj nazw. void drawRect(int, int, int, int); nic czytelnikowi nie mówi, a void drawRect(int x, int y, int width, int height); mówi wszystko.
Co daje prototyp
Nie tylko możliwość zmiany kolejności definicji: to dzięki niemu kompilator sprawdza twoje wywołania.
Gdy prototyp jest w zasięgu, 3 i 2 są zamieniane na double przed wywołaniem. Bez niego zostałyby przekazane jako int, a scale odczytałaby je jako double: śmieci, i to bez żadnego komunikatu w starych dialektach C.
Prototyp wyłapuje też złą liczbę argumentów i niezgodne typy już podczas kompilacji:
scale(3.0); /* błąd: za mało argumentów */
scale(3.0, "two"); /* błąd: char * tam, gdzie oczekiwany jest double */
Takich błędów właśnie chcesz: głośnych, zgłaszanych podczas kompilacji i wskazujących linię.
Błędy niejawnej deklaracji
Wywołaj funkcję, o której kompilator nigdy nie słyszał, a dostaniesz jeden z najczęstszych komunikatów w C:
warning: implicit declaration of function 'add' [-Wimplicit-function-declaration]
W C89 kompilator zgadywał: zakładał, że funkcja zwraca int, i przyjmował dowolne przekazane argumenty. To zgadywanie częściej jest błędne niż trafne, a gdy prawdziwa funkcja zwraca double albo wskaźnik, wynik jest bez sensu. C99 usunęło niejawne deklaracje z języka, więc w C99 i nowszych to błąd; nowe wersje GCC i clang domyślnie go odrzucają.
Dwie przyczyny, dwa rozwiązania:
Własna funkcja, jeszcze niezadeklarowana. Dodaj prototyp nad wywołaniem albo przenieś definicję wyżej.
Funkcja biblioteczna, której nagłówek pominięto. Prototypy biblioteki leżą w jej nagłówku, więc potrzebujesz #include:
printf, scanf, fopen -> #include <stdio.h>
malloc, free, exit, atoi -> #include <stdlib.h>
strlen, strcpy, strcmp -> #include <string.h>
sqrt, pow, sin, fabs -> #include <math.h>
isdigit, toupper -> #include <ctype.h>
bool, true, false -> #include <stdbool.h>
Pokrewny komunikat, conflicting types for 'add', oznacza, że prototyp i definicja się nie zgadzają: różni się typ parametru albo typ zwracany. Popraw ten, który jest zły; muszą pasować dokładnie.
Rekurencja wzajemna wymaga prototypu
Czasem zmiana kolejności nie pomoże: dwie funkcje, które wywołują się nawzajem, nie mogą obie stać pierwsze.
Jeden prototyp przerywa ten krąg. Na tym samym mechanizmie opiera się zwykła funkcja rekurencyjna: nazwa funkcji jest w zasięgu wewnątrz jej ciała, więc bezpośrednia rekurencja nie potrzebuje prototypu, ale rekurencja wzajemna już tak.
Prototypy w plikach nagłówkowych
Gdy program obejmuje kilka plików .c, prototypy przenosi się do nagłówka, żeby każdy plik widział te same deklaracje z jednego miejsca.
mathutils.h, czyli interfejs:
#ifndef MATHUTILS_H
#define MATHUTILS_H
int add(int a, int b);
int multiply(int a, int b);
double average(const int values[], int count);
#endif
mathutils.c, czyli implementacja:
#include "mathutils.h"
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
double average(const int values[], int count) {
if (count == 0) return 0.0;
int total = 0;
for (int i = 0; i < count; i++) total += values[i];
return (double) total / count;
}
main.c, czyli plik, który z tego korzysta:
#include <stdio.h>
#include "mathutils.h"
int main(void) {
printf("%d\n", add(2, 3));
return 0;
}
Potem:
gcc main.c mathutils.c -o program
Warto tu przyjąć dwa nawyki. Otoczka #ifndef / #define / #endif to include guard: nie pozwala przetworzyć deklaracji dwa razy, jeśli nagłówek zostanie dołączony dwiema ścieżkami. A mathutils.c dołącza własny nagłówek, co wygląda na zbędne, ale nie jest: dzięki temu kompilator sprawdza każdą definicję względem deklaracji, której będą używać inne pliki. Jeśli kiedyś się rozjadą, dowiesz się od razu, a nie dopiero przy linkowaniu. Resztę opisuje strona o plikach nagłówkowych.
Nawiasy ostre (<stdio.h>) przeszukują systemowe ścieżki dołączania; cudzysłowy ("mathutils.h") najpierw przeszukują katalog projektu. Dla własnych nagłówków używaj cudzysłowów.
f(void) a f()
To naprawdę zaskakuje i właśnie dlatego każdy przykład w tej dokumentacji ma int main(void).
void ping(void); /* NIE przyjmuje argumentów: wywołania z argumentami są odrzucane */
void pong(); /* NIC nie mówi o parametrach */
void ping(void); to prototyp: deklaruje, że funkcja nie ma parametrów, więc ping(1, 2, 3) to błąd kompilacji.
void pong(); to deklaracja w starym stylu, odziedziczona po C sprzed standardu. Deklaruje typ zwracany i nic o parametrach, więc kompilator nie może sprawdzać wywołań: pong(1, 2, 3) kompiluje się po cichu i robi coś niezdefiniowanego.
int main(void) { /* dobrze: main nie przyjmuje argumentów */
int main() { /* poprawne, ale sprawdzanie argumentów jest wyłączone */
Dla funkcji bez parametrów zawsze pisz (void). C23 zmienia znaczenie () na takie samo jak (void), co wreszcie usuwa tę pułapkę, ale mnóstwo kodu i kompilatorów jeszcze tam nie dotarło, a (void) jest poprawne w każdym standardzie C.
To samo rozróżnienie dotyczy definicji. void ping(void) { } to definicja w stylu prototypu; void ping() { } nią nie jest i nie włącza sprawdzania wywołań, które pojawiają się przed nią.
Częste błędy
- Średnik po definicji.
int add(int a, int b); { return a + b; }deklarujeadd, a potem zostawia osierocony blok. Komunikat błędu jest mylący, a przyczyną jest jeden znak. - Brak średnika po prototypie. Kompilator czyta dalej to, co następuje, i zgłasza coś niezrozumiałego kilka linii niżej.
- Prototyp i definicja się nie zgadzają.
conflicting types for .... Niech będą identyczne; jeszcze lepiej dołącz nagłówek w pliku implementacji, żeby sprawdzenie było automatyczne. - Deklarowanie funkcji wewnątrz innej funkcji. To poprawne (
int add(int, int);wewnątrzmain), ale wtedy deklaracja ma zasięg tylko tej funkcji. Prototypy umieszczaj na poziomie pliku. - Definiowanie funkcji w nagłówku. Dołącz go w dwóch plikach
.c, a linker zgłosi podwójną definicję. Nagłówki zawierają deklaracje; definicje trafiają do pliku.c. - Poleganie na
()przy sprawdzaniu. Ono niczego nie sprawdza. Pisz(void).
Najczęściej zadawane pytania
Czym jest prototyp funkcji w C?
Deklaracją sygnatury funkcji, czyli typu zwracanego, nazwy i typów parametrów, zakończoną średnikiem zamiast ciała: int add(int a, int b);. Mówi kompilatorowi, jak funkcję się wywołuje, żeby mógł sprawdzać wywołania, ale nie mówi, co funkcja robi.
Czym różni się deklaracja od definicji w C?
Deklaracja wprowadza nazwę i typ (int add(int, int);) i może się pojawić wiele razy. Definicja dostarcza ciało (int add(int a, int b) { return a + b; }) i musi wystąpić dokładnie raz w całym programie. Każda definicja jest też deklaracją.
Jak naprawić błąd "implicit declaration of function" w C?
Dodaj prototyp przed wywołaniem. Dla własnych funkcji wpisz returnType name(paramTypes); na początku pliku albo w nagłówku; dla funkcji bibliotecznych dołącz właściwy nagłówek: #include <stdio.h> dla printf, <stdlib.h> dla malloc, <string.h> dla strlen, <math.h> dla sqrt.
Czym różni się f(void) od f() w C?
void f(void); deklaruje funkcję, która nie przyjmuje argumentów, i kompilator odrzuca każde wywołanie, które jakieś przekazuje. void f(); to deklaracja w starym stylu, która nie mówi nic o parametrach, więc sprawdzanie argumentów jest wyłączone. Zawsze pisz (void); w C23 oba zapisy wreszcie znaczą to samo, ale starszy kod i kompilatory wciąż je rozróżniają.