Je suis tombé sur une question intéressante sur un forum il y a longtemps et je veux connaître la réponse.
Considérons la fonction C suivante:
#include <stdbool.h>
bool f1()
{
int var1 = 1000;
int var2 = 2000;
int var3 = var1 + var2;
return (var3 == 0) ? true : false;
}
Cela devrait toujours renvoyer false
puisque var3 == 3000
. La fonction main
ressemble à ceci:
#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 renvoyer false
, on s’attendrait à ce que le programme n’imprime qu’un false à l’écran. Mais après compilation et exécution, 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 indéfini?
Remarque: je l'ai compilé avec gcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2
.
Comme indiqué dans d'autres réponses, le problème est que vous utilisez gcc
sans jeu d'options du compilateur. Si vous faites cela, il utilisera par défaut ce que l’on appelle "gnu90", qui est une implémentation non standard de l’ancienne norme C90 retirée de 1990.
Dans l’ancien standard C90, il existait une faille majeure dans le langage C: si vous n’aviez pas déclaré un prototype avant d’utiliser une fonction, la valeur par défaut serait int func ()
(où ( )
signifie "accepte n’importe quel paramètre" ). Cela change la convention d'appel de la fonction func
, mais cela ne change pas la définition de la fonction réelle. Étant donné que la taille de bool
et int
est différente, votre code invoque un comportement non défini lorsque la fonction est appelée.
Ce comportement absurde dangereux a été corrigé en 1999, avec la publication de la norme C99. Les déclarations de fonction implicites ont été interdites.
Malheureusement, jusqu'à la version 5.x.x, GCC utilise toujours l'ancien standard C par défaut. Il n'y a probablement aucune raison pour que vous souhaitiez compiler votre code autrement qu'en standard C. Vous devez donc explicitement dire à GCC qu'il doit compiler votre code en tant que code C moderne, au lieu de plus de 25 ans, GNU merde.
Corrigez le problème en compilant toujours votre programme en tant que:
gcc -std=c11 -pedantic-errors -Wall -Wextra
-std=c11
lui dit d'effectuer une tentative peu enthousiaste de compiler selon le standard C (actuel) (plus communément appelé C11).-pedantic-errors
lui dit de tout cœur de faire ce qui précède et de donner des erreurs au compilateur lorsque vous écrivez un code incorrect qui ne respecte pas le standard C.-Wall
signifie me donner quelques avertissements supplémentaires qu'il serait peut-être bon d'avoir.-Wextra
signifie me donner quelques avertissements supplémentaires qu'il serait peut-être bon d'avoir.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 qu'il s'agit d'une fonction qui prend un nombre inconnu d'arguments et renvoie un int
.
Si int
et bool
sont de tailles différentes, le résultat sera comportement non défini. Par exemple, sur ma machine, int
correspond à 4 octets et bool
à un octet. Étant donné que la fonction est définie pour renvoyer bool
, elle place un octet sur la pile lors de son retour. Cependant, comme il est implicitement déclaré de renvoyer int
à partir de main.c, la fonction appelante essaiera de lire 4 octets dans la pile.
Les options par défaut des compilateurs dans gcc ne vous diront pas que c'est ce qui se passe. 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 f1
dans 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 f1
dans f1.c devrait également être modifiée pour refléter cela.
Je pense qu'il est intéressant de voir où se produit réellement le décalage de taille mentionné dans l'excellente réponse de Lundin.
Si vous compilez avec --save-temps
, vous obtiendrez des fichiers d'assemblage que vous pourrez consulter. Voici la partie où f1()
effectue la comparaison == 0
et renvoie sa valeur:
cmpl $0, -4(%rbp)
sete %al
La partie renvoyée est sete %al
. Dans les conventions d'appel x86 de C, les valeurs de retour égales ou inférieures à 4 octets (ce qui inclut int
et bool
) 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()
:
call f1
testl %eax, %eax
je .L2
Ceci vérifie si le entier de %eax
est égal à zéro car il pense qu'il teste un int.
L'ajout d'une déclaration de fonction explicite modifie main()
en:
call f1
testb %al, %al
je .L2
c'est ce que nous voulons.
S'il vous plaît compiler avec une commande comme celle-ci:
gcc -Wall -Wextra -Werror -std=gnu99 -o main.exe main.c
Sortie:
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 d’éditeur de liens sont plus difficiles à comprendre, donc même si -std-gnu99
n’est pas nécessaire, essayez de toujours utiliser au moins -Wall -Werror
cela vous épargnera beaucoup de douleur.