Diese C-Funktion sollte immer false zurückgeben, tut dies jedoch nicht

316

Ich bin vor langer Zeit in einem Forum über eine interessante Frage gestolpert und möchte die Antwort wissen.

Betrachten Sie die folgende C-Funktion:

f1.c

#include <stdbool.h>

bool f1()
{
    int var1 = 1000;
    int var2 = 2000;
    int var3 = var1 + var2;
    return (var3 == 0) ? true : false;
}

Dies sollte falseseitdem immer wieder zurückkehren var3 == 3000. Die mainFunktion sieht folgendermaßen aus:

Haupt c

#include <stdio.h>
#include <stdbool.h>

int main()
{
    printf( f1() == true ? "true\n" : "false\n");
    if( f1() )
    {
        printf("executed\n");
    }
    return 0;
}

Da f1()sollte immer zurückkehren false, würde man erwarten, dass das Programm nur eine falsche auf dem Bildschirm druckt . Aber nach dem Kompilieren und Ausführen es, ausgeführt auch angezeigt wird:

$ gcc main.c f1.c -o test
$ ./test
false
executed

Warum ist das so? Hat dieser Code ein undefiniertes Verhalten?

Hinweis: Ich habe es mit kompiliert gcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2.

Dimitri Podborski
quelle
9
Andere haben erwähnt, dass Sie einen Prototyp benötigen, da sich Ihre Funktionen in separaten Dateien befinden. Aber selbst wenn Sie f1()in dieselbe Datei wie kopieren main(), werden Sie etwas seltsam: Während es in C ++ korrekt ist, ()eine leere Parameterliste zu verwenden, wird sie in C für eine Funktion mit einer noch nicht definierten Parameterliste verwendet ( Grundsätzlich wird nach dem )) eine Parameterliste im K & R-Stil erwartet . Um korrekt C zu sein, sollten Sie Ihren Code in ändern bool f1(void).
uliwitness
1
Das main()könnte vereinfacht werden int main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; }- dies würde die Diskrepanz besser zeigen.
Palec
@uliwitness Was ist mit K & R 1st ed. (1978) als es keine gab void?
Ho1
@uliwitness Es gab keine trueund falsein K & R 1st ed., also gab es überhaupt keine solchen Probleme. Es war nur 0 und nicht Null für wahr und falsch. Ist es nicht? Ich weiß nicht, ob zu diesem Zeitpunkt Prototypen verfügbar waren.
Ho1
1
K & R 1st Edn ging Prototypen (und dem C-Standard) um mehr als ein Jahrzehnt voraus (1978 für das Buch gegenüber 1989 für den Standard) - tatsächlich war C ++ (C mit Klassen) noch in der Zukunft, als K & R1 veröffentlicht wurde. Außerdem gab es vor C99 keinen _BoolTyp und keinen <stdbool.h>Header.
Jonathan Leffler

Antworten:

396

Wie in anderen Antworten erwähnt, besteht das Problem darin, dass Sie gcckeine Compiler-Optionen festlegen. Wenn Sie dies tun, wird standardmäßig "gnu90" verwendet, eine nicht standardmäßige Implementierung des alten, zurückgezogenen C90-Standards aus dem Jahr 1990.

Im alten C90-Standard gab es einen großen Fehler in der C-Sprache: Wenn Sie vor der Verwendung einer Funktion keinen Prototyp deklariert haben, wird standardmäßig int func ()(wobei ( )"Parameter akzeptieren" bedeutet) verwendet. Dies ändert die Aufrufkonvention der Funktion func, aber nicht die tatsächliche Funktionsdefinition. Da die Größe von boolund intunterschiedlich sind, ruft Ihr Code beim Aufruf der Funktion ein undefiniertes Verhalten auf.

Dieses gefährliche Unsinnverhalten wurde im Jahr 1999 mit der Veröffentlichung des C99-Standards behoben. Implizite Funktionsdeklarationen wurden verboten.

Leider verwendet GCC bis Version 5.xx standardmäßig immer noch den alten C-Standard. Es gibt wahrscheinlich keinen Grund, warum Sie Ihren Code als etwas anderes als Standard C kompilieren möchten. Sie müssen GCC daher ausdrücklich mitteilen, dass Ihr Code als moderner C-Code kompiliert werden soll, anstatt als über 25 Jahre alter, nicht standardmäßiger GNU-Mist .

