Cette fonction C doit toujours retourner false, mais elle ne le fait pas

317

Je suis tombé sur une question intéressante dans un forum il y a longtemps et je veux connaître la réponse.

Considérez la fonction C suivante:

f1.c

#include <stdbool.h>

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

Cela devrait toujours revenir falsedepuis var3 == 3000. La mainfonction ressemble à ceci:

principal c

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

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

Puisque f1()devrait toujours revenir false, on s'attendrait à ce que le programme n'imprime qu'un seul faux à l'écran. Mais après l'avoir compilé et exécuté, exécuté est également affiché:

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

Pourquoi donc? Ce code a-t-il une sorte de comportement non défini?

Remarque: je l'ai compilé avec gcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2.

Dimitri Podborski
la source
9
D'autres ont mentionné que vous avez besoin d'un prototype car vos fonctions sont dans des fichiers séparés. Mais même si vous copiez f1()dans le même fichier que main(), vous obtiendrez une certaine bizarrerie: bien qu'il soit correct en C ++ à utiliser ()pour une liste de paramètres vide, en C qui est utilisé pour une fonction avec une liste de paramètres non encore définie ( il attend essentiellement une liste de paramètres de style K & R après le )). Pour être correct C, vous devez changer votre code en bool f1(void).
uliwitness
1
Le main()pourrait être simplifié en int main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; }- cela montrerait mieux l'écart.
Palec
@uliwitness Qu'en est-il de K&R 1st ed. (1978) quand il n'y en avait pas void?
Ho1
@uliwitness Il n'y en avait pas trueet falsedans K&R 1st ed., il n'y avait donc pas du tout de tels problèmes. C'était juste 0 et non nul pour vrai et faux. N'est-ce pas? Je ne sais pas si des prototypes étaient disponibles à l'époque.
Ho1
1
K&R 1st Edn a précédé les prototypes (et la norme C) de plus d'une décennie (1978 pour le livre contre 1989 pour la norme) - en effet, C ++ (C avec classes) était encore dans le futur lorsque K & R1 a été publié. De plus, avant C99, il n'y avait ni _Booltype ni en- <stdbool.h>tête.
Jonathan Leffler

Réponses:

396

Comme indiqué dans d'autres réponses, le problème est que vous utilisez gcc sans options de compilation définies. Si vous faites cela, il s'agit par défaut de ce qu'on appelle "gnu90", qui est une implémentation non standard de l'ancienne norme C90 retirée de 1990.

Dans l'ancienne norme C90, il y avait une faille majeure dans le langage C: si vous ne déclariez pas un prototype avant d'utiliser une fonction, il serait par défaut int func ()(où ( )signifie "accepter n'importe quel paramètre"). Cela modifie la convention d'appel de la fonction func, mais cela ne change pas la définition réelle de la fonction. Étant donné que la taille de boolet intsont différentes, votre code appelle un comportement non défini lorsque la fonction est appelée.

Ce comportement non-sens dangereux a été corrigé en 1999, avec la publication de la norme C99. Les déclarations de fonctions implicites ont été interdites.

Malheureusement, GCC jusqu'à la version 5.xx utilise toujours l'ancien standard C par défaut. Il n'y a probablement aucune raison pour que vous souhaitiez compiler votre code en tant que tout autre que le standard C. Donc, vous devez explicitement dire à GCC qu'il devrait compiler votre code en tant que code C moderne, au lieu d'une merde GNU non standard de plus de 25 ans .

Résolvez le problème en compilant toujours votre programme comme:

gcc -std=c11 -pedantic-errors -Wall -Wextra
  • -std=c11 lui dit de faire une tentative sans enthousiasme de compiler selon la norme C (actuelle) (informellement connue sous le nom de C11).
  • -pedantic-errors lui dit de faire de tout cœur ce qui précède, et de donner des erreurs de compilation lorsque vous écrivez un code incorrect qui viole la norme C.
  • -Wall signifie me donner quelques avertissements supplémentaires qui pourraient être utiles.
  • -Wextra signifie me donner quelques autres avertissements supplémentaires qui pourraient être utiles.
