Diese C-Funktion sollte immer false zurückgeben, tut es aber nicht

Lesezeit: 9 Minuten

Benutzeravatar von Dimitri Podborski
Dimitri Podborski

Ich bin vor langer Zeit über eine interessante Frage in einem Forum 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 immer zurückkehren false seit var3 == 3000. Das main Funktion sieht so 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;
}

Seit f1() sollte immer wiederkommen falsewürde man erwarten, dass das Programm nur eine ausgibt FALSCH zum Bildschirm. Aber nach dem Kompilieren und Ausführen hingerichtet wird auch angezeigt:

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

Warum ist das so? Hat dieser Code eine Art undefiniertes Verhalten?

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

  • Andere haben erwähnt, dass Sie einen Prototyp benötigen, da sich Ihre Funktionen in separaten Dateien befinden. Aber auch wenn Sie kopiert haben f1() in dieselbe Datei wie main()würden Sie eine gewisse Verrücktheit bekommen: Während es in C++ korrekt ist, es zu verwenden () für eine leere Parameterliste, in C, das für eine Funktion mit einer noch nicht definierten Parameterliste verwendet wird (es erwartet grundsätzlich eine Parameterliste im K&R-Stil nach der )). Um korrekt C zu sein, sollten Sie Ihren Code in ändern bool f1(void).

    – Ulizeuge

    8. April 2016 um 12:21 Uhr


  • Das main() vereinfacht werden könnte int main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; } – dies würde die Diskrepanz besser zeigen.

    – Paläc

    10. April 2016 um 12:05 Uhr

  • @uliwitness Was ist mit K&R 1st ed. (1978), als es keine gab void?

    – Ho1

    11. April 2016 um 16:39 Uhr


  • @uliwitness Es gab keine true und false in K&R 1st ed., also gab es solche Probleme überhaupt nicht. Es war nur 0 und ungleich Null für wahr und falsch. Ist es nicht? Ich weiß nicht, ob es damals Prototypen gab.

    – Ho1

    11. April 2016 um 16:45 Uhr


  • 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 lag C++ (C mit Klassen) noch in der Zukunft, als K&R1 veröffentlicht wurde. Auch vor C99 gab es keine _Bool Typ und Nr <stdbool.h> Header.

    – Jonathan Leffler

    13. April 2016 um 6:50 Uhr

Benutzeravatar von Lundin
Lundin

Wie in anderen Antworten erwähnt, besteht das Problem darin, dass Sie verwenden gcc ohne Compiler-Optionen gesetzt. Wenn Sie dies tun, wird standardmäßig das sogenannte “gnu90” verwendet, das eine nicht standardmäßige Implementierung des alten, zurückgezogenen C90-Standards von 1990 ist.

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, würde dies standardmäßig der Fall sein int func () (wo ( ) bedeutet “jeden Parameter akzeptieren”). Dadurch ändert sich die Aufrufkonvention der Funktion func, ändert aber nicht die eigentliche Funktionsdefinition. Da die Größe von bool und int unterschiedlich sind, ruft Ihr Code undefiniertes Verhalten auf, wenn die Funktion aufgerufen wird.

Dieses gefährliche Unsinnsverhalten 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 noch den alten C-Standard. Es gibt wahrscheinlich keinen Grund, warum Sie Ihren Code als etwas anderes als Standard-C kompilieren sollten. Sie müssen GCC also explizit mitteilen, dass es Ihren Code als modernen C-Code kompilieren soll, anstatt als über 25 Jahre alten, nicht standardmäßigen 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, gemäß dem (aktuellen) C-Standard (umgangssprachlich als C11 bekannt) zu kompilieren.
  • -pedantic-errors weist es an, das Obige von ganzem Herzen zu tun und Compilerfehler zu geben, wenn Sie falschen Code schreiben, der gegen den C-Standard verstößt.
  • -Wall bedeutet, mir einige zusätzliche Warnungen zu geben, die gut sein könnten.
  • -Wextra bedeutet, mir einige andere zusätzliche Warnungen zu geben, die gut sein könnten.

  • Diese Antwort ist insgesamt richtig, jedoch für kompliziertere Programme -std=gnu11 viel wahrscheinlicher als erwartet funktioniert -std=c11, aufgrund einiger oder aller der folgenden: Bibliotheksfunktionalität jenseits von C11 (POSIX, X/Open usw.) erforderlich, die in den erweiterten Modi “gnu” verfügbar ist, aber im strikten Konformitätsmodus unterdrückt wird; Fehler in System-Headern, die in den erweiterten Modi versteckt 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).

    – zol

    7. April 2016 um 14:51 Uhr


  • Aus ähnlichen Gründen empfehle ich zwar im Allgemeinen die Verwendung hoher Warnstufen, kann aber die Verwendung von Warnungen-sind-Fehler-Modi nicht unterstützen. -pedantic-errors ist weniger lästig als -Werror aber beides kann und kann dazu führen, dass Programme auf Betriebssystemen, die nicht in den Tests des ursprünglichen Autors enthalten sind, nicht kompiliert werden, selbst wenn kein tatsächliches Problem vorliegt.

    – zol

    7. April 2016 um 14:55 Uhr

  • @Lundin Im Gegensatz dazu ist das zweite Problem, das ich erwähnt habe (Fehler in den Systemheadern, die durch strenge Konformitätsmodi aufgedeckt werden). allgegenwärtig; Ich habe umfangreiche, systematische Tests durchgeführt, und es gibt sie nein weit verbreitete 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 ebenfalls eher die Ausnahme als die Regel.

    – zol

    7. April 2016 um 15:10 Uhr


  • @joop Wenn Sie Standard-C verwenden bool/_Bool dann können Sie Ihren C-Code auf “C++-artige” Weise schreiben, wobei Sie davon ausgehen, dass alle Vergleiche und logischen Operatoren a zurückgeben bool wie in C++, obwohl sie ein zurückgeben int in C, aus historischen Gründen. Dies hat den großen Vorteil, dass Sie statische Analysewerkzeuge verwenden können, um die Typsicherheit all dieser Ausdrücke zu überprüfen und alle Arten von Fehlern zur Kompilierzeit aufzudecken. Es ist auch eine Möglichkeit, Absichten in Form von selbstdokumentierendem Code auszudrücken. Und weniger wichtig, es spart auch ein paar Bytes RAM.

    – Ludin

    8. April 2016 um 13:47 Uhr


  • Beachten Sie, dass die meisten neuen Sachen in C99 von diesem über 25 Jahre alten GNU-Mist stammten.

    – Shahbaz

    10. April 2016 um 16:04 Uhr