Beheben Sie das Problem, indem Sie Ihr Programm immer wie folgt kompilieren:

gcc -std=c11 -pedantic-errors -Wall -Wextra
  • -std=c11 weist es an, einen halbherzigen Versuch zu unternehmen, nach dem (aktuellen) C-Standard (informell bekannt als C11) zu kompilieren.
  • -pedantic-errors fordert es auf, das oben Gesagte von ganzem Herzen zu tun und Compilerfehler zu geben, wenn Sie falschen Code schreiben, der gegen den C-Standard verstößt.
  • -Wall bedeutet, geben Sie mir einige zusätzliche Warnungen, die gut zu haben sein könnten.
  • -Wextra bedeutet, geben Sie mir einige andere zusätzliche Warnungen, die gut zu haben sein könnten.
Lundin
quelle
19
Diese Antwort ist insgesamt richtig, aber für kompliziertere Programme -std=gnu11ist es viel wahrscheinlicher, dass sie wie erwartet funktionieren, als -std=c11aufgrund einiger oder aller der folgenden Funktionen: Sie benötigen Bibliotheksfunktionen über C11 hinaus (POSIX, X / Open usw.), die im "gnu" verfügbar sind. erweiterte Modi, jedoch im strengen Konformitätsmodus unterdrückt; Fehler in Systemheadern, die in den erweiterten Modi ausgeblendet sind, z. B. unter der Annahme, dass nicht standardmäßige Typedefs verfügbar sind; unbeabsichtigte Verwendung von Trigraphen (diese Standardfehlfunktion ist im "gnu" -Modus deaktiviert).
zwol
5
Aus ähnlichen Gründen kann ich die Verwendung hoher Warnmodi nicht unterstützen, obwohl ich generell die Verwendung hoher Warnstufen empfehle. -pedantic-errorsist weniger problematisch als, -Werroraber beide können und können dazu führen, dass Programme auf Betriebssystemen, die nicht in den Tests des ursprünglichen Autors enthalten sind, nicht kompiliert werden können, selbst wenn kein tatsächliches Problem vorliegt.
zwol
7
@Lundin Im Gegenteil, das zweite Problem, das ich erwähnt habe (Fehler in den Systemheadern, die durch strenge Konformitätsmodi aufgedeckt werden), ist allgegenwärtig . Ich habe umfangreiche, systematische Tests durchgeführt, und es gibt keine weit verbreiteten Betriebssysteme, die nicht mindestens einen solchen Fehler aufweisen (jedenfalls seit zwei Jahren). C-Programme, die nur die Funktionalität von C11 ohne weitere Ergänzungen benötigen, sind meiner Erfahrung nach eher die Ausnahme als die Regel.
zwol
6
@joop Wenn Sie Standard-C bool/ verwenden _Bool, können Sie Ihren C-Code "C ++ - esque" schreiben, wobei Sie davon ausgehen, dass alle Vergleiche und logischen Operatoren aus historischen Gründen ein " Gefällt mir " boolin C ++ zurückgeben, obwohl sie ein " intIn C" zurückgeben . Dies hat den großen Vorteil, dass Sie statische Analysewerkzeuge verwenden können, um die Typensicherheit aller dieser Ausdrücke zu überprüfen und alle Arten von Fehlern beim Kompilieren aufzudecken. Es ist auch eine Möglichkeit, Absichten in Form von selbstdokumentierendem Code auszudrücken. Und was noch wichtiger ist, es spart auch ein paar Bytes RAM.
Lundin
7
Beachten Sie, dass die meisten neuen Sachen in C99 von diesem über 25-jährigen GNU-Mist stammen.
Shahbaz
141

f1()In main.c ist kein Prototyp deklariert , daher ist er implizit definiert als int f1(), dh es handelt sich um eine Funktion, die eine unbekannte Anzahl von Argumenten akzeptiert und eine zurückgibt int.

Wenn intund boolunterschiedlich groß sind, führt dies zu undefiniertem Verhalten . Auf meinem Computer sind intes beispielsweise 4 Bytes und boolein Byte. Da die Funktion für die Rückgabe definiert istbool , legt sie bei der Rückgabe ein Byte auf den Stapel. Da es jedoch implizit deklariert ist , intvon main.c zurückzukehren, versucht die aufrufende Funktion, 4 Bytes vom Stapel zu lesen.

