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 false
depuis var3 == 3000
. La main
fonction 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
.
f1()
dans le même fichier quemain()
, 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 enbool f1(void)
.main()
pourrait être simplifié enint main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; }
- cela montrerait mieux l'écart.void
?true
etfalse
dans 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._Bool
type ni en-<stdbool.h>
tête.Réponses:
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 fonctionfunc
, mais cela ne change pas la définition réelle de la fonction. Étant donné que la taille debool
etint
sont 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:
-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.la source
-std=gnu11
est 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").-pedantic-errors
est moins gênant que,-Werror
mais 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.bool
/ standard,_Bool
vous pouvez écrire votre code C de manière "C ++ - esque", où vous supposez que toutes les comparaisons et les opérateurs logiques renvoient unbool
like en C ++, même s'ils renvoient unint
en 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.Vous n'avez pas de prototype déclaré pour
f1()
dans main.c, il est donc implicitement défini commeint f1()
, ce qui signifie que c'est une fonction qui prend un nombre inconnu d'arguments et renvoie unint
.Si
int
etbool
sont de tailles différentes, cela entraînera un comportement indéfini . Par exemple, sur ma machine,int
est de 4 octets etbool
est d'un octet. Étant donné que la fonction est définie pour renvoyerbool
, 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:Pour résoudre ce problème, ajoutez une déclaration pour
f1
dans main.c, avantmain
: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éfinitionf1
dans f1.c devrait également être modifiée pour refléter cela.la source
-Werror-implicit-function-declaration
aux 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.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== 0
comparaison et renvoie sa valeur: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 inclutint
etbool
) sont renvoyées via le registre%eax
.%al
est 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()
: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()
à:c'est ce que nous voulons.
la source
Veuillez compiler avec une commande comme celle-ci:
Production:
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-gnu99
n'est pas nécessaire, essayez toujours d'utiliser au moins-Wall -Werror
cela vous fera économiser beaucoup de douleur dans le cul.la source