Confusion sur l'initialisation du tableau en C

102

En langage C, si initialisez un tableau comme celui-ci:

int a[5] = {1,2};

alors tous les éléments du tableau qui ne sont pas initialisés explicitement seront initialisés implicitement avec des zéros.

Mais, si j'initialise un tableau comme celui-ci:

int a[5]={a[2]=1};

printf("%d %d %d %d %d\n", a[0], a[1],a[2], a[3], a[4]);

production:

1 0 1 0 0

Je ne comprends pas, pourquoi a[0]imprime- t-il à la 1place 0? Est-ce un comportement non défini?

Remarque: Cette question a été posée lors d'une entrevue.

msc
la source
35
L'expression est a[2]=1évaluée à 1.
tkausl
14
Une question très profonde. Je me demande si l'intervieweur connaît lui-même la réponse. Je ne. En effet, la valeur de l'expression a[2] = 1est apparemment 1, mais je ne suis pas sûr que vous soyez autorisé à prendre le résultat d'une expression d'initialisation désignée comme valeur du premier élément. Le fait que vous ayez ajouté l'étiquette d'avocat signifie que je pense que nous avons besoin d'une réponse citant la norme.
Bathsheba
15
Eh bien, si c'est leur question préférée, vous avez peut-être esquivé une balle. Personnellement, je préfère qu'un exercice de programmation écrit (avec accès à un compilateur et un débogueur) soit pris en compte sur quelques heures plutôt que des questions de style "ace" comme celles ci-dessus. Je pourrais conjecturer une réponse, mais je ne pense pas qu'elle aurait une véritable base factuelle.
Bathsheba
1
@Bathsheba Je ferais le contraire, car la réponse ici répond maintenant aux deux questions.
Au revoir SE
1
@Bathsheba serait le meilleur. Pourtant, je donnerais le crédit pour la question à OP, car il a abordé le sujet. Mais ce n'est pas à moi de décider de ce que je pense être "la bonne chose".
Au revoir SE

Réponses:

95

TL; DR: Je ne pense pas que le comportement de int a[5]={a[2]=1};soit bien défini, du moins en C99.

Ce qui est amusant, c'est que le seul élément qui a du sens pour moi est la partie sur laquelle vous demandez: a[0]est définie sur 1parce que l'opérateur d'affectation renvoie la valeur qui a été affectée. C'est tout le reste qui n'est pas clair.

Si le code avait été int a[5] = { [2] = 1 }, tout aurait été facile: c'est un paramètre d'initialisation désigné a[2]à 1et tout le reste à 0. Mais avec { a[2] = 1 }nous avons un initialiseur non désigné contenant une expression d'affectation, et nous tombons dans un terrier de lapin.


