34 votes

À quel point le comportement non défini est-il indéfini?

Je ne suis pas sûr de bien comprendre à quel point un comportement non défini peut compromettre un programme.

Disons que j'ai ce code:

 #include <stdio.h>

int main()
{
    int v = 0;
    scanf("%d", &v);
    if (v != 0)
    {
        int *p;
        *p = v;  // Oops
    }
    return v;
}
 

Le comportement de ce programme est-il indéfini uniquement dans les cas où v est différent de zéro ou est-il indéfini même si v est nul?

15voto

Matteo Italia Points 53117

Je dirais que le comportement n'est pas défini uniquement si l'utilisateur insère un nombre différent de 0. Après tout, si l'infraction code d'article n'est pas fait exécuter les conditions d'AC ne sont pas remplies (c'est à dire la non-initialisé pointeur n'est pas créé ni déréférencé).

Un soupçon de ce qui peut être trouvé dans la norme, au 3.4.3:

problème, lors de l'utilisation d'un non-compatibles ou erronées programme de construction ou de données erronées, pour lequel la présente Norme Internationale n'impose pas d'exigences

Cela semble impliquer que, si de telles données erronées" était plutôt correct, le problème serait parfaitement définies, qui semble assez bien applicables à notre cas.


Autre exemple: débordement d'entier. Tout programme qui fait une addition avec les données fournies par l'utilisateur sans faire de vastes vérifier sur elle est soumise à ce genre de comportement indéfini - mais une addition UB uniquement lorsque l'utilisateur fournit ces données particulières.

12voto

Keith Thompson Points 85120

Puisque c'est la de la balise, j'ai un très tatillonne argument que le programme du comportement n'est pas défini, indépendamment de la saisie de l'utilisateur, mais pas pour les raisons que vous pourriez vous attendre, même s'il peut être bien définis (lorsqu' v==0) en fonction de la mise en œuvre.

Le programme définit main comme

int main()
{
    /* ... */
}

C99 5.1.2.2.1 dit que la principale fonction doit être définie soit comme

int main(void) { /* ... */ }

ou comme

int main(int argc, char *argv[]) { /* ... */ }

ou l'équivalent; ou dans certains autres de la mise en œuvre-définis.

int main() n'est pas équivalent à int main(void). L'ancienne, comme une déclaration, dit qu' main prend un fixe, mais non spécifié nombre et le type des arguments; ce dernier dit qu'il ne prend pas d'arguments. La différence est qu'un appel récursif à l' main comme

main(42);

est une violation de contrainte si vous utilisez int main(void), mais pas si vous utilisez int main().

Par exemple, ces deux programmes:

int main() {
    if (0) main(42); /* not a constraint violation */
}


int main(void) {
    if (0) main(42); /* constraint violation, requires a diagnostic */
}

ne sont pas équivalentes.

Si la mise en œuvre des documents qu'il accepte int main() comme une extension, alors cela ne s'applique pas pour que la mise en œuvre.

C'est un très tatillonne, point sur lequel tout le monde ne l'accepte), et est facilement évitée en déclarant int main(void) (que vous devriez faire de toute façon; toutes les fonctions doivent avoir des prototypes, pas de style ancien déclarations et les définitions).

Dans la pratique, chaque compilateur j'ai vu accepte int main() sans plainte.

Pour répondre à la question qui était prévu:

Une fois que le changement est fait, le programme a un comportement bien défini si v==0, et n'est pas définie si v!=0. Oui, le definedness le comportement dépend de la saisie de l'utilisateur. Il n'y a rien de particulièrement inhabituel.

9voto

Nemo Points 32838

Permettez-moi de vous donner un argument pour expliquer pourquoi je pense que ce n'est toujours pas défini.

Tout d'abord, les répondants disent que c'est "la plupart du temps définies" ou somesuch, basées sur leur expérience avec certains compilateurs, sont tout simplement faux. Une petite modification de votre exemple servira à illustrer:

