Der Compiler liest von oben nach unten
Ein C-Compiler verarbeitet eine Quelldatei in einem Durchgang, von der ersten bis zur letzten Zeile. Wenn er einen Funktionsaufruf erreicht, muss er drei Dinge bereits kennen: was die Funktion liefert, wie viele Argumente sie nimmt und welche Typen die haben. Ohne das kann er weder korrekten Code erzeugen noch den Aufruf prüfen.
Das hier ist also ein Problem:
#include <stdio.h>
int main(void) {
printf("%d\n", add(2, 3)); /* add wurde noch nicht gesehen */
return 0;
}
int add(int a, int b) {
return a + b;
}
Die Lösung ist ein Prototyp: die Signatur der Funktion, vor dem Aufruf geschrieben, mit einem Semikolon dort, wo der Rumpf stünde.
Jetzt darf main zuerst kommen, und so sind die meisten C-Dateien aufgebaut: Die Prototypen oben sagen, was die Datei bietet, main liest sich als Überblick, und die Einzelheiten folgen darunter.
Deklaration gegen Definition
Zwei Wörter, die leicht verschwimmen und die man auseinanderhalten sollte:
- Eine Deklaration sagt, dass eine Funktion existiert, und gibt ihren Typ an. Sie endet mit einem Semikolon und hat keinen Rumpf. Du darfst sie so oft schreiben, wie du möchtest.
- Eine Definition liefert den Rumpf. Sie muss im ganzen Programm genau einmal auftauchen - zwei Definitionen derselben Funktion sind ein Linkerfehler („multiple definition of").
int add(int a, int b); /* Deklaration (Prototyp) */
int add(int a, int b) { return a + b; } /* Definition - auch eine Deklaration */
Parameternamen sind in einem Prototyp optional; für den Compiler zählen nur die Typen:
int add(int, int); /* erlaubt und gleichwertig */
int add(int a, int b); /* besser: die Namen dokumentieren die Reihenfolge */
Benutze die Namen. void drawRect(int, int, int, int); sagt einem Leser nichts, während void drawRect(int x, int y, int width, int height); ihm alles sagt.
Was der Prototyp dir bringt
Nicht nur die Möglichkeit, Definitionen umzuordnen - er ist das, was den Compiler deine Aufrufe prüfen lässt.
Mit dem Prototyp im Sichtbereich werden die 3 und die 2 vor dem Aufruf in double umgewandelt. Ohne ihn würden sie als ints übergeben und scale würde sie als doubles lesen - Müll, und in alten C-Dialekten ohne jede Meldung.
Der Prototyp fängt auch falsche Argumentzahlen und unverträgliche Typen zur Übersetzungszeit ab:
scale(3.0); /* Fehler: zu wenige Argumente */
scale(3.0, "zwei"); /* Fehler: char * übergeben, wo double erwartet wird */
Das sind die Fehler, die du willst - laut, zur Übersetzungszeit, mit dem Finger auf der Zeile.
Fehler wegen impliziter Deklaration
Rufe eine Funktion auf, von der der Compiler nie gehört hat, und du bekommst eine der häufigsten Meldungen in C:
warning: implicit declaration of function 'add' [-Wimplicit-function-declaration]
In C89 riet der Compiler: Er nahm an, die Funktion liefere int, und akzeptierte, welche Argumente du auch übergabst. Diese Vermutung ist öfter falsch als richtig, und wenn die echte Funktion ein double oder einen Zeiger liefert, ist das Ergebnis Unsinn. C99 entfernte implizite Deklarationen aus der Sprache, in C99 und später ist das also ein Fehler - moderne GCC- und clang-Versionen weisen es in neueren Fassungen standardmäßig zurück.
Zwei Ursachen, zwei Lösungen:
Deine eigene, noch nicht deklarierte Funktion. Setze den Prototyp über den Aufruf oder verschiebe die Definition nach oben.
Eine Bibliotheksfunktion, deren Header du vergessen hast. Die Prototypen der Bibliothek stehen in ihrem Header, du brauchst also das #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>
Eine verwandte Meldung, conflicting types for 'add', bedeutet, dass Prototyp und Definition sich widersprechen - ein Parametertyp weicht ab oder der Rückgabetyp. Korrigiere das, was falsch ist; sie müssen exakt übereinstimmen.
Wechselseitige Rekursion braucht einen Prototyp
Manchmal löst Umordnen es nicht: Zwei Funktionen, die einander aufrufen, können nicht beide zuerst kommen.
Ein Prototyp durchbricht den Kreis. Das ist derselbe Mechanismus, auf den sich eine schlichte rekursive Funktion stützt - der eigene Name einer Funktion ist in ihrem Rumpf sichtbar, direkte Rekursion braucht also keinen Prototyp, wechselseitige schon.
Prototypen in Header-Dateien
Sobald ein Programm mehrere .c-Dateien umfasst, wandern Prototypen in einen Header, damit jede Datei dieselben Deklarationen aus einer Quelle sieht.
mathutils.h - die Schnittstelle:
#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 - die Implementierung:
#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 - ein Benutzer:
#include <stdio.h>
#include "mathutils.h"
int main(void) {
printf("%d\n", add(2, 3));
return 0;
}
Dann:
gcc main.c mathutils.c -o program
Zwei Gewohnheiten lohnen sich hier. Die Hülle aus #ifndef / #define / #endif ist ein Include-Schutz: Er verhindert, dass die Deklarationen zweimal verarbeitet werden, wenn der Header über zwei Pfade eingebunden wird. Und mathutils.c bindet seinen eigenen Header ein - was überflüssig aussieht, aber es nicht ist, denn so prüft der Compiler jede Definition gegen die Deklaration, die andere Dateien benutzen werden. Sollten sie je auseinanderdriften, erfährst du es sofort statt beim Linken. Header-Dateien behandelt den Rest.
Spitze Klammern (<stdio.h>) durchsuchen die System-Include-Pfade; Anführungszeichen ("mathutils.h") zuerst das Verzeichnis deines Projekts. Benutze Anführungszeichen für deine eigenen Header.
f(void) gegen f()
Das hier ist wirklich überraschend, und es ist der Grund, warum jedes Beispiel in dieser Dokumentation int main(void) schreibt.
void ping(void); /* nimmt KEINE Argumente - Aufrufe mit Argumenten werden abgelehnt */
void pong(); /* sagt NICHTS über die Parameter */
void ping(void); ist ein Prototyp: Er erklärt, dass die Funktion keine Parameter nimmt, ping(1, 2, 3) ist also ein Übersetzungsfehler.
void pong(); ist eine Deklaration alten Stils aus vorstandardisiertem C. Sie deklariert den Rückgabetyp und überhaupt nichts über die Parameter, der Compiler kann Aufrufe also nicht prüfen - pong(1, 2, 3) kompiliert stillschweigend und tut etwas Undefiniertes.
int main(void) { /* richtig: main nimmt keine Argumente */
int main() { /* erlaubt, aber die Argumentprüfung ist aus */
Schreibe für eine parameterlose Funktion immer (void). C23 ändert () so, dass es dasselbe wie (void) bedeutet, was die Falle endlich beseitigt - aber viel Code und viele Compiler sind noch nicht so weit, und (void) ist in jedem C-Standard korrekt.
Dieselbe Unterscheidung gilt für die Definition. void ping(void) { } ist eine Definition im Prototypstil; void ping() { } ist es nicht und schaltet die Prüfung von Aufrufen davor nicht ein.
Häufige Fehler
- Semikolon an der Definition.
int add(int a, int b); { return a + b; }deklariertaddund hinterlässt dann einen verirrten Block. Die Fehlermeldung ist verwirrend; die Ursache ist ein Zeichen. - Fehlendes Semikolon am Prototyp. Der Compiler liest in das hinein, was folgt, und meldet etwas Rätselhaftes mehrere Zeilen weiter unten.
- Prototyp und Definition widersprechen sich.
conflicting types for .... Mach sie identisch - oder besser, binde den Header in der Implementierungsdatei ein, damit die Prüfung automatisch geschieht. - Eine Funktion innerhalb einer anderen Funktion deklarieren. Erlaubt (
int add(int, int);innerhalb vonmain), aber die Deklaration gilt dann nur in dieser Funktion. Setze Prototypen auf Dateiebene. - Eine Funktion in einem Header definieren. Binde ihn aus zwei
.c-Dateien ein, und der Linker meldet eine doppelte Definition. Header halten Deklarationen; Definitionen gehören in eine.c-Datei. - Sich auf
()zur Prüfung verlassen. Es prüft nicht. Schreibe(void).
Häufig gestellte Fragen
Was ist ein Funktionsprototyp in C?
Eine Deklaration der Signatur einer Funktion - Rückgabetyp, Name und Parametertypen - die mit einem Semikolon statt mit einem Rumpf endet: int add(int a, int b);. Sie sagt dem Compiler, wie die Funktion aufgerufen wird, damit Aufrufe geprüft werden können, ohne zu sagen, was sie tut.
Was ist der Unterschied zwischen einer Deklaration und einer Definition in C?
Eine Deklaration führt Name und Typ ein (int add(int, int);) und darf mehrfach auftauchen. Eine Definition liefert den Rumpf (int add(int a, int b) { return a + b; }) und muss im ganzen Programm genau einmal auftauchen. Jede Definition ist zugleich eine Deklaration.
Wie behebe ich „implicit declaration of function" in C?
Füge vor dem Aufruf einen Prototyp hinzu. Für eigene Funktionen schreibe Rueckgabetyp name(Parametertypen); oben in die Datei oder in einen Header; für Bibliotheksfunktionen binde den richtigen Header ein - #include <stdio.h> für printf, <stdlib.h> für malloc, <string.h> für strlen, <math.h> für sqrt.
Was ist der Unterschied zwischen f(void) und f() in C?
void f(void); deklariert eine Funktion, die keine Argumente nimmt, und der Compiler weist jeden Aufruf mit Argumenten zurück. void f(); ist eine Deklaration alten Stils, die nichts über die Parameter sagt, die Argumentprüfung ist also abgeschaltet. Schreibe immer (void); C23 lässt die beiden endlich dasselbe bedeuten, aber älterer Code und ältere Compiler unterscheiden sich weiterhin.