Séance 4 — Structures et unions

Champs, alignement, padding, unions et allocation

Dr. El Hadji Bassirou TOURÉ

Département de Mathématiques et Informatique, Faculté des Sciences et Techniques, Université Cheikh Anta Diop de Dakar

Objectifs de la séance

Ce que la séance construit

Les types composés du C vus en mémoire : une struct range des champs de types différents côte à côte, avec du padding pour respecter l’alignement ; une union superpose ses membres aux mêmes adresses ; les structures se composent, se rangent en tableaux, se manipulent par pointeur et s’allouent sur le tas.

  • Déclarer, initialiser et typer une structure (typedef), accéder à ses champs par . et par ->.
  • Calculer l’offset de chaque champ, le padding et sizeof d’une structure, et réordonner les champs pour la réduire.
  • Lire le dump d’une structure, d’une structure imbriquée et d’un tableau de structures.
  • Choisir entre passage par valeur et passage par adresse d’une structure.
  • Réinterpréter des octets par une union, et utiliser une union discriminée.
  • Allouer une structure sur le tas, chaîner des nœuds et libérer membres et structure dans le bon ordre.

Les structures

Quoi, pourquoi, quand ça échoue : la structure

Un tableau range des objets de même type ; une structure range des objets de types différents.

Quoi

Un type composé qui regroupe des membres (champs) de types quelconques sous un même nom. Les champs sont rangés dans l’ordre de la déclaration ; chacun a un offset fixe, connu à la compilation, par rapport au premier octet de la structure.

Pourquoi

Manipuler un enregistrement (un point, une station, un relevé) comme un tout : l’affecter, le passer à une fonction, le ranger dans un tableau, l’allouer sur le tas. s.champ et p->champ ne coûtent qu’une addition d’offset.

Quand ça échoue

Entre les champs, le compilateur insère du padding : sizeof dépasse la somme des champs, memcmp compare aussi ces octets indéterminés, et une structure passée par valeur est copiée en entier.

Déclaration et initialisation

struct Point {
    int x;
    int y;
};
int main(void)
{
    struct Point p1 = {10, 20};             /* positionnelle */
    struct Point p2 = {.y = 20, .x = 10};   /* designee */
    struct Point p3 = {.x = 5};             /* y vaut 0 */
    struct Point p4 = (struct Point){100, 200};
    printf("p1 = (%d, %d)\n", p1.x, p1.y);
    printf("p2 = (%d, %d)\n", p2.x, p2.y);
    printf("p3 = (%d, %d)\n", p3.x, p3.y);
    printf("p4 = (%d, %d)\n", p4.x, p4.y);
    printf("sizeof p1 = %zu\n", sizeof p1);
    return 0;
}

Résultat \(\rightarrow\)

p1 = (10, 20)
p2 = (10, 20)
p3 = (5, 0)
p4 = (100, 200)
sizeof p1 = 8

Lecture. La déclaration crée le type struct Point ; les champs non cités dans l’initialisation valent 0 (p3.y). La forme désignée .y = 20 ne dépend pas de l’ordre des champs.

Une structure et un pointeur, pas à pas

Adresses simplifiées ; sur la machine, p est en 0x7fffffffece0 et a en 0x7fffffffec90.

Sortie \(\rightarrow\)

a = 10, b = 20
p = (15, 25)

Lecture. Ligne 9 : p occupe 8 octets : x à l’offset 0, y à l’offset 4, dans l’ordre de la déclaration.

Lecture. Ligne 10 : ptr reçoit l’adresse de p, celle de son premier octet (0x60).

Lecture. Ligne 11 : p.x lit 4 octets à l’offset 0 de p.

Lecture. Ligne 12 : ptr->y lit l’adresse dans ptr, ajoute l’offset de y (4) et lit 4 octets en 0x64.

Lecture. Premier affichage.

Lecture. Ligne 14 : l’écriture passe par l’adresse : p.x devient 15 (0f).

Lecture. Ligne 15 : (*ptr).y est ptr->y : 20 + 5 = 25 (19). Les parenthèses sont nécessaires : . lie plus fort que *.

Lecture. a et b sont des copies : elles gardent 10 et 20.

Passage par valeur, passage par adresse

Adresses simplifiées ; sous gdb, p est en 0x7fffffffec60 et q en 0x7fffffffec48, dans les deux appels.