Lundin
la source
19
Cette réponse est globalement correcte, mais pour les programmes plus compliqués, il -std=gnu11est beaucoup plus probable de fonctionner comme prévu que -std=c11, en raison de tout ou partie: avoir besoin de fonctionnalités de bibliothèque au-delà de C11 (POSIX, X / Open, etc.) qui est disponible dans le "gnu" modes étendus mais supprimés dans le mode de stricte conformité; bogues dans les en-têtes du système qui sont cachés dans les modes étendus, par exemple en supposant la disponibilité de typedefs non standard; utilisation involontaire de trigraphes (cette anomalie standard est désactivée en mode "gnu").
zwol
5
Pour des raisons similaires, bien que j'encourage généralement l'utilisation de niveaux d'avertissement élevés, je ne peux pas prendre en charge l'utilisation des modes avertissements sont des erreurs. -pedantic-errorsest moins gênant que, -Werrormais les deux peuvent entraîner la compilation de programmes sur les systèmes d'exploitation qui ne sont pas inclus dans les tests de l'auteur d'origine, même en l'absence de problème réel.
zwol
7
@Lundin Au contraire, le deuxième problème que j'ai mentionné (les bogues dans les en-têtes du système qui sont exposés par des modes de conformité stricts) est omniprésent ; J'ai fait des tests approfondis et systématiques, et il n'y a pas de systèmes d'exploitation largement utilisés qui n'ont pas au moins un tel bug (il y a deux ans, de toute façon). Les programmes C qui ne nécessitent que la fonctionnalité de C11, sans autres ajouts, sont également l'exception plutôt que la règle dans mon expérience.
zwol
6
@joop Si vous utilisez C bool/ standard, _Boolvous pouvez écrire votre code C de manière "C ++ - esque", où vous supposez que toutes les comparaisons et les opérateurs logiques renvoient un boollike en C ++, même s'ils renvoient un inten C, pour des raisons historiques . Cela a le grand avantage que vous pouvez utiliser des outils d'analyse statique pour vérifier la sécurité de type de toutes ces expressions et exposer toutes sortes de bogues au moment de la compilation. C'est aussi un moyen d'exprimer l'intention sous la forme d'un code auto-documenté. Et moins important encore, il économise également quelques octets de RAM.
Lundin
7
Notez que la plupart des nouveautés de C99 provenaient de cette merde GNU de plus de 25 ans.
Shahbaz
141

Vous n'avez pas de prototype déclaré pour f1()dans main.c, il est donc implicitement défini comme int f1(), ce qui signifie que c'est une fonction qui prend un nombre inconnu d'arguments et renvoie unint .

Si intet boolsont de tailles différentes, cela entraînera un comportement indéfini . Par exemple, sur ma machine, intest de 4 octets et boolest d'un octet. Étant donné que la fonction est définie pour renvoyer bool, elle place un octet sur la pile lorsqu'elle revient. Cependant, puisqu'il est implicitement déclaré de revenirint partir de main.c, la fonction appelante essaiera de lire 4 octets de la pile.

Les options des compilateurs par défaut dans gcc ne vous diront pas que c'est le cas. Mais si vous compilez avec -Wall -Wextra, vous obtiendrez ceci:

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

Pour résoudre ce problème, ajoutez une déclaration pour f1dans main.c, avant main:

bool f1(void);

Notez que la liste d'arguments est explicitement définie sur void, ce qui indique au compilateur que la fonction ne prend aucun argument, par opposition à une liste de paramètres vide qui signifie un nombre inconnu d'arguments. La définition f1dans f1.c devrait également être modifiée pour refléter cela.

dbush
la source
2
Quelque chose que j'avais l'habitude de faire dans mes projets (quand j'utilisais encore GCC) a été ajoutée -Werror-implicit-function-declarationaux options de GCC, de cette façon celle-ci ne passe plus. Un choix encore meilleur consiste -Werrorà transformer tous les avertissements en erreurs. Vous oblige à corriger tous les avertissements lorsqu'ils apparaissent.
uliwitness
2
Vous ne devez pas non plus utiliser de parenthèses vides car cela est une fonction obsolète. Cela signifie qu'ils peuvent interdire ce code dans la prochaine version de la norme C.
Lundin
1
@uliwitness Ah. De bonnes informations pour ceux qui viennent de C ++ et qui ne se
plongent
La valeur de retour n'est généralement pas placée dans la pile, mais dans un registre. Voir la réponse d'Owen. En outre, vous ne mettez généralement jamais un octet dans la pile, mais un multiple de la taille du mot.
rsanchez
Les versions plus récentes de GCC (5.xx) donnent cet avertissement sans les drapeaux supplémentaires.
Overv
36

Je pense qu'il est intéressant de voir où la différence de taille mentionnée dans l'excellente réponse de Lundin se produit réellement.

Si vous compilez avec --save-temps, vous obtiendrez des fichiers d'assemblage que vous pourrez consulter. Voici la partie où f1()fait la == 0comparaison et renvoie sa valeur:

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

La partie de retour est sete %al. Dans les conventions d'appel x86 de C, les valeurs de retour de 4 octets ou moins (ce qui inclut intet bool) sont renvoyées via le registre %eax. %alest l'octet le plus bas de %eax. Ainsi, les 3 octets supérieurs de%eax sont laissés dans un état non contrôlé.

Maintenant dans main():

call    f1
testl   %eax, %eax
je  .L2

Cela vérifie si l' ensemble de%eax est nul, car il pense qu'il teste un int.

L'ajout d'une déclaration de fonction explicite change main()à:

call    f1
testb   %al, %al
je  .L2

c'est ce que nous voulons.

Owen
la source
27

Veuillez compiler avec une commande comme celle-ci:

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

Production:

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

Avec un tel message, vous devez savoir quoi faire pour le corriger.

Edit: Après avoir lu un commentaire (maintenant supprimé), j'ai essayé de compiler votre code sans les drapeaux. Eh bien, cela m'a conduit à des erreurs de l'éditeur de liens sans avertissements du compilateur au lieu d'erreurs du compilateur. Et ces erreurs de l'éditeur de liens sont plus difficiles à comprendre, donc même si ce -std-gnu99n'est pas nécessaire, essayez toujours d'utiliser au moins -Wall -Werrorcela vous fera économiser beaucoup de douleur dans le cul.

jdarthenay
la source