Une question sur l'union dans le magasin C comme un type et comme un autre - est-ce que sa mise en œuvre est définie?

StackOverflow https://stackoverflow.com/questions/1812348

Question

Je lisais au sujet de l’union en C de K & R; pour autant que je sache, une seule variable en union peut contenir l’un des types, et si quelque chose est stocké dans un type et extrait comme un autre, le résultat est purement mise en œuvre définie.

Maintenant, veuillez vérifier l'extrait de code:

#include<stdio.h>

int main(void)
{
  union a
  {
     int i;
     char ch[2];
  };

  union a u;
  u.ch[0] = 3;
  u.ch[1] = 2;

  printf("%d %d %d\n", u.ch[0], u.ch[1], u.i);

  return 0;
}

Sortie:

3 2 515

Ici, j'attribue des valeurs dans u.ch , mais je les récupère à la fois dans u.ch et u.i . Est-ce que la mise en œuvre est définie? Ou est-ce que je fais quelque chose de vraiment stupide?

Je sais que cela peut paraître très novice à la plupart des gens, mais je suis incapable de comprendre la raison de cette sortie.

Merci.

Était-ce utile?

La solution

Ceci est un comportement indéfini. u.i et u.ch sont situés à la même adresse mémoire. Ainsi, le résultat de l'écriture dans l'un et de la lecture de l'autre dépend du compilateur, de la plate-forme, de l'architecture et parfois même du niveau d'optimisation du compilateur. Par conséquent, la sortie pour u.i peut ne pas toujours être 515 .

Exemple

Par exemple, gcc sur ma machine produit deux réponses différentes pour -O0 et -O2 .

  1. Étant donné que ma machine a une architecture little-endian 32 bits, avec -O0 , je me retrouve avec deux octets les moins significatifs initialisés à 2 et à 3, deux octets les plus significatifs ne sont pas initialisés. La mémoire de l'union se présente ainsi: {3, 2, garbage, garbage}

    Par conséquent, le résultat est similaire à 3 2 -1216937469 .

  2. Avec -O2 , je reçois la sortie de 3 2 515 comme vous le faites, ce qui rend la mémoire syndicale {3, 2, 0, 0} . Ce qui se passe, c’est que gcc optimise l’appel de printf avec les valeurs réelles, de sorte que la sortie de l’assemblage ressemble à l’équivalent de:

    #include <stdio.h>
    int main() {
        printf("%d %d %d\n", 3, 2, 515);
        return 0;
    }
    

    La valeur 515 peut être obtenue comme d’autres expliquées dans d’autres réponses à cette question. En gros, cela signifie que lorsque gcc optimise l'appel, il choisit la valeur zéro comme valeur aléatoire d'une union présumée non initialisée.

Écrire à un membre du syndicat et lire un autre n’a généralement aucun sens, mais parfois cela peut être utile pour les programmes compilés avec aliasing strict .

Autres conseils

La réponse à cette question dépend du contexte historique, car la spécification de la langue a changé avec le temps. Et cette question se trouve être celle affectée par les changements.

Vous avez dit que vous lisiez K & R. La dernière édition de ce livre (à ce jour) décrit la première version normalisée du langage C - C89 / 90. Dans cette version du langage C, l’écriture d’un membre de l’union et la lecture d’un autre membre constituent un comportement non défini . Pas implémentation définie (ce qui est différent), mais comportement non défini . La partie pertinente de la norme linguistique dans ce cas est 6.5 / 7.

Maintenant, à un moment ultérieur de l'évolution de C (application de la version C99 de la spécification du langage avec le corrigendum technique 3), il est soudainement devenu légal d'utiliser Union pour le typage, c'est-à-dire d'écrire un membre du syndicat puis d'en lire un autre. / p>

Notez que tenter de le faire peut néanmoins conduire à un comportement indéfini. S'il s'avère que la valeur que vous lisez est invalide (ainsi appelée "représentation de trappe") pour le type que vous avez lu, le comportement reste indéterminé. Sinon, la valeur que vous lisez est définie par l'implémentation.

