Hintergrund der AC-Funktionsdeklaration
In C funktionieren Funktionsdeklarationen nicht wie in anderen Sprachen: Der C-Compiler selbst sucht in der Datei nicht vorwärts und rückwärts, um die Funktionsdeklaration an der Stelle zu finden, an der Sie sie aufrufen, und scannt die Datei nicht mehr Male , um herauszufinden , die Beziehungen entweder: der Compiler nur untersucht , nach vorn in der Datei genau einmal , von oben nach unten. Das Verbinden von Funktionsaufrufen mit Funktionsdeklarationen ist Teil der Aufgabe des Linkers und erfolgt erst, nachdem die Datei bis zu den Anweisungen für die unformatierte Assembly kompiliert wurde.
Dies bedeutet, dass beim Durchlaufen der Datei durch den Compiler, wenn der Compiler zum ersten Mal auf den Namen einer Funktion stößt, eines von zwei Dingen der Fall sein muss: Entweder wird die Funktionsdeklaration selbst angezeigt. In diesem Fall weiß der Compiler Bescheid genau, was die Funktion ist und welche Typen sie als Argumente verwendet und welche Typen sie zurückgibt - oder es ist ein Aufruf der Funktion, und der Compiler muss raten, wie die Funktion schließlich deklariert wird.
(Es gibt eine dritte Option, bei der der Name in einem Funktionsprototyp verwendet wird, aber wir werden dies vorerst ignorieren, da Sie wahrscheinlich keine Prototypen verwenden, wenn Sie dieses Problem überhaupt sehen.)
Geschichtsunterricht
In den frühesten Tagen von C war die Tatsache, dass der Compiler Typen erraten musste, kein wirkliches Problem: Alle Typen waren mehr oder weniger gleich - so ziemlich alles war entweder ein Int oder ein Zeiger, und das waren sie auch die gleiche Größe. (Tatsächlich gab es in B, der Sprache vor C, überhaupt keine Typen; alles war nur ein Int oder ein Zeiger, und sein Typ wurde ausschließlich davon bestimmt, wie Sie ihn verwendet haben!) Der Compiler konnte also das Verhalten von jedem sicher erraten Funktion nur basierend auf der Anzahl der übergebenen Parameter: Wenn Sie zwei Parameter übergeben würden, würde der Compiler zwei Dinge auf den Aufrufstapel schieben, und vermutlich würde der Angerufene zwei Argumente deklarieren, und das würde alles in einer Reihe stehen. Wenn Sie nur einen Parameter übergeben würden, die Funktion jedoch zwei erwarten würde, würde dies immer noch funktionieren, und das zweite Argument würde einfach ignoriert / garbage. Wenn Sie drei Parameter übergeben und die Funktion zwei erwartet, würde dies ebenfalls funktionieren, und der dritte Parameter würde von den lokalen Variablen der Funktion ignoriert und darauf getreten. (Einige alte C-Codes erwarten immer noch, dass diese Regeln für nicht übereinstimmende Argumente auch funktionieren.)
Mit dem Compiler können Sie jedoch alles an etwas übergeben, um eine Programmiersprache zu entwerfen. In den frühen Tagen funktionierte es gut, da die frühen C-Programmierer meistens Assistenten waren und wussten, dass sie den falschen Typ nicht an Funktionen weitergeben sollten, und selbst wenn sie die Typen falsch verstanden hatten, gab es immer lintsolche Tools , die eine eingehendere Überprüfung durchführen konnten von Ihrem C-Code und warnen Sie vor solchen Dingen.
Schneller Vorlauf bis heute, und wir sitzen nicht ganz im selben Boot. C ist erwachsen geworden, und viele Leute programmieren darin, die keine Zauberer sind, und um sie unterzubringen (und um alle anderen unterzubringen, die es lintsowieso regelmäßig benutzen ), haben die Compiler viele der Fähigkeiten übernommen, zu denen sie zuvor gehörten lint- insbesondere der Teil, in dem Ihr Code überprüft wird, um sicherzustellen, dass er typsicher ist. Frühe C-Compiler ließen Sie schreiben int foo = "hello";und wiesen den Zeiger nur munter auf die Ganzzahl zu, und es lag an Ihnen, sicherzustellen, dass Sie nichts Dummes taten. Moderne C-Compiler beschweren sich lautstark, wenn Sie Ihre Typen falsch verstehen, und das ist gut so.
Typ Konflikte
Was hat das alles mit dem mysteriösen Konfliktfehler in der Zeile der Funktionsdeklaration zu tun? Wie ich oben sagte, müssen C-Compiler immer noch wissen oder raten, was ein Name bedeutet, wenn sie diesen Namen zum ersten Mal sehen, wenn sie die Datei vorwärts scannen: Sie können wissen, was es bedeutet, wenn es sich um eine tatsächliche Funktionsdeklaration selbst handelt (oder um eine Funktion "Prototyp", mehr dazu in Kürze), aber wenn es nur ein Aufruf der Funktion ist, müssen sie raten . Und leider ist die Vermutung oft falsch.
Als der Compiler Ihren Aufruf von sah do_something(), sah er sich an, wie er aufgerufen wurde, und kam zu dem Schluss, dass do_something()dies schließlich wie folgt deklariert werden würde:
int do_something(char arg1[], char arg2[])
{
...
}
Warum kam es zu dem Schluss? Weil du es so genannt hast ! (Einige C - Compiler schließen kann , dass es int do_something(int arg1, int arg2)oder einfach int do_something(...), beide sind noch weiter von dem, was Sie wollen, aber der wichtige Punkt ist , dass unabhängig davon , wie der Compiler die Typen Vermutungen, es errät sie anders aus , was Ihre eigentliche Funktion verwendet. )
Später, wenn der Compiler in der Datei vorwärts scannt, sieht er Ihre tatsächliche Deklaration von char *do_something(char *, char *). Diese Funktionsdeklaration kommt nicht einmal der vom Compiler erratenen Deklaration nahe, was bedeutet, dass die Zeile, in der der Compiler den Aufruf kompiliert hat, falsch kompiliert wurde und das Programm einfach nicht funktioniert. Es wird also zu Recht ein Fehler ausgegeben, der Sie darüber informiert, dass Ihr Code nicht wie geschrieben funktioniert.
Sie fragen sich vielleicht: "Warum wird davon ausgegangen, dass ich eine zurückgebe int?" Nun, es wird dieser Typ angenommen, weil es keine gegenteiligen Informationen gibt: printf()Kann jeden Typ in seinen variablen Argumenten aufnehmen, ist also ohne eine bessere Antwort inteine ebenso gute Vermutung wie jede andere. (Viele frühe C-Compiler haben immer intfür jeden nicht spezifizierten Typ angenommen, und Sie haben ...für die Argumente für jede deklarierte Funktion gemeint f()- nicht void-, weshalb viele moderne Codestandards empfehlen, immer voiddie Argumente einzugeben, wenn es wirklich keine geben soll .)
Die Reparatur
Es gibt zwei allgemeine Korrekturen für den Funktionsdeklarationsfehler.
Die erste Lösung, die von vielen anderen Antworten hier empfohlen wird, besteht darin, einen Prototyp in den Quellcode über der Stelle zu setzen, an der die Funktion zum ersten Mal aufgerufen wird. Ein Prototyp sieht genauso aus wie die Deklaration der Funktion, hat jedoch ein Semikolon, in dem sich der Körper befinden sollte:
char *do_something(char *dest, const char *src);
Wenn der Prototyp an erster Stelle steht, weiß der Compiler dann , wie die Funktion letztendlich aussehen wird, sodass er nicht raten muss. Konventionell setzen Programmierer Prototypen häufig ganz oben in die Datei, direkt unter den #includeAnweisungen, um sicherzustellen, dass sie immer definiert werden, bevor sie möglicherweise verwendet werden.
Die andere Lösung, die auch in einem realen Code angezeigt wird, besteht darin, Ihre Funktionen einfach neu zu ordnen, sodass die Funktionsdeklarationen immer vor allem stehen, was sie aufruft! Sie könnten die gesamte char *do_something(char *dest, const char *src) { ... }Funktion über den ersten Aufruf verschieben, und der Compiler würde dann genau wissen, wie die Funktion aussieht, und müsste nicht raten.
In der Praxis verwenden die meisten Benutzer Funktionsprototypen, da Sie auch Funktionsprototypen in Header ( .h) -Dateien verschieben können, damit Code in anderen .cDateien diese Funktionen aufrufen kann. Beide Lösungen funktionieren jedoch, und viele Codebasen verwenden beide.
C99 und C11
Es ist nützlich zu beachten, dass sich die Regeln in den neueren Versionen des C-Standards geringfügig unterscheiden. In den früheren Versionen (C89 und K & R), der Compiler wirklich würden die Typen bei Funktionsaufruf Zeit erraten (und K & R-Ära Compiler oft würden Sie nicht einmal warnen , wenn sie falsch sind). C99 und C11 erfordern beide, dass die Funktionsdeklaration / der Prototyp vor dem ersten Aufruf steht, und es ist ein Fehler, wenn dies nicht der Fall ist. Viele moderne C-Compiler - hauptsächlich aus Gründen der Abwärtskompatibilität mit früherem Code - warnen jedoch nur vor einem fehlenden Prototyp und betrachten ihn nicht als Fehler.