#include <stdio.h>

int
main()
{
    int v;
    scanf("%d", &v);
    if (v != 0)
    {
        printf("Hello\n");
        int *p;
        *p = v;  // Oops
    }
    return v;
}

Ce que ce programme fait-il que si vous fournissez de "1" en entrée? Si la réponse est "Il imprime Bonjour et puis se bloque", vous avez tort. "Un comportement indéfini" ne signifie pas que le comportement de certains aspects spécifiques de la déclaration n'est pas défini; cela signifie que le comportement de l' ensemble du programme n'est pas défini. Le compilateur est permis de supposer que vous ne vous engagez pas dans un comportement indéfini, donc dans ce cas, on peut supposer que l' v est non-nul, et tout simplement émet pas de code entre crochets à tous, y compris l' printf.

Si vous pensez que c'est peu probable, détrompez-vous. GCC ne peut pas effectuer cette analyse exactement, mais il le fait très similaires. Mon exemple préféré, qui fait illustre le point pour de vrai:

int test(int x) { return x+1 > x; }

Essayez d'écrire un petit programme de test pour imprimer INT_MAX, INT_MAX+1, et test(INT_MAX). (Assurez-vous d'activer l'optimisation.) Une implémentation typique pourrait démontrer INT_MAX à 2147483647, INT_MAX+1 à -2147483648, et test(INT_MAX) 1.

En fait, GCC compile cette fonction pour renvoyer une constante à 1. Pourquoi? En raison de dépassement d'entier est un comportement indéfini, donc le compilateur peut supposer que vous ne le faisons pas, donc x ne peut pas égaler INT_MAXdonc x+1 est plus grand que x, donc cette fonction peut renvoyer 1 inconditionnellement.

Un comportement indéfini, peut entraîner des variables qui ne sont pas égaux à eux-mêmes, les nombres négatifs qui comparent de plus que des nombres positifs (voir l'exemple ci-dessus), et d'autres comportements bizarres. Le plus intelligent le compilateur, le plus bizarre le comportement.

OK, je l'avoue, je ne peux pas citer le chapitre et le verset de la norme afin de répondre à la question exacte que vous avez demandé. Mais les gens qui disent "Ouais ouais, mais dans la vraie vie déréférencement NULL donne juste un seg fault" sont de plus mal que ce qu'ils peuvent imaginer, et ils obtiennent plus de mal avec chaque compilateur génération.

Et dans la vraie vie, si le code est morte, vous devriez le retirer; si elle n'est pas morte, vous ne devez pas appeler un comportement indéfini. Voilà donc ma réponse à votre question.

2voto

Pete Points 13373

Si v vaut 0, votre assignation de pointeur aléatoire ne sera jamais exécutée et la fonction retournera zéro, donc ce n'est pas un comportement indéfini.

1voto

ludesign Points 929

Lorsque vous déclarez des variables (en particulier les pointeurs), un morceau de la mémoire est allouée (généralement un int). Cette paix de la mémoire est marqué comme free le système, mais l'ancienne valeur stockée, il n'est pas effacé (cela dépend de l'allocation de mémoire mis en œuvre par le compilateur, il peut remplir la place avec des zéros) de sorte que votre int *p auront une valeur aléatoire (junk), à qui il a à interpréter en tant que integer. Le résultat est l'endroit de la mémoire où l' p de points à (p pointee). Lorsque vous essayez d' dereference (aka. l'accès à cette pièce de la mémoire), il sera (presque à chaque fois) occupé par un autre processus/programme, et donc essayer de modifier/modifier quelques autres, de la mémoire entraîne en access violation questions par l' memory manager.

Donc, dans cet exemple, toute autre valeur à 0 entraînera un comportement indéfini, parce que personne ne sait ce qu' *p sera point à ce moment.

J'espère que cette explication est d'aucun secours.

Edit: Ah, désolé, encore quelques réponses avant moi :)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X