Menu

Prototypy funkcji w C: deklaracja a definicja

Dlaczego C musi znać kształt funkcji, zanim ją wywołasz: pisanie prototypów, naprawianie błędów niejawnej deklaracji, prototypy w plikach nagłówkowych i prawdziwa różnica między f(void) a f().

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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; } deklaruje add, 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ątrz main), 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ą.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