Votre exemple spécifique est relativement sûr pour le type punning du tableau int à char [2] . Il est toujours légal en langage C de réinterpréter le contenu de tout objet en tant que tableau de caractères (à nouveau, 6.5 / 7).

Cependant, l'inverse n'est pas vrai. Écrire des données dans le membre du tableau char [2] de votre union, puis les lire en tant que int peut potentiellement créer une représentation d'interruption et conduire à un comportement non défini . Le danger potentiel existe même si votre tableau de caractères a une longueur suffisante pour couvrir l'intégralité de int .

Mais dans votre cas particulier, si int est plus grand que char [2] , le int que vous lisez couvrira une zone non initialisée. au-delà de la fin du tableau, ce qui conduit à nouveau à un comportement indéfini.

La sortie est due au fait que sur votre machine, les entiers sont stockés dans little-endian format: les octets les moins significatifs sont stockés en premier. D'où la séquence d'octets [3,2,0,0] représente le nombre entier 3 + 2 * 256 = 515.

Ce résultat dépend de l'implémentation spécifique et de la plate-forme.

La sortie de ce code dépendra de votre plate-forme et de l'implémentation du compilateur C. Votre sortie me fait penser que vous utilisez ce code sur un système litte-endian (probablement x86). Si vous deviez mettre 515 dans i et le regarder dans un débogueur, vous verriez que l'octet d'ordre le plus bas serait un 3 et que le prochain octet en mémoire serait un 2, qui correspond exactement à ce que vous avez mis dans la chaîne.

Si vous agissiez ainsi sur un système big-endian, vous auriez (probablement) obtenu 770 (en supposant un ints de 16 bits) ou 50462720 (en supposant un ints de 32 bits).

Cela dépend de la mise en œuvre et les résultats peuvent varier sur une plate-forme / un compilateur différent, mais il semble que ce soit ce qui se passe:

515 en binaire est

1000000011

Ajoute des zéros à deux octets (en supposant que 16 bits int):

0000001000000011

Les deux octets sont les suivants:

00000010 and 00000011

Qui est 2 et 3

J'espère que quelqu'un explique pourquoi ils sont inversés - je suppose que les caractères ne sont pas inversés, mais que l'int est un peu endian.

La quantité de mémoire allouée à une union est égale à la mémoire requise pour stocker le membre le plus gros. Dans ce cas, vous avez un entier int et un tableau de caractères de longueur 2. En supposant que int soit 16 bits et que caractères est 8 bits, ils exigent tous deux le même espace, de sorte que l'union est allouée sur deux octets.

Lorsque vous attribuez trois (00000011) et deux (00000010) au tableau de caractères, l'état d'union est 0000001100000010 . Lorsque vous lisez l'int de cette union, l'intégralité est convertie en entier. En supposant une version little-endian où LSB est stocké à l'adresse la plus basse, la lecture int à partir de l'union serait 0000001000000011 , qui est le binaire de 515.

REMARQUE: cela est vrai même si l'int était en 32 bits - Vérifiez La réponse d'Amnon

Si vous utilisez un système 32 bits, alors un entier entier est de 4 octets, mais vous n’initialisez que 2 octets. Accéder aux données non initialisées est un comportement indéfini.

En supposant que vous soyez sur un système avec une entrée de 16 bits, alors ce que vous faites est toujours défini par l'implémentation. Si votre système est un peu endian, alors u.ch [0] correspondra à l'octet le moins significatif d'ui et u.ch 1 sera l'octet le plus significatif. Sur un système big endian, c'est l'inverse. De plus, le standard C ne force pas l'implémentation à utiliser le complément à deux pour représenter un entier signé. valeurs, bien que le complément de deux soit le plus commun. Bien entendu, la taille d’un entier est également définie par la mise en oeuvre.

Astuce: il est plus facile de voir ce qui se passe si vous utilisez des valeurs hexadécimales. Sur un système little endian, le résultat en hexadécimal serait 0x0203.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top