Chapitre 2    Les types composés


A partir des types prédéfinis du C (caractères, entiers, flottants), on peut créer de nouveaux types, appelés types composés, qui permettent de représenter des ensembles de données organisées.



2.1  Les tableaux

Un tableau est un ensemble fini d'éléments de même type, stockés en mémoire à des adresses contiguës.

La déclaration d'un tableau à une dimension se fait de la façon suivante :
type nom-du-tableau[nombre-éléments];
nombre-éléments est une expression constante entière positive. Par exemple, la déclaration int tab[10]; indique que tab est un tableau de 10 éléments de type int. Cette déclaration alloue donc en mémoire pour l'objet tab un espace de 10 × 4 octets consécutifs.

Pour plus de clarté, il est recommandé de donner un nom à la constante nombre-éléments par une directive au préprocesseur, par exemple
#define nombre-éléments 10

On accède à un élément du tableau en lui appliquant l'opérateur []. Les éléments d'un tableau sont toujours numérotés de 0 à nombre-éléments -1. Le programme suivant imprime les éléments du tableau tab :
#define N 10
main()
{
  int tab[N];
  int i;
  ...
  for (i = 0; i < N; i++)
    printf("tab[%d] = %d\n",i,tab[i]);
}
Un tableau correspond en fait à un pointeur vers le premier élément du tableau. Ce pointeur est constant. Cela implique en particulier qu'aucune opération globale n'est autorisée sur un tableau. Notamment, un tableau ne peut pas figurer à gauche d'un opérateur d'affectation. Par exemple, on ne peut pas écrire ``tab1 = tab2;''. Il faut effectuer l'affectation pour chacun des éléments du tableau :
#define N 10
main()
{
  int tab1[N], tab2[N];
  int i;
  ...
  for (i = 0; i < N; i++)
    tab1[i] = tab2[i];
}
On peut initialiser un tableau lors de sa déclaration par une liste de constantes de la façon suivante :
type nom-du-tableau[N] = {constante-1,constante-2,...,constante-N};

Par exemple, on peut écrire
#define N 4
int tab[N] = {1, 2, 3, 4};
main()
{
  int i;
  for (i = 0; i < N; i++)
    printf("tab[%d] = %d\n",i,tab[i]);
}
Si le nombre de données dans la liste d'initialisation est inférieur à la dimension du tableau, seuls les premiers éléments seront initialisés. Les autres éléments seront mis à zéro si le tableau est une variable globale (extérieure à toute fonction) ou une variable locale de classe de mémorisation static (cf. chapitre 4).

De la même manière un tableau de caractères peut être initialisé par une liste de caractères, mais aussi par une chaîne de caractères littérale. Notons que le compilateur complète toute chaîne de caractères avec un caractère nul '\0'. Il faut donc que le tableau ait au moins un élément de plus que le nombre de caractères de la chaîne littérale.

#define N 8
char tab[N] = "exemple";
main()
{
  int i;
  for (i = 0; i < N; i++)
    printf("tab[%d] = %c\n",i,tab[i]);
}
Lors d'une initialisation, il est également possible de ne pas spécifier le nombre d'éléments du tableau. Par défaut, il correspondra au nombre de constantes de la liste d'initialisation. Ainsi le programme suivant imprime le nombre de caractères du tableau tab, ici 8.
char tab[] = "exemple";
main()
{
  int i;
  printf("Nombre de caracteres du tableau = %d\n",sizeof(tab)/sizeof(char));
}
De manière similaire, on peut déclarer un tableau à plusieurs dimensions. Par exemple, pour un tableau à deux dimensions :
type nom-du-tableau[nombre-lignes][nombre-colonnes]
En fait, un tableau à deux dimensions est un tableau unidimensionnel dont chaque élément est lui-même un tableau. On accède à un élément du tableau par l'expression ``tableau[i][j]''. Pour initialiser un tableau à plusieurs dimensions à la compilation, on utilise une liste dont chaque élément est une liste de constantes :
#define M 2
#define N 3
int tab[M][N] = {{1, 2, 3}, {4, 5, 6}};

main()
{
  int i, j;
  for (i = 0 ; i < M; i++)
    {
      for (j = 0; j < N; j++)
        printf("tab[%d][%d]=%d\n",i,j,tab[i][j]);
    }
}

2.2  Les structures

Une structure est une suite finie d'objets de types différents. Contrairement aux tableaux, les différents éléments d'une structure n'occupent pas nécessairement des zones contiguës en mémoire. Chaque élément de la structure, appelé membre ou champ, est désigné par un identificateur.

On distingue la déclaration d'un modèle de structure de celle d'un objet de type structure correspondant à un modèle donné. La déclaration d'un modèle de structure dont l'identificateur est modele suit la syntaxe suivante :

struct modele
{type-1 membre-1;
  type-2 membre-2;
    ...
  type-n membre-n;  
};
Pour déclarer un objet de type structure correspondant au modèle précédent, on utilise la syntaxe :
struct modele objet;
ou bien, si le modèle n'a pas été déclaré au préalable :

