Questa funzione C dovrebbe sempre restituire false, ma non lo è


317

Molto tempo fa mi sono imbattuto in un'interessante domanda in un forum e voglio conoscere la risposta.

Considera la seguente funzione C:

f1.c

#include <stdbool.h>

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

Questo dovrebbe sempre tornare da falseallora var3 == 3000. La mainfunzione è simile alla seguente:

main.c

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

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

Dato che f1()dovrebbe sempre tornare false, ci si aspetterebbe che il programma stampi solo uno falso sullo schermo. Ma dopo averlo compilato ed eseguito, viene visualizzato anche eseguito :

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

Perché? Questo codice ha una sorta di comportamento indefinito?

Nota: l'ho compilato con gcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2.


9
Altri hanno detto che hai bisogno di un prototipo perché le tue funzioni sono in file separati. Ma anche se copiassi f1()nello stesso file di main(), otterrai qualche stranezza: mentre è corretto in C ++ da usare ()per un elenco di parametri vuoto, in C che viene utilizzato per una funzione con un elenco di parametri non ancora definito ( fondamentalmente si aspetta un elenco di parametri in stile K e R dopo il )). Per essere C corretto, dovresti cambiare il tuo codice in bool f1(void).
uliwitness

1
Il main()potrebbe essere semplificata a int main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; }- questo avrebbe mostrato la discrepanza meglio.
Palec,

@uliwitness Che dire di K&R 1st ed. (1978) quando non c'era void?
Ho1,

@uliwitness Non c'erano truee falsein K&R 1st ed., quindi non c'erano affatto problemi del genere. Era solo 0 e diverso da zero per vero e falso. No? Non so se i prototipi fossero disponibili in quel momento.
Ho1,

1
K&R 1st Edn ha preceduto i prototipi (e lo standard C) di oltre un decennio (1978 per il libro contro 1989 per lo standard) - in effetti, C ++ (C con Classi) era ancora in futuro quando K & R1 fu pubblicato. Inoltre, prima di C99, non vi era alcun _Booltipo e nessuna <stdbool.h>intestazione.
Jonathan Leffler,

Risposte:


396

Come notato in altre risposte, il problema è che usi gcc senza opzioni di compilatore impostate. Se lo fai, il valore predefinito è quello che viene chiamato "gnu90", che è un'implementazione non standard del vecchio standard C90 ritirato dal 1990.

Nel vecchio standard C90 c'era un grosso difetto nel linguaggio C: se non si dichiarasse un prototipo prima di usare una funzione, sarebbe predefinito int func ()(dove ( )significa "accetta qualsiasi parametro"). Ciò modifica la convenzione di chiamata della funzione func, ma non cambia la definizione effettiva della funzione. Poiché le dimensioni di boole intsono diverse, il codice richiama comportamenti indefiniti quando viene chiamata la funzione.

Questo pericoloso comportamento senza senso è stato risolto nel 1999, con il rilascio dello standard C99. Le dichiarazioni di funzioni implicite sono state vietate.

Sfortunatamente, GCC fino alla versione 5.xx utilizza ancora il vecchio standard C per impostazione predefinita. Probabilmente non c'è motivo per cui dovresti voler compilare il tuo codice come qualcosa di diverso dallo standard C. Quindi devi dire esplicitamente a GCC che dovrebbe compilare il tuo codice come codice C moderno, invece di qualche merda GNU non standard di oltre 25 anni .

Risolvi il problema compilando sempre il tuo programma come:

gcc -std=c11 -pedantic-errors -Wall -Wextra
  • -std=c11 gli dice di fare un tentativo sincero di compilare secondo lo standard (attuale) C (noto informalmente come C11).
  • -pedantic-errors gli dice di fare con tutto il cuore quanto sopra, e dare errori di compilatore quando si scrive un codice errato che viola lo standard C.
  • -Wall significa darmi alcuni avvertimenti extra che potrebbero essere utili.
  • -Wextra significa darmi altri avvertimenti extra che potrebbero essere utili.

19
Questa risposta è nel complesso corretta, ma per programmi più complicati -std=gnu11è molto più probabile che funzioni come previsto rispetto a -std=c11, a causa di una o tutte le: necessità di funzionalità di libreria oltre C11 (POSIX, X / Open, ecc.) Disponibile nel "gnu" modalità estese ma soppresse nella modalità di conformità rigorosa; bug nelle intestazioni di sistema che sono nascosti nelle modalità estese, ad esempio supponendo la disponibilità di typedef non standard; uso involontario di trigrafi (questa disfunzione standard è disabilitata in modalità "gnu").
zwol,

5
Per ragioni simili, mentre in genere incoraggio l'uso di livelli di avvertenze elevati, non posso supportare l'uso delle modalità avvertenze per errori. -pedantic-errorsè meno problematico di -Werrorma entrambi possono causare errori di compilazione dei programmi su sistemi operativi che non sono inclusi nei test dell'autore originale, anche se non c'è nessun problema reale.
zwol,

7
@Lundin Al contrario, il secondo problema che ho citato (bug nelle intestazioni del sistema che sono esposti da rigorose modalità di conformità) è onnipresente ; Ho fatto test approfonditi, sistematico, e non ci sono nessun ampiamente utilizzati sistemi operativi che non hanno almeno uno di questi bug (come di due anni fa, in ogni caso). I programmi C che richiedono solo la funzionalità di C11, senza ulteriori aggiunte, sono anche l'eccezione piuttosto che la regola nella mia esperienza.
zwol,

6
@joop Se usi C bool/ standard _Boolpuoi scrivere il tuo codice C in un modo "C ++ - esque", dove supponi che tutti i confronti e gli operatori logici restituiscano un boollike in C ++, anche se restituiscono un intin C, per ragioni storiche . Questo ha il grande vantaggio di poter usare strumenti di analisi statica per verificare la sicurezza dei tipi di tutte queste espressioni ed esporre tutti i tipi di bug in fase di compilazione. È anche un modo per esprimere l'intento sotto forma di codice auto-documentante. E, soprattutto, consente di risparmiare alcuni byte di RAM.
Lundin,

7
Nota che la maggior parte delle nuove cose in C99 proviene da quella merda GNU di oltre 25 anni.
Shahbaz,

141

Non hai un prototipo dichiarato f1()in main.c, quindi è implicitamente definito come int f1(), il che significa che è una funzione che accetta un numero sconosciuto di argomenti e restituisce un int.

Se inte boolsono di dimensioni diverse, ciò comporterà un comportamento indefinito . Ad esempio, sulla mia macchina, intè di 4 byte ed boolè di un byte. Poiché la funzione è definita per il ritorno bool, inserisce un byte nello stack quando ritorna. Tuttavia, poiché viene implicitamente dichiarato di tornare intda main.c, la funzione chiamante tenterà di leggere 4 byte dallo stack.

Le opzioni predefinite dei compilatori in gcc non ti diranno che lo sta facendo. Ma se compili con -Wall -Wextra, otterrai questo:

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

Per risolvere questo problema, aggiungi una dichiarazione per f1in main.c, prima di main:

bool f1(void);

Si noti che l'elenco degli argomenti è esplicitamente impostato su void, il che indica al compilatore che la funzione non accetta argomenti, al contrario di un elenco di parametri vuoto che significa un numero sconosciuto di argomenti. Anche la definizione f1in f1.c dovrebbe essere modificata per riflettere questo.


2
Qualcosa che facevo nei miei progetti (quando usavo ancora GCC) era l'aggiunta -Werror-implicit-function-declarationdelle opzioni di GCC, in questo modo questo non scivola più. Una scelta ancora migliore è -Werrortrasformare tutti gli avvisi in errori. Ti obbliga a correggere tutti gli avvisi quando vengono visualizzati.
uliwitness

2
Inoltre, non dovresti usare parentesi vuote perché farlo è una funzione obsoleta. Ciò significa che possono vietare tale codice nella prossima versione dello standard C.
Lundin,

1
@uliwitness Ah. Buone informazioni per chi proviene da C ++ che si dilettano solo in C.
SeldomNeedy

Il valore restituito di solito non viene inserito nello stack, ma in un registro. Vedi la risposta di Owen. Inoltre, di solito non si inserisce mai un byte nello stack, ma un multiplo della dimensione della parola.
rsanchez,

Le versioni più recenti di GCC (5.xx) danno quell'avvertimento senza i flag extra.
Overv

36

Penso che sia interessante vedere dove avvenga effettivamente la discrepanza dimensionale menzionata nell'eccellente risposta di Lundin.

Se si compila con --save-temps, si otterranno i file di assieme che è possibile guardare. Ecco la parte in cui f1()fa il == 0confronto e restituisce il suo valore:

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

La parte di ritorno è sete %al. Nelle convenzioni di chiamata x86 di C, restituisci valori di 4 byte o inferiori (che includeint e bool) vengono restituiti tramite il registro %eax. %alè il byte più basso di %eax. Quindi, i 3 byte superiori di%eax vengono lasciati in uno stato non controllato.

Ora in main():

call    f1
testl   %eax, %eax
je  .L2

Questo controlla se il complesso di%eax è pari a zero, perché si pensa che sta mettendo alla prova un int.

Aggiunta di modifiche esplicite a una dichiarazione di funzione main() in:

call    f1
testb   %al, %al
je  .L2

che è quello che vogliamo.


27

Compilare con un comando come questo:

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

Produzione:

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

Con un messaggio del genere, dovresti sapere cosa fare per correggerlo.

Modifica: dopo aver letto un commento (ora eliminato), ho provato a compilare il codice senza i flag. Bene, questo mi ha portato a errori del linker senza avvisi del compilatore anziché errori del compilatore. E quegli errori del linker sono più difficili da capire, quindi anche se -std-gnu99non è necessario, prova ad usare sempre almeno -Wall -Werrorti farà risparmiare molto dolore nel culo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.