web-dev-qa-db-fra.com

Cette fonction C devrait toujours retourner false, mais elle ne le fera pas.

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:

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 renvoyer false puisque var3 == 3000. La fonction main 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 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.

312
incBrain

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.
389
Lundin

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.

138
dbush

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.

35
Owen

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.

27
jdarthenay