Les structures

Dans le chapitre précédent, nous vous avions dit que les pointeurs étaient entre autres utiles pour manipuler des données complexes. Les structures sont les premières que nous allons étudier.

Une structure est un regroupement de plusieurs objets, de types différents ou non. Grosso modo, une structure est finalement une boîte qui regroupe plein de données différentes.

Définition, initialisation et utilisation

Définition d’une structure

Une structure étant un regroupement d’objets, la première chose à réaliser est la description de celle-ci (techniquement, sa définition), c’est-à-dire préciser de quel(s) objet(s) cette dernière va se composer.

La syntaxe de toute définition est la suivante.

struct étiquette
{
    /* Objet(s) composant(s) la structure. */
};

Prenons un exemple concret : vous souhaitez demander à l’utilisateur deux mesures de temps sous la forme heure(s):minute(s):seconde(s).milliseconde(s) et lui donner la différence entre les deux en secondes. Vous pourriez utiliser six variables pour stocker ce que vous fournit l’utilisateur, toutefois cela reste assez lourd. À la place, nous pourrions représenter chaque mesure à l’aide d’une structure composée de trois objets : un pour les heures, un pour les minutes et un pour les secondes.

struct temps {
    unsigned heures;
    unsigned minutes;
    double secondes;
};

Comme vous le voyez, nous avons donné un nom (plus précisément, une étiquette) à notre structure : « temps ». Les règles à respecter sont les mêmes que pour les noms de variable et de fonction.

Pour le reste, la composition de la structure (sa définition) est décrite à l’aide d’une suite de déclarations de variables. Ces différentes déclarations constituent les membres ou champs de la structure.

Enfin, notez la présence d’un point-virgule obligatoire à la fin de la définition de la structure.

Une structure ne peut pas comporter plus de cent vingt-sept membres.

Définition d’une variable de type structure

Une fois notre structure décrite, il ne nous reste plus qu’à créer une variable de ce type. Pour ce faire, la syntaxe est la suivante.

struct étiquette identificateur;

La méthode est donc la même que pour définir n’importe quelle variable, si ce n’est que le type de la variable est précisé à l’aide du mot-clé struct et de l’étiquette de la structure. Avec notre exemple de la structure temps, cela donne ceci.

struct temps {
    unsigned heures;
    unsigned minutes;
    double secondes;
};


int main(void)
{
    struct temps t;

    return 0;
}

Notez qu’il est parfaitement possible d’utiliser le même identificateur (le même nom si vous préférez) pour une variable, une étiquette de structure et un membre de structure. Ainsi, le code suivant est parfaitement valide.

struct test {
    int test;
};

int main(void)
{
    struct test test;
    test.test = 10;
    return 0;
}

De part la présence du mot-clé struct pour désigner l’étiquette de la structure ou de l’opérateur . (ou -> que nous verrons bientôt) pour désigner un membre de la structure, il n’y a pas de risque de confusion avec une variable. Du point de vue du langage, on dit que les identificateurs d’étiquette de structure, de membre de structure et de variable appartiennent à des espaces de nom (namespace en anglais) différents.

Initialisation

Comme pour n’importe quelle autre variable, il est possible d’initialiser une variable de type structure dès sa définition. Toutefois, à l’inverse des autres, l’initialisation s’effectue à l’aide d’une liste fournissant une valeur pour un ou plusieurs membres de la structure.

Initialisation séquentielle

L’initialisation séquentielle permet de spécifier une valeur pour un ou plusieurs membres de la structure en suivant l’ordre de la définition. Ainsi, l’exemple ci-dessous initialise le membre heures à 1, minutes à 45 et secondes à 30.560.

struct temps t = { 1, 45, 30.560 };
Initialisation sélective

L’initialisation séquentielle n’est toutefois pas toujours pratique, surtout si les champs que vous souhaitez initialiser sont par exemple au milieu d’une grande structure. Pour éviter ce problème, il est possible de recourir à une initialisation sélective en spécifiant explicitement le ou les champs à initialiser. L’exemple ci-dessous est identique au premier si ce n’est qu’il recourt à une initialisation sélective.

struct temps t = { .secondes = 30.560, .minutes =  45, .heures = 1 };

