In den ersten Tagen der C, wenn Methoden wie sah:
int add(a,b)
int a;
int b;
{
return a+b;
}
gegeben int x;
eine Aussage add(3,5);
würde erzeugen Code wie:
push 5
push 3
call _add
store r0,_x
Der Compiler nicht brauchen würde nichts wissen über die add
Methode über den Rückgabetyp (die es als int
erraten könnte), um den obigen Code zu generieren; Wenn die Methode sich herausstellt, wird sie zur Zeit der Verbindung entdeckt. Code, der mehr Parameter als eine erwartete Routine lieferte, würde diese Werte auf den Stapel schieben, wo sie ignoriert würden, bis der Code sie einige Zeit später platzte.
Probleme würden auftreten, wenn der Code zu wenige Parameter, Parameter der falschen Typen oder beides übergibt. Wenn der Code weniger Parameter übergab als eine Routine erwartet hatte, hatte dies oft keine Auswirkung, wenn der aufgerufene Code nicht die späteren Parameter verwendete.Wenn Code gelesen Parameter, die nicht übergeben wurden, würden sie in der Regel "Müll" Werte lesen (obwohl es keine Garantie gibt, dass der Versuch, diese Variablen lesen würde keine anderen Nebenwirkungen haben). Auf vielen Plattformen wäre der erste nicht übergebene Parameter die Rückgabeadresse, wenn auch nicht immer. Wenn der Code in Parameter geschrieben wurde, die nicht übergeben wurden, würde das den Return-Stack in der Regel durcheinander bringen und zu merkwürdigem Verhalten führen.
Die Übergabe von inkorrekten Parametern würde häufig dazu führen, dass die aufgerufene Funktion an ihren falschen Stellen nach dem falschen Ort sucht. Wenn die übergebenen Typen kleiner als erwartet waren, konnte dies dazu führen, dass die aufgerufene Methode auf Stack-Variablen zugreift, die ihr nicht gehören (wie im Fall der Übergabe von zu wenigen Parametern).
ANSI C standardisierte die Verwendung von Funktionsprototypen; Wenn der Compiler eine Definition oder Deklaration wie int add(int a, int b)
sieht, bevor er einen Aufruf an add
sieht, dann stellt er sicher, dass er zwei Argumente vom Typ int
übergibt, auch wenn der Aufrufer etwas anderes liefert (zB wenn der Aufrufer eine double
übergibt, wird sein Wert gezwungen werden, vor dem Anruf int
einzugeben). Ein Versuch, zu wenig Parameter an eine Methode zu übergeben, die der Compiler gesehen hat, wird zu einem Fehler führen.
Im obigen Code wird der Aufruf an add
vorgenommen, bevor der Compiler irgendeine Ahnung hat, was die Methode erwartet. Während neuere Dialekte eine solche Verwendung nicht zulassen würden, würde der Compiler standardmäßig davon ausgehen, dass add
eine Methode ist, die Argumente übernimmt, die der Aufrufer liefert, und int
zurückgibt. In dem obigen Fall würde der aufgerufene Code wahrscheinlich erwarten, dass zwei Wörter auf den Stapel geschoben werden, aber die Konstante double
würde wahrscheinlich als zwei oder vier Wörter gedrückt werden. Somit würde das Verfahren wahrscheinlich für a
und b
zwei Wörter aus der binären Darstellung des double
Wertes 15.3 erhalten.
BTW, selbst Compiler, die die angegebene Syntax akzeptieren, quieken fast immer, wenn eine Funktion, deren Typ implizit angenommen wurde, als etwas zurückgegeben wird, das nicht int
ist; Viele werden auch quieken, wenn eine solche Funktion mit etwas anderem als der alten Syntax definiert wird. Während die ersten C-Compiler immer Funktionsargumente auf den Stack geschoben haben, erwarten neuere sie normalerweise in Registern. wenn man also
/* Old-style declaration for function that accepts whatever it's given
and returns double */
double add();
dann der Code erklären add(12.3,4.56)
zwei double
Werte auf den Stapel schieben würde und rufen Sie die add
Funktion, aber wenn man stattdessen erklärt hatte:
double add(double a, double b);
dann auf vielen Systemen Der Code add(12.3,4.56)
würde vor dem Aufruf das Gleitkommaregister 0 mit 12.3
und das Gleitkommaregister 1 mit 4.56
laden (nichts schieben). Folglich sind die beiden Deklarationstypen nicht kompatibel. Wenn Sie auf solchen Systemen versuchen, eine Methode aufzurufen, ohne sie zu deklarieren, und sie anschließend in der gleichen Kompilierungseinheit unter Verwendung der Syntax im neuen Stil deklarieren, wird ein Kompilierungsfehler verursacht. Ferner geben einige dieser Systeme den Namen von Funktionen, die Registerargumente mit zwei und nicht mit einem Unterstrich belegen, die Präfixe an, wenn die Methode mit einer neuen Stilsyntax definiert wurde, aber aufgerufen wurde, ohne dass der Compiler einen Prototyp gesehen hat würde versuchen, aufzurufen, obwohl die Funktion __add
genannt wurde. Dies würde den Linker dazu veranlassen, das Programm abzulehnen, anstatt eine ausführbare Datei zu erzeugen, die nicht funktionieren würde.
Es ist undefiniertes Verhalten, Warnungen verwenden, um diese Art von Fehler zu vermeiden. –
IIRC, vor der Einführung der varargs-Syntax, würden Leute varargs fälschen, indem sie eine Reihe von Parametern deklarieren und dann einige nicht übergeben. Es war nie ein definiertes Verhalten, aber die Leute taten es, und später mussten die Compiler-Versionen den früheren Code unterstützen. – user2357112
@ user2357112: Sind Sie sicher, dass das Verhalten in den Tagen, als Parameter zwischen der schließenden und der öffnenden Klammer aufgelistet wurden, nicht definiert wurde? Ich dachte, ich hätte in (1980s) Unix einige Funktionen gesehen, die den ersten Parameter benutzten, um zu entscheiden, ob man spätere Parameter untersuchen sollte, deren spätere Parameter manchmal weggelassen wurden, wenn sie nicht benutzt wurden. – supercat