Benutzeravatar von dbush
dbusch

Sie haben keinen Prototyp deklariert für f1() in main.c, also ist es implizit definiert als int f1()was bedeutet, dass es sich um eine Funktion handelt, die eine unbekannte Anzahl von Argumenten akzeptiert und ein zurückgibt int.

Wenn int und bool unterschiedlich groß sind, wird dies zur Folge haben undefiniertes Verhalten. Auf meiner Maschine z. int ist 4 Byte und bool ist ein Byte. Da ist die Funktion definiert zurückgeben bool, legt es ein Byte auf den Stapel, wenn es zurückkehrt. Da es jedoch implizit deklariert zurückgeben int von main.c versucht die aufrufende Funktion, 4 Bytes aus dem Stack zu lesen.

Die standardmäßigen Compileroptionen in gcc sagen Ihnen nicht, dass dies der Fall ist. Aber wenn Sie mit kompilieren -Wall -Wextradas bekommst du:

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 hinzu f1 in main.c, vorher main:

bool f1(void);

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

  • Etwas, das ich in meinen Projekten gemacht habe (als ich noch GCC benutzte), war das Hinzufügen -Werror-implicit-function-declaration zu den Optionen von GCC, damit diese nicht mehr vorbeirutscht. Eine noch bessere Wahl ist -Werror um alle Warnungen in Fehler umzuwandeln. Zwingt Sie, alle Warnungen zu beheben, wenn sie angezeigt werden.

    – Ulizeuge

    8. April 2016 um 12:41 Uhr


  • Sie sollten auch keine leeren Klammern verwenden, da dies eine veraltete Funktion ist. Das heißt, sie können solchen Code in der nächsten Version des C-Standards verbieten.

    – Ludin

    8. April 2016 um 13:52 Uhr

  • @uliwitness Ah. Gute Informationen für diejenigen, die von C++ kommen und sich nur in C versuchen.

    – Selten ‘Wo ist Monica’ Needy

    8. April 2016 um 18:00 Uhr

  • Der Rückgabewert wird normalerweise nicht auf den Stack, sondern in ein Register gelegt. Siehe Owens Antwort. Außerdem legt man in der Regel nie ein Byte in den Stack, sondern ein Vielfaches der Wortgröße.

    – rsanchez

    9. April 2016 um 22:00 Uhr

  • Neuere Versionen von GCC (5.xx) geben diese Warnung ohne die zusätzlichen Flags aus.

    – Übersicht

    12. April 2016 um 19:20 Uhr

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

Wenn Sie mit kompilieren --save-temps, erhalten Sie Assembly-Dateien, die Sie sich ansehen können. Hier ist der Teil, wo f1() tut das == 0 Vergleich und gibt seinen Wert zurück:

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

Der zurückkehrende Teil ist sete %al. Geben Sie in den x86-Aufrufkonventionen von C Werte zurück, die 4 Byte oder kleiner sind (einschließlich int und bool) werden über Register zurückgegeben %eax. %al ist das niedrigste Byte von %eax. Also die oberen 3 Bytes von %eax bleiben in einem unkontrollierten Zustand.

Jetzt in main():

call    f1
testl   %eax, %eax
je  .L2

Diese prüft, ob die ganz von %eax ist null, weil es denkt, dass es ein int testet.

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

call    f1
testb   %al, %al
je  .L2

das wollen wir.

Benutzeravatar von jdarthenay
jdarthenay

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

Bei einer solchen Meldung sollten Sie wissen, was zu tun ist, um sie zu korrigieren.

Bearbeiten: Nachdem ich einen (jetzt gelöschten) Kommentar gelesen habe, habe ich versucht, Ihren Code ohne die Flags zu kompilieren. Nun, das führte zu Linker-Fehlern ohne Compiler-Warnungen anstelle von Compiler-Fehlern. Und diese Linker-Fehler sind schwieriger zu verstehen, also selbst wenn -std-gnu99 ist nicht notwendig, versuchen Sie es zumindest immer -Wall -Werror das erspart dir viel Ärger.

1427290cookie-checkDiese C-Funktion sollte immer false zurückgeben, tut es aber nicht

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy