Le langage C/C++ permet la définition de types personnalisés construits à partir
des types de base du langage. Outre les tableaux, que l'on a déjà présentés, il est possible de définir
différents types de données évolués, principalement à l'aide de la notion de structure. Par ailleurs,
les variables déclarées dans un programme se distinguent, outre par leur type, par ce que l'on appelle
leur classe de stockage. La première section de ce chapitre traitera donc de la manière dont on peut
créer et manipuler de nouveaux types de données en C/C++, et la deuxième section présentera les différentes
classes de stockage existantes et leur signification précise.
3.1. 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 structures, les unions et les énumérations,
mais il est également possible de définir de nouveaux types à partir de ces types complexes.
3.1.1. Les structures
Les types complexes peuvent se construire à l'aide
de structures. Pour cela, on utilise le mot clé struct.
Sa syntaxe est la suivante :
struct [nom_structure]
{
type champ;
[type champ;
[...]]
};
Il n'est pas nécessaire de donner un nom à la structure. La structure
contient plusieurs autres variables, appelées champs. Leur type est donné dans
la déclaration de la structure. Ce type peut être n'importe quel autre type, même une structure.
La structure ainsi définie peut alors être utilisée pour définir
une variable dont le type est cette structure.
Pour cela, deux possibilités :
faire suivre la définition de la structure
par l'identificateur de la variable ;
Exemple 3-1. Déclaration de variable de type structure
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-2. Déclaration de structure
struct Client
{
unsigned char Age;
unsigned char Taille;
};
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 variables
Jean et Philippe de 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 variable Christophe ci-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 de Jean est désigné
par Jean.Age.
Note : Le typage du C++ est plus fort que celui du C, parce qu'il
considère que deux types ne sont identiques que s'ils ont le même nom. Alors que le C considère
que deux types qui ont la même structure sont des types identiques, le C++ les distingue. Cela peut être
un inconvénient, car des programmes qui pouvaient être compilés en C ne le seront pas forcément
par un compilateur C++. Considérons l'exemple suivant :
int main(void)
{
struct st1
{
int a;
} variable1 = {2};
struct
{
int a;
} variable2; /* variable2 a exactement la même structure
que variable1, */
variable2 = variable1; /* mais cela est ILLÉGAL en C++ ! */
return 0;
}
Bien que les deux variables aient exactement la même structure,
elles sont de type différents ! En effet, variable1 est de type
« st1 », et variable2 de type « »
(la structure qui a permis de la construire n'a pas de nom). On ne peut donc pas faire l'affectation.
Pourtant, ce programme était compilable en C pur...
Note : Il est possible de ne pas donner de nom à une structure
lors de sa définition sans pour autant déclarer 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.1.2. Les unions
Les unions constituent un autre type de structure.
Elles sont déclarées avec le mot clé union, qui a la même syntaxe
que struct. 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-3. Déclaration d'une union
union entier_ou_reel
{
int entier;
float reel;
};
union entier_ou_reel x;
x peut prendre l'aspect soit d'un entier, soit
d'un réel. Par exemple :
x.entier=2;
affecte la valeur 2 à x.entier, ce qui détruit
x.reel.
Si, à présent, on fait :
x.reel=6.546;
la valeur de x.entier est perdue, car le réel 6.546 a été stocké
au même emplacement mémoire que l'entier x.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-4. 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)
{
int result;
switch (e.iEventType)
{
case MOUSE_EVENT:
/* 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;
}
3.1.3. Les énumérations
Les énumérations sont des types
intégraux (c'est-à-dire qu'ils sont basés sur les entiers), pour lesquels chaque
valeur dispose d'un nom unique. Leur utilisation permet de définir les constantes entières
dans un programme et de les nommer. La syntaxe des énumérations est la suivante :
Dans cette syntaxe, enumeration représente le nom
de l'énumération et nom1, nom2, etc. représentent les noms
des énumérés. Par défaut, les énumérés reçoivent les valeurs entières 0,
1, etc. sauf si une valeur explicite leur est donnée dans la déclaration
de l'énumération. Dès qu'une valeur est donnée, le compteur de valeurs se synchronise avec cette valeur,
si bien que l'énuméré suivant prendra pour valeur celle de l'énuméré précédent augmentée
de 1.
Exemple 3-5. Déclaration d'une énumération
enum Nombre
{
un=1, deux, trois, cinq=5, six, sept
};
Dans cet exemple, les énumérés prennent respectivement leurs valeurs.
Comme quatre n'est pas défini, une resynchronisation a lieu lors de la définition
de cinq.
Les énumérations suivent les mêmes règles que les structures et
les unions en ce qui concerne la déclaration des variables : on doit répéter le mot clé
enum en C, ce n'est pas nécessaire en C++.
3.1.4. Les champs de bits
Il est possible de définir des champs de bits
et de donner des noms aux bits de ces champs. Pour cela, on utilisera le mot clé struct
et on donnera le type des groupes de bits, leurs noms, et enfin leurs tailles :
Exemple 3-6. 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.1.5. Initialisation des structures et des tableaux
Les tableaux et les structures peuvent être initialisées, 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-7. 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}};
La variable John est ici déclarée comme étant de type
Client et initialisée comme suit : son âge est de 35, sa taille
de 190 et ses deux premiers comptes de 13594 et
45796. 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 de John aurait pu se faire ainsi :
struct Client John={35, 190, 13594, 45796};
Note : La norme C99 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-8. 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). On notera toutefois que cette syntaxe 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 8. C'est l'un
des rares points syntaxiques où il y a incompatibilité entre le C et le C++.
3.1.6. Les alias de types
Le C/C++ dispose d'un mécanisme de création d'alias,
ou de synonymes, des types complexes. Le mot clé à utiliser est typedef. Sa syntaxe
est la suivante :
typedef définition alias;
où alias est le nom que doit avoir le synonyme du type et définition
est sa définition. Pour les tableaux, la syntaxe est particulière :
type_tableau est alors le type des éléments du tableau.
Exemple 3-9. Définition de type simple
typedef unsigned int mot;
mot est strictement équivalent à unsigned int.
Exemple 3-10. Définition de type tableau
typedef int tab[10];
tab est le synonyme de « tableau de 10 entiers ».
Exemple 3-11. Définition de type structure
typedef struct client
{
unsigned int Age;
unsigned int Taille;
} Client;
Client représente la structure client.
Attention à ne pas confondre le nom de la structure (« struct client ») avec
le nom de l'alias (« Client »).
Note : Pour comprendre la syntaxe de typedef,
il suffit de raisonner de la manière suivante. Si l'on dispose d'une expression qui permet de déclarer
une variable d'un type donné, alors il suffit de placer le mot clé typedef devant
cette expression pour faire en sorte que l'identificateur de la variable devienne un identificateur
de type. Par exemple, si on supprime le mot clé typedef dans la déclaration du type
Client ci-dessus, alors Client devient une variable dont le type est
struct client.
Une fois ces définitions d'alias effectuées, on peut les utiliser
comme n'importe quel type, puisqu'ils représentent des types :
unsigned int i = 2, j; /* Déclare deux unsigned int */
tab Tableau; /* Déclare un tableau de 10 entiers */
Client John; /* Déclare une structure client */
John.Age = 35; /* Initialise la variable John */
John.Taille = 175;
for (j=0; j<10; j = j+1) Tableau[j]=j; /* Initialise Tableau */
3.1.7. Transtypages
Il est parfois utile de changer le type d'une valeur. Considérons l'exemple
suivant : la division de 5 par 2 renvoie 2.
En effet, 5/2 fait appel à la division euclidienne. Comment faire pour obtenir
le résultat avec un nombre réel ? Il faut faire 5./2, car alors
5. est un nombre flottant. Mais que faire quand on se trouve avec des variables
entières (i et j par exemple) ? Le compilateur signale
une erreur après i dans l'expression i./j ! Il faut changer
le type de l'une des deux variables. Cette opération s'appelle le transtypage.
On la réalise simplement en faisant précéder l'expression à transtyper du type désiré entouré
de parenthèses :
(type) expression
Exemple 3-12. Transtypage en C
int i=5, j=2;
((float) i)/j
Dans cet exemple, i est transtypé en flottant
avant la division. On obtient donc 2.5.
Le transtypage C est tout puissant et peut être relativement dangereux.
Le langage C++ fournit donc des opérateurs de transtypages plus spécifiques, qui permettent par exemple
de conserver la constance des variables lors de leur transtypage. Ces opérateurs seront décrits
dans la Section 10.2 du chapitre traitant de l'identification dynamique des types.