95 votes

(Pourquoi) l'utilisation d'une variable non initialisée est-elle un comportement non défini ?

Si je l'ai fait :

unsigned int x;
x -= x;

il est clair que x debe être nulle après cette expression, mais partout où je regarde, on dit que la comportement de ce code est indéfini, pas seulement la valeur de x (jusqu'à avant la soustraction).

Deux questions :

  • Est-ce que le comportement de ce code est en effet indéfini ?
    (Par exemple, le code pourrait-il se planter [ou pire] sur un système conforme ?)

  • Si c'est le cas, pourquoi ne dit pas que le comportement est indéfinie, alors qu'il est parfaitement clair que x devrait être zéro ici ?

    C'est-à-dire, quel est le avantage donné en ne définissant pas le comportement ici ?

De toute évidence, le compilateur pourrait simplement utiliser quoi que ce soit la valeur de l'ordure qu'il jugeait "pratique" dans la variable, et cela fonctionnerait comme prévu... qu'y a-t-il de mal à cette approche ?

96voto

Jens Gustedt Points 40410

Oui, ce comportement est indéfini, mais pour des raisons différentes de celles dont la plupart des gens ont conscience.

Premièrement, l'utilisation d'une valeur unitialisée n'est pas en soi un comportement indéfini, mais la valeur est simplement indéterminée. Accéder à cette valeur est donc une erreur si la valeur est une représentation piège pour le type. Les types non signés ont rarement des représentations pièges, donc vous seriez relativement en sécurité de ce côté.

Ce qui rend le comportement indéfini est une propriété additionnelle de votre variable, à savoir qu'elle "aurait pu être déclarée avec register "c'est-à-dire que son adresse n'est jamais prise. De telles variables sont traitées de manière spéciale parce qu'il y a des architectures qui ont de vrais registres de CPU qui ont une sorte d'état supplémentaire qui est " non initialisé " et qui ne correspond pas à une valeur dans le domaine des types.

Edita: La phrase pertinente de la norme est 6.3.2.1p2 :

Si la lvalue désigne un objet de durée de stockage automatique que aurait pu être déclaré avec la classe de stockage de registre (n'a jamais eu son adresse prise), et que cet objet n'est pas initialisé (non déclaré). pas déclaré avec un initialisateur et qu'aucune affectation n'a été effectuée avant son avant son utilisation), le comportement est indéfini.

Et pour rendre cela plus clair, le code suivant est légal en toutes circonstances :

unsigned char a, b;
memcpy(&a, &b, 1);
a -= a;
  • Ici les adresses de a y b sont prises, leur valeur est donc juste indéterminée.
  • Depuis unsigned char n'a jamais de représentations de pièges cette valeur indéterminée est juste non spécifiée, toute valeur de unsigned char pourrait se produire.
  • A la fin a doit conserver la valeur 0 .

Edit2 : a y b ont des valeurs non spécifiées :

3.19.3 valeur non spécifiée
une valeur valide du type pertinent lorsque la présente Norme internationale n'impose aucune exigence quant à cette valeur. est choisie dans un cas quelconque

25voto

Gilles Points 37537

La norme C donne aux compilateurs une grande latitude pour effectuer des optimisations. Les conséquences de ces optimisations peuvent être surprenantes si l'on part d'un modèle naïf de programmes où la mémoire non initialisée est définie par un motif de bits aléatoire et où toutes les opérations sont exécutées dans l'ordre où elles sont écrites.

Remarque : les exemples suivants ne sont valables que parce que x n'a jamais son adresse prise, il est donc "de type registre". Ils seraient également valables si le type de x avaient des représentations pièges ; c'est rarement le cas pour les types non signés (cela nécessite de "gaspiller" au moins un bit de stockage, et doit être documenté), et impossible pour les types unsigned char . Si x avait un type signé, alors l'implémentation pouvait définir la configuration binaire qui n'est pas un nombre entre -(2 n-1 -1) et 2 n-1 -1 comme représentation du piège. Voir La réponse de Jens Gustedt .

Les compilateurs essaient d'affecter des registres aux variables, car les registres sont plus rapides que la mémoire. Comme le programme peut utiliser plus de variables que le processeur n'a de registres, les compilateurs effectuent une allocation de registres, ce qui fait que différentes variables utilisent le même registre à différents moments. Considérons le fragment de programme

unsigned x, y, z;   /* 0 */
y = 0;              /* 1 */
z = 4;              /* 2 */
x = - x;            /* 3 */
y = y + z;          /* 4 */
x = y + 1;          /* 5 */

Quand la ligne 3 est évaluée, x n'est pas encore initialisé, donc (raisonne le compilateur) la ligne 3 doit être une sorte de coup de chance qui ne peut pas se produire en raison d'autres conditions que le compilateur n'était pas assez intelligent pour comprendre. Puisque z n'est pas utilisé après la ligne 4, et x n'est pas utilisé avant la ligne 5, le même registre peut être utilisé pour les deux variables. Ainsi ce petit programme est compilé aux opérations suivantes sur les registres :

r1 = 0;
r0 = 4;
r0 = - r0;
r1 += r0;
r0 = r1;

La valeur finale de x est la valeur finale de r0 et la valeur finale de y est la valeur finale de r1 . Ces valeurs sont x = -3 et y = -4, et non 5 et 4 comme cela se produirait si x a été correctement initialisé.

Pour un exemple plus élaboré, considérez le fragment de code suivant :

unsigned i, x;
for (i = 0; i < 10; i++) {
    x = (condition() ? some_value() : -x);
}

Supposons que le compilateur détecte que condition n'a aucun effet secondaire. Depuis condition ne modifie pas x le compilateur sait que la première exécution de la boucle ne peut pas être un accès x puisqu'il n'est pas encore initialisé. Par conséquent, la première exécution du corps de la boucle est équivalente à x = some_value() il n'y a pas besoin de tester la condition. Le compilateur peut compiler ce code comme si vous aviez écrit

unsigned i, x;
i = 0; /* if some_value() uses i */
x = some_value();
for (i = 1; i < 10; i++) {
    x = (condition() ? some_value() : -x);
}

La façon dont cela peut être modélisé dans le compilateur est de considérer que toute valeur dépendant de x tiene la valeur qui vous convient à condition que x n'est pas initialisé. Parce que le comportement lorsqu'une variable non initialisée est indéfinie, plutôt que la variable ayant simplement une valeur non spécifiée, le compilateur n'a pas besoin de garder la trace d'une relation mathématique spéciale entre les valeurs quelconques. Ainsi, le compilateur peut analyser le code ci-dessus de cette manière :

  • pendant la première itération de la boucle, x n'est pas initialisé au moment où -x est évaluée.
  • -x a un comportement non défini, donc sa valeur est ce qui est le plus commode.
  • La règle d'optimisation _condition_ ? _value_ : _value_ s'applique, donc ce code peut être simplifié en _condition_; _value_ .

Lorsqu'il est confronté au code de votre question, ce même compilateur analyse que lorsque x = - x est évaluée, la valeur de -x est ce qui est le plus pratique. Ainsi, l'affectation peut être optimisée.

Je n'ai pas cherché d'exemple de compilateur qui se comporte comme décrit ci-dessus, mais c'est le genre d'optimisations que les bons compilateurs essaient de faire. Je ne serais pas surpris d'en rencontrer un. Voici un exemple moins plausible d'un compilateur avec lequel votre programme se plante. (Il n'est peut-être pas si invraisemblable si vous compilez votre programme dans une sorte de mode de débogage avancé).

Ce compilateur hypothétique place chaque variable dans une page de mémoire différente et configure les attributs de page de sorte que la lecture d'une variable non initialisée provoque un piège du processeur qui invoque un débogueur. Toute affectation à une variable doit d'abord s'assurer que sa page mémoire est mappée normalement. Ce compilateur n'essaie pas d'effectuer une optimisation avancée - il est en mode débogage, destiné à localiser facilement les bogues tels que les variables non initialisées. Lorsque x = - x est évalué, le côté droit provoque un piège et le débogueur se déclenche.

17voto

eq- Points 6181

Oui, le programme peut se planter. Il peut, par exemple, y avoir des représentations pièges (des configurations binaires spécifiques qui ne peuvent pas être traitées) qui peuvent provoquer une interruption du CPU, qui, si elle n'est pas traitée, peut faire planter le programme.

(Le paragraphe 6.2.6.1 d'un projet tardif de la C11 dit) Certaines représentations d'objets ne doivent pas nécessairement représenter une valeur du type d'objet. Si la valeur stockée d'un objet possède une telle représentation représentation et est lue par une expression lvalue qui n'a pas le type de caractère type de caractère, le comportement est indéfini. Si une telle représentation est produite par un effet secondaire qui modifie tout ou partie de l'objet, le comportement est indéfini. par une expression lvalue qui n'a pas de type de caractère, le comportement n'est pas défini.50) Une telle représentation est appelée une représentation représentation.

(Cette explication ne s'applique qu'aux plateformes où unsigned int peut avoir des représentations de pièges, ce qui est rare sur les systèmes du monde réel ; voir les commentaires pour les détails et les références à d'autres causes, peut-être plus courantes, qui conduisent à la formulation actuelle de la norme).

12voto

Eric Postpischil Points 36641

(Cette réponse concerne C 1999. Pour C 2011, voir la réponse de Jens Gustedt).

La norme C ne dit pas que l'utilisation de la valeur d'un objet de durée de stockage automatique qui n'est pas initialisé est un comportement indéfini. La norme C 1999 dit, dans le paragraphe 6.7.8 10, "Si un objet qui a une durée de stockage automatique n'est pas initialisé explicitement, sa valeur est indéterminée." (Ce paragraphe définit ensuite comment les objets statiques sont initialisés, donc les seuls objets non initialisés qui nous concernent sont les objets automatiques).

3.17.2 définit la "valeur indéterminée" comme étant "soit une valeur non spécifiée, soit une représentation piège". 3.17.3 définit une "valeur non spécifiée" comme étant "une valeur valide du type pertinent où la présente Norme internationale n'impose aucune exigence quant à la valeur choisie dans un cas donné".

Donc, si le non initialisé unsigned int x a une valeur non spécifiée, alors x -= x doit produire zéro. Reste la question de savoir s'il ne s'agit pas d'une représentation piège. L'accès à une valeur piège entraîne un comportement non défini, conformément à l'article 6.2.6.1 5.

Certains types d'objets peuvent avoir des représentations pièges, comme les NaN de signalisation des nombres à virgule flottante. Mais les entiers non signés sont spéciaux. Conformément à l'article 6.2.6.2, chacun des N bits de valeur d'un entier non signé représente une puissance de 2, et chaque combinaison des bits de valeur représente l'une des valeurs comprises entre 0 et 2. N -1. Les entiers non signés ne peuvent donc avoir des représentations pièges qu'en raison de certaines valeurs dans leurs bits de remplissage (comme un bit de parité).

Si, sur votre plate-forme cible, un int non signé n'a pas de bits de remplissage, alors un int non signé non initialisé ne peut pas avoir de représentation piège, et l'utilisation de sa valeur ne peut pas provoquer de comportement non défini.

11voto

David Schwartz Points 70129

Oui, c'est indéfini. Le code peut se planter. C dit que le comportement est indéfini parce qu'il n'y a pas de raison spécifique de faire une exception à la règle générale. L'avantage est le même que pour tous les autres cas de comportement indéfini : le compilateur n'a pas à produire de code spécial pour que cela fonctionne.

Il est clair que le compilateur pourrait simplement utiliser n'importe quelle valeur résiduelle qu'il jugerait "pratique" dans la variable, et cela fonctionnerait comme prévu... qu'y a-t-il de mal à cette approche ?

Pourquoi pensez-vous que ça n'arrive pas ? C'est exactement l'approche adoptée. Le compilateur n'est pas tenu de le faire fonctionner, mais il n'est pas tenu de le faire échouer.

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