Sortie \(\rightarrow\)

p.x = 1
p.x = 99

Lecture. Dans main : p, 8 octets dans le stack frame de main.

Lecture. Par valeur : l’appel copie les 8 octets de p dans le paramètre q, plus bas dans la pile.

Lecture. q.x = 99 modifie la copie ; p n’est pas touché (gcc avertit : q est modifié mais jamais lu).

Lecture. Au retour, le stack frame est rendu : p.x vaut toujours 1.

Lecture. Par adresse : q reçoit l’adresse de p (0x28), au même emplacement de pile.

Lecture. q->x = 99 écrit à l’adresse contenue dans q : c’est p.x qui change.

Lecture. La modification survit au retour. Passer l’adresse évite aussi de copier une grande structure.

typedef et structure auto-référentielle

typedef struct {
    int x, y;
} Point;
typedef struct Noeud {
    int valeur;
    struct Noeud *suivant;
} Noeud;
int main(void)
{
    Point p = {3, 4};
    Noeud n2 = {2, NULL};
    Noeud n1 = {1, &n2};
    printf("p = (%d, %d)\n", p.x, p.y);
    printf("%d\n", n1.suivant->valeur);
    printf("%zu %zu\n", sizeof(Point), sizeof(Noeud));
    return 0;
}

Résultat \(\rightarrow\)

p = (3, 4)
2
8 16

Lecture. Point s’écrit sans struct ; dans struct Noeud, suivant utilise l’étiquette, car le nom Noeud n’existe qu’après le typedef. 16 octets : 4 + 4 de padding + 8.

Exercice : accès aux membres

Addr +0 +1 +2 +3 +4 +5 +6 +7
0x00 0707 0000 0000 0000 fdfd ffff ffff ffff
0x08 2c2c 0101 0000 0000 dddd 0000 0000 0000
0x10 0808 0000 0000 0000 0000 0000 0000 0000
struct Point {
    int x, y;
};
struct Point a;     /* &a = 0x00 */
struct Point b;     /* &b = 0x08 */
struct Point *p;    /* &p = 0x10 */

Machine little-endian, 64 bits.

Énoncé : donner

  1. a.x et a.y ;
  2. p et p->x ;
  3. (*p).y et &p->y ;
  4. les octets de a après a = *p;.

Correction

  1. 8 octets en 0x00 : x à l’offset 0, 07 00 00 00 \(=\) 7 ; y à l’offset 4, 0xfffffffd \(=\) \(-3\) (complément à deux).
  2. 8 octets en 0x10 : p = 0x08, l’adresse de b. p->x lit 4 octets en 0x08 : 0x12c \(=\) 300.
  3. (*p).y \(\equiv\) p->y : 0x0c, 0xdd \(=\) 221. &p->y \(=\) 0x08 \(+\) offsetof(struct Point, y) \(=\) 0x0c.
  4. L’affectation copie les 8 octets de b dans a : 2c 01 00 00 dd 00 00 00 ; p et b ne changent pas.

Alignement et padding

Quoi, pourquoi, quand ça échoue : l’alignement

Les structures précédentes ont montré des octets inutilisés entre les champs : ils viennent de l’alignement.

Quoi

Un objet d’alignement \(n\) (_Alignof) se range à une adresse multiple de \(n\) ; sur x86-64, l’alignement d’un type de base est sa taille. Une structure prend l’alignement de son membre le plus exigeant ; le padding précède un champ qui serait mal aligné et complète la fin.

Pourquoi

Le processeur lit la mémoire par mots alignés : un int à cheval sur deux mots coûterait deux accès (certains processeurs le refusent). Le padding final garde chaque élément d’un tableau aligné.

Quand ça échoue

La taille dépend de l’ordre des champs et de la machine : un format de fichier ou de réseau ne se lit pas en recopiant une struct. Le contenu du padding est indéterminé.

Taille et alignement des types

struct S1 {
    char a;
    int b;
    char c;
};
#define LIGNE(T) \
    printf("%-10s %6zu %6zu\n", #T, sizeof(T), _Alignof(T))
int main(void)
{
    LIGNE(char);
    LIGNE(short);
    LIGNE(int);
    LIGNE(long);
    LIGNE(float);
    LIGNE(double);
    LIGNE(void *);
    LIGNE(struct S1);
    return 0;
}

Résultat \(\rightarrow\)

char            1      1
short           2      2
int             4      4
long            8      8
float           4      4
double          8      8
void *          8      8
struct S1      12      4

Lecture. Taille et alignement sont égaux pour les types de base ; struct S1 : 12 octets pour 6 de champs.

Le padding, pas à pas

Adresses simplifiées ; sur la machine, s est en 0x7fffffffecdc, multiple de 4 (l’alignement de struct S1).

Sortie \(\rightarrow\)

sizeof s = 12
0x7fffffffecdc : 41 7f 00 00 44 33 22 11
0x7fffffffece4 : 5a 7f 00 00

Lecture. Ligne 11 : 12 octets réservés : a à l’offset 0, b à l’offset 4 (multiple de 4), c à l’offset 8 ; les 6 octets hachurés sont du padding.

Lecture. s.a = 'A' écrit un seul octet (41).

Lecture. s.b écrit 4 octets en little-endian, à partir de l’offset 4 : les octets 1 à 3 ne sont pas touchés.

Lecture. s.c = 'Z' : un octet (5a) à l’offset 8.

Lecture. sizeof s vaut 12, et non 6 : la taille compte le padding, y compris les 3 octets de fin.

Lecture. Le dump montre aussi le padding : aucune affectation ne l’a écrit, il garde ce que contenait la pile (7f 00 00).

Mesure des offsets : offsetof et sizeof

struct Data {
    char tag;
    int value;
    short flags;
};
int main(void)
{
    size_t somme = sizeof(char) + sizeof(int) + sizeof(short);
    printf("tag   : %zu\n", offsetof(struct Data, tag));
    printf("value : %zu\n", offsetof(struct Data, value));
    printf("flags : %zu\n", offsetof(struct Data, flags));
    printf("somme des champs : %zu\n", somme);
    printf("sizeof   : %zu\n", sizeof(struct Data));
    printf("_Alignof : %zu\n", _Alignof(struct Data));
    return 0;
}

Résultat \(\rightarrow\)

tag   : 0
value : 4
flags : 8
somme des champs : 7
sizeof   : 12
_Alignof : 4

Lecture. offsetof(type, champ) (<stddef.h>) donne l’offset d’un champ en octets. Ici value est à 4 (3 octets de padding après tag), flags à 8, et 2 octets de padding final portent la taille à 12 : la somme des champs n’en fait que 7.

Réordonnancement des champs

struct Mauvaise {
    char a;
    double b;
    char c;
};
struct Bonne {
    double b;
    char a;
    char c;
};

Résultat \(\rightarrow\)

Mauvaise : a 0, b 8, c 16, sizeof 24
Bonne    : b 0, a 8, c 9, sizeof 16
1000000 elements : 24000000 contre 16000000 octets

Lecture. Dans Mauvaise, chaque char est suivi de 7 octets de padding pour aligner b et la fin. Ranger les champs par alignement décroissant ne laisse que 6 octets de padding final : 16 octets au lieu de 24, soit 8 000 000 octets (7,6 Mio) de moins pour un million d’éléments.

Comparaison de deux structures

struct S1 {
    char a;
    int b;
    char c;
};
int main(void)
{
    struct S1 u;
    struct S1 v;
    memset(&u, 0x00, sizeof u);
    memset(&v, 0xff, sizeof v);
    u.a = 'A';
    u.b = 7;
    u.c = 'Z';
    v.a = 'A';
    v.b = 7;
    v.c = 'Z';
    int egaux = u.a == v.a && u.b == v.b && u.c == v.c;
    printf("champs egaux : %d\n", egaux);
    printf("memcmp : %d\n", memcmp(&u, &v, sizeof u) != 0);
    vider(&u, sizeof u);
    vider(&v, sizeof v);
    return 0;
}

Résultat \(\rightarrow\)

champs egaux : 1
memcmp : 1
0x7fffffffecd0 : 41 00 00 00 07 00 00 00
0x7fffffffecd8 : 5a 00 00 00
0x7fffffffecdc : 41 ff ff ff 07 00 00 00
0x7fffffffece4 : 5a ff ff ff

Lecture. Champs égaux, padding différent (00, ff) : memcmp voit une différence.

Exercice : offsets, padding et taille

struct A {
  char c;
  double d;
  short s;
};
struct B {
  short s;
  char c;
  int i;
  char t;
};
struct C {
  char c1;
  char *p;
  char c2;
  int i;
};

x86-64 : l’alignement de chaque type de base est égal à sa taille.

Énoncé

  1. Offsets des champs de A, octets de padding, sizeof(struct A) ;
  2. même question pour B ;
  3. même question pour C ;
  4. réordonner les champs de A et de C pour réduire leur taille.

Correction

Chaque champ à la première adresse multiple de son alignement ; la taille, arrondie au multiple de l’alignement le plus grand.

  1. c 0, 7 octets de padding, d 8, s 16, puis 6 octets de padding final : 24 (alignement 8).
  2. s 0, c 2, 1 octet de padding, i 4, t 8, 3 octets de padding final : 12 (alignement 4).
  3. c1 0, 7 octets, p 8, c2 16, 3 octets, i 20 : 24, sans padding final.
  4. A : d, s, c aux offsets 0, 8, 10 : 16. C : p, i, c1, c2 aux offsets 0, 8, 12, 13 : 16.

Structures imbriquées et tableaux de structures

Une structure imbriquée

Adresses simplifiées ; sur la machine, r est en 0x7fffffffecd0.

Sortie \(\rightarrow\)

sizeof r = 20
offsetof bd.y = 12
hg.x = 25

Lecture. Les deux struct Point sont rangées en place, puis coul ; 3 octets de padding : 20 octets.

Lecture. r.bd.y : offset de bd dans Rect (8), plus offset de y dans Point (4) : octet 12.

Lecture. r.bd.x / 4 lit l’int à l’offset 8 et range 25 à l’offset 0.

Lecture. sizeof r : 20 octets, padding compris.

Lecture. offsetof accepte un membre imbriqué : bd.y est à 12 octets du début.

Lecture. Chaque opérateur . ajoute un offset fixé à la compilation : aucun pointeur n’est stocké.

Un tableau de structures

Adresses simplifiées ; sur la machine, t est en 0x7fffffffecd0 ; padding : octets non montrés.

Sortie \(\rightarrow\)

D 31
T 34
K 38
16

Lecture. sizeof(struct Releve) vaut 8 : ville (1 octet), 3 octets de padding, temp à l’offset 4. Les trois éléments se suivent, de 8 en 8.

Lecture. t devient &t[0] : p contient 0x38.

Lecture. p->ville lit l’octet à l’offset 0, p->temp les 4 octets à l’offset 4.

Lecture. p++ avance d’un élément : \(1\times\texttt{sizeof(struct Releve)} = 8\) octets, 0x40.

Lecture. p désigne t[1].

Lecture. p + 1 : encore 8 octets, 0x48 \(=\) &t[2].

Lecture. p désigne t[2].

Lecture. Entre char *, l’écart se compte en octets : \(2\times8=16\). Le padding se répète dans chaque élément.

Exercice : un tableau de structures

Addr +0 +1 +2 +3 +4 +5 +6 +7
0x40 4444 a5a5 0c0c 0000 5e5e 0101 0000 0000
0x48 545454 a5a5 0d0d 0000 7878 0000 0000 0000
0x50 5a5a a5a5 0e0e0e 000000 c8c8c8 050505 000000 000000
struct Mesure {
    char code;
    short jour;
    int pluie;
};
struct Mesure m[3];         /* en 0x40 */
struct Mesure *q = &m[1];

Énoncé

  1. sizeof, offsets, padding ;
  2. les éléments de m (44 D, 54 T, 5a Z) ;
  3. l’adresse &m[2].pluie ;
  4. q->code et (q + 1)->jour.

Correction

  1. code 0, 1 octet de padding (a5, gris), jour 2, pluie 4 : 8 octets, un élément par ligne.
  2. {'D', 12, 350}, {'T', 13, 120}, {'Z', 14, 1480} (0x15e, 0x78, 0x5c8).
  3. \(\texttt{0x40}+2\times8+4=\texttt{0x54}\) : c8 05 00 00.
  4. q \(=\) 0x48 : q->code \(=\) 'T' ; q + 1 \(=\) 0x50 : jour \(=\) 14.

Les unions

Quoi, pourquoi, quand ça échoue : l’union

Une structure range ses membres côte à côte ; une union les superpose.

Quoi

Un type composé dont tous les membres commencent à l’offset 0 et partagent les mêmes octets ; sizeof est celle du plus grand membre, arrondie à l’alignement. Écrire un membre modifie les autres.

Pourquoi

Ranger l’une ou l’autre de plusieurs valeurs sans réserver la place de toutes (variantes d’un message), ou relire les octets d’un objet avec un autre type (un float vu comme entier).

Quand ça échoue

Rien n’indique le dernier membre écrit : en lire un autre réinterprète les octets (permis en C, pas en C++), selon l’endianness et le format des types. Une union discriminée garde ce renseignement dans un champ.

Une union, pas à pas

Adresses simplifiées ; sur la machine, d est en 0x7fffffffecf4.

Sortie \(\rightarrow\)

DCBA
i = 0x3f800000
f = -4
sizeof d = 4

Lecture. Une union réserve la place de son plus grand membre : 4 octets, partagés par i, f et c.

Lecture. d.i écrit les 4 octets : 44 43 42 41. Lus comme float, ils valent environ 12,14.

Lecture. c[0] et c[1] : 'D' et 'C' ; rien n’est encore affiché (pas de fin de ligne).

Lecture. "DCBA" : l’ordre des octets en mémoire, little-endian.

Lecture. d.f = 1.0f écrit le motif IEEE 754 de 1,0 : 0x3f800000.

Lecture. Relu comme int, le même motif donne 0x3f800000 : aucune conversion, une réinterprétation.

Lecture. Un seul octet change : le poids fort, qui porte le signe et une partie de l’exposant.

Lecture. 0xc0800000 : signe 1, exposant \(129-127=2\), mantisse 1 : \(-1\times2^2=-4\).

Lecture. sizeof d : 4, la taille du plus grand membre.

L’union discriminée (tagged union)

enum Type { ENTIER, REEL, TEXTE };
struct Variant {
    enum Type type;         /* discriminant */
    union {
        int i;
        double d;
        const char *s;
    } val;
};

Résultat \(\rightarrow\)

entier 221
reel 2.5
texte Thies
sizeof v = 16
void afficher(const struct Variant *v)
{
    switch (v->type) {
    case ENTIER:
        printf("entier %d\n", v->val.i);
        break;
    case REEL:
        printf("reel %g\n", v->val.d);
        break;
    case TEXTE:
        printf("texte %s\n", v->val.s);
        break;
    }
}

Lecture. Le champ type (le discriminant) dit quel membre de val est valide ; afficher le teste avant toute lecture. L’union anonyme val n’a pas de nom de type : ses membres s’écrivent v.val.i, v.val.d, v.val.s.

L’union discriminée, pas à pas

Adresses simplifiées ; sur la machine, v est en 0x7fffffffecd0 ; le littéral est en 0x555555556026.

Sortie \(\rightarrow\)

entier 221
reel 2.5
texte Thies
sizeof v = 16

Lecture. 16 octets : type (4), 4 octets de padding, puis l’union val, alignée sur 8 à cause du double et du pointeur.

Lecture. ENTIER vaut 0 : l’énumération est rangée comme un int.

Lecture. val.i n’écrit que les 4 premiers octets de l’union.

Lecture. afficher lit type, puis le membre qui lui correspond.

Lecture. Le discriminant change d’abord : REEL (1).

Lecture. 2.5 occupe les 8 octets : 0x4004000000000000.

Lecture. type vaut REEL : afficher lit val.d.

Lecture. val.s reçoit l’adresse du littéral "Thies", en .rodata : 8 octets.

Lecture. Le discriminant dit quel membre lire : rien d’autre ne garde la trace du dernier membre écrit.

Exercice : une union discriminée

Addr +0 +1 +2 +3 +4 +5 +6 +7
0x00 0101 0000 0000 0000 0000 0000 0000 0000
0x08 0000 0000 0000 0000 0000 0000 0c0c 4040
0x10 0000 0000 0000 0000 0000 0000 0000 0000
0x18 dddd 0000 0000 0000 0000 0000 0c0c 4040
struct Variant {
    int type;      /* 0 int, 1 double */
    union {
        int i;
        double d;
    } val;
};
struct Variant v[2];        /* en 0x00 */

Énoncé

  1. sizeof, offset de val, padding ;
  2. v[0].type et la valeur rangée ;
  3. v[1].type et la valeur rangée ;
  4. ce que lirait v[1].val.d.

Correction

  1. val (un double) est alignée sur 8 : offset 8, après 4 octets de padding ; 16 octets.
  2. 1 : val.d \(=\) 0x400c000000000000 \(=\) \(1{,}75\times2=3{,}5\).
  3. 0 : val.i \(=\) 0xdd \(=\) 221 (en 0x18).
  4. 0x400c0000000000dd : un peu plus de 3,5 ; 0x1c à 0x1f restent d’un double antérieur.

Champs de bits

Des champs de bits, pas à pas

Adresses simplifiées ; sur la machine, f est en 0x7fffffffecf4.

Sortie \(\rightarrow\)

sizeof f = 4
0x7fffffffecf4 : cb 00 00 00
mode = 1
0x7fffffffecf4 : c3 00 00 00

Lecture. Les trois champs tiennent dans une unité de 4 octets (unsigned int) ; {0} met les champs à zéro.

Lecture. L’ordre des bits dépend de l’implémentation (C17 6.7.2.1 §11) ; ici gcc, ABI System V x86-64 : le premier champ occupe les bits de poids faible.

Lecture. mode occupe les bits 1 à 3 : \(5 = 101_2\), d’où 0000 1011 \(=\) 0x0b.

Lecture. priorite occupe les bits 4 à 7 : \(12 = 1100_2\), d’où 0xcb.

Lecture. 8 bits utiles, 4 octets : la taille est celle de l’unité de stockage.

Lecture. Le dump confirme cb 00 00 00.

Lecture. 9 ne tient pas sur 3 bits : seuls les 3 bits de poids faible sont gardés, 1. gcc l’avertit (-Woverflow).

Lecture. mode vaut 1, pas 9.

Lecture. Seul le champ mode (bits 1 à 3) est réécrit ; de 101 à 001, seul le bit 3 change : cb devient c3.

Portabilité : masques et décalages

struct Flags1 {
    unsigned int a : 1;
    unsigned int b : 3;
    unsigned int c : 4;
};
struct Flags2 {
    unsigned int a : 1;
    unsigned int b : 3;
    unsigned int : 0;
    unsigned int c : 4;
};
int main(void)
{
    unsigned char o = 0;
    o |= 1u;                /* actif : bit 0 */
    o |= 5u << 1;           /* mode : bits 1 a 3 */
    o |= 12u << 4;          /* priorite : bits 4 a 7 */
    printf("masques : 0x%02x\n", o);
    printf("mode = %u\n", (o >> 1) & 0x7u);
    printf("Flags1 : %zu\n", sizeof(struct Flags1));
    printf("Flags2 : %zu\n", sizeof(struct Flags2));
    return 0;
}

Résultat \(\rightarrow\)

masques : 0xcb
mode = 5
Flags1 : 4
Flags2 : 8

Lecture. Masques et décalages donnent 0xcb, avec une disposition fixée par le programme. Celle des champs de bits dépend de l’implémentation ; : 0 (ligne 10) ouvre une nouvelle unité : 8 octets.

Exercice : champs de bits

struct Etat {
    unsigned int marche : 1;
    unsigned int vitesse : 3;
    unsigned int niveau : 4;
};
struct Etat e = {1, 6, 9};

gcc, x86-64 : le premier champ occupe les bits de poids faible de l’unité.

Énoncé

  1. sizeof e ;
  2. l’octet 0 de e, en binaire puis en hexadécimal ;
  3. ce que contiennent les octets 1 à 3 ;
  4. après e.vitesse = 10; : la valeur de e.vitesse et l’octet 0.

Correction

  1. 8 bits utiles, une unité unsigned int : 4 octets.
  2. niveau \(9=1001_2\) (bits 7 à 4), vitesse \(6=110_2\) (bits 3 à 1), marche 1 (bit 0) : 1001 110 1 \(=\) 0x9d.
  3. Aucun champ : 24 bits de padding de l’unité, au contenu indéterminé (l’initialisation ne fixe que les champs).
  4. \(10=1010_2\) ne tient pas sur 3 bits : il reste \(010_2=2\). L’octet devient 1001 010 1 \(=\) 0x95.

Structures et allocation dynamique

Une structure sur le tas

Adresses simplifiées ; sous gdb, st est en 0x7fffffffec68 (pile), le bloc en 0x5555555592a0 (tas).

Sortie \(\rightarrow\)

Kedougou

Lecture. 16 octets sur le tas : nom (8), releves (4), 4 de padding ; st, sur la pile, en garde l’adresse.

Lecture. st->releves : offset 8 dans le bloc.

Lecture. nom reçoit l’adresse d’un second bloc de 9 octets : la structure ne garde que le pointeur.

Lecture. strcpy écrit les 8 lettres et le terminateur dans le second bloc.

Lecture. st->nom suit deux adresses : st, puis nom.

Lecture. Les membres alloués se libèrent d’abord : après free(st), st->nom ne serait plus lisible.

Lecture. Puis la structure. st contient toujours 0x40 : une adresse qui ne doit plus servir.

Création et destruction

Conteneur *creer(const char *nom, size_t n)
{
    Conteneur *c = malloc(sizeof *c);
    if (c == NULL)
        return NULL;
    c->nom = malloc(strlen(nom) + 1);
    c->donnees = calloc(n, sizeof(int));
    c->taille = n;
    if (c->nom == NULL || c->donnees == NULL) {
        detruire(c);
        return NULL;
    }
    strcpy(c->nom, nom);
    return c;
}
typedef struct {
    char *nom;
    int *donnees;
    size_t taille;
} Conteneur;
void detruire(Conteneur *c)
{
    if (c != NULL) {
        free(c->nom);       /* membres */
        free(c->donnees);
        free(c);            /* structure */
    }
}

Lecture. Trois blocs par conteneur : la structure, le nom, les données. detruire libère les membres, puis la structure (free(NULL) est sans effet). Avec creer("Matam", 4) puis detruire, valgrind affiche « All heap blocks were freed ».

Une structure auto-référentielle : deux nœuds

Adresses simplifiées ; sous gdb, les nœuds sont en 0x5555555592a0 et 0x5555555592c0 (tas) ; a et b, sur la pile, contiennent ces adresses.

Sortie \(\rightarrow\)

10
20

Lecture. Un struct Noeud contient un pointeur vers un objet de son propre type : 16 octets, dont 4 de padding avant suivant.

Lecture. Second bloc, 32 octets plus loin : la glibc place 16 octets d’en-tête et d’arrondi entre les deux (non montrés).

Lecture. a->suivant reçoit l’adresse du second nœud : le chaînage n’est qu’une adresse rangée dans un champ.

Lecture. NULL (8 octets à zéro) marque la fin de la liste.

Lecture. p = a : premier tour, p->valeur lit 10.

Lecture. p = p->suivant : p prend 0x60 ; second tour, 20.

Lecture. p prend NULL : la condition est fausse, la boucle s’arrête.

Flexible array member (C99)

struct Tableau {
    size_t longueur;
    int donnees[];          /* en dernier */
};
int main(void)
{
    size_t n = 4;
    struct Tableau *t = malloc(sizeof *t + n * sizeof(int));
    t->longueur = n;
    for (size_t i = 0; i < n; i++)
        t->donnees[i] = 10 * (int)i;
    printf("sizeof : %zu\n", sizeof *t);
    printf("offsetof : %zu\n", offsetof(struct Tableau, donnees));
    printf("donnees[3] = %d\n", t->donnees[3]);
    free(t);
    return 0;
}

Résultat \(\rightarrow\)

sizeof : 8
offsetof : 8
donnees[3] = 30

Lecture. donnees[], dernier champ, n’a pas de taille : sizeof vaut 8. Un seul malloc réserve l’en-tête et les \(n\) éléments (offset 8) ; un seul free rend le tout.

Structures opaques

pile.h (vu par le client)

typedef struct Pile Pile;
Pile *pile_creer(void);
void pile_empiler(Pile *s, int v);
int pile_depiler(Pile *s);
void pile_detruire(Pile *s);

pile.c (seul à connaître les champs)

struct Pile {
    int *donnees;
    size_t capacite;
    size_t sommet;
};
typedef struct Pile Pile;   /* type incomplet */
Pile *pile_creer(void);
int main(void)
{
    Pile *p = NULL;         /* permis : 8 octets */
    Pile q;                 /* taille inconnue */
    printf("%p\n", (void *)p);
    return 0;
}

Compilateur \(\rightarrow\)

s6_opaque.c:7:10: error: storage size of 'q' isn't known
s6_opaque.c:7:10: warning: unused variable 'q' [-Wunused-variable]

Lecture. Le client ne voit qu’une déclaration anticipée : struct Pile est un type incomplet. Un pointeur Pile * se déclare (sa taille est connue), une variable Pile non ; seules les fonctions de pile.c accèdent aux champs, qui peuvent changer sans modifier le client.

Exercice : une liste chaînée dans un dump

Addr +0 +1 +2 +3 +4 +5 +6 +7
0x100 0505 0000 0000 0000 0000 0000 0000 0000
0x108 0000 0000 0000 0000 0000 0000 0000 0000
0x110 0909 0000 0000 0000 0000 0000 0000 0000
0x118 2020 0101 0000 0000 0000 0000 0000 0000
0x120 0202 0000 0000 0000 0000 0000 0000 0000
0x128 0000 0101 0000 0000 0000 0000 0000 0000
struct Noeud {
    int valeur;
    struct Noeud *suivant;
};
struct Noeud pool[3];        /* en 0x100 */
struct Noeud *tete = &pool[1];

Énoncé

  1. sizeof, offsets, padding ;
  2. tete->valeur et tete->suivant ;
  3. tete->suivant->valeur et tete->suivant->suivant ;
  4. les valeurs affichées en suivant la liste.

Correction

  1. valeur 0, 4 octets de padding (gris), suivant 8 : 16 octets.
  2. En 0x110 : valeur \(=\) 9, suivant \(=\) 0x120.
  3. En 0x120 : 2 et 0x100.
  4. En 0x100 : 5 et NULL : la liste affiche 9, 2, 5, dans l’ordre des pointeurs, pas des adresses.

Synthèse

À retenir

À retenir

  1. Une struct range ses champs dans l’ordre de la déclaration, chacun à un offset fixe ; s.c et p->c ((*p).c) ajoutent cet offset à l’adresse de la structure.
  2. Chaque champ est aligné sur son type ; le padding comble les trous et la fin : sizeof \(\geq\) somme des champs, multiple de l’alignement le plus exigeant. offsetof et sizeof le mesurent.
  3. Ranger les champs par alignement décroissant réduit le padding. Le contenu du padding est indéterminé : pas de memcmp sur des structures.
  4. L’affectation et le passage par valeur copient toute la structure ; passer l’adresse permet de la modifier et évite la copie.
  5. Une union superpose ses membres à l’offset 0 : sizeof est celle du plus grand ; lire un autre membre réinterprète les octets ; un discriminant dit quel membre est valide.
  6. Les champs de bits dépendent de l’implémentation ; masques et décalages sont portables.
  7. Sur le tas, chaque membre alloué se libère avant la structure qui le contient ; un nœud contient l’adresse du suivant, NULL en fin de liste.

Prochaine séance

Séance 5 : allocation dynamique et structures chaînées

Propriété d’un bloc, tableau dynamique et coût amorti, listes chaînées par pointeur de pointeur, piles et files sur le tas, erreurs du tas sous valgrind et AddressSanitizer, et intérieur de malloc (en-têtes, tcache, mini-allocateur first fit).

À préparer

Le TD et le TP de la séance 4 : offsets et padding à calculer, dumps de structures et d’unions à lire, union discriminée et liste chaînée à compléter et à vérifier sous valgrind.

Références

  • R. E. Bryant, D. R. O’Hallaron, Computer Systems: A Programmer’s Perspective, 3e éd., Pearson, 2016 : chap. 3.9 (structures, unions, alignement des données).
  • B. W. Kernighan, D. M. Ritchie, The C Programming Language, 2e éd., Prentice Hall, 1988 : chap. 6 (structures, typedef, unions, champs de bits).
  • K. N. King, C Programming: A Modern Approach, 2e éd., Norton, 2008 : chap. 16 (structures, unions, énumérations), 17.5 (listes chaînées), 19.4 (types opaques), 20.2 (champs de bits).
  • ISO/IEC 9899:2018 (C17) : 6.2.8 (alignement), 6.5.2.3 (accès aux membres), 6.7.2.1 (structures, unions, champs de bits, flexible array member), 7.19 (offsetof).

Mesures de la séance : x86-64, Linux 6.18, gcc 13.3.0 (-std=c17 -Wall -Wextra -O0 -g), glibc 2.39, valgrind 3.22.0 ; adresses rendues déterministes par setarch -R, pile de 8192 Kio.