Die Standard-Compileroptionen in gcc sagen Ihnen nicht, dass dies der Fall ist. Wenn Sie jedoch mit kompilieren -Wall -Wextra, erhalten Sie Folgendes:

main.c: In function ‘main’:
main.c:6: warning: implicit declaration of function ‘f1’

Um dies zu beheben, fügen Sie eine Deklaration für f1in main.c hinzu, bevor main:

bool f1(void);

Beachten Sie, dass die Argumentliste explizit auf gesetzt ist void, was dem Compiler mitteilt, dass die Funktion keine Argumente akzeptiert, im Gegensatz zu einer leeren Parameterliste, die eine unbekannte Anzahl von Argumenten bedeutet. Die Definition f1in f1.c sollte ebenfalls geändert werden, um dies widerzuspiegeln.

dbush
quelle
2
Etwas, was ich in meinen Projekten getan habe (als ich noch GCC verwendet habe), war -Werror-implicit-function-declaration, die Optionen von GCC zu erweitern, so dass diese nicht mehr vorbei gleiten. Eine noch bessere Wahl ist es -Werror, alle Warnungen in Fehler umzuwandeln. Zwingt Sie, alle Warnungen zu korrigieren, wenn sie angezeigt werden.
uliwitness
2
Sie sollten auch keine leere Klammer verwenden, da dies eine veraltete Funktion ist. Das heißt, sie können solchen Code in der nächsten Version des C-Standards verbieten.
Lundin
1
@uliwitness Ah. Gute Informationen für diejenigen, die aus C ++ kommen und sich nur in C.
versuchen
Der Rückgabewert wird normalerweise nicht in den Stapel, sondern in ein Register gestellt. Siehe Owens Antwort. Außerdem wird normalerweise nie ein Byte in den Stapel eingefügt, sondern ein Vielfaches der Wortgröße.
Rsanchez
Neuere Versionen von GCC (5.xx) geben diese Warnung ohne die zusätzlichen Flags aus.
Overv
36

Ich finde es interessant zu sehen, wo die in Lundins hervorragender Antwort erwähnte Größeninkongruenz tatsächlich auftritt.

Wenn Sie mit kompilieren --save-temps, erhalten Sie Assembly-Dateien, die Sie anzeigen können. Hier ist der Teil, in dem f1()der == 0Vergleich durchgeführt und sein Wert zurückgegeben wird:

cmpl    $0, -4(%rbp)
sete    %al

Der zurückkehrende Teil ist sete %al. In den x86-Aufrufkonventionen von C werden Rückgabewerte von 4 Byte oder weniger (einschließlich intund bool) über das Register zurückgegeben %eax. %alist das niedrigste Byte von %eax. Die oberen 3 Bytes von %eaxbleiben also in einem unkontrollierten Zustand.

Jetzt in main():

call    f1
testl   %eax, %eax
je  .L2

Diese prüft , ob die ganze von %eaxNull ist , weil sie es testet einen int denkt.

Das Hinzufügen einer expliziten Funktionsdeklaration ändert sich main()zu:

call    f1
testb   %al, %al
je  .L2

Welches ist, was wir wollen.

Owen
quelle
27

Bitte kompilieren Sie mit einem Befehl wie diesem:

gcc -Wall -Wextra -Werror -std=gnu99 -o main.exe main.c

Ausgabe:

main.c: In function 'main':
main.c:14:5: error: implicit declaration of function 'f1' [-Werror=impl
icit-function-declaration]
     printf( f1() == true ? "true\n" : "false\n");
     ^
cc1.exe: all warnings being treated as errors

Mit einer solchen Meldung sollten Sie wissen, wie Sie sie korrigieren können.

Bearbeiten: Nachdem ich einen (jetzt gelöschten) Kommentar gelesen hatte, versuchte ich, Ihren Code ohne die Flags zu kompilieren. Nun, dies führte mich zu Linkerfehlern ohne Compilerwarnungen anstelle von Compilerfehlern. Und diese Linker-Fehler sind schwieriger zu verstehen. Selbst wenn dies -std-gnu99nicht erforderlich ist, versuchen Sie bitte, sie immer zu verwenden -Wall -Werror, da dies Ihnen viel Schmerz im Arsch erspart .

jdarthenay
quelle