Voici ce que j'ai trouvé jusqu'à présent:

  • a doit être une variable locale.

    6.7.8 Initialisation

    1. Toutes les expressions dans un initialiseur pour un objet qui a une durée de stockage statique doivent être des expressions constantes ou des littéraux de chaîne.

    a[2] = 1n'est pas une expression constante, donc adoit avoir un stockage automatique.

  • a est dans la portée de sa propre initialisation.

    6.2.1 Portées des identifiants

    1. Les balises de structure, d'union et d'énumération ont une portée qui commence juste après l'apparition de la balise dans un spécificateur de type qui déclare la balise. Chaque constante d'énumération a une portée qui commence juste après l'apparition de son énumérateur définissant dans une liste d'énumérateurs. Tout autre identificateur a une portée qui commence juste après la fin de son déclarateur.

    Le déclarateur est a[5], donc les variables sont dans la portée de leur propre initialisation.

  • a est vivant dans sa propre initialisation.

    6.2.4 Durées de stockage des objets

    1. Un objet dont l' identifiant est déclaré sans liaison et sans le spécificateur de classe de stockage statica une durée de stockage automatique .

    2. Pour un tel objet qui n'a pas de type tableau de longueur variable, sa durée de vie s'étend de l'entrée dans le bloc auquel il est associé jusqu'à ce que l'exécution de ce bloc se termine de quelque manière que ce soit. (La saisie d'un bloc fermé ou l'appel d'une fonction suspend, mais ne termine pas, l'exécution du bloc actuel.) Si le bloc est saisi de manière récursive, une nouvelle instance de l'objet est créée à chaque fois. La valeur initiale de l'objet est indéterminée. Si une initialisation est spécifiée pour l'objet, elle est effectuée à chaque fois que la déclaration est atteinte lors de l'exécution du bloc; sinon, la valeur devient indéterminée à chaque fois que la déclaration est atteinte.

  • Il y a un point de séquence après a[2]=1.

    6.8 Déclarations et blocs

    1. Une expression complète est une expression qui ne fait pas partie d'une autre expression ou d'un déclarateur. Chacun des éléments suivants est une expression complète: un initialiseur ; l'expression dans une instruction d'expression; l'expression de contrôle d'une instruction de sélection ( ifou switch); l'expression dominante d'une instruction whileou do; chacune des expressions (facultatives) d'une forinstruction; l'expression (facultative) dans une returninstruction. La fin d'une expression complète est un point de séquence.

    Notez que, par exemple, dans int foo[] = { 1, 2, 3 }la { 1, 2, 3 }partie se trouve une liste d'initialiseurs entre accolades, chacun d'entre eux étant suivi d'un point de séquence.

  • L'initialisation est effectuée dans l'ordre de la liste d'initialisation.

    6.7.8 Initialisation

    1. Chaque liste d'initialiseurs entre accolades a un objet courant associé . Lorsqu'aucune désignation n'est présente, les sous-objets de l'objet courant sont initialisés dans l'ordre en fonction du type de l'objet courant: les éléments du tableau dans l'ordre d'indice croissant, les membres de la structure dans l'ordre des déclarations et le premier membre nommé d'une union. [...]

     

    1. L'initialisation doit avoir lieu dans l'ordre de la liste des initialiseurs, chaque initialiseur fourni pour un sous-objet particulier remplaçant tout initialiseur précédemment répertorié pour le même sous-objet; tous les sous-objets qui ne sont pas initialisés explicitement doivent être initialisés implicitement de la même manière que les objets qui ont une durée de stockage statique.
  • Cependant, les expressions d'initialisation ne sont pas nécessairement évaluées dans l'ordre.

    6.7.8 Initialisation

    1. L'ordre dans lequel les effets secondaires se produisent parmi les expressions de la liste d'initialisation n'est pas spécifié.

Cependant, cela laisse encore quelques questions sans réponse:

  • Les points de séquence sont-ils même pertinents? La règle de base est:

    6.5 Expressions

    1. Entre le point de séquence précédent et suivant, un objet doit voir sa valeur stockée modifiée au plus une fois par l'évaluation d'une expression . En outre, la valeur antérieure doit être lue uniquement pour déterminer la valeur à stocker.

    a[2] = 1 est une expression, mais l'initialisation ne l'est pas.

    Ceci est légèrement contredit par l'annexe J:

    J.2 Comportement indéfini

    • Entre deux points de séquence, un objet est modifié plus d'une fois, ou est modifié et la valeur précédente est lue autrement que pour déterminer la valeur à stocker (6.5).

    L'annexe J indique que toute modification compte, pas seulement les modifications par expressions. Mais étant donné que les annexes ne sont pas normatives, nous pouvons probablement l'ignorer.

  • Comment les initialisations de sous-objets sont-elles séquencées par rapport aux expressions d'initialisation? Tous les initialiseurs sont-ils évalués en premier (dans un certain ordre), puis les sous-objets sont initialisés avec les résultats (dans l'ordre de la liste des initialiseurs)? Ou peuvent-ils être entrelacés?


Je pense qu'il int a[5] = { a[2] = 1 }est exécuté comme suit:

  1. Le stockage pour aest alloué lorsque son bloc conteneur est entré. Le contenu est indéterminé à ce stade.
  2. Le (seul) initialiseur est exécuté ( a[2] = 1), suivi d'un point de séquence. Ce magasins 1dans a[2]et retours 1.
  3. Cela 1est utilisé pour initialiser a[0](le premier initialiseur initialise le premier sous-objet).

Mais ici les choses deviennent floues parce que les autres éléments ( a[1], a[2], a[3], a[4]) sont censés être initialisé à 0, mais il ne sait pas quand: - t - il arriver avant a[2] = 1est évaluée? Si tel est le cas, est- a[2] = 1ce que «gagner» et écraser a[2], mais cette affectation aurait-elle un comportement indéfini car il n'y a pas de point de séquence entre l'initialisation zéro et l'expression d'affectation? Les points de séquence sont-ils même pertinents (voir ci-dessus)? Ou est-ce que zéro initialisation se produit après que tous les initialiseurs sont évalués? Si c'est le cas, a[2]devrait finir par l'être 0.

