• Aucun résultat trouvé

Chapitre 3. Types avancés et classes de stockage

3.2. Structures de données et types complexes

En dehors des types de variables simples, le C/C++ permet de créer des types plus complexes. Ces types comprennent essentiellement les tableaux, les structures, les unions et les énumérations.

3.2.1. Les tableaux

La définition d’un tableau se fait en faisant suivre le nom de l’identificateur d’une paire de crochets, contenant le nombre d’élément du tableau :

type identificateur[taille]([taille](...));

Note :Attention ! Les caractères ’[’ et ’]’ étant utilisés par la syntaxe des tableaux, ils ne signifient plus les éléments facultatifs ici. Ici, et ici seulement, les éléments facultatifs sont donnés entre parenthèses.

Dans la syntaxe précédente, type représente le type des éléments du tableau.

Exemple 3-1. Définition d’un tableau

int MonTableau[100];

MonTableauest un tableau de 100 entiers. On référence les éléments des tableaux en donnant l’indice de l’élément entre crochet :

MonTableau[3]=0;

Les indices des tableaux varient de0àtaille-1. Il y a donc bientailleéléments dans le tableau.

Dans l’exemple donné ci-dessus, l’élément MonTableau[100]n’existe pas : y accéder plantera le programme. C’est au programmeur de vérifier que ses programmes n’utilisent jamais les tableaux avec des indices négatifs ou plus grands que leur taille.

En C/C++, les tableaux à plus d’une dimension sont des tableaux de tableaux. On prendra garde au fait que dans la définition d’un tableau à plusieurs dimensions, la dernière taille indiquée spécifie la taille du tableau dont on fait un tableau. Ainsi, dans l’exemple suivant :

int Matrice[5][4];

Matriceest un tableau de taille 5 dont les éléments sont eux-mêmes des tableaux de taille 4. L’ordre de déclaration des dimensions est donc inversé : 5 est la taille de la dernière dimension et 4 est la taille de la première dimension. L’élément suivant :

Matrice[2];

est donc le troisième élément de ce tableau de taille cinq, et est lui-même un tableau de quatre élé-ments.

Il est possible, lors d’unedéclarationde tableau (pas lors d’une définition), d’omettre la taille de la dernière dimension d’un tableau (donc la taille du premier groupe de crochets). En effet, le C/C++ ne contrôle pas la taille des tableaux lui-même et, pour lui, un tableau peut avoir une taille quelconque.

Chapitre 3. Types avancés et classes de stockage

Seul le type de données des éléments est important pour calculer leur taille et donc la position de chacun d’eux en mémoire par rapport au début du tableau. De ce fait, la taille de la dernière dimension n’est pas nécessaire dans la déclaration, elle n’est en fait utile que lors de la définition pour réserver effectivement la mémoire du tableau.

Cette écriture peut par exemple être utilisée pour passer des tableaux en paramètre à une fonction.

Exemple 3-2. Passage de tableau en paramètre

void f(int taille, int t[][20]) {

/* Utilisation de t[i][j] ... */

return;

}

int main(void) {

int tab[10][20];

test(10, tab); /* Passage du tableau en paramètre. */

return 0;

}

Dans cet exemple, la fonction f reçoit un tableau à deux dimensions en paramètre, dont seule la première dimension est spécifiée. Bien entendu, pour qu’elle puisse l’utiliser correctement, elle doit en connaître la taille, donc la taille de la deuxième dimension. Cela peut se faire soit en passant un paramètre explicitement comme c’est le cas ici, ou en utilisant une convention dont le programmeur doit s’assurer lors de l’appel de la fonction.

Note :Attention, les tableaux sont passés par référence dans les fonctions. Cela signifie que si la fonction modifie les éléments du tableau, elle modifie les éléments du tableau que l’appelant lui a fourni. Autrement dit, il n’y a pas de copie du tableau lors de l’appel de la fonction.

Les tailles des premières dimensions sont absolument nécessaires. Outre le fait qu’elles per-mettent de déterminer la taille de chaque élément de la dernière dimension, elle perper-mettent également au compilateur de connaître le rapport des dimensions entre elles. Par exemple, la syntaxe :

int tableau[][];

utilisée pour référencer un tableau de 12 entiers ne permettrait pas de faire la différence entre les tableaux de deux lignes et de six colonnes et les tableaux de trois lignes et de quatre colonnes (et leurs transposés respectifs). Une référence telle que :

tableau[1][3]

ne représenterait rien. Selon le type de tableau, l’élément référencé serait le quatrième élément de la deuxième ligne (de six éléments), soit le dixième élément, ou bien le quatrième élément de la deuxième ligne (de quatre éléments), soit le huitième élément du tableau. En précisant tous les indices sauf un, il est possible de connaître la taille du tableau pour cet indice à partir de la taille globale du tableau, en la divisant par les tailles sur les autres dimensions (2 = 12/6 ou 3 = 12/4 par exemple).

Chapitre 3. Types avancés et classes de stockage

3.2.2. Les chaînes de caractères

Comme on l’a déjà dit, il n’existe pas de type de données spécifique pour manipuler les chaînes de caractères en C/C++. Les chaînes de caractères sont en effet considérées comme des tableaux de caractères, dont le dernier caractère est nul. Ce carctère permet de marquer la fin de la chaîne de caractères et ne devra jamais être oublié.

De ce fait, les tableaux de caractères sont très utilisés pour stocker des chaînes de caractères. Il faudra toutefois faire très attention à toujours utiliser des tailles de tableaux d’une unité supérieures à la taille des chaînes de caractères à stocker, afin de pouvoir placer le caractère nul terminal de la chaîne.

Par exemple, pour créer une chaîne de caractères de 12 caractères au plus, il faut un tableau pour 13 caractères.

Exemple 3-3. Chaîne de caractères C et tableau

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

int main(void) {

/* Réserve assez de place pour

la chaîne et son caractère nul terminal : */

char chaine[13];

/* Copie la chaîne dans le tableau : */

strcpy(chaine, "Hello World!");

/* Affiche la chaîne : */

printf("%s\n", chaine); /* Affiche "Hello World!" */

/* Tronque la chaîne avec un autre nul : */

chaine[5] = ’\0’;

printf("%s\n", chaine); /* Affiche "Hello" */

/* Insère un caractère ’_’ : */

chaine[5] = ’_’;

printf("%s\n", chaine); /* Affiche "Hello_World!" */

return EXIT_SUCCESS;

}

Cet exemple montre bien que le caractère nul marque la fin la chaîne de caractère pour les fonctions de la bibliothèque C. Cela n’implique pas que les caractères situés après ce caractère nul sont détruits, mais que du point de vue de la bibliothèque, la chaîne s’arrête au niveau de ce caractère.

Note :La fonctionstrcpyutilisée dans cet exemple est l’une des nombreuses fonctions de ma-nipulation de chaînes de caractères de la bibliothèque C. Elle permet de réaliser la copie d’une chaîne de caractères dans une autre (attention, elle ne vérifie pas que la taille de la chaîne cible est suffisante). Ces fonctions sont déclarées dans le fichier d’en-têtestring.h.

Chapitre 3. Types avancés et classes de stockage

3.2.3. Les structures

Les structuressont des aggrégats de données de types plus simples. Les structures permettent de construire des types complexes à partir des types de base ou d’autres types complexes.

La définition d’une structure se fait à l’aide du mot cléstruct: struct [nom_structure]

{

type champ;

[type champ;

[...]]

};

La structure contient plusieurs données membres, appeléeschamps. Leurs types sont donnés dans la déclaration de la structure. Ces types peuvent être n’importe quel autre type, même une structure. Le nom de la structure est facultatif et peut ne pas être précisé dans les situations où il n’est pas nécessaire (voir plus loin).

La structure ainsi définie peut alors être utilisée pour définir une variable dont le type est cette struc-ture.

Pour cela, deux possibilités :

faire suivre la définition de la structure par l’identificateur de la variable ; Exemple 3-4. Déclaration de variable de type structure

struct Client {

unsigned char Age;

unsigned char Taille;

} Jean;

ou, plus simplement : struct

{

unsigned char Age;

unsigned char Taille;

} Jean;

Dans le deuxième exemple, le nom de la structure n’est pas mis.

déclarer la structure en lui donnant un nom, puis déclarer les variables avec la syntaxe suivante : [struct] nom_structure identificateur;

Exemple 3-5. Déclaration de structure

struct Client {

unsigned char Age;

unsigned char Taille;

};

Chapitre 3. Types avancés et classes de stockage struct Client Jean, Philippe;

Client Christophe; // Valide en C++ mais invalide en C

Dans cet exemple, le nom de la structure doit être mis, car on utilise cette structure à la ligne suivante. Pour la déclaration des variablesJean et Philippede type struct Client, le mot clé struct a été mis. Cela n’est pas nécessaire en C++, mais l’est en C. Le C++ permet donc de déclarer des variables de type structure exactement comme si le type structure était un type prédéfini du langage. La déclaration de la variableChristopheci-dessus est invalide en C.

Les éléments d’une structure sont accédés par un point, suivi du nom du champ de la structure à accéder. Par exemple, l’âge deJeanest désigné parJean.Age.

Il est possible de ne pas donner de nom à une structure lors de sa définition sans pour autant décla-rer une variable. De telles structures anonymes ne sont utilisables que dans le cadre d’une structure incluse dans une autre structure :

struct struct_principale {

struct {

int champ1;

};

int champ2;

};

Dans ce cas, les champs des structures imbriquées seront accédés comme s’il s’agissait de champs de la structure principale. La seule limitation est que, bien entendu, il n’y ait pas de conflit entre les noms des champs des structures imbriquées et ceux des champs de la structure principale. S’il y a conflit, il faut donner un nom à la structure imbriquée qui pose problème, en en faisant un vrai champ de la structure principale.

3.2.4. Les unions

Lesunionsconstituent un autre type de structure. Elles sont déclarées avec le mot cléunion, qui a la même syntaxe questruct. La différence entre les structures et les unions est que les différents champs d’une union occupent le même espace mémoire. On ne peut donc, à tout instant, n’utiliser qu’un des champs de l’union.

Exemple 3-6. Déclaration d’une union

union entier_ou_reel {

int entier;

float reel;

};

union entier_ou_reel x;

xpeut prendre l’aspect soit d’un entier, soit d’un réel. Par exemple : x.entier=2;

Chapitre 3. Types avancés et classes de stockage

affecte la valeur2àx.entier, ce qui détruitx.reel. Si, à présent, on fait :

x.reel=6.546;

la valeur dex.entierest perdue, car le réel6.546a été stocké au même emplacement mémoire que l’entierx.entier.

Les unions, contrairement aux structures, sont assez peu utilisées, sauf en programmation système où l’on doit pouvoir interpréter des données de différentes manières selon le contexte. Dans ce cas, on aura avantage à utiliser des unions de structures anonymes et à accéder aux champs des structures, chaque structure permettant de manipuler les données selon une de leurs interprétations possibles.

Exemple 3-7. Union avec discriminant

struct SystemEvent {

int iEventType; /* Discriminant de l’événement.

Permet de choisir comment l’interpréter. */

union {

struct

{ /* Structure permettant d’interpréter */

int iMouseX; /* les événements souris. */

int iMouseY;

};

struct

{ /* Structure permettant d’interpréter */

char cCharacter; /* les événements clavier. */

int iShiftState;

};

/* etc. */

};

};

/* Exemple d’utilisation des événements : */

int ProcessEvent(struct SystemEvent e)

/* Traitement de l’événement souris... */

result = ProcessMouseEvent(e.iMouseX, e.iMouseY);

break;

case KEYBOARD_EVENT:

/* Traitement de l’événement clavier... */

result = ProcessKbdEvent(e.cCharacter, e.iShiftState);

break;

}

return result;

}

Chapitre 3. Types avancés et classes de stockage

3.2.5. Les champs de bits

Il est possible de définir des structures dont les champs ne sont stockés que sur quelques bits et non sur des types de base du langage complets. De telles structures sont appelées des «champs de bits».

Les champs de bits se déclarent comme des structures, avec le mot cléstruct, mais dans lesquelles la taille en bits de chaque champ est spécifiée. Les différents groupes de bits doivent être consécutifs.

Exemple 3-8. Déclaration d’un champs de bits

struct champ_de_bits {

int var1; /* Définit une variable classique. */

int bits1a4 : 4; /* Premier champ : 4 bits. */

int bits5a10 : 6; /* Deuxième champ : 6 bits. */

unsigned int bits11a16 : 6; /* Dernier champ : 6 bits. */

};

La taille d’un champ de bits ne doit pas excéder celle d’un entier. Pour aller au-delà, on créera un deuxième champ de bits. La manière dont les différents groupes de bits sont placés en mémoire dépend du compilateur et n’est pas normalisée.

Les différents bits ou groupes de bits seront tous accessibles comme des variables classiques d’une structure ou d’une union :

struct champ_de_bits essai;

int main(void) {

essai.bits1a4 = 3;

/* suite du programme */

return 0;

}

3.2.6. Initialisation des structures et des tableaux

Les tableaux et les structures peuvent être initialisés dès leur création, tout comme les types classiques peuvent l’être. La valeur servant à l’initialisation est décrite en mettant les valeurs des membres de la structure ou du tableau entre accolades et en les séparant par des virgules :

Exemple 3-9. Initialisation d’une structure

/* Définit le type Client : */

struct Client {

unsigned char Age;

unsigned char Taille;

unsigned int Comptes[10];

};

/* Déclare et initialise la variable John : */

struct Client John={35, 190, {13594, 45796, 0, 0, 0, 0, 0, 0, 0, 0}};

Chapitre 3. Types avancés et classes de stockage

La variableJohnest ici déclarée comme étant de type Client et initialisée comme suit : son âge est de 35, sa taille de190et ses deux premiers comptes de13594et45796. Les autres comptes sont nuls.

Il n’est pas nécessaire de respecter l’imbrication du type complexe au niveau des accolades, ni de fournir des valeurs d’initialisations pour les derniers membres d’un type complexe. Les valeurs par défaut qui sont utilisées dans ce cas sont les valeurs nulles du type du champ non initialisé. Ainsi, la déclaration deJohnaurait pu se faire ainsi :

struct Client John={35, 190, 13594, 45796};

La norme C99 du langage C fournit également une autre syntaxe plus pratique pour initialiser les structures. Cette syntaxe permet d’initialiser les différents champs de la structure en les nommant explicitement et en leur affectant directement leur valeur. Ainsi, avec cette nouvelle syntaxe, l’initialisation précédente peut être réalisée de la manière suivante :

Exemple 3-10. Initialisation de structure C99

/* Déclare et initialise la variable John : */

struct Client John={

.Taille = 190, .Age = 35,

.Comptes[0] = 13594, .Comptes[1] = 45796 };

On constatera que les champs qui ne sont pas explicitement initialisés sont, encore une fois, initialisés à leur valeur nulle. De plus, comme le montre cet exemple, il n’est pas nécessaire de respecter l’ordre d’apparition des différents champs dans la déclaration de la structure pour leur initialisation.

Il est possible de mélanger les deux syntaxes. Dans ce cas, les valeurs pour lesquelles aucun nom de champ n’est donné seront affectées au champs suivants le dernier champ nommé. De plus, si plusieurs valeurs différentes sont affectées au même champ, seule la dernière valeur indiquée sera utilisée.

Cette syntaxe est également disponible pour l’initialisation des tableaux. Dans ce cas, on utilisera les crochets directement, sans donner le nom du tableau (exactement comme l’initialisation des membres de la structure utilise directement le point, sans donner le nom de la structure en cours d’initialisation).

Exemple 3-11. Initialisation de tableau C99

/* Déclare et initialise un tableau d’entier : */

int tableau[6]={

[0] = 0, [1] = 2, [5] = 7 };

Note : La syntaxe d’initialisation C99 n’est pas disponible en C++. Avec ce langage, il est préférable d’utiliser la notion de classe et de définir un constructeur. Les notions de classe et de constructeur seront présentées plus en détails dans le Chapitre 7. C’est l’un des rares points syntaxiques où il y a incompatibilité entre le C et le C++.

Chapitre 3. Types avancés et classes de stockage