Que font les appels à memcpy?
La question est mal posée. Regardons d'abord le code :
char repr[offsetof(struct first_member_padded_t, b)] = //some value
memcpy(repr, &a, sizeof(a));
memcpy(&(s.a), repr, sizeof(repr));
Remarquez d'abord que repr
est initialisé, donc tous les éléments qui le composent ont des valeurs données.
Le premier memcpy
est bon - il copie les octets de a
dans repr
.
Si le deuxième memcpy
était memcpy(&s, repr, sizeof repr);
, il copierait les octets de repr
dans s
. Cela écrirait des octets dans s.a
et, en raison de la taille de repr
, dans tout padding entre s.a
et s.b
. Selon le C 2018 6.5 7 et d'autres parties de la norme, il est permis d'accéder aux octets d'un objet (et "accéder" signifie à la fois lire et écrire, selon 3.1 1). Ainsi, cette copie dans s
est correcte, et s.a
prend la même valeur que a
.
Cependant, le memcpy
utilise &(s.a)
au lieu de &s
. Il utilise l'adresse de s.a
plutôt que l'adresse de s
. Nous savons que convertir s.a
en un pointeur vers un type de caractère nous permettrait d'accéder aux octets de s.a
(6.5 7 et plus) (et le passer à memcpy
a le même effet qu'une telle conversion, car memcpy
est spécifié pour avoir l'effet de copier des octets), mais il n'est pas clair si cela nous permet d'accéder à d'autres octets dans s
. En d'autres termes, la question est de savoir si nous pouvons utiliser &s.a
pour accéder à des octets autres que ceux dans s.a
.
6.7.2.1 15 nous indique que, si on convertit un pointeur vers le premier membre d'une structure de manière "appropriée", le résultat pointe vers la structure. Ainsi, si nous convertissons &s.a
en un pointeur vers struct first_member_padding_t
, il pointerait vers s
, et nous pouvons certainement utiliser un pointeur vers s
pour accéder à tous les octets dans s
. Ainsi, cela serait également bien défini :
memcpy((struct first_member_padding t *) &s.a, repr, sizeof repr);
Cependant, memcpy(&s.a, repr, sizeof repr);
convertit seulement &s.a
en void *
(parce que memcpy
est déclaré pour prendre un void *
, donc &s.a
est automatiquement converti lors de l'appel de la fonction) et non en un pointeur vers le type de structure. Est-ce une conversion appropriée? Notez que si nous faisions memcpy(&s, repr, sizeof repr);
, cela convertirait &s
en void *
. 6.2.5 28 nous dit qu'un pointeur vers void
a la même représentation qu'un pointeur vers un type de caractère. Considérez donc ces deux déclarations :
memcpy(&s.a, repr, sizeof repr);
memcpy(&s, repr, sizeof repr);
Les deux déclarations passent un void *
à memcpy
, et ces deux void *
ont la même représentation l'un que l'autre et pointent vers le même octet. Maintenant, nous pourrions interpréter la norme de manière pédestre et stricte de sorte qu'ils sont différents en ce sens que le second peut être utilisé pour accéder à tous les octets de s
et le premier ne le peut pas. Alors il est étrange que nous ayons deux pointeurs nécessairement identiques qui se comportent différemment.
Une telle interprétation stricte de la norme C semble possible en théorie - la différence entre les pointeurs pourrait survenir pendant l'optimisation plutôt que dans l'implémentation réelle de memcpy
- mais je ne connais aucun compilateur qui ferait cela. Notez qu'une telle interprétation est en contradiction avec la section 6.2 de la norme, qui nous parle des types et des représentations. Interpréter la norme de telle sorte que (void *) &s.a
et (void *) &s
se comportent différemment signifie que deux choses de même valeur et type peuvent se comporter différemment, ce qui signifie qu'une valeur consiste en quelque chose de plus que sa valeur et son type, ce qui ne semble pas être l'intention de 6.2 ou de la norme en général.
Type Casting
La question déclare :
J'essaie de comprendre comment fonctionne le type casting lorsqu'il s'agit de stocker une valeur dans un membre d'une structure ou d'une union.
Ce n'est pas du type casting tel que le terme est communément utilisé. Techniquement, le code accède à s.a
en utilisant des lvalues d'un type différent de sa définition (parce qu'il utilise memcpy
, qui est défini pour copier comme avec un type de caractère, alors que le type défini est int
), mais les octets proviennent d'un int
et sont copiés sans modification, et ce genre de copie des octets d'un objet est généralement considéré comme une procédure mécanique ; elle est faite pour effectuer une copie et non pour réinterpréter les octets dans un nouveau type. Le "type casting" fait généralement référence à l'utilisation des différentes lvalues dans le but de réinterpréter la valeur, comme écrire un unsigned int
et lire un float
.
Quoi qu'il en soit, le type casting n'est pas vraiment le sujet de la question.
Valeurs Dans les Membres
Le titre pose la question :
Quelles valeurs pouvons-nous stocker dans les membres d'une structure ou d'une union?
Ce titre semble ne pas correspondre au contenu de la question. La question posée dans le titre est facilement répondue : Les valeurs que nous pouvons stocker dans un membre sont celles que le type du membre peut représenter. Mais la question explore ensuite le padding entre les membres. Le padding n'affecte pas les valeurs dans les membres.
Padding Prend des Valeurs Non Spécifiées
La question cite la norme :
Lorsqu'une valeur est stockée dans un objet de type structure ou union, y compris dans un objet membre, les octets de la représentation de l'objet qui correspondent à tout padding prennent des valeurs non spécifiées.
et dit :
Donc je l'ai interprété comme si nous avons un objet à stocker dans un membre tel que la taille de l'objet égale à sizeof(type_déclaré_du_membre) + padding
les octets relatifs au padding auront une valeur non spécifiée...
Le texte cité dans la norme signifie que, si les octets de padding dans s
ont été définis sur certaines valeurs, comme avec memcpy
, et que nous faisons ensuite s.a = quelque_chose;
, alors les octets de padding ne sont plus tenus de conserver leurs valeurs précédentes.
Le code dans la question explore une situation différente. Le code memcpy(&(s.a), repr, sizeof(repr));
ne stocke pas une valeur dans un membre de la structure au sens de 6.2.6.1 6. Il ne stocke ni dans s.a
ni dans s.b
. Il copie des octets, ce qui est une chose différente de ce qui est discuté en 6.2.6.1.
6.2.6.1 6 signifie que, par exemple, si nous exécutons ce code :
char repr[sizeof s] = { 0 };
memcpy(&s, repr, sizeof s); // Met tous les octets de s à des valeurs connues.
s.a = 0; // Stocke une valeur dans un membre.
memcpy(repr, &s, sizeof s); // Obtient tous les octets de s pour les examiner.
for (size_t i = sizeof s.a; i < offsetof(struct first_member_padding_t, b); ++i)
printf("Octet %zu = %d.\n", i, repr[i]);
alors il n'est pas nécessaire que tous les zéros soient imprimés - les octets dans le padding peuvent avoir changé.