Parce que la norme C ne définit pas clairement ce qui se passe ici, je pense que le comportement n'est pas défini (par omission).

melpomène
la source
1
Au lieu d'indéfini, je dirais qu'il n'est pas spécifié , ce qui laisse les choses ouvertes à l'interprétation par les implémentations.
certain programmeur mec
1
"nous tombons dans un terrier de lapin" LOL! Jamais entendu ça pour un UB ou des trucs non spécifiés.
BЈовић
2
@Someprogrammerdude Je ne pense pas que cela puisse être indéfini (" comportement où la présente Norme internationale fournit deux ou plusieurs possibilités et n'impose aucune exigence supplémentaire sur laquelle est choisie en aucun cas ") car la norme ne fournit pas vraiment de possibilités parmi lesquelles choisir. Il ne dit tout simplement pas ce qui se passe, ce qui, je crois, relève du " comportement indéfini [...] indiqué dans la présente Norme internationale [...] par l'omission de toute définition explicite du comportement. "
melpomene
2
@ BЈовић C'est aussi une très belle description non seulement pour un comportement indéfini, mais aussi pour un comportement défini qui nécessite un thread comme celui-ci pour l'expliquer.
gnasher729
1
@JohnBollinger La différence est que vous ne pouvez pas réellement initialiser le a[0]sous - objet avant d'évaluer son initialiseur, et l'évaluation de tout initialiseur inclut un point de séquence (car c'est une "expression complète"). Par conséquent, je pense que la modification du sous-objet que nous initialisons est une bonne chose.
melpomene le
22

Je ne comprends pas, pourquoi a[0]imprime- t-il à la 1place 0?

a[2]=1Initialise vraisemblablement en a[2]premier et le résultat de l'expression est utilisé pour l'initialisation a[0].

À partir de N2176 (projet C17):

6.7.9 Initialisation

  1. Les évaluations des expressions de liste d'initialisation sont séquencées de manière indéterminée les unes par rapport aux autres et ainsi l'ordre dans lequel les effets secondaires se produisent n'est pas spécifié. 154)

Il semblerait donc que la sortie 1 0 0 0 0aurait également été possible.

Conclusion: n'écrivez pas d'initialiseurs qui modifient la variable initialisée à la volée.

user694733
la source
1
Cette partie ne s'applique pas: il n'y a qu'une seule expression d'initialisation ici, donc elle n'a pas besoin d'être séquencée avec quoi que ce soit.
melpomene
@melpomene Il y a l' {...}expression qui s'initialise a[2]à 0, et la a[2]=1sous-expression qui s'initialise a[2]à 1.
user694733
1
{...}est une liste d'initialiseurs accolés. Ce n'est pas une expression.
melpomene
@melpomene Ok, vous êtes peut-être là. Mais je dirais toujours qu'il y a encore 2 effets secondaires concurrents de sorte que le paragraphe reste.
user694733
@melpomene, il y a deux choses à séquencer: le premier initialiseur et le réglage des autres éléments sur 0
MM
6

Je pense que la norme C11 couvre ce comportement et dit que le résultat n'est pas spécifié , et je ne pense pas que C18 ait apporté des changements pertinents dans ce domaine.

Le langage standard n'est pas facile à analyser. La section pertinente de la norme est le §6.7.9 Initialisation . La syntaxe est documentée comme suit:

initializer:
                assignment-expression
                { initializer-list }
                { initializer-list , }
initializer-list:
                designationopt initializer
                initializer-list , designationopt initializer
designation:
                designator-list =
designator-list:
                designator
                designator-list designator
designator:
                [ constant-expression ]
                . identifier

Notez que l'un des termes est expression d'affectation , et comme il a[2] = 1s'agit indubitablement d'une expression d'affectation, il est autorisé à l'intérieur des initialiseurs pour les tableaux avec une durée non statique:

§4 Toutes les expressions d'un initialiseur pour un objet qui a une durée de stockage statique ou de threads doivent être des expressions constantes ou des chaînes littérales.

L'un des paragraphes clés est:

§19 L'initialisation doit avoir lieu dans l'ordre de la liste des initialiseurs, chaque initialiseur fourni pour un sous-objet particulier remplaçant tout initialiseur précédemment répertorié pour le même sous-objet; 151) tous les sous-objets qui ne sont pas initialisés explicitement doivent être initialisés implicitement de la même manière que les objets qui ont une durée de stockage statique.

