J'étudie les bases du langage C. Je suis arrivé au chapitre des structures avec des champs de bits. Le livre montre un exemple de structure avec deux types de données différents: divers bools et diverses ints non signées.
Le livre déclare que la structure a une taille de 16 bits et que, sans rembourrage, la structure mesurerait 10 bits.
C'est la structure que le livre utilise dans l'exemple:
#include <stdio.h>
#include <stdbool.h>
struct test{
bool opaque : 1;
unsigned int fill_color : 3;
unsigned int : 4;
bool show_border : 1;
unsigned int border_color : 3;
unsigned int border_style : 2;
unsigned int : 2;
};
int main(void)
{
struct test Test;
printf("%zu\n", sizeof(Test));
return 0;
}
Pourquoi mon compilateur a-t-il exactement la même structure qui mesure 16 octets (plutôt que des bits) avec remplissage et 16 octets sans remplissage?
J'utilise
GCC (tdm-1) 4.9.2 compiler;
Code::Blocks as IDE.
Windows 7 64 Bit
Intel CPU 64 bit
C'est le résultat que j'obtiens:
Voici une image de la page où l'exemple est:
Microsoft ABI définit les champs de bits d’une manière différente de celle de GCC sur d’autres plates-formes. Vous pouvez choisir d'utiliser la présentation compatible avec Microsoft avec l'option -mms-bitfields
ou la désactiver avec -mno-ms-bitfields
. Il est probable que votre version de GCC utilise -mms-bitfields
par défaut.
Selon la documentation, lorsque -mms-bitfields
est activé:
- Chaque objet de données a une exigence d'alignement. L'exigence d'alignement pour toutes les données, à l'exception des structures, des unions et des tableaux, correspond à la taille de l'objet ou à la taille d'emballage actuelle (spécifiée avec l'attribut aligné ou le pragma de l'emballage), selon la valeur la moins élevée. Pour les structures, les unions et les tableaux, l'exigence d'alignement est la plus grande exigence d'alignement de ses membres. Un offset est attribué à chaque objet pour que: offset% alignment_requirement == 0
- Les champs de bits adjacents sont regroupés dans la même unité d'allocation de 1, 2 ou 4 octets si les types d'intégrale ont la même taille et si le champ de bits suivant s'inscrit dans l'unité d'allocation actuelle sans dépasser la limite imposée par le commun exigences d'alignement des champs de bits.
Puisque bool
et unsigned int
ont des tailles différentes, ils sont compressés et alignés séparément, ce qui augmente considérablement la taille de la structure. L'alignement de unsigned int
est de 4 octets, et devoir réaligner trois fois au milieu de la structure conduit à une taille totale de 16 octets.
Vous pouvez obtenir le même comportement du livre en remplaçant bool
par unsigned int
ou en spécifiant -mno-ms-bitfields
(bien que cela signifie que vous ne pourrez pas interagir avec du code compilé sur des compilateurs Microsoft).
Notez que la norme C ne spécifie pas comment les champs de bits sont disposés. Donc, ce que votre livre dit peut être vrai pour certaines plates-formes mais pas pour toutes.
Considéré comme décrivant les dispositions de la norme de langage C, votre texte contient des revendications injustifiées. De manière spécifique, non seulement la norme not indique que unsigned int
est l'unité de base de la structure de tout type, mais elle exclut explicitement toute exigence relative à la taille des unités de stockage dans lesquelles les représentations de champ binaire sont stockées:
Une implémentation peut allouer toute unité de stockage adressable de grande taille assez pour tenir un champ de bits.
Le texte émet également des hypothèses sur le remplissage qui ne sont pas prises en charge par la norme. Une implémentation C est libre d'inclure une quantité arbitraire de remplissage après tout ou partie des éléments et des unités de stockage de champ binaire d'une struct
, y compris la dernière. Les implémentations utilisent généralement cette liberté pour résoudre les problèmes d'alignement des données, mais C ne limite pas le remplissage à cette utilisation. Ceci est tout à fait séparé des champs de bits non nommés que votre texte appelle "bourrage".
Cependant, je suppose que le livre mérite d’être félicité d’avoir évité l’idée fausse très répandue selon laquelle C exige que le type de données déclaré d’un champ de bits ait un rapport quelconque avec la taille de la ou des unités de stockage dans lesquelles se trouve sa représentation. La norme ne fait pas une telle association.
Pourquoi mon compilateur a-t-il exactement la même structure qui mesure 16 octets (plutôt que des bits) avec remplissage et 16 octets sans remplissage?
Pour réduire autant que possible le texte, il fait la distinction entre le nombre de bits de données occupés par les membres (16 bits au total, 6 appartenant à des champs binaires non nommés) et la taille globale des instances de la variable struct
. Il semble affirmer que la structure globale aura la taille d'un unsigned int
, qui correspond apparemment à 32 bits sur le système qu'il décrit, et que ce serait la même chose pour les deux versions de la structure.
En principe, vos tailles observées pourraient être expliquées par votre implémentation utilisant une unité de stockage 128 bits pour les champs de bits. En pratique, il utilise probablement une ou plusieurs unités de stockage plus petites, de sorte qu'une partie de l'espace supplémentaire dans chaque structure est imputable au remplissage fourni par la mise en œuvre, tel que celui que j'ai mentionné ci-dessus.
Il est très courant que les implémentations C imposent une taille minimale à tous les types de structure et, par conséquent, étendent les représentations à cette taille lorsque cela est nécessaire. Souvent, cette taille correspond à l'exigence d'alignement la plus stricte de tout type de données pris en charge par le système, bien qu'il s'agisse là encore d'une considération d'implémentation, et non d'une exigence du langage.
En bout de ligne: ce n'est qu'en vous fiant aux détails d'implémentation et/ou aux extensions que vous pouvez prédire la taille exacte d'une struct
, indépendamment de la présence ou non de membres de champ de bits.
À ma grande surprise, il semble y avoir une différence entre certains compilateurs en ligne de GCC 4.9.2. Tout d'abord, voici mon code:
#include <stdio.h>
#include <stdbool.h>
struct test {
bool opaque : 1;
unsigned int fill_color : 3;
unsigned int : 4;
bool show_border : 1;
unsigned int border_color : 3;
unsigned int border_style : 2;
unsigned int : 2;
};
struct test_packed {
bool opaque : 1;
unsigned int fill_color : 3;
unsigned int : 4;
bool show_border : 1;
unsigned int border_color : 3;
unsigned int border_style : 2;
unsigned int : 2;
} __attribute__((packed));
int main(void)
{
struct test padding;
struct test_packed no_padding;
printf("with padding: %zu bytes = %zu bits\n", sizeof(padding), sizeof(padding) * 8);
printf("without padding: %zu bytes = %zu bits\n", sizeof(no_padding), sizeof(no_padding) * 8);
return 0;
}
Et maintenant, résultats de différents compilateurs.
GCC 4.9.2 de WandBox:
with padding: 4 bytes = 32 bits
without padding: 2 bytes = 16 bits
GCC 4.9.2 de http://cpp.sh/ :
with padding: 4 bytes = 32 bits
without padding: 2 bytes = 16 bits
MAIS
GCC 4.9.2 de theonlinecompiler.com:
with padding: 16 bytes = 128 bits
without padding: 16 bytes = 128 bits
(pour compiler correctement, vous devez chagne %zu
à %u
)
MODIFIER
@ interjay's answer pourrait expliquer cela. Quand j'ai ajouté -mms-bitfields
à GCC 4.9.2 de WandBox, j'ai eu ceci:
with padding: 16 bytes = 128 bits
without padding: 16 bytes = 128 bits
La norme C ne décrit pas tous les détails sur la manière dont les variables doivent être placées en mémoire. Cela laisse place à une optimisation qui dépend de la plate-forme utilisée.
Pour vous donner une idée de la façon dont les choses se situent dans la mémoire, vous pouvez faire comme ceci:
#include <stdio.h>
#include <stdbool.h>
struct test{
bool opaque : 1;
unsigned int fill_color : 3;
unsigned int : 4;
bool show_border : 1;
unsigned int border_color : 3;
unsigned int border_style : 2;
unsigned int : 2;
};
int main(void)
{
struct test Test = {0};
int i;
printf("%zu\n", sizeof(Test));
unsigned char* p;
p = (unsigned char*)&Test;
for(i=0; i<sizeof(Test); ++i)
{
printf("%02x", *p);
++p;
}
printf("\n");
Test.opaque = true;
p = (unsigned char*)&Test;
for(i=0; i<sizeof(Test); ++i)
{
printf("%02x", *p);
++p;
}
printf("\n");
Test.fill_color = 3;
p = (unsigned char*)&Test;
for(i=0; i<sizeof(Test); ++i)
{
printf("%02x", *p);
++p;
}
printf("\n");
return 0;
}
En exécutant ceci sur ideone ( https://ideone.com/wbR5tI ) je reçois:
4
00000000
01000000
07000000
Donc, je peux voir que opaque
et fill_color
sont tous deux dans le premier octet . Exécuter exactement le même code sur une machine Windows (en utilisant gcc) donne:
16
00000000000000000000000000000000
01000000000000000000000000000000
01000000030000000000000000000000
Donc ici je peux voir que opaque
et fill_color
sont pas tous les deux dans le premier octet. Il semble que opaque
occupe 4 octets.
Ceci explique que vous obtenez un total de 16 octets, c’est-à-dire que la bool
prend 4 octets chacun, puis 4 octets pour les champs situés entre et après.
Avant que l'auteur définisse la structure, il dit qu'il veut diviser les champs de bits en deux octets. Un octet contient les champs de bits pour les informations de remplissage et un octet pour les informations de bordure.
Pour cela, il ajoute (insère) quelques bits inutilisés (bitfield):
unsigned int 4; // padding of the first byte
il affiche également le deuxième octet, mais cela n’est pas nécessaire.
Donc, avant le remplissage, il y aurait 10 bits utilisés et après le remplissage, 16 bits ont été définis (mais pas tous utilisés) .
bool
pour indiquer un champ 1/0. L’auteur suppose ensuite que le type _Bool
C99 a un alias de bool
. Mais il semble que les compilateurs soient un peu confus ici. Remplacer bool
par unsigned int
le résoudrait. À partir de C99:6.3.2: Les éléments suivants peuvent être utilisés dans une expression partout où un int ou un unsigned int peut être utilisé:
- Un champ de bits de type
_Bool
,int
,signed int
ouunsigned int
.
Vous interprétez complètement ce que dit le livre.
Il y a 16 bits de champs de bits déclarés. 6 bits sont des champs non nommés qui ne peuvent être utilisés pour rien - c'est le remplissage mentionné. 16 bits moins 6 bits équivaut à 10 bits. Sans compter les champs de remplissage, la structure a 10 bits utiles.
Le nombre d'octets de la structure dépend de la qualité du compilateur. Apparemment, vous avez rencontré un compilateur qui ne contient pas de champs de bits bool dans une structure et utilise 4 octets pour un bool, de la mémoire pour les champs de bits, plus un remplissage de structure, un total de 4 octets, 4 autres octets pour un bool et davantage de mémoire pour les champs , plus le remplissage de structure, total 4 octets, ajoutant jusqu'à 16 octets. C'est plutôt triste vraiment. Cette structure pourrait être raisonnablement deux octets.
Il y a toujours eu deux façons courantes d'interpréter les types d'éléments de champ de bits:
Examinez si le type est signé ou non signé, mais ignorez les distinctions Entre "char", "short", "int", etc. pour décider où un élément devrait être placé
À moins qu'un champ de bits ne soit précédé d'un autre de même type, ou du type signé/non signé correspondant, allouez un objet de ce type et place le champ de bits qu'il contient. Placez les champs de bits suivants avec le même , Tapez cet objet s’ils y correspondent.
Je pense que la motivation derrière # 2 était que sur une plate-forme où les valeurs 16 bits doivent être alignées sur Word, un compilateur étant donné quelque chose comme:
struct foo {
char x; // Not a bitfield
someType f1 : 1;
someType f2 : 7;
};
pourrait être en mesure d'allouer une structure à deux octets, les deux champs étant placés dans le deuxième octet, mais si la structure avait été:
struct foo {
char x; // Not a bitfield
someType f1 : 1;
someType f2 : 15;
};
il serait nécessaire que tout f2
soit contenu dans un seul mot de 16 bits, ce qui nécessiterait donc un octet de remplissage avant f1
. En raison de la règle de séquence initiale commune, f1
doit être placé de manière identique dans ces deux structures, ce qui impliquerait que si f1
pouvait satisfaire à la règle de séquence initiale commune, il aurait besoin d'un remplissage avant même la première structure.
En l'état, le code qui veut permettre la mise en page plus dense dans le premier cas peut dire:
struct foo {
char x; // Not a bitfield
unsigned char f1 : 1;
unsigned char f2 : 7;
};
et invitez le compilateur à mettre les deux champs de bits dans un octet juste après x
. Comme le type est spécifié en tant que unsigned char
, le compilateur n'a pas à s'inquiéter de la possibilité d'un champ de 15 bits. Si la mise en page était:
struct foo {
char x; // Not a bitfield
unsigned short f1 : 1;
unsigned short f2 : 7;
};
et l'intention était que f1
et f2
se trouvent dans le même stockage, le compilateur devrait alors placer f1 de manière à prendre en charge un accès aligné sur Word pour son "bunkmate" f2
. Si le code était:
struct foo {
char x; // Not a bitfield
unsigned char f1 : 1;
unsigned short f2 : 15;
};
alors f1
serait placé après x
et f2
dans un mot seul.
Notez que la norme C89 a ajouté une syntaxe pour forcer la disposition empêchant que f1
soit placé dans un octet avant le stockage utilisé f2
:
struct foo {
char x; // Not a bitfield
unsigned short : 0; // Forces next bitfield to start at a "short" boundary
unsigned short f1 : 1;
unsigned short f2 : 15;
};
L'ajout de la syntaxe: 0 dans C89 élimine en grande partie le besoin de demander aux compilateurs de considérer les types changeants comme un alignement forcé, sauf lors du traitement de code ancien.