V. Objets structurés▲
V-A. Tableaux▲
V-A-1. Cas général▲
Dans le cas le plus simple la déclaration d'un tableau se fait par une formule comme :
type-de-base nom [ expressionopt ] ;
Exemple :
unsigned
long
table[10
];
L'expression optionnelle qui figure entre les crochets spécifie le nombre d'éléments du tableau. Le premier élément possède toujours l'indice 0. Par conséquent, un tableau t déclaré de taille N possède les éléments t0, t1, ... tN-1. Ainsi la définition précédente alloue un tableau de 10 éléments, nommés respectivement table[0], table[1], ... table[9].
L'expression optionnelle qui figure entre les crochets doit être de type entier et constante au sens de la section 1.3.4, c'est-à-dire une expression dont tous les éléments sont connus au moment de la compilation. De cette manière le compilateur peut l'évaluer et connaitre la quantité d'espace nécessaire pour loger le tableau. Elle est obligatoire lorsqu'il s'agit d'une définition, car il y a alors allocation effective du tableau. Elle est facultative dans le cas des déclarations qui ne sont pas des définitions, c'est-à-dire :
- lors de la déclaration d'un tableau qui est un argument formel d'une fonction ;
- lors de la déclaration d'un tableau externe (défini dans un autre fichier).
Sémantique du nom d'un tableau. En général lorsque le nom d'un tableau apparait dans une expression il y joue le même rôle qu'une constante de type adresse ayant pour valeur l'adresse du premier élément du tableau. Autrement dit, si t est de type tableau, les deux expressions t et &t[0] sont équivalentes.
Les exceptions à cette règle, c'est-à-dire les expressions de type tableau qui ne sont pas équivalentes à l'adresse du premier élément du tableau, sont
- l'occurrence du nom du tableau dans sa propre déclaration ;
- l'apparition d'un nom du tableau comme argument de sizeof.
Ce sont les seules occasions ou le compilateur veut bien se souvenir qu'un nom de tableau représente aussi un espace mémoire possédant une certaine taille.
De tout cela, il faut retenir en tout cas que le nom d'un tableau n'est pas une lvalue et donc qu'il n'est pas possible d'affecter un tableau à un autre. Si a et b sont deux tableaux, même ayant des types identiques, l'affectation b = a sera
- comprise comme l'affectation d'une adresse à une autre, et
- rejetée, car b n'est pas modifiable.
Tableaux multidimensionnels. C ne prévoit que les tableaux à un seul indice, mais les éléments des tableaux peuvent à leur tour être des tableaux. La déclaration d'un tableau avec un nombre quelconque d'indices suit la syntaxe :
type nom [ expressionopt ] [ expressionopt ] ... [ expressionopt ]
qui est un cas particulier de formules plus générales expliquées à la section 5.4.1. Par exemple, la déclaration
double
matrice[10
][20
];
introduit matrice comme étant le nom d'un tableau de 10 éléments 33 qui, chacun à son tour, est un tableau de 20 éléments de type double.
33Dans le cas d'une matrice, la coutume est d'appeler ces éléments les lignes de la matrice. Cela permet de dire alors : « en C les matrices sont rangées par lignes ».
V-A-2. Initialisation des tableaux▲
Une variable de type tableau peut être initialisée lors de sa déclaration. Cela se fait par une expression ayant la syntaxe :
{
expression , expression , ... expression }
Exemple :
int
t[5
] =
{
10
, 20
, 30
, 40
, 50
}
;
Les expressions indiquées doivent être des expressions constantes au sens de la section 1.3.4. S'il y a moins d'expressions que le tableau a d'éléments, les éléments restants sont remplis de zéros (cas des variables globales) ou bien restent indéterminés (cas des variables locales). S'il y a plus d'expressions d'initialisation que d'éléments dans le tableau, une erreur est signalée.
D'autre part, la présence d'un initialisateur dispense d'indiquer la taille du tableau. Il est convenu que ce dernier doit avoir alors pour taille le nombre effectif de valeurs initiales. Par exemple, la définition
int
t[] =
{
10
, 20
, 30
, 40
, 50
}
;
alloue un tableau à 5 composantes garni avec les valeurs indiquées.
Lorsque le tableau comporte des sous-tableaux, c'est-à-dire lorsque c'est un tableau à plusieurs indices, la liste des expressions d'initialisation peut comporter des sous-listes indiquées à leur tour avec des accolades, mais ce n'est pas une obligation. Le cas échéant, la règle de complétion par des zéros s'applique aussi aux sous-listes. Par exemple, les déclarations
int
t1[4
][4
] =
{
{
1
, 2
, 3
}
, {
4
, 5
, 6
}
, {
7
, 8
, 9
}
}
;
int
t2[4
][4
] =
{
1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, 9
}
;
allouent et garnissent les deux tableaux de la figure 5.
Remarque. Rappelons que ces représentations rectangulaires sont très conventionnelles. Dans la mémoire de l'ordinateur ces deux tableaux sont plutôt arrangés comme sur la figure .
Si la taille d'une composante est un diviseur de la taille d'un entier, les tableaux sont « tassés » : chaque composante occupe le moins de place possible. En particulier, dans un tableau de char (resp. de short), chaque composante occupe un (resp. deux) octet(s).
V-A-3. Chaines de caractères▲
Les chaines de caractères sont représentées comme des tableaux de caractères. Un caractère nul suit le dernier caractère utile de la chaine et en indique la fin. Cette convention est suivie par le compilateur (qui range les chaines constantes en leur ajoutant un caractère nul), ainsi que par les fonctions de la librairie standard qui construisent des chaines. Elle est supposée vérifiée par les fonctions de la librairie standard qui exploitent des chaines. Par conséquent, avant de passer une chaine à une telle fonction, il faut s'assurer qu'elle comporte bien un caractère nul à la fin.
Donnons un exemple classique de traitement de chaines : la fonction strlen (extraite de la bibliothèque standard) qui calcule le nombre de caractères utiles d'une chaine :
int
strlen
(
char
s[]) {
register
int
i =
0
;
while
(
s[i++
] !=
'
\0
'
)
;
return
i -
1
;
}
Une constante-chaine de caractères apparaissant dans une expression a le même type qu'un tableau de caractères c'est-à-dire, pour l'essentiel, le type « adresse d'un caractère ». Par exemple, si p a été déclaré
char
*
p;
l'affectation suivante est tout à fait légitime :
p =
"
Bonjour. Comment allez-vous?
"
;
Elle affecte à p l'adresse de la zone de la mémoire ou le compilateur a rangé le texte en question (l'adresse du 'B' de "Bonjour..."). Dans ce cas, bien que l'expression *p soit une lvalue (ce qui est une propriété syntaxique), elle ne doit pas être considérée comme telle car *p représente un objet mémorisé dans la zone des constantes, et toute occurrence de *p comme opérande gauche d'une affectation :
*
p =
expression;
constituerait une tentative de modification d'un objet constant. Cette erreur est impossible à déceler à la compilation. Selon le système sous-jacent, soit une erreur sera signalée à l'exécution, provoquant l'abandon immédiat du programme, soit aucune erreur ne sera signalée mais la validité de la suite du traitement sera compromise.
Initialisation. Comme tous les tableaux, les variables-chaines de caractères peuvent être initialisées lors de leur déclaration. De ce qui a été dit pour les tableaux en général il découle que les expressions suivantes sont correctes :
char
message1[80
] =
{
'
S
'
, '
a
'
, '
l
'
, '
u
'
, '
t
'
, '
\0
'
}
;
char
message2[] =
{
'
S
'
, '
a
'
, '
l
'
, '
u
'
, '
t
'
, '
\0
'
}
;
(la deuxième formule définit un tableau de taille 6). Heureusement, C offre un raccourci : les deux expressions précédentes peuvent s'écrire de manière tout à fait équivalente :
char
message1[80
] =
"
Salut
"
;
char
message2[] =
"
Salut
"
;
Il faut bien noter cependant que l'expression "Salut" qui apparait dans ces deux exemples n'est pas du tout traité par le compilateur comme les autres chaines de caractères constantes rencontrées dans les programmes. Ici, il ne s'agit que d'un moyen commode pour indiquer une collection de valeurs initiales à ranger dans un tableau. A ce propos, voir aussi la remarque 2 de la section 6.2.1.
V-B. Structures et unions▲
V-B-1. Structures▲
Les structures sont des variables composées de champs de types différents. Elles se déclarent sous la syntaxe suivante :
A la suite du mot-clé struct on indique :
- facultativement, le nom que l'on souhaite donner à la structure. Par la suite, l'expression « struct nom » pourra être employée comme une sorte de nom de type ;
- facultativement, entre accolades, la liste des déclarations des champs de la structure. Chaque champ peut avoir un type quelconque (y compris struct, bien sûr) ;
- facultativement, la liste des variables que l'on définit ou déclare comme possédant la structure ici définie.
Exemple.
struct
fiche {
int
numero;
char
nom[32
], prenom[32
];
}
a, b, c;
Cette déclaration introduit trois variables a, b et c, chacune constituée des trois champs numero, nom et prenom ; en même temps elle donne à cette structure le nom fiche. Plus loin, on pourra déclarer de nouvelles variables analogues à a, b et c en écrivant simplement
struct
fiche x, y;
(remarquez qu'il faut écrire « struct fiche » et non pas « fiche »). Nous aurions pu aussi bien faire les déclarations :
struct
fiche {
int
numero;
char
nom[32
], prenom[32
];
}
;
struct
fiche a, b, c, x, y;
ou encore
struct
{
int
numero;
char
nom[32
], prenom[32
];
}
a, b, c, x, y;
mais cette dernière forme nous aurait empêché par la suite de déclarer aussi facilement d'autres variables de même type.
Comme dans beaucoup de langages, on accède aux champs d'une variable de type structure au moyen de l'opérateur « . ». Exemple :
a.numero =
1234
;
Les structures supportent les manipulations « globales » 34 :
- on peut affecter une expression d'un type structure à une variable de type structure (pourvu que ces deux types soient identiques) ;
- les structures sont passées par valeur lors de l'appel des fonctions ;
- le résultat d'une fonction peut être une structure.
La possibilité pour une fonction de rendre une structure comme résultat est officielle dans le C ANSI ; sur ce point les compilateurs plus anciens présentent des différences de comportement. D'autre part il est facile de voir que -sauf dans le cas de structures vraiment très simples- les transmettre comme résultats des fonctions est en général peu efficace.
Voici quelques informations sur la disposition des champs des structures. Elles sont sans importance lorsque les structures sont exploitées par le programme qui les crée, mais deviennent indispensables lorsque les structures sont partagées entre sous-programmes écrits dans divers langages ou lorsqu'elles servent à décrire les articles d'un fichier existant en dehors du programme.
Position. C garantit que les champs d'une structure sont alloués dans l'ordre ou ils apparaissent dans la déclaration. Ainsi, avec la définition
struct
modele {
type1 a, b;
type2 c, d, e;
type3 f;
}
on trouve, dans le sens des adresses croissantes : x.a, x.b, x.c, x.d, x.e et enfin x.f 35. Contiguïté. Les champs des structures subissent en général des contraintes d'alignement dépendant du système sous-jacent, qui engendrent des trous anonymes entre les champs. Il s'agit souvent des mêmes règles que celles qui pèsent sur les variables simples. Deux cas relativement fréquents sont les suivants :
- les champs de type char sont à n'importe quelle adresse, tous les autres champs devant commencer à une adresse paire ;
- les champs d'un type simple doivent commencer à une adresse multiple de leur taille (les short à une adresse paire, les long à une adresse multiple de quatre, etc.).
Initialisation. Une variable de type structure peut être initialisée au moment de sa déclaration, du moins dans le cas d'une variable globale. La syntaxe est la même que pour un tableau. Exemple :
struct
fiche {
int
numero;
char
nom[32
], prenom[32
];
}
u =
{
1234
, "
DURAND
"
, "
Pierre
"
}
;
Comme pour les tableaux, si on indique moins de valeurs que la structure ne comporte de champs, alors les champs restants sont initialisés par des zéros. Donner plus de valeurs qu'il n'y a de champs constitue une erreur.
34L'expression correcte de cette propriété consisterait à dire qu'une structure, comme une expression d'un type primitif, bénéficie de la sémantique des valeurs, par opposition à un tableaux qui, lui, est assujetti à la sémantique des valeurs.
35Alors que, avec la déclaration correspondante en Pascal (facile à imaginer), beaucoup de compilateurs alloueraient les champs dans l'ordre : x.b, x.a, x.e, x.d, x.c et enfin x.f.
V-B-2. Unions▲
Tandis que les champs des structures se suivent sans se chevaucher, les champs d'une union commencent tous au même endroit et, donc, se superposent. Ainsi, une variable de type union peut contenir, à des moments différents, des objets de types et de tailles différents. La taille d'une union est celle du plus volumineux de ses champs.
La syntaxe de la déclaration et de l'utilisation d'une union est exactement la même que pour les structures, avec le mot union à la place du mot struct. Exemple (la struct adresse virt est définie à la section 5.2.3) :
union
mot_machine {
unsigned
long
mot;
char
*
ptr;
struct
adresse_virt adrv;
}
mix;
La variable mix pourra être vue tantôt comme un entier long, tantôt comme l'adresse d'un caractère, tantôt comme la structure bizarre définie ci-après. Par exemple, le programme suivant teste si les deux bits hauts d'une certaine variable m, de type unsigned long, sont nuls :
mix.mot =
m;
if
(
mix.adrv.tp ==
0
) /* etc. */
Remarque 1. Dans ce genre de problèmes, les opérateurs binaires de bits (&, |, ^) s'avèrent également très utiles. L'instruction précédente pourrait aussi s'écrire (sur une machine 32 bits, ou int est synonyme de long) :
if
(
m &
0xC0000000
==
0
) /* etc. */
(l'écriture binaire du chiffre hexa C est 1100). Bien sûr, les programmes de cette sorte n'étant pas portables, ces deux manières de faire peuvent être équivalentes sur une machine et ne pas l'être sur une autre.
Remarque 2. Les unions permettent de considérer un même objet comme possédant plusieurs types ; elles semblent donc faire double emploi avec l'opérateur de conversion de type. Ce n'est pas le cas. D'une part, cet opérateur ne supporte pas les structures, tandis qu'un champ d'une union peut en être une.
D'autre part, lorsque l'on change le type d'un objet au moyen de l'opérateur de changement de type, C convertit la donnée, afin que l'expression ait, sous le nouveau type, une valeur vraisemblable et utile. Par exemple, si x est une variable réelle (float), l'expression « (int) x » convertit x en l'entier dont la valeur est la plus voisine de la valeur qu'avait le réel x. Cela implique une transformation, éventuellement assez coûteuse, de la valeur de x. A l'opposé, lorsqu'on fait référence à un champ d'une union, la donnée ne subit aucune transformation, même si elle a été affectée en faisant référence à un autre champ. Par exemple, si l'on déclare
union
{
float
reel;
int
entier;
}
x;
et qu'ensuite on exécute les affectations :
x.reel =
12
.5
;
i =
x.entier;
l'entier i obtenu sera celui dont la représentation binaire coïncide avec la représentation binaire du nombre flottant 12.5 ; cet entier n'a aucune raison d'être voisin de 12.
V-B-3. Champs de bits▲
Le langage C permet aussi d'utiliser des structures et des unions dont les champs, tous ou seulement certains, sont faits d'un nombre quelconque de bits. De tels champs doivent être ainsi déclarés :
Chaque constante précise le nombre de bits qu'occupe le champ. Le type doit obligatoirement être entier. Lorsque le type et le nom du champ sont absents, le nombre indiqué de bits est quand même réservé : on suppose qu'il s'agit de bits de « rembourrage » entre deux champs nommés.
Chacun de ces champs est logé immédiatement après le champ précédent, sauf si cela le met à cheval sur deux mots (de type int) ; dans ce cas, le champ est logé au début du mot suivant.
Exemple : la structure
struct
adresse_virt {
unsigned
depl : 9
;
unsigned
numpagv : 16
;
:
5
;
unsigned
tp : 2
;
}
;
découpe un mot de 32 bits en en quatre champs, comme indiqué 36 sur la figure 7.
Attention. Dans tous les cas, les champs de bits sont très dépendants de l'implantation. Par exemple, certaines machines rangent les champs de gauche à droite, d'autres de droite à gauche. Ainsi, la portabilité des structures contenant des champs de bits n'est pas assurée ; cela limite leur usage à une certaine catégorie de programmes, comme ceux qui communiquent avec leur machine-hote à un niveau très bas (noyau d'un système d'exploitation, gestion des périphériques, etc.). Ces programmes sont rarement destinés à être portés d'un système à un autre.
36Sur un système où un mot est fait de 32 bits (sinon un champ serait à cheval sur deux mots, ce qui est interdit).
V-C. Enumérations▲
Les énumérations ne constituent pas un type structuré. Si elles figurent ici c'est uniquement parce que la syntaxe de leur déclaration possède des points communs avec celle des structures. Dans les cas simples, cette syntaxe est la suivante :
ou chaque id-valeur est à son tour de la forme
Exemple :
enum
jourouvrable {
lundi, mardi, mercredi, jeudi, vendredi }
x, y;
A la suite d'une telle déclaration, le type enum jourouvrable est connu ; les variables x et y le possèdent. Ce type est formé d'une famille finie de symboles qui représentent des constantes entières : lundi (égale à 0), mardi (égale à 1), etc. Dans ce programme on pourra écrire :
enum
jourouvrable a, b, c;
On aurait aussi bien pu introduire toutes ces variables par les déclarations
enum
jourouvrable {
lundi, mardi, mercredi, jeudi, vendredi }
;
...
enum
jourouvrable x, y, a, b, c;
ou encore
enum
{
lundi, mardi, mercredi, jeudi, vendredi }
x, y, a, b, c;
même type. Bien sûr, les énumérations peuvent se combiner avec la déclaration typedef (cf. section V.D.3) : après la déclaration
typedef
enum
jourouvrable {
lundi, mardi, mercredi, jeudi, vendredi }
JOUROUVRABLE;
les expressions « enum jourouvrable » et « JOUROUVRABLE » sont équivalentes.
Les nombres entiers et les valeurs de tous les types énumérés sont totalement compatibles entre eux. Par conséquent, l'affectation d'un entier à une variable d'un type énuméré ou l'affectation réciproque ne provoquent en principe aucun avertissement de la part du compilateur . Ainsi, en C, les types énumérés ne sont qu'une deuxième manière de donner des noms à des nombres entiers (la première manière étant l'emploi de la directive #define, voir section 8.1.2).
Par défaut, la valeur de chacune de ces constantes est égale au rang de celle-ci dans l'énumération (lundi vaut 0, mardi vaut 1, etc.). On peut altérer cette séquence en associant explicitement des valeurs à certains éléments :
enum
mois_en_r {
janvier =
1
, fevrier, mars, avril, septembre =
9
, octobre, novembre, decembre }
;
(fevrier vaut 2, mars vaut 3, octobre vaut 10, etc.)
V-D. Déclarateurs complexes▲
Jusqu'ici nous avons utilisé des formes simplifiées des déclarations. Tous les objets étaient soit simples, soit des tableaux, des fonctions ou des adresses d'objets simples. Il nous reste à voir comment déclarer des « tableaux de pointeurs » , des « tableaux d'adresses de fonctions », des « fonctions rendant des adresses de tableaux », etc.
Autrement dit, nous devons expliquer la syntaxe des formules qui permettent de décrire des types de complexité quelconque.
La question concerne au premier chef les descripteurs de types, qui apparaissent dans trois sortes d'énoncés :
- les définitions de variables (globales et locales), les déclarations des paramètres formels des fonctions, les déclarations des variables externes et les déclarations des champs des structures et des unions ;
- les définitions des fonctions et les déclarations des fonctions externes ;
- les déclarations de types (typedef)
La même question réapparait dans des constructions qui décrivent des types sans les appliquer à des identificateurs. Appelons cela des types désincarnés. Ils sont utilisés
- par l'opérateur de changement de type lorsque le type de destination n'est pas défini par un identificateur : « (char *) expression » ;
- comme argument de sizeof (rare) : « sizeof(int [100]) » ;
- dans les prototypes des fonctions : « strcpy(char *, char *) ; »
V-D-1. Cas des déclarations▲
Un déclarateur complexe se compose de trois sortes d'ingrédients :
- un type « terminal » qui est un type de base (donc numérique), enum, struct ou union ou bien un type auquel on a donné un nom par une déclaration typedef ;
- un certain nombre d'occurrences des opérateurs (), [] et * qui représentent des procédés récursifs de construction de types complexes ;
- l'identificateur qu'il s'agit de déclarer.
Ces formules ont mauvaise réputation. Si elles sont faciles à lire et à traiter par le compilateur, elles s'avèrent difficiles à composer et à lire par le programmeur, car les symboles qui représentent les procédés récursifs de construction sont peu expressifs et se retrouvent placés à l'envers de ce qui aurait semblé naturel.
Pour cette raison nous allons donner un procédé mécanique qui, à partir d'une sorte de description « à l'endroit », facile à concevoir, fabrique la déclaration C souhaitée. Appelons description naturelle d'un type une expression de l'un des types suivants :
- la description correcte en C d'un type de base, d'un type enum, d'une struct ou union ou le nom d'un type baptisé par typedef ;
- la formule « tableau de T », ou T est la description naturelle d'un type ;
- la formule « adresse d'un T » (ou « pointeur sur un T »), ou T est la description naturelle d'un type ;
- la formule « fonction rendant un T », ou T est la description naturelle d'un type.
Dans ces conditions, nous pouvons énoncer la « recette de cuisine » du tableau 3.
Exemple 1 : soit à déclarer tabPtr comme un tableau (de dimension indéterminée) de pointeurs d'entiers.
Appliquant les règles du tableau 3, nous écrirons successivement :
tableau de pointeur sur int
tabPtr
pointeur sur int
(
tabPtr)[]
pointeur sur int
tabPtr[]
int
*
tabPtr[]
La partie gauche de la ligne ci-dessus se réduisant à un type simple, cette ligne constitue la déclaration C cherchée, comme il faudra l'écrire dans un programme : « int *tabPtr[] ; ».
Exemple 2 : pour la comparer à la précédente, cherchons à écrire maintenant la déclaration de ptrTab, un pointeur sur un tableau 37 d'entiers :
pointeur sur un tableau de int
ptrTab
tableau de int
*
ptrTab
int
(*
ptrTab)[]
Les parenthèses ci-dessus ne sont pas facultatives, car elles encadrent une expression qui commence par * (dit autrement, elles sont nécessaires, car [] a une priorité supérieure à *).
Les déclarations pour lesquelles le membre gauche est le même (à la fin des transformations) peuvent être regroupées. Ainsi, les déclarations de tabPtr et ptrTab peuvent être écrites ensemble :
int
*
tabPtr[], (*
ptrTab)[];
En toute rigueur les règles a et b de ce tableau devraient être plus complexes, car la description naturelle du type d'une fonction inclut en général la spécification des types des arguments (le prototype de la fonction), et la description naturelle d'un type tableau comporte l'indication du nombre de composantes de celui-ci.
En réalité ces indications s'ajoutent aux règles données ici, presque sans modifier le processus que ces règles établissent : tout simplement, des informations annexes sont écrites à l'intérieur des crochets des tableaux et des parenthèses des fonctions.
Exemple 3 : soit à déclarer une matrice de 10 lignes et 20 colonnes. Nos pouvons écrire successivement :
tableau de 10
tableau de 20
double
matrice
tableau de 20
double
(
matrice)[10
]
tableau de 20
double
matrice[10
]
double
(
matrice[10
])[20
]
double
matrice[10
][20
]
Exemple 4 : quand on écrit des programmes relevant du calcul numérique on a souvent besoin de variables de type « adresse d'une fonction R → R ». Voici comment écrire un tel type
adresse de fonction (
avec argument double
) rendant double
adrFon
fonction (
avec argument double
) rendant double
*
adrFon
double
(*
adrFon)(
double
)
La déclaration cherchée est donc « double (*adrFon)(double) ».
Exemple 5 : cet exemple abominable est extrait de la bibliothèque UNIX. La fonction signal renvoie l'adresse d'une « procédure », c'est-à-dire d'une fonction rendant void. Voici comment il faudrait la déclarer en syntaxe originale, c'est-à-dire sans spécifier les arguments de la fonction signal (cette déclaration est fournie dans le fichier <signal.h>) :
fonction rendant un pointeur sur fonction rendant void
signal
pointeur sur fonction rendant un void
(
signal)(
)
pointeur sur fonction rendant un void
signal
(
)
fonction rendant un void
*
signal
(
)
void
(*
signal
(
))(
)
En syntaxe ANSI il faut en plus donner les types des arguments des fonctions. On nous dit que les arguments de la fonction signal sont un int et un pointeur sur une fonction prenant un int et rendant un void. En outre, signal rend un pointeur vers une telle fonction. Nous savons déjà écrire le type « pointeur sur une fonction prenant un int et rendant un void » (ce n'est pas très différent de l'exemple précédent) :
void
(*
fon)(
int
);
Attaquons-nous à signal :
fonction (
avec arguments int
sig et int
(*
fon)(
int
))
rendant un pointeur sur fonction
(
avec argument int
) rendant void
signal
pointeur sur fonction (
avec argument int
) rendant void
(
signal)(
int
sig, int
(*
fon)(
int
))
pointeur sur fonction (
avec argument int
) rendant void
signal
(
int
sig, int
(*
fon)(
int
))
fonction (
avec argument int
s) rendant void
*
signal
(
int
sig, int
(*
fon)(
int
))
void
(*
signal
(
int
sig, int
(*
fon)(
int
)))(
int
)
Finalement, la déclaration cherchée est
void
(*
signal
(
int
sig, int
(*
fon)(
int
)))(
int
) ;
Comme annoncé, les expressions finales obtenues sont assez lourdes à digérer pour un humain. A leur décharge on peut tout de même dire qu'une fois écrites elles se révèlent bien utiles puisqu'elles font apparaitre les types de leurs éléments terminaux comme ils seront employés dans la partie exécutable des programmes. Ainsi, à la vue de l'expression
float
matrice[10
][20
];
on comprend sans effort supplémentaire qu'un élément de la matrice s'écrira matrice[i][j] et que cette formule représentera un objet de type float. De même, l'énoncé
void
(*
signal
(
sig, func))(
);
nous fera reconnaitre immédiatement l'expression (*signal(2,f))(n) comme un appel correct de la « procédure » rendue par l'appel de signal.
37La notion de pointeur sur un tableau est assez académique, puisqu'une variable de type tableau représente déjà l'adresse de celui-ci (dans la notion d'« adresse d'un tableau » il y a une double indirection). En pratique on n'en a presque jamais besoin.
V-D-2. Pointeurs et tableaux constants et volatils▲
Les qualifieurs const et volatile peuvent aussi apparaitre dans les déclarateurs complexes. Plutôt que de rendre encore plus lourdes des explications qui le sont déjà beaucoup, contentons-nous de montrer des exemples significatifs. Les cas les plus utiles sont les suivants :
- La formule :
Sélectionnez
const
type*
nomdéclare nom comme un pointeur vers un objet constant ayant le type indiqué (la variable pointée, *nom, ne doit pas être modifiée).
- La formule
Sélectionnez
type
*
const
nompas être modifiée).
- Plus simplement, la formule
Sélectionnez
const
type nom[expropt]déclare nom comme un tableau d'objets constants 38 (nom[i] ne doit pas être modifié).
Ainsi par exemple les déclarations
int
e, *
pe, *
const
pce =
&
e, te[100
];
const
int
ec =
0
, *
pec, *
const
pcec =
&
ec, tec[100
];
déclarent successivement :
- e : un entier
- pe : un pointeur vers un entier
- pce : un pointeur constant vers un entier (qui doit être obligatoirement initialisé, puisqu'il est constant)
- te : un tableau de 100 entiers
- ec : un entier constant
- pec : un pointeur vers un entier constant
- pcec : un pointeur constant vers un entier constant
- tec : un tableau de 100 entiers constants
Toute affectation avec pour membre gauche pce, ec, *pec, pcec, *pcec ou tec[i] est illégale.
En résumé, la seule vraie nouveauté introduite ici est la notion de pointeur constant et la syntaxe de sa déclaration « type-pointé *const pointeur ». Signalons enfin que toute cette explication reste valable en remplaçant le mot const et la notion d'objet constant par le mot volatile et le concept d' objet volatil (cf. section I.E.7). On a donc la possibilité de déclarer des tableaux d'objets volatils, des pointeurs volatils vers des objets, volatils ou non, etc.
38Notez qu'il n'y a pas besoin d'une notation spéciale pour déclarer un tableau constant d'objets (variables) puisque la signification ordinaire des tableaux est exactement celle-là.
V-D-3. La déclaration typedef▲
Cette construction est l'homologue de la déclaration type du langage Pascal. Elle permet de donner un nom à un type dans le but d'alléger par la suite les expressions ou il figure. Syntaxe :
typedef
déclaration
Pour déclarer un nom de type, on fait suivre le mot réservé typedef d'une expression identique à une déclaration de variable, dans laquelle le rôle du nom de la variable est joué par le nom du type qu'on veut définir. Exemple : les déclarations
typedef
float
MATRICE[10
][20
];
...
MATRICE mat;
sont équivalentes à
float
mat[10
][20
];
Voici un autre exemple :
typedef
struct
noeud {
int
info;
struct
noeud *
fils_gauche, *
fils_droit;
}
NOEUD;
Cette expression déclare l'identificateur NOEUD comme le nom du type « la structure noeud » . Des variables a, b, c de ce type pourront par la suite être déclarées aussi bien par l'expression
struct
noeud a, b, c;
que par
NOEUD a, b, c;
Remarquez l'utilisation différente d'un nom de structure (précédé du mot struct) et d'un nom de type. Par tradition, les noms des types définis à l'aide de la déclaration typedef sont écrits en majuscules. L'ensemble des règles de syntaxe qui s'appliquent aux déclarations de variables s'appliquent également ici. Par exemple, on peut introduire plusieurs noms de types dans un seul énoncé typedef. Ainsi, à la suite de
typedef
struct
noeud {
int
info;
struct
noeud *
fils_gauche, *
fils_droit;
}
NOEUD, *
ARBRE;
les identificateurs NOEUD et ARBRE sont des synonymes pour « struct noeud » et « struct noeud * ». Dans la construction des déclarateurs complexes, le nom d'un type qui a fait l'objet d'une déclaration typedef peut être utilisé comme type de base. Par exemple :
typedef
float
LIGNE[20
];
typedef
LIGNE MATRICE[10
];
est une autre manière d'introduire le type MATRICE déjà vu. De même
typedef
void
PROCEDURE
(
int
);
PROCEDURE *
signal
(
int
sig, PROCEDURE *
func);
est une autre manière (nettement plus facile à lire, n'est-ce pas ?) de déclarer la fonction signal vue précédemment.
V-D-4. Cas des types désincarnés▲
Tout cela devient encore plus ésotérique lorsqu'on doit construire un type sans l'appliquer à un identificateur. Le principe est alors le suivant : construisez la déclaration correcte d'un nom quelconque X, puis effacez X. De telles expressions sont utiles dans trois circonstances :
- Pour construire un opérateur de changement de type. Exemple : la fonction malloc alloue un bloc de mémoire de taille donnée, dont elle renvoie l'adresse. Dans les bibliothèques du C original , cette fonction est déclarée comme rendant l'adresse d'un char, et l'utilisateur doit convertir le résultat de malloc dans le type qui l'intéresse. Supposons avoir besoin d'espace pour ranger un tableau de N réels en double précision.
Déclarations 40 :Sélectionnezchar
*
malloc
(
size_t); ...double
*
p;Utilisation :
Sélectionnezp
=
(
double
*
)malloc
(
N*
sizeof
(
double
));/* DéCONSEILLé EN C ANSI */
Le type désincarné « double * » apparaissant ci-dessus a été déduit d'une déclaration de pointeur, « double *ptr » en effaçant le nom déclaré, ptr.
Rappelons qu'en C ANSI l'expression ci-dessus s'écrit plus simplement (voyez à la section 2.2.12, notamment à la page 26, des indications et mises en garde à propos de l'opérateur de changement de type) :
Sélectionnezp
=
malloc
(
N*
sizeof
(
double
)); - Comme argument de l'opérateur sizeof. Essayons par exemple d'écrire autrement l'affectation précédente. Déclarons un tableau T de N doubles :
Sélectionnez
double
T[N]Supprimons T, il reste « double [N] ». Par conséquent, l'appel de la fonction malloc montré ci-dessus peut s'écrire aussi 41 :
Sélectionnezp
=
(
double
*
)malloc
(
sizeof
(
double
[N])); - Dans les prototypes des fonctions, en C ANSI. Par exemple, la bibliothèque standard C comporte la fonction qsort, qui implémente une méthode de tri rapide d'un tableau (algorithme Quicksort) programmé en toute généralité. Les arguments de cette fonction sont le tableau à trier, le nombre de ses éléments, la taille de chacun et une fonction qui représente la relation d'ordre utilisée (le « critère de tri ») : elle reçoit deux adresses d'éléments du tableau et rend un nombre négatif, nul ou positif exprimant la comparaison.
La fonction qsort est donc ainsi déclarée dans le fichier stdlib.h :
Sélectionnezvoid
qsort
(
const
void
*
, size_t, size_t,int
(*
)(
const
void
*
,const
void
*
));
Note. Ce n'est pas requis par la syntaxe mais, lorsqu'ils sont bien choisis, les noms des arguments sont une aide pour le programmeur. Exemple :
void
qsort
(
const
void
*
tableau, size_t nombre, size_t taille,
int
(*
compare)(
const
void
*
adr1, const
void
*
adr2));
39Dans la bibliothèque ANSI (fichier stdlib.h) la fonction malloc est déclarée comme rendant une valeur de type « void * ». Ce type étant compatible avec tous les autres types pointeur, le problème exposé ici ne se pose pas.
40Le type size t (défini dans <stddef.h>) représente un entier sans signe
41L'exemple montré ici, « sizeof(double [N]) », est franchement ésotérique. La notation « N * sizeof(double) » est bien plus raisonnable.