151) Tout initialiseur pour le sous-objet qui est remplacé et donc non utilisé pour initialiser ce sous-objet peut ne pas être évalué du tout.

Et un autre paragraphe clé est:

§23 Les évaluations des expressions de la liste d'initialisation sont séquencées de manière indéterminée les unes par rapport aux autres et donc l'ordre dans lequel les effets secondaires se produisent n'est pas spécifié. 152)

152) En particulier, l'ordre d'évaluation n'a pas besoin d'être le même que l'ordre d'initialisation des sous-objets.

Je suis assez sûr que le paragraphe §23 indique que la notation dans la question:

int a[5] = { a[2] = 1 };

conduit à un comportement non spécifié. L'affectation à a[2]est un effet secondaire et l'ordre d'évaluation des expressions est séquencé de manière indéterminée les uns par rapport aux autres. Par conséquent, je ne pense pas qu'il existe un moyen de faire appel à la norme et d'affirmer qu'un compilateur particulier gère cela correctement ou incorrectement.

Jonathan Leffler
la source
Il n'y a qu'une seule expression de liste d'initialisation, donc le §23 n'est pas pertinent.
melpomene
2

Ma compréhension a[2]=1renvoie la valeur 1 donc le code devient

int a[5]={a[2]=1} --> int a[5]={1}

int a[5]={1}attribuer une valeur pour a [0] = 1

Par conséquent, il imprime 1 pour un [0]

Par exemple

char str[10]={‘H’,‘a’,‘i’};


char str[0] = H’;
char str[1] = a’;
char str[2] = i;
Karthika
la source
2
Il s'agit d'une question [de la langue-avocat], mais ce n'est pas une réponse qui fonctionne avec la norme, ce qui la rend inutile. De plus, il y a aussi 2 réponses beaucoup plus approfondies disponibles et votre réponse ne semble rien ajouter.
Au revoir SE le
J'ai un doute, est-ce que le concept que j'ai publié est erroné? Pouvez-vous me clarifier cela?
Karthika le
1
Vous spéculez simplement pour des raisons, alors qu'il y a déjà une très bonne réponse avec des parties pertinentes de la norme. La question n'est pas de dire simplement comment cela pourrait arriver. Il s'agit de ce que la norme dit devrait se produire.
Au revoir SE le
Mais la personne qui a posté la question ci-dessus a demandé la raison et pourquoi cela se produit-il? Alors seulement j'ai laissé tomber cette réponse, mais le concept est correct.
Karthika le
OP a demandé " Est-ce un comportement non défini? ". Votre réponse ne le dit pas.
melpomene
1

J'essaie de donner une réponse courte et simple au puzzle: int a[5] = { a[2] = 1 };

  1. Le premier a[2] = 1est réglé. Cela signifie que le tableau dit:0 0 1 0 0
  2. Mais voici, étant donné que vous l'avez fait { }entre crochets, qui sont utilisés pour initialiser le tableau dans l'ordre, il prend la première valeur (qui est 1) et la définit sur a[0]. C'est comme si int a[5] = { a[2] };resterait là où nous en sommes déjà a[2] = 1. Le tableau résultant est maintenant:1 0 1 0 0

Un autre exemple: int a[6] = { a[3] = 1, a[4] = 2, a[5] = 3 };- Même si l'ordre est quelque peu arbitraire, en supposant qu'il va de gauche à droite, il irait dans ces 6 étapes:

0 0 0 1 0 0
1 0 0 1 0 0
1 0 0 1 2 0
1 2 0 1 2 0
1 2 0 1 2 3
1 2 3 1 2 3
Bataille
la source
1
A = B = C = 5n'est pas une déclaration (ou une initialisation). C'est une expression normale qui analyse A = (B = (C = 5))parce que l' =opérateur est associatif juste. Cela n'aide pas vraiment à expliquer le fonctionnement de l'initialisation. Le tableau commence réellement à exister lorsque le bloc dans lequel il est défini est entré, ce qui peut être long avant que la définition réelle ne soit exécutée.
melpomène
1
" Il va de gauche à droite, chacun commençant par la déclaration interne " est incorrect. La norme C dit explicitement " L'ordre dans lequel les effets secondaires se produisent parmi les expressions de la liste d'initialisation n'est pas spécifié. "
melpomene
1
" Vous testez le code de mon exemple suffisamment de fois et voyez si les résultats sont cohérents. " Ce n'est pas ainsi que cela fonctionne. Vous ne semblez pas comprendre ce qu'est un comportement indéfini. Tout en C a un comportement non défini par défaut; c'est juste que certaines pièces ont un comportement défini par la norme. Pour prouver que quelque chose a défini un comportement, vous devez citer la norme et montrer où elle définit ce qui doit se passer. En l'absence d'une telle définition, le comportement n'est pas défini.
melpomene le
1
L'affirmation du point (1) est un énorme saut par-dessus la question clé ici: l'initialisation implicite de l'élément a [2] à 0 se produit-elle avant que l'effet secondaire de l' a[2] = 1expression d'initialisation ne soit appliqué? Le résultat observé est comme si c'était le cas, mais la norme ne semble pas préciser que cela devrait être le cas. C'est le centre de la controverse, et cette réponse l'ignore complètement.
John Bollinger le
1
«Comportement indéfini» est un terme technique au sens étroit. Cela ne veut pas dire «comportement dont nous ne sommes pas vraiment sûrs». L'idée clé ici est qu'aucun test, sans compilateur, ne peut jamais montrer qu'un programme particulier se comporte ou ne se comporte pas bien selon la norme , car si un programme a un comportement non défini, le compilateur est autorisé à faire quoi que ce soit - y compris travailler d'une manière parfaitement prévisible et raisonnable. Ce n'est pas simplement un problème de qualité d'implémentation où les rédacteurs du compilateur documentent les choses - c'est un comportement non spécifié ou défini par l'implémentation.
Jeroen Mostert
0

L'affectation a[2]= 1est une expression qui a la valeur 1, et vous avez essentiellement écrit int a[5]= { 1 };(avec l'effet secondaire qui a[2]est également attribué 1).

Yves Daoust
la source
Mais on ne sait pas quand l'effet secondaire est évalué et le comportement peut changer en fonction du compilateur. De plus, la norme semble indiquer qu'il s'agit d'un comportement non défini, ce qui rend les explications pour les réalisations spécifiques au compilateur inutiles.
Au revoir SE
@KamiKaze: bien sûr, la valeur 1 a atterri là-bas par accident.
Yves Daoust
0

Je crois que int a[5]={ a[2]=1 }; c'est un bon exemple pour un programmeur qui se tire dans son propre pied.

Je pourrais être tenté de penser que ce que vous vouliez dire était int a[5]={ [2]=1 };un élément de réglage d'initialisation désigné C99 2 à 1 et le reste à zéro.

Dans le cas rare où vous pensiez vraiment vraiment int a[5]={ 1 }; a[2]=1;, alors ce serait une façon amusante de l'écrire. Quoi qu'il en soit, c'est à cela que se résume votre code, même si certains ont souligné que ce n'est pas bien défini lorsque l'écriture a[2]est réellement exécutée. Le piège ici est qu'il a[2]=1ne s'agit pas d'un initialiseur désigné mais d'une simple affectation qui a elle-même la valeur 1.

Sven
la source
On dirait que ce sujet de langue-avocat demande des références à partir de projets standard. C'est pourquoi vous êtes contre-évalué (je ne l'ai pas fait comme vous le voyez, je suis contre-évalué pour la même raison). Je pense que ce que vous avez écrit est tout à fait correct, mais il semble que tous ces juristes linguistiques ici viennent d'un comité ou quelque chose du genre. Donc, ils ne demandent pas du tout d'aide, ils essaient de vérifier si le projet couvre l'affaire ou non et la plupart des gars ici sont déclenchés si vous leur donnez une réponse comme vous les aidiez. Je suppose que je vais supprimer ma réponse :) Si les règles de ce sujet étaient clairement énoncées, cela aurait été utile
Abdurrahim