Notez qu’il n’est plus nécessaire de suivre l’ordre de la définition dans ce cas.

Il est parfaitement possible de mélanger les initialisations séquentielles et sélectives. Dans un tel cas, l’initialisation séquentielle reprend au dernier membre désigné par une initialisation sélective. Partant, le code suivant initialise le membre heures à 1 et le membre secondes à 30.560.

struct temps t = { 1, .secondes = 30.560 };

Alors que le code ci-dessous initialise le membre minutes à 45 et le membre secondes à 30.560.

struct temps t = { .minutes = 45, 30.560 };

Dans le cas où vous n’initialisez pas tous les membres de la structure, les autres seront initialisés à zéro ou, s’il s’agit de pointeurs, seront des pointeurs nuls.

#include <stdio.h>

struct temps {
    unsigned heures;
    unsigned minutes;
    double secondes;
};


int main(void)
{
    struct temps t = { 1 };

    printf("%d, %d, %f\n", t.heures, t.minutes, t.secondes);
    return 0;
}
Résultat
1, 0, 0.000000

Notez que si vous n’initialisez aucun champ, mais que votre structure est de classe de stockage statique, tous les champs seront initialisés à zéro ou, s’il s’agit de pointeurs, seront des pointeurs nuls.

Accès à un membre

L’accès à un membre d’une structure se réalise à l’aide de la variable de type structure et de l’opérateur . suivi du nom du champ visé.

variable.membre

Cette syntaxe peut être utilisée aussi bien pour obtenir la valeur d’un champ que pour en modifier le contenu. L’exemple suivant effectue donc la même action que l’initialisation présentée précédemment.

t.heures = 1;
t.minutes = 45;
t.secondes = 30.560;
Exercice

Afin d’assimiler tout ceci, voici un petit exercice.
Essayez de réaliser ce qui a été décrit plus haut : demandez à l’utilisateur de vous fournir deux mesures de temps sous la forme heure(s):minute(s):seconde(s).milliseconde(s) et donnez lui la différence en seconde entre celles-ci.

Voici un exemple d’utilisation.

Première mesure (hh:mm:ss.xxx) : 12:45:50.640
Deuxième mesure (hh:mm:ss.xxx) : 13:30:35.480
Il y 2684.840 seconde(s) de différence.
Correction
#include <stdio.h>
#include <stdlib.h>

struct temps {
    unsigned heures;
    unsigned minutes;
    double secondes;
};


int main(void)
{
    struct temps t1;
    struct temps t2;

    printf("Première mesure (hh:mm:ss.xxx) : ");

    if (scanf("%u:%u:%lf", &t1.heures, &t1.minutes, &t1.secondes) != 3)
    {
        printf("Mauvaise saisie\n");
        return EXIT_FAILURE;
    }

    printf("Deuxième mesure (hh:mm:ss.xxx) : ");

    if (scanf("%u:%u:%lf", &t2.heures, &t2.minutes, &t2.secondes) != 3)
    {
        printf("Mauvaise saisie\n");
        return EXIT_FAILURE;
    }

    t1.minutes += t1.heures * 60;
    t1.secondes += t1.minutes * 60;
    t2.minutes += t2.heures * 60;
    t2.secondes += t2.minutes * 60;

    printf("Il y a %.3f seconde(s) de différence.\n",
    t2.secondes - t1.secondes);
    return 0;
}

Structures et pointeurs

Certains d’entre vous s’en étaient peut-être doutés : s’il existe un objet d’un type, il doit être possible de créer un pointeur vers un objet de ce type. Si oui, sachez que vous aviez raison. :)

struct temps *p;

La définition ci-dessus crée un pointeur p vers un objet de type struct temps.

Accès via un pointeur

L’utilisation d’un pointeur sur structure est un peu plus complexe que celle d’un pointeur vers un type de base. En effet, il y a deux choses à gérer : l’accès via le pointeur et l’accès à un membre. Intuitivement, vous combineriez sans doute les opérateurs * et . comme ceci.

*p.heures = 1;

Toutefois, cette syntaxe ne correspond pas à ce que nous voulons car l’opérateur . s’applique prioritairement à l’opérateur *. Autrement dit, le code ci-dessus accède au champ heures et tente de lui appliquer l’opérateur d’indirection, ce qui est incorrect puisque le membre heures est un entier non signé.