struct modele
{type-1 membre-1;
  type-2 membre-2;
    ...
  type-n membre-n;  
}objet;
On accède aux différents membres d'une structure grâce à l'opérateur membre de structure, noté ``.''. Le i-ème membre de objet est désigné par l'expression
objet.membre-i
On peut effectuer sur le i-ème membre de la structure toutes les opérations valides sur des données de type type-i. Par exemple, le programme suivant définit la structure complexe, composée de deux champs de type double ; il calcule la norme d'un nombre complexe.
#include <math.h>
struct complexe
{
  double reelle;
  double imaginaire;
};

main()
{
  struct complexe z;
  double norme;
   ...
  norme = sqrt(z.reelle * z.reelle + z.imaginaire * z.imaginaire);
  printf("norme de (%f + i %f) = %f \n",z.reelle,z.imaginaire,norme);
}
Les règles d'initialisation d'une structure lors de sa déclaration sont les mêmes que pour les tableaux. On écrit par exemple :
struct complexe z = {2. , 2.};
En ANSI C, on peut appliquer l'opérateur d'affectation aux structures (à la différence des tableaux). Dans le contexte précédent, on peut écrire :
...
main()
{
  struct complexe z1, z2;
   ...
  z2 = z1;
}

2.3  Les champs de bits

Il est possible en C de spécifier la longueur des champs d'une structure au bit près si ce champ est de type entier (int ou unsigned int). Cela se fait en précisant le nombre de bits du champ avant le ; qui suit sa déclaration. Par exemple, la structure suivante
struct registre
{
  unsigned int actif : 1;
  unsigned int valeur : 31;
};
possède deux membres, actif qui est codé sur un seul bit, et valeur qui est codé sur 31 bits. Tout objet de type struct registre est donc codé sur 32 bits. Toutefois, l'ordre dans lequel les champs sont placés à l'intérieur de ce mot de 32 bits dépend de l'implémentation.

Le champ actif de la structure ne peut prendre que les valeurs 0 et 1. Aussi, si r est un objet de type struct registre, l'opération r.actif += 2; ne modifie pas la valeur du champ.

La taille d'un champ de bits doit être inférieure au nombre de bits d'un entier. Notons enfin qu'un champ de bits n'a pas d'adresse ; on ne peut donc pas lui appliquer l'opérateur &.



2.4  Les unions

Une union désigne un ensemble de variables de types différents susceptibles d'occuper alternativement une même zone mémoire. Une union permet donc de définir un objet comme pouvant être d'un type au choix parmi un ensemble fini de types. Si les membres d'une union sont de longueurs différentes, la place réservée en mémoire pour la représenter correspond à la taille du membre le plus grand.

Les déclarations et les opérations sur les objets de type union sont les mêmes que celles sur les objets de type struct. Dans l'exemple suivant, la variable hier de type union jour peut être soit un entier, soit un caractère.
union jour
{
  char lettre;
  int numero;
};

main()
{
  union jour hier, demain;
  hier.lettre = 'J';
  printf("hier = %c\n",hier.lettre);
  hier.numero = 4;
  demain.numero = (hier.numero + 2) % 7;
  printf("demain = %d\n",demain.numero);
}
Les unions peuvent être utiles lorsqu'on a besoin de voir un objet sous des types différents (mais en général de même taille). Par exemple, le programme suivant permet de manipuler en même temps les deux champs de type unsigned int d'une structure en les identifiant à un objet de type unsigned long (en supposant que la taille d'un entier long est deux fois celle d'un int).
struct coordonnees
{ 
  unsigned int x;
  unsigned int y;
};
union point
{
  struct coordonnees coord;
  unsigned long mot;
};

main()
{
  union point p1, p2, p3;
  p1.coord.x = 0xf;
  p1.coord.y = 0x1;
  p2.coord.x = 0x8;
  p2.coord.y = 0x8;  
  p3.mot = p1.mot ^ p2.mot;
  printf("p3.coord.x = %x \t p3.coord.y = %x\n", p3.coord.x, p3.coord.y);
}

2.5  Les énumérations

Les énumérations permettent de définir un type par la liste des valeurs qu'il peut prendre. Un objet de type énumération est défini par le mot-clef enum et un identificateur de modèle, suivis de la liste des valeurs que peut prendre cet objet :
enum modele {constante-1, constante-2,...,constante-n};

En réalité, les objets de type enum sont représentés comme des int. Les valeurs possibles constante-1, constante-2,...,constante-n sont codées par des entiers de 0 à n-1. Par exemple, le type enum booleen défini dans le programme suivant associe l'entier 0 à la valeur faux et l'entier 1 à la valeur vrai.
main()
{
  enum booleen {faux, vrai};
  enum booleen b;
  b = vrai;
  printf("b = %d\n",b);
}
On peut modifier le codage par défaut des valeurs de la liste lors de la déclaration du type énuméré, par exemple :
enum booleen {faux = 12, vrai = 23};



2.6  Définition de types composés avec typedef

Pour alléger l'écriture des programmes, on peut affecter un nouvel identificateur à un type composé à l'aide de typedef :
typedef type synonyme;
Par exemple,
struct complexe
{
  double reelle;
  double imaginaire;
};
typedef struct complexe complexe;

main()
{
  complexe z;
   ...
}

This document was translated from LATEX by HEVEA.