Nous avons déjà vu les types de base disponibles en C. Nous allons maintenant aborder les types plus complexes que sont les tableaux, les pointeurs, les structures et les unions.
Il est possible d'utiliser des tableaux de valeurs. Pour déclarer un tableau il faut donner le type de ses éléments puis son nom et enfin sa taille entre crochets. Tous les éléments d'un tableau sont obligatoirement du même type.
Pour un tableau de taille N, l'indice du premier élément est 0 et celui du dernier est (N -1). On peut utiliser des tableaux de dimension 2 ou plus.
Dans l'exemple suivant, nous définissons deux
tableaux de 100 éléments, l'un contenant des float
, l'autre des
char
. Le dernier tableau définit une matrice
de
double
.
float VecteurA[100];
int VecteurB[100];
double MatriceTroisTrois[3][3];
On peut initialiser un tableau dès sa déclaration en lui
affectant une liste de valeurs séparées par des virgules et
entourée par des accolades. L'exemple suivant initialise le
tableau Platon
et une matrice
identité :
int Platon[5] = {4, 6, 8, 12, 20};
double Matrice[3][3] = {{ 1, 0, 0 },
{ 0, 1, 0 },
{ 0, 0, 1 }};
Un cas particulier est l'initialisation d'un tableau de caractères pour laquelle on peut utiliser une chaîne de caractères. Les deux lignes suivantes sont équivalentes :
char Str[20] = {'B', 'o', 'n', 'j', 'o', 'u', 'r'};
char Str[20] = "Bonjour";
Pour accéder à un élément d'un tableau, on utilise l'opérateur
[]
. La valeur mise entre crochets peut être un
calcul. Dans l'exemple suivant, on stocke dans le troisième
élément de Tab
la valeur du ième élément :
Tab[2] = Tab[i - 1];
Un pointeur contient l'adresse en mémoire d'un objet d'un type
donné. Ainsi, on parler de « pointeur sur int
» ou de «
pointeur sur double
». L'utilisation des pointeurs en C
est l'un des points les plus complexes du langage. Mais c'est
aussi une fonctionnalité qui rend le C très puissant surtout
si on l'utilise avec les fonctions d'allocation dynamique de
la mémoire que nous verrons plus tard.
Pour définir un pointeur, on doit écrire le type d'objet sur
lequel il pointera suivi du caractère *
pour préciser
que c'est un pointeur puis enfin son nom.
Dans l'exemple suivant, p
est défini comme un pointeur
sur un double
et q
est défini comme un pointeur
sur un pointeur sur int
:
double * p;
int * * q;
Attention : dans la définition d'un pointeur, le
caractère *
est rattaché au nom qui le suit et non pas
au type. Ainsi, dans la définition qui suit, p
est bien
un pointeur sur char
mais t
est simplement
une variable de type char
. La seconde ligne, par
contre, définit deux pointeurs sur double
:
char * p, t;
double * p2, * p3;
Pour récupérer l'adresse en mémoire d'un objet, on utilise
l'opérateur &
. Cette adresse pourra être stockée dans
un pointeur. Dans l'exemple suivant, le pointeur p
contient l'adresse en mémoire de la variable car
:
char car;
char * p;
p = & car;
Pour accéder au contenu de l'adresse mémoire pointée par un
pointeur, on utilise l'opérateur *
. Ainsi, en
continuant l'exemple précédent, la ligne suivante stockera
dans la variable car
le caractère A puisque p
pointe sur son adresse en mémoire :
*p = 'A';
On peut récupérer l'adresse de n'importe quel objet. Par exemple, il est possible d'obtenir l'adresse d'un élément d'un tableau (dans cet exemple, le onzième élément6.1) :
double a[20];
double * p;
p = & (a[10]);
Par convention, le nom d'un tableau est une constante égale à l'adresse du premier élément du tableau. En continuant l'exemple précédent, les deux lignes suivantes sont équivalentes :
p = & a[0];
p = a;
Il est possible de faire des calculs sur les pointeurs. On
peut ajouter ou soustraire une valeur entière à un
pointeur. Dans l'exemple suivant, p
pointe à la fin sur
le troisième élément du tableau a
(donc sur
a[2]
) :
double a[20];
double * p;
p = & (a[10]);
p = p - 8;
Pour effectuer ce calcul tous les opérateurs classiques
d'addition et de soustraction sont utilisables en particulier
les opérateurs d'incrémentation. Nous avons vu qu'une chaîne
de caractères se terminait toujours par le caractère de code
ASCII 0 (\0
). L'exemple suivant permet de compter le
nombre de caractères stockés dans le tableau de caractères
str
(le caractère nul ne fait pas partie du compte) :
char * p = str;
int NbCar = 0;
while ( *p != '\
0') {
p++;
NbCar++;
}
En fait, les calculs sur pointeurs et l'utilisation de
l'opérateur []
d'accès à un élément d'un tableau
peuvent être considérés comme équivalent. Sachant que
Tab
est un tableau de double
, les deux lignes
suivantes sont équivalentes :
Tab[45] = 123.456;
*(Tab + 45) = 123.456;
Ceci est tellement vrai qu'on peut même utiliser un
pointeur directement comme un tableau. Les deux écritures
suivantes sont donc exactement équivalentes que p
soit
le nom d'un pointeur ou celui d'un tableau :
p[i] *(p + i)
On a le même type d'équivalence au niveau des paramètres d'une
fonction. Les deux lignes suivantes déclarent toutes les deux
que le paramètre p
de la fonction f
est un
pointeur sur double
:
void f(double * p);
void f(double q[]);
En général, les types de base que propose le C ne suffisent
pas pour stocker les données à utiliser dans un programme. Par
exemple, il serait bien embêtant de devoir utiliser deux
variables de type double
pour stocker un nombre
complexe. Heureusement le C permet de déclarer de nouveaux
types. Nous ne ferons qu'évoquer les unions pour nous
focaliser sur les structures qui permettent de répondre à la
plupart des besoins.
Une structure possède un nom et est composée de plusieurs
champs. Chaque champ à son propre type et son propre nom.
Pour déclarer un structure on utilise le mot-clé
struct
:
struct nomStructure {
type1 champ1;
...
typeN champN;
} ;
Voici un exemple qui déclare une structure permettant de stocker un nombre complexe :
struct complex {
double reel; /* partie reelle */
double imag; /* partie imaginaire */
};
À partir de cette déclaration, il est possible d'utiliser ce
nouveau type. L'opérateur .
permet d'accéder à l'un des
champs d'une structure. En continuant l'exemple précédent, les
lignes suivantes initialisent un complexe à la valeur (2 +
3i).
struct complex a;
a.reel = 2;
a.imag = 3;
typedef
Le mot-clé typedef
permet d'associer un nom à un type
donné. On l'utilise suivi de la déclaration d'un type (en
général une structure ou une union) puis du nom qui remplacera
ce type. Ceci permet, par exemple, de s'affranchir de l'emploi
de struct
à chaque utilisation d'un complexe. Il n'est
pas alors nécessaire de donner un nom à la structure.
L'exemple précédent peut donc se réécrire de la manière
suivante :
typedef struct {
double reel; /* partie reelle */
double imag; /* partie imaginaire */
} complexe;
complexe a;
a.reel = 2;
a.imag = 3;
Il est possible d'affecter une variable de type structure dans une autre variable du même type. Le contenu de chacun des champs de la première variable sera alors recopié dans le champ correspondant de la seconde variable. On peut initialiser une variable de type structure dès sa définition en lui affectant une liste de valeurs séparées par des virgules et entourées par des accolades.
complexe a = { 1, 0 }; /* le reel 1 */
complexe b;
b = a;
Il est par contre impossible de comparer ou d'effectuer des calculs entre deux structures.
On peut imbriquer plusieurs structures. Dans l'exemple suivant nous déclarons une structure pour stocker une commande d'un client contenant :
refProd
),
prix
) stockant :
HT
),
TVA
),
q
),
remise
).
typedef struct {
int refProd; /* reference produit */
struct {
double HT; /* prix hors taxe */
double TVA; /* taux de TVA en pourcentage */
} prix;
int q; /* quantite commandee */
double remise; /* remise en pourcentage */
} commande;
Pour accéder aux champs de la sous-structure, il
faut utiliser deux fois l'opérateur .
d'accès aux
champs. En supposant que com
contienne une telle
commande, voici le calcul du prix total :
double P_TTC, P_AvantRemise, P_Total;
P_TTC = com.prix.HT * (1 + com.prix.TVA / 100);
P_AvantRemise = P_TTC * com.q;
P_Total = P_AvantRemise - P_AvantRemise * com.remise / 100;
Les unions se déclarent de la même manière que les structures. Elles possèdent donc elles aussi des champs typés. Mais on ne peut utiliser qu'un seul champ à la fois. En fait tous les champs d'une union se partagent le même espace mémoire. Les unions sont rarement nécessaires sauf lors de la programmation système.
L'utilisation de pointeurs sur structures est très courante en C. Voici un exemple d'utilisation d'un pointeur sur un complexe :
complexe a = { 3.5, -5.12 };
complexe * p = &a;
(*p).reel = 1;
(*p).imag = -1;
/* a vaut (1 - i) */
Nous avons été obligé de mettre des parenthèses autour de
*p
car l'opérateur .
est plus prioritaire que
l'opérateur *
. Cela rend difficile la lecture d'un tel
programme. Heureusement, l'utilisation de pointeurs sur
structures est si courante que le C définit l'opérateur
->
pour accéder aux champs d'une structure via un
pointeur. Les deux expressions suivantes sont donc
équivalentes :
(*pointeur).champ
pointeur->champ
Ainsi l'exemple précédent s'écrit beaucoup plus facilement de la manière suivante :
complexe a = { 3.5, -5.12 };
complexe * p = &a;
p->reel = 1;
p->imag = -1;
/* a vaut (1 - i) */