Pour résoudre ce problème, nous devons utiliser des parenthèses afin que l’opérateur . soit appliqué après le déréférencement, ce qui donne la syntaxe suivante.

(*p).heures = 1;

Cette écriture étant un peu lourde, le C fourni un autre opérateur qui combine ces deux opérations : l’opérateur ->.

p->heures = 1;

Le code suivant initialise donc la structure t via le pointeur p.

struct temps t;
struct temps *p = &t;

p->heures = 1;
p->minutes = 45;
p->secondes = 30.560;
Adressage

Il est important de préciser que l’opérateur d’adressage peut s’appliquer aussi bien à une structure qu’à un de ses membres. Ainsi, dans l’exemple ci-dessous, nous définissons un pointeur p pointant sur la structure t et un pointeur q pointant sur le champ heures de la structure t.

struct temps t;
struct temps *p = &t;
int *q = &t.heures;
Pointeurs sur structures et fonctions

Indiquons enfin que l’utilisation de pointeurs est particulièrement propice dans le cas du passage de structures à des fonctions. En effet, rappelez-vous, lorsque vous fournissez un argument lors d’un appel de fonction, la valeur de celui-ci est affectée au paramètre correspondant. Cette règle s’applique également aux structures : la valeur de chacun des membres est copiée une à une. Dès lors, si la structure passée en argument comporte beaucoup de champs, la copie risque d’être longue. L’utilisation d’un pointeur évite ce problème puisque seule la valeur du pointeur (autrement dit, l’adresse vers la structure) sera copiée et non toute la structure.

Portée et déclarations

Portée

Dans les exemples précédents, nous avons toujours placé notre définition de structure en dehors de toute fonction. Cependant, sachez que celle-ci peut être circonscrite à un bloc de sorte de limiter sa portée, comme pour les définitions de variables et les déclarations de variables et fonctions.

Dans l’exemple ci-dessous, la structure temps ne peut être utilisée que dans le bloc de la fonction main() et les éventuels sous-blocs qui la composent.

int main(void)
{
    struct temps {
        unsigned heures;
        unsigned minutes;
        double secondes;
    };

    struct temps t;

    return 0;
}

Notez qu’il est possible de combiner une définition de structure et une définition de variable. En effet, une définition de structure n’étant rien d’autre que la définition d’un nouveau type, celle-ci peut être placée là où est attendu un type dans une définition de variable. Avec l’exemple précédent, cela donne ceci.

int main(void)
{
    struct temps {
        unsigned heures;
        unsigned minutes;
        double secondes;
    } t1;
    struct temps t2;

    return 0;
}

Il y a trois définitions dans ce code : celle du type struct temps , celle de la variable t1 et celle de la variable t2. Après sa définition, le type struct temps peut tout à fait être utilisé pour définir d’autres variables de ce type, ce qui est le cas de t2.

Notez qu’il est possible de condenser la définition du type struct temps et de la variable t1 sur une seule ligne, comme ceci.

struct temps { unsigned heures; unsigned minutes; double secondes; } t1;

S’agissant d’une définition de variable, il est également parfaitement possible de l’initialiser.

int main(void)
{
    struct temps {
        unsigned heures;
        unsigned minutes;
        double secondes;
    } t1 = { 1, 45, 30.560 };
    struct temps t2;

    return 0;
}

Enfin, précisons que l’étiquette d’une structure peut être omise lors de sa définition. Néanmoins, cela ne peut avoir lieu que si la définition de la structure est combinée avec une définition de variable. C’est assez logique étant donné qu’il ne peut pas être fait référence à cette définition à l’aide d’une étiquette.

int main(void)
{
    struct {
        unsigned heures;
        unsigned minutes;
        double secondes;
    } t;

    return 0;
}
Déclarations

Jusqu’à présent, nous avons parlé de définitions de structures, toutefois, comme pour les variables et les fonctions, il existe également des déclarations de structures. Une déclaration de structure est en fait une définition sans le corps de la structure.

struct temps;

Quel est l’intérêt de la chose me direz-vous ? Résoudre deux types de problèmes : les structures interdépendantes et les structures comportant un ou des membres qui sont des pointeurs vers elle-même.

Les structures interdépendantes

Deux structures sont interdépendantes lorsque l’une comprend un pointeur vers l’autre et inversement.

struct a {
    struct b *p;
};

struct b {
    struct a *p;
};

Voyez-vous le problème ? Si le type struct b ne peut être utilisé qu’après sa définition, alors le code ci-dessus est faux et le cas de structures interdépendantes est insoluble. Heureusement, il est possible de déclarer le type struct b afin de pouvoir l’utiliser avant sa définition.

Une déclaration de structure crée un type dit incomplet (comme le type void). Dès lors, il ne peut pas être utilisé pour définir une variable (puisque les membres qui composent la structure sont inconnus). Ceci n’est utilisable que pour définir des pointeurs.

Le code ci-dessous résout le « problème » en déclarant le type struct b avant son utilisation.

struct b;

struct a {
    struct b *p;
};

struct b {
    struct a *p;
};

Nous avons entouré le mot « problème » de guillemets car le premier code que nous vous avons montré n’en pose en fait aucun et compile sans sourciller. :-°

En fait, afin d’éviter ce genre d’écritures, le langage C prévoit l’ajout de déclarations implicites. Ainsi, lorsque le compilateur rencontre un pointeur vers un type de structure qu’il ne connaît pas, il ajoute implicitement une déclaration de cette structure juste avant. Ainsi, le premier et le deuxième code sont équivalents si ce n’est que le premier comporte une déclaration implicite et non une déclaration explicite.

Structure qui pointe sur elle-même

Le deuxième cas où les déclarations de structure s’avèrent nécessaires est celui d’une structure qui comporte un pointeur vers elle-même.

struct a {
    struct a *p;
};

De nouveau, si le type struct a n’est utilisable qu’après sa définition, c’est grillé. Toutefois, comme pour l’exemple précédent, le code est correct, mais pas tout à fait pour les mêmes raisons. Dans ce cas ci, le type struct a est connu car nous sommes en train de le définir. Techniquement, dès que la définition commence, le type est déclaré.

Notez que, comme les définitions, les déclarations (implicites ou non) ont une portée.

Les structures littérales

Nous venons de le voir, manipuler une structure implique de définir une variable. Toutefois, il est des cas où devoir définir une variable est peu utile, par exemple si la structure n’est pas amenée à être modifiée par la suite. Aussi est-il possible de construire des structures dites « littérales », c’est-à-dire sans passer par la définition d’une variable.

La syntaxe est la suivante.

(type) { élément1, élément2, ... }

type est le type de la structure et où élément1 et élément2 représentent les éléments d’une liste d’initialisation (comme dans le cas de l’initialisation d’une variable de type structure).

Ces structures littérales peuvent être utilisées partout où une structure est attendue, par exemple comme argument d’un appel de fonction.

#include <stdio.h>

struct temps {
    unsigned heures;
    unsigned minutes;
    double secondes;
};


void affiche_temps(struct temps temps)
{
    printf("%d:%d:%f\n", temps.heures, temps.minutes, temps.secondes);
}


int
main(void)
{
    affiche_temps((struct temps) { .heures = 12, .minutes = 0, .secondes = 1. });
    return 0;
}
Résultat
12:0:1.000000

Comme vous le voyez, au lieu de définir une variable et de passer celle-ci en argument lors de l’appel à la fonction affiche_temps(), nous avons directement fourni une structure littérale.

Classe de stockage

Les structures littérales sont de classe de stockage automatique, elles sont donc détruites à la fin du bloc dans lequel elles ont été créées.

Notez qu’à l’inverse des chaînes de caractères littérales, les structures littérales peuvent parfaitement être modifiées. L’exemple suivant est donc parfaitement correct.

#include <stdio.h>

struct temps {
    unsigned heures;
    unsigned minutes;
    double secondes;
};


int
main(void)
{
    struct temps *p = &(struct temps) { .heures = 12, .minutes = 0, .secondes = 1. };

    p->secondes = 42.; /* Ok. */
    return 0;
}

Un peu de mémoire

Savez-vous comment sont représentées les structures en mémoire ?

Les membres d’une structure sont placés les uns après les autres en mémoire. Par exemple, prenons cette structure.

struct exemple
{
    double flottant;
    char lettre;
    unsigned entier;
};

Si nous supposons qu’un double a une taille de huit octets, un char d’un octet, et un unsigned int de quatre octets, voici ce que devrait donner cette structure en mémoire.

+---------+----------+
| Adresse |  Champ   |
+---------+----------+
|    0    |          |
+---------+          |
|    1    |          |
+---------+          |
|    2    |          |
+---------+          |
|    3    |          |
+---------+ flottant |
|    4    |          |
+---------+          |
|    5    |          |
+---------+          |
|    6    |          |
+---------+          |
|    7    |          |
+---------+----------+
|    8    | lettre   |
+---------+----------+
|    9    |          |
+---------+          |
|    10   |          |
+---------+ entier   |
|    11   |          |
+---------+          |
|    12   |          |
+---------+----------+

Les adresses spécifiées dans le schéma sont fictives et ne servent qu’à titre d’illustration.

L’opérateur sizeof

Voyons à présent comment déterminer cela de manière plus précise, en commençant par la taille des types. L’opérateur sizeof permet de connaître la taille en multiplets (bytes en anglais) de son opérande. Cet opérande peut être soit un type (qui doit alors être entre parenthèses), soit une expression (auquel cas les parenthèses sont facultatives).

Le résultat de cet opérateur est de type size_t. Il s’agit d’un type entier non signé défini dans l’en-tête <stddef.h> qui est capable de contenir la taille de n’importe quel objet. L’exemple ci-dessous utilise l’opérateur sizeof pour obtenir la taille des types de bases.

Une expression de type size_t peut être affichée à l’aide de l’indicateur de conversion zu.

#include <stdio.h>


int main(void)
{
    double f;

    printf("_Bool : %zu\n", sizeof(_Bool));
    printf("char : %zu\n", sizeof(char));
    printf("short : %zu\n", sizeof(short));
    printf("int : %zu\n", sizeof(int));
    printf("long : %zu\n", sizeof(long));
    printf("float : %zu\n", sizeof(float));
    printf("double : %zu\n", sizeof(double));
    printf("long double : %zu\n", sizeof(long double));

    printf("int : %zu\n", sizeof 5);
    printf("double : %zu\n", sizeof f);
    return 0;
}
Résultat
_Bool : 1
char : 1
short : 2
int : 4
long : 8
float : 4
double : 8
long double : 16
int : 4
double : 8

Le type char a toujours une taille d’un multiplet.

Il est parfaitement possible que vous n’obteniez pas les même valeurs que nous, celles-ci dépendent de votre machine.

Remarquez que les parenthèses ne sont pas obligatoires dans le cas où l’opérande de l’opérateur sizeof est une expression (dans notre exemple : 5 et f).

Ceci étant dit, voyons à présent ce que donne la taille de la structure présentée plus haut. En toute logique, elle devrait être égale à la somme des tailles de ses membres, chez nous : 13 (8 + 1 + 4).

#include <stddef.h>
#include <stdio.h>

struct exemple
{
    double flottant;
    char lettre;
    unsigned int entier;
};


int main(void)
{
    printf("struct exemple : %zu\n", sizeof(struct exemple));
    return 0;
}
Résultat
struct exemple : 16

Ah ! Il semble que nous avons loupé quelque chose… :-°
Pourquoi obtenons-nous 16 et non 13, comme attendu ? Pour répondre à cette question, nous allons devoir plonger un peu dans les entrailles de notre machine.

Alignement en mémoire

En fait, il faut savoir qu’il n’est dans les faits pas possible de lire ou modifier un multiplet spécifique de la mémoire vive. Techniquement, il est uniquement possible de lire ou écrire des blocs de données de taille fixe appelés mots mémoire. Dès lors, il est nécessaire que les données à lire ou à écrire soient contenues dans un de ces mots mémoires.

Quel rapport avec notre structure me direz-vous ? Supposons que notre RAM utilise des blocs de huit octets. Si notre structure faisait treize octets, voici comment elle serait vue en termes de blocs.

      +---------+----------+
      | Adresse |  Champ   |
  +-> +---------+----------+
  |   |    0    |          |
  |   +---------+          |
  |   |    1    |          |
  |   +---------+          |
  |   |    2    |          |
  |   +---------+          |
  |   |    3    |          |
1 |   +---------+ flottant |
  |   |    4    |          |
  |   +---------+          |
  |   |    5    |          |
  |   +---------+          |
  |   |    6    |          |
  |   +---------+          |
  |   |    7    |          |
  +-> +---------+----------+
  |   |    8    | lettre   |
  |   +---------+----------+
  |   |    9    |          |
  |   +---------+          |
  |   |    10   |          |
  |   +---------+ entier   |
  |   |    11   |          |
2 |   +---------+          |
  |   |    12   |          |
  |   +---------+----------+
  |
  |
  |
  .
  .
  .

Comme vous le voyez, le champ flottant remplit complètement le premier mot, alors que les champs lettre et entier ne remplissent que partiellement le second. Dans le seul cas de notre structure, cela ne pose pas de problème, sauf que celle-ci n’est pas seule en mémoire. En effet, imaginons maintenant qu’un autre int suive cette structure, nous obtiendrions alors ceci.

      +---------+----------+
      | Adresse |  Champ   |
  +-> +---------+----------+
  |   |    0    |          |
  |   +---------+          |
  |   |    1    |          |
  |   +---------+          |
  |   |    2    |          |
  |   +---------+          |
  |   |    3    |          |
1 |   +---------+ flottant |
  |   |    4    |          |
  |   +---------+          |
  |   |    5    |          |
  |   +---------+          |
  |   |    6    |          |
  |   +---------+          |
  |   |    7    |          |
  +-> +---------+----------+
  |   |    8    | lettre   |
  |   +---------+----------+
  |   |    9    |          |
  |   +---------+          |
  |   |    10   |          |
  |   +---------+ entier   |
  |   |    11   |          |
2 |   +---------+          |
  |   |    12   |          |
  |   +---------+----------+
  |   |    13   |          |
  |   +---------+          |
  |   |    14   |          |
  |   +---------+ int      |
  |   |    15   |          |
  +-> +---------+          |
  |   |    16   |          |
  |   +---------+----------+
  |
  |
3 |
  .
  .
  .

Et là, c’est le drame, le second entier est à cheval sur le deuxième et le troisième mot. Pour éviter ces problèmes, les compilateurs ajoutent des multiplets dit « de bourrage » (padding en anglais), afin que les données ne soient jamais à cheval sur plusieurs mots. Dans le cas de notre structure, le compilateur a ajouté trois octets de bourrage juste après le membre lettre afin que les deux derniers champs occupent un mot complet.

      +---------+----------+
      | Adresse |  Champ   |
  +-> +---------+----------+
  |   |    0    |          |
  |   +---------+          |
  |   |    1    |          |
  |   +---------+          |
  |   |    2    |          |
  |   +---------+          |
  |   |    3    |          |
1 |   +---------+ flottant |
  |   |    4    |          |
  |   +---------+          |
  |   |    5    |          |
  |   +---------+          |
  |   |    6    |          |
  |   +---------+          |
  |   |    7    |          |
  +-> +---------+----------+
  |   |    8    | lettre   |
  |   +---------+----------+
  |   |    9    |          |
  |   +---------+          |
  |   |    10   | bourrage |
  |   +---------+          |
  |   |    11   |          |
2 |   +---------+----------+
  |   |    12   |          |
  |   +---------+          |
  |   |    13   |          |
  |   +---------+ entier   |
  |   |    14   |          |
  |   +---------+          |
  |   |    15   |          |
  +-> +---------+----------+
  |   |    16   |          |
  |   +---------+          |
  |   |    17   |          |
  |   +---------+ int      |
  |   |    18   |          |
  |   +---------+          |
  |   |    19   |          |
3 |   +---------+----------+
  |
  |
  |
  .
  .
  .

Ainsi, il n’y a plus de problèmes, l’entier suivant la structure n’est plus à cheval sur deux mots.

Mais, comment le compilateur sait-il qu’il doit ajouter des multiplets de bourrage avant le champ entier et précisément trois ?

Cela est dû à la position du champ entier dans la structure. Dans notre exemple, il y a 9 multiplets qui précèdent le champ entier, or le compilateur sait que la taille d’un unsigned int est de quatre multiplets et que la RAM utilise des blocs de huit octets, il en déduit alors qu’il y aura un problème. En effet, pour qu’il n’y ait pas de soucis, il est nécessaire qu’un champ de type unsigned int soit précédé d’un nombre de multiplets qui soit multiple de quatre (d’où les trois multiplets de bourrage pour arriver à 12).

Notez que la même réflexion peut s’opérer en termes d’adresses : dans notre exemple, un champ de type unsigned int doit commencer à une adresse multiple de 4.

Cette contrainte est appelée une contrainte d’alignement (car il est nécessaire d’« aligner » les données en mémoire). Chaque type possède sa propre contrainte d’alignement, dans notre exemple le type unsigned int a une contrainte d’alignement (ou simplement un alignement) de 4.

L’opérateur _Alignof

Il est possible de connaître les contraintes d’alignement d’un type, à l’aide de l’opérateur _Alignof (ou alignof si l’en-tête <stdalign.h> est inclus)1. Ce dernier s’utilise de la même manière que l’opérateur sizeof, si ce n’est que seul un type peut lui être fourni et non une expression. Le résultat de l’opérateur est, comme pour sizeof, une expression de type size_t.

Le code suivant vous donne les contraintes d’alignement pour chaque type de base.

#include <stdalign.h>
#include <stdio.h>


int main(void)
{
    printf("_Bool: %zu\n", _Alignof(_Bool));
    printf("char: %zu\n", _Alignof(char));
    printf("short: %zu\n", _Alignof(short));
    printf("int : %zu\n", _Alignof(int));
    printf("long : %zu\n", _Alignof(long));
    printf("long long : %zu\n", _Alignof(long long));
    printf("float : %zu\n", alignof(float));
    printf("double : %zu\n", alignof(double));
    printf("long double : %zu\n", alignof(long double));
    return 0;
}
Résultat
_Bool: 1
char: 1
short: 2
int : 4
long : 8
long long : 8
float : 4
double : 8
long double : 16

Le type char ayant une taille d’un multiplet, il peut toujours être contenu dans un mot. Il n’a donc pas de contraintes d’alignement ou, plus précisément, une contrainte d’alignement de 1.

Comme pour l’opérateur sizeof, il est possible que vous n’obteniez pas les même valeurs que nous, celles-ci dépendent de votre machine.


  1. l’opérateur _Alignof a été introduit par la norme C11. Auparavant, pour connaître les contraintes d’alignement, il était nécessaire de se reposer sur les structures et la macrofonction offsetof (définie dans l’en-tête <stddef.h>).

Les structures en elles-mêmes ne sont pas compliquées à comprendre, mais l’intérêt est parfois plus difficile à saisir. Ne vous en faites pas, nous aurons bientôt l’occasion de découvrir des cas où les structures se trouvent être bien pratiques. En attendant, n’hésitez pas à relire le chapitre s’il vous reste des points obscurs.

Continuons notre route et découvrons à présent un deuxième type de données complexes : les tableaux.

En résumé
  1. Une structure est un regroupement de plusieurs objets, potentiellement de types différents ;
  2. La composition d’une structure est précisée à l’aide d’une définition ;
  3. Si certains membres d’une structure ne se voient pas attribuer de valeurs durant l’initialisation d’une variable, ils seront initialisés à zéro ou, s’il s’agit de pointeurs, seront des pointeurs nuls ;
  4. L’opérateur . permet d’accéder aux champs d’une variable de type structure ;
  5. L’opérateur -> peut être utilisé pour simplifier l’accès aux membres d’une structure via un pointeur ;
  6. Une structure peut être déclarée en omettant sa composition (son corps), le type ainsi déclaré est incomplet jusqu’à ce qu’il soit défini ;
  7. Tous les types ont des contraintes d’alignement, c’est-à-dire un nombre dont les adresses d’un type doivent être le multiple ;
  8. Les contraintes d’alignement peuvent induire l’ajout de multiplets de bourrage entre certains champs d’une structure.