Nothing Special   »   [go: up one dir, main page]

Info CH 6

Télécharger au format pdf ou txt
Télécharger au format pdf ou txt
Vous êtes sur la page 1sur 37

Chapitre 6

Les structures et les pointeurs

Ce chapitre est consacré à l’étude de deux mécanismes de programmation essentiels à la


plupart des applications : la définition de types de données structurés, et la notion de pointeur
que nous avons déjà rencontrée au chapitre 5 dans le cadre de la manipulation de tableaux.

6.1 Les données structurées

Nous avons vu aux sections 2.2 et 4.4 comment définir une variable, qui correspond à un
emplacement de la mémoire de l’ordinateur capable de retenir une valeur d’un type donné. Nous
avons ensuite au chapitre 5 généralisé cette notion de variable en montrant comment représenter
des tableaux, qui correspondent à un ensemble d’emplacements de mémoire numérotés, retenant
des valeurs partageant le même type.

La notion de structure généralise ces concepts. Une structure peut être vue comme un en-
semble d’emplacements de la mémoire qui ne sont pas identifiés par un numéro mais par un nom
appelé champ. Chaque champ d’une structure peut retenir une donnée d’un type déterminé, ce
type pouvant être différent d’un champ à un autre.

À l’aide d’une telle structure, on peut par exemple représenter la fiche signalétique d’un étu-
diant au sein d’un logiciel gérant les inscriptions à l’Université. Une telle structure possédera des
champs correspondant respectivement au nom, au prénom, à la date de naissance, aux données
d’inscription, à une photographie, . . . , de l’étudiant concerné. L’intérêt du mécanisme de struc-
ture est de permettre de manipuler l’ensemble de ces données en tant qu’une seule entité, même
si certaines d’entre elles possèdent des types différents.

En programmation, on est ainsi fréquemment amené à traiter des assemblages de données


de types hétérogènes. Un autre exemple concerne la représentation informatique d’une image.

155
Celle-ci peut prendre la forme d’une structure dont les champs correspondent

— aux dimensions horizontale et verticale de l’image, qui sont des nombres entiers,
— à son contenu, représenté par un tableau bidimensionnel de pixels,
— à un commentaire, prenant la forme d’une chaîne de caractères,
— ...

Dans une telle structure, chaque pixel peut à son tour être représenté par une structure regroupant
ses composantes de couleur.

On peut également employer des structures pour représenter des données dont les compo-
santes partagent le même type, mais pour lesquelles on préfère nommer ces composantes plutôt
que de les numéroter comme on le ferait pour les éléments d’un tableau. Par exemple, on peut
représenter un nombre complexe par une structure définissant deux champs réels pour ses parties
réelle et imaginaire, et appeler ces deux champs re et im. On pourrait aussi utiliser une structure
pour représenter les transformations géométriques introduites à la section 5.5.3, en définissant
des champs différents pour la matrice de transformation et le vecteur de translation.

6.1.1 La définition d’un type de données structuré

En langage C, la construction suivante permet de définir un nouveau type structuré.

struct id
{
type1 id1 [, id1b [, ... ]];
type2 id2 [, id2b [, ... ]];
..
.
};

Ce nouveau type a pour nom struct id, où id est l’identificateur qui figure dans la définition.
Chaque variable de ce type possédera les champs id1, id2, . . . , dont le type respectif est donné
par type1, type2, . . . Il est aussi permis de définir plusieurs champs qui partagent le même type
en les séparant par des virgules.

Par exemple, la définition

struct complexe
{
double re, im;
};

156
est celle d’un type structuré appelé struct complexe, que l’on peut ensuite utiliser dans des
définitions et déclarations de variables. On peut par exemple écrire

struct complexe x, y, z;

pour définir trois variables x, y et z de ce type. Chacune de ces variables contient alors deux
champs re et im capables de retenir une valeur réelle de type double.

On accède aux champs d’une variable possédant un type structuré grâce à l’opérateur “.” :
on place à gauche de cet opérateur le nom de la variable concernée, et à droite le nom du champ
auquel on souhaite accéder. La forme obtenue constitue à la fois une expression, dont l’évaluation
fournit la valeur du champ en question, mais aussi une valeur à gauche qui peut servir à modifier
cette valeur. Dans l’exemple précédent, les parties réelle et imaginaire des variables x, y et z se
notent ainsi x.re , x.im , y.re , y.im , z.re et z.im .

Les définitions de types structurés sont habituellement placées en dehors des fonctions ; dans
ce cas, ces types sont utilisables dans la suite du programme, jusqu’à la fin du fichier source cou-
rant. Il est également autorisé de placer une telle définition à l’intérieur d’un bloc d’instructions
exécutables. Dans ce cas, la portée de cette définition suit les mêmes règles que pour les décla-
rations de variables locales, et s’étend jusqu’à la fin du plus petit bloc contenant cette définition.

Notons qu’il est permis de fusionner la définition d’un type structuré avec celle d’une ou de
plusieurs variables de ce type ; il est alors autorisé (mais pas obligatoire) d’omettre l’identificateur
du type après le mot-clé struct. Par exemple, on aurait pu définir de la façon suivante les
variables x, y et z de l’exemple précédent.

struct
{
double re, im;
} x, y, z;

Cette forme de définition n’est utile que si le type concerné n’est utilisé qu’une seule fois
dans le programme. En pratique, il est fréquent qu’un type intervienne dans plusieurs définitions
de variables, ou dans les paramètres d’une ou plusieurs fonctions. Dans un tel cas, il vaut mieux
nommer le type afin de pas devoir le redéfinir de façon redondante.

Lorsqu’on utilise des types structurés, le fait de devoir systématiquement préfixer leur nom
du mot-clé struct nuit parfois à la lisibilité des programmes. Cela peut être évité grâce à la
construction

typedef type id; ,

157
qui permet dans la suite du programme de faire référence au type type par le nom abrégé id. Le
type type peut correspondre à n’importe quel type étudié jusqu’ici : primitif, tableau ou struc-
turé. Dans le cas d’un type structuré, il est permis de ne pas le nommer en omettant d’écrire un
identificateur à la suite du le mot-clé struct. Par exemple, la construction

typedef struct
{
double re, im;
} complexe;

définit un nouveau type appelé complexe, et non struct complexe comme précédemment.
Pour définir trois variables x, y et z de ce type, on écrit alors

complexe x, y, z;

Signalons pour terminer qu’il est permis qu’un type structuré possède des champs de n’im-
porte quel type : primitif, tableau, ou même structuré. L’exemple suivant est celui d’une structure
représentant la fiche signalétique d’un étudiant ; il n’est valide que si le type struct date a
précédemment été défini.

#define LONGUEUR_MAX_NOM 50

struct etudiant
{
char nom[LONGUEUR_MAX_NOM + 1],
prenom[LONGUEUR_MAX_NOM + 1];
struct date date_naissance;
..
.
};

6.1.2 L’utilisation des données structurées

Les types structurés peuvent être attribués aux paramètres d’une fonction. Le mécanisme qui
est alors mis en œuvre est celui d’un passage par valeur, similaire au cas des types primitifs
étudié à la section 4.2.2. Cela signifie que lorsqu’une donnée structurée est passée en argument
à une fonction ou une procédure, cette donnée est recopiée dans le paramètre correspondant au
moment de l’appel. Si la fonction ou la procédure modifie ensuite la valeur de ce paramètre, cette
modification aura un effet purement local et ne sera pas répercutée vers le code appelant.

158
#include <stdio.h>

struct s
{
int t[2];
};

void permuter(struct s a)
{
int aux;

aux = a.t[0];
a.t[0] = a.t[1];
a.t[1] = aux;
}

int main()
{
struct s x;

x.t[0] = 10;
x.t[1] = 20;

permuter(x);

printf("%d %d\n", x.t[0], x.t[1]);

return 0;
}

Figure 6.1 – Passage d’une structure à une fonction

Il est important de bien réaliser que ce mécanisme diffère fondamentalement de celui qui
régit le passage d’un tableau en argument à une fonction, étudié aux sections 5.2 et 5.5.2. Pour
rappel, dans le cas d’un tableau, seul un pointeur vers son contenu est passé à la fonction ; cela
entraîne qu’une modification des éléments du tableau effectuée par la fonction devient visible
pour le code qui invoque celle-ci.

Pour illustrer cette différence, nous considérons le programme de la figure 6.1. Celui-ci définit
un type structuré struct s possédant un champ t qui prend la forme d’un vecteur de deux
entiers. La fonction principale de ce programme alloue une variable x de ce type, et place les
valeurs 10 et 20 dans les deux cases du tableau. Elle invoque ensuite la procédure permuter en
lui passant en argument la donnée structurée contenue dans x, ce qui revient à recopier le contenu
de x dans le paramètre a de cette procédure. L’opération de permutation des deux éléments du

159
#include <stdio.h>

void permuter(int a[2])


{
int aux;

aux = a[0];
a[0] = a[1];
a[1] = aux;
}

int main()
{
int x[2];

x[0] = 10;
x[1] = 20;

permuter(x);

printf("%d %d\n", x[0], x[1]);

return 0;
}

Figure 6.2 – Passage d’un tableau à une fonction

vecteur effectuée par la procédure affecte alors uniquement le contenu de a et pas celui de x, qui
reste inchangé. Après cette opération, la fonction main affiche donc “10 20”.

On peut maintenant comparer ce programme à celui de la figure 6.2, dans lequel le type struc-
turé a été remplacé par un vecteur de deux entiers. Dans ce deuxième programme, la procédure
permuter reçoit en argument un pointeur vers le vecteur x alloué par la fonction main. Cela
entraîne que les modifications réalisées par permuter sont effectuées dans ce vecteur x et non
dans une copie de ce vecteur comme pour le programme de la figure 6.1. Ces modifications sont
donc maintenant visibles pour le code qui invoque permuter. Contrairement au précédent, ce
programme affiche donc “20 10”.

Pour terminer cette discussion portant sur l’utilisation des types structurés par les fonctions,
signalons qu’il est permis qu’une fonction retourne une valeur de type structuré. Il est également
possible d’appliquer l’opérateur d’affectation “=” à une variable de type structuré ; par exemple,
si x et y sont des variables partageant le même type structuré, alors l’expression x = y recopie
l’entièreté du contenu de y dans x. En revanche, l’opérateur de comparaison “==” ne peut pas
être employé avec des valeurs de type structuré.

160
6.1.3 Application : manipulation de nombres complexes

Un exemple de programme exploitant les types structurés est donné aux figures 6.3 et 6.4.
Ce programme implémente une bibliothèque de fonctions permettant de manipuler des nombres
complexes 1 . Son fichier d’en-tête complexe.h contient les prototypes de ces fonctions, mais
également la définition du type structuré complexe. Cela permet de faire intervenir ce type dans
le prototype des fonctions, mais aussi de l’utiliser dans n’importe quel programme qui inclut ce
fichier d’en-tête.

6.2 Les pointeurs

La notion de pointeur a déjà été rencontrée à la section 5.2, dans le cadre du passage de
tableaux en argument à une fonction ou une procédure. Nous généralisons à présent ce concept
à des données de type quelconque.

6.2.1 Le passage d’arguments par variable

À la section 6.1.2, nous avons vu que le passage d’une donnée structurée en argument à
une fonction s’effectue par valeur, ce qui signifie que le contenu de cet argument est entièrement
recopié dans le paramètre correspondant au moment de l’appel. Ce mécanisme présente plusieurs
inconvénients. Premièrement, dans le cas d’un donnée de grande taille comme par exemple une
image, cette opération de copie est inefficace : elle prend du temps, et elle nécessite de disposer
d’un double espace mémoire, pour la donnée originale et pour la copie. Un autre inconvénient est
que la fonction ne dispose pas de la possibilité de retourner des valeurs au code qui l’appelle par
l’intermédiaire de ses arguments, ce qui pourrait être utile dans le cas où l’on souhaite retourner
plusieurs valeurs à la fois.

En revanche, quand une fonction ou une procédure reçoit un tableau en argument, nous sa-
vons que le mécanisme employé, appelé passage par variable, est différent et consiste à trans-
mettre un pointeur vers le tableau. Ces notions de pointeur et de passage par variable peuvent
être généralisées à des données de n’importe quel type : un pointeur vers une donnée représente
l’endroit où celle-ci se trouve dans la mémoire de l’ordinateur. Lorsqu’on passe un pointeur vers
une donnée à une fonction, cette donnée n’est pas recopiée, ce qui signifie qu’elle ne reste pré-
sente qu’en un seul exemplaire dans la mémoire de l’ordinateur. Si la fonction appelée modifie
la donnée désignée par le pointeur, alors cette modification devient visible pour le code appelant,

1. Signalons que les versions modernes du langage C disposent déjà d’une représentation et d’opérations de
manipulation de nombres complexes.

161
complexe.c:

#include <math.h>
#include "complexe.h"

complexe complexe_init(double re, double im)


{
complexe c;

c.re = re;
c.im = im;

return c;
}

double complexe_re(complexe c)
{
return c.re;
}

double complexe_im(complexe c)
{
return c.im;
}

complexe complexe_somme(complexe a, complexe b)


{
complexe c;

c.re = a.re + b.re;


c.im = a.im + b.im;

return c;
}

complexe complexe_difference(complexe a, complexe b)


{
complexe c;

c.re = a.re - b.re;


c.im = a.im - b.im;

return c;
}

Figure 6.3 – Bibliothèque de manipulation de nombres complexes (1/2)

162
complexe complexe_produit(complexe a, complexe b)
{
complexe c;

c.re = a.re * b.re - a.im * b.im;


c.im = a.re * b.im + a.im * b.re;

return c;
}

double complexe_module(complexe a)
{
return hypot(a.re, a.im);
}

double complexe_distance(complexe a, complexe b)


{
return complexe_module(complexe_difference(a, b));
}

complexe.h:

typedef struct
{
double re, im;
} complexe;

complexe complexe_init(double, double);


double complexe_re(complexe);
double complexe_im(complexe);
complexe complexe_somme(complexe, complexe);
complexe complexe_difference(complexe, complexe);
complexe complexe_produit(complexe, complexe);
double complexe_module(complexe);
double complexe_distance(complexe, complexe);

Figure 6.4 – Bibliothèque de manipulation de nombres complexes (2/2)

163
comme nous l’avons vu avec le programme de la figure 6.2. Ce mécanisme permet donc à une
fonction ou une procédure de retourner des valeurs au code qui l’invoque par l’intermédiaire de
ses arguments.

6.2.2 Les pointeurs en langage C

En langage C, si T est un type, alors T * désigne le type d’un pointeur vers une valeur de
type T. Par exemple, la définition

int *p;

est celle d’une variable p contenant un pointeur vers une valeur entière. Il est aussi possible
de définir des pointeurs vers des données arbitraires, c’est-à-dire de n’importe quel type, en
employant le type 2 void * .

Le langage C définit deux opérateurs liés aux pointeurs. L’opérateur “&” s’applique à une
valeur à gauche, comme le nom d’une variable, un champ d’une donnée structurée ou un élé-
ment d’un tableau. L’expression obtenue fournit un pointeur vers la donnée correspondante. Par
exemple, si v est une variable de type int, alors l’évaluation de l’expression &v retourne un
pointeur vers v. La valeur de ce pointeur représente l’endroit où se trouve la variable v dans la
mémoire de l’ordinateur ; on dit alors que ce pointeur pointe vers v. Le type de ce pointeur se
note int * .

Le deuxième opérateur, qui peut être vu comme le dual de “&”, est l’opérateur 3 unaire “*”.
Lorsque cet opérateur est appliqué à un pointeur, il déréférence ce pointeur, ce qui signifie qu’il
accède à la donnée pointée par celui-ci. En d’autres termes, l’évaluation de l’expression *expr ,
où expr s’évalue en un pointeur p, retourne la donnée pointée par p. Cette expression constitue
également une valeur à gauche, et peut donc être placée avant l’opérateur d’affectation “=” pour
modifier la donnée pointée. Lorsqu’on utilise ce mécanisme, il est important de garantir que
le pointeur pointe bien vers un emplacement valide de la mémoire de l’ordinateur ; c’est au
programmeur qu’incombe la responsabilité d’assurer cela.

Un exemple de programme manipulant un pointeur à l’aide de ces opérateurs est donné à la


figure 6.5. Ce programme définit une variable entière a et un pointeur p vers un entier. Signalons
qu’il aurait été possible de fusionner les déclarations de ces deux variables en écrivant

2. Il s’agit de la troisième application du mot-clé void que nous rencontrons, après son utilisation pour spécifier
qu’une fonction ne possède pas de valeur de retour, ou n’accepte pas d’argument.
3. Il s’agit aussi de la troisième utilisation du symbole “*”, qui dénote également l’opérateur binaire de multi-
plication, et qui apparaît dans le type des pointeurs.

164
#include <stdio.h>

int main()
{
int a;
int *p;

p = &a;
*p = 42;

printf("%d\n", a);

return 0;
}

Figure 6.5 – Exemple de programme utilisant un pointeur

1000 : 42 1000 : 42

1004 : 1000 1004 :

Figure 6.6 – Représentation graphique d’un pointeur

int a, *p; .

Le programme attribue ensuite à p la valeur retournée par l’évaluation de &a , ce qui a pour
effet de faire pointer p vers a. L’instruction suivante affecte la valeur 42 à la donnée pointée par
p, ce qui est équivalent à attribuer cette valeur à a. Ce programme affiche donc “42”.

Nous voyons dans cet exemple qu’il est autorisé d’employer l’opérateur d’affectation “=”
pour attribuer une valeur à un pointeur. Il est aussi possible d’appliquer l’opérateur de compa-
raison “==” à des pointeurs : deux pointeurs sont considérés comme étant égaux si et seulement
s’ils pointent vers la même donnée, c’est-à-dire s’ils désignent le même endroit dans la mémoire
de l’ordinateur.

D’un point de vue technique, on peut considérer que les cases qui composent la mémoire de
l’ordinateur sont numérotées, le numéro attribué à une case étant appelé son adresse. La valeur
d’un pointeur vers une donnée est alors égale à l’adresse de celle-ci. Cette situation est illustrée à
la figure 6.6. Dans cet exemple, on suppose que les variables a et p de l’exemple précédent sont
respectivement placées aux adresses 1000 et 1004 de la mémoire. La partie gauche de la figure

165
#include <stdio.h>

/* Permute les entiers pointés par p1 et p2. */

void permuter(int *p1, int *p2)


{
int aux;

aux = *p1;
*p1 = *p2;
*p2 = aux;
}

int main()
{
int a, b;

a = 42;
b = -7;

printf("a = %d, b = %d\n", a, b);


permuter(&a, &b);
printf("a = %d, b = %d\n", a, b);

return 0;
}

Figure 6.7 – Exemple de passage d’arguments par variable

montre le contenu de ces variables lorsque a vaut 42 et p pointe vers a. En pratique, les adresses
précises des variables ne sont pas connues par le programmeur. On dessine alors un diagramme
comme celui de la partie droite de la figure pour indiquer que p pointe vers a.

Il existe une valeur spéciale de pointeur, appelée le pointeur vide, qui ne pointe pas vers un
emplacement valide de la mémoire, mais par rapport à laquelle n’importe quel pointeur peut être
comparé. Le pointeur vide peut être utilisé pour signaler une situation particulière, comme par
exemple l’échec d’une opération censée retourner un pointeur. Le pointeur vide ne peut pas être
déréférencé. En langage C, le pointeur vide se note NULL , qui est une macro définie notamment
dans les fichiers d’en-tête stdlib.h et stdio.h. Lorsqu’on la convertit en un entier grâce à une
conversion de type explicite ou implicite, NULL prend la valeur 0.

Un exemple de programme utilisant des pointeurs pour implémenter un passage d’argu-


ments par variable à une procédure est donné à la figure 6.7. Dans ce programme, la procédure
permuter reçoit en arguments deux pointeurs vers des valeurs entières, et a pour effet de per-

166
muter ces deux valeurs. On peut donc l’employer dans le reste du programme pour permuter
n’importe quelle paire de valeurs entières, à condition de disposer de pointeurs vers ces valeurs.
Dans la fonction main, cette procédure est invoquée en lui passant un pointeur vers la variable
a et un autre vers b ; à l’issue de cet appel, les valeurs de a et de b auront donc été permutées.
Le programme affiche donc “a = 42, b = -7” avant l’appel, et “a = -7, b = -42” après
celui-ci.

L’instruction permuter(&a, &b); qui figure dans ce programme ressemble à l’instruction


scanf("%d %d", &a, &b); présente dans celui de la figure 1.2, qui est le premier programme
que nous avons présenté dans ce cours. Nous sommes maintenant à même d’expliquer les prin-
cipes de la fonction scanf de la bibliothèque standard, qui permet de lire des valeurs entrées
par l’utilisateur. Cette fonction admet comme arguments une chaîne de caractères suivie par un
certain nombre de pointeurs de type void *. Chaque fois que la fonction détecte un marqueur
de format commençant par “%” dans la chaîne de caractères, elle lit une valeur du type indiqué
par ce marqueur, et l’écrit à l’endroit spécifié par un des pointeurs, en considérant ceux-ci dans
l’ordre où ils apparaissent. Les marqueurs de format pouvant être employés sont similaires à
ceux de la fonction printf et incluent
— “%d” pour les entiers signés (de type int),
— “%u” pour les entiers non signés (de type unsigned),
— “%ld”, “%lu”, “%lld” et “%llu” pour (respectivement) des valeurs de type long int,
long unsigned, long long int et long long unsigned.
— “%f”, “%lf” pour (respectivement) des valeurs de type float et double,
— ...
L’instruction scanf("%d %d", &a, &b); a donc pour effet de lire deux valeurs entières
signées et de les écrire successivement dans les variables a et b. Mentionnons pour terminer que
la fonction scanf retourne un entier égal au nombre de valeurs qui ont été correctement lues.

6.2.3 Les pointeurs vers des données structurées

Il est assez fréquent d’utiliser des pointeurs pour accéder à des données structurées. Comme
nous l’avons vu en discutant des mécanismes de passage d’arguments à une fonction, cela peut
permettre d’éviter de recopier ces données, ce qui présente un coût prohibitif quand elles sont de
grande taille.

Le langage C définit un opérateur dédié à l’accès aux champs d’une donnée structurée pointée
par un pointeur. Si p est un pointeur vers une donnée structurée possédant un champ c, alors
l’expression

p -> c

167
θ

Figure 6.8 – Configuration spatiale d’un mobile

est un raccourci d’écriture pour

(*p).c .

En d’autres termes, cette expression déréférence le pointeur p et accède au champ c de la valeur


structurée pointée par celui-ci. Une expression de cette forme constitue également une valeur à
gauche.

Nous illustrons l’utilisation de pointeurs vers des données structurées à l’aide du programme
des figures 6.9 et 6.10. Celui-ci fournit une bibliothèque permettant de manipuler la configuration
spatiale d’un mobile évoluant dans un espace à deux dimensions, c’est-à-dire la combinaison de
sa position et de son orientation. La figure 6.8 montre comment cette configuration s’exprime :
la position du mobile correspond aux coordonnées cartésiennes (x, y) de son point de référence,
et son orientation est représentée par l’angle θ que fait son axe géométrique avec l’axe des y.

Les angles manipulés par les fonctions trigonométriques de la bibliothèque standard doivent
être exprimés en radians. La fonction normaliser_angle a pour but de normaliser la repré-
sentation d’un angle en ramenant sa valeur dans l’intervalle fermé à gauche et ouvert à droite
[−π, π). Son implémentation repose sur la fonction fmod de la bibliothèque standard, qui calcule
le reste de la division réelle de son premier argument par le second.

La procédure init_configuration sert à initialiser une configuration à l’aide de valeurs


données de x, y et θ. Le premier argument de cette procédure est passé par variable, ce qui permet
à la procédure d’en modifier le contenu. Ce mécanisme de passage par variable est également
mis en œuvre par les procédures deplacer_configuration et tourner_configuration ;
celles-ci utilisent leur premier argument comme donnée d’entrée, représentant la configuration du
mobile avant le mouvement qu’elles calculent, mais aussi comme donnée de sortie pour recevoir
le résultat de ce calcul.

168
configuration-2d.c:

#include <math.h>
#include "configuration-2d.h"

/*
Retourne un angle équivalent à <angle>, mais appartenant à
l’intervalle [-PI, PI).
*/

static double normaliser_angle(double angle)


{
double r;

r = fmod(angle, 2.0 * M_PI);


if (r < -M_PI)
r += 2.0 * M_PI;
else
if (r >= M_PI)
r -= 2.0 * M_PI;

return r;
}

/*
Initialise la configuration *<c> avec la position (<x>, y>) et
l’orientation <theta>.
*/

void init_configuration(configuration *c, double x, double y, double theta)


{
c -> x = x;
c -> y = y;
c -> theta = normaliser_angle(theta);
}

/*
Met à jour la configuration *<c> après un déplacement de longueur <l>
dans la direction courante.
*/

void deplacer_configuration(configuration *c, double l)


{
c -> x -= l * sin(c -> theta);
c -> y += l * cos(c -> theta);
}

Figure 6.9 – Bibliothèque de manipulation de configurations 2D (1/2)

169
/*
Met à jour la configuration *<c> après une rotation d’angle <angle>.
*/

void tourner_configuration(configuration *c, double angle)


{
c -> theta = normaliser_angle(c -> theta + angle);
}

configuration-2d.h:

typedef struct
{
double x, y; /* position */
double theta; /* orientation */
} configuration;

void init_configuration(configuration *, double, double, double);


void deplacer_configuration(configuration *, double);
void tourner_configuration(configuration *, double);

Figure 6.10 – Bibliothèque de manipulation de configurations 2D (2/2)

6.2.4 Les pointeurs et les tableaux

Nous avons déjà évoqué à la section 5.2.3 le lien qui existe entre les pointeurs et les tableaux :
si v est l’identificateur d’un vecteur, par exemple s’il a été déclaré par l’instruction

int v[100]; ,

alors l’expression v s’évalue en un pointeur vers le premier élément de v, et une expression de


la forme v + i fournit un pointeur vers le (i + 1)-ème élément de v.

On en déduit que les expressions v et &v[0] sont équivalentes ; en effet, elles s’évaluent
toutes deux en un pointeur vers le premier élément de v. De même, les expressions v + i et
&v[i] sont équivalentes pour n’importe quelle valeur de i.

Nous savons que quand un tableau est passé en argument à une fonction, le paramètre de
celle-ci prend pour valeur un pointeur vers le premier élément de ce tableau. On en déduit que
pour un type T quelconque, les déclarations T a[] et T *a d’un paramètre a d’une fonction
ou d’une procédure sont équivalentes. Ces deux formes de déclarations de paramètres peuvent
être utilisées indifféremment.

170
/*
Retourne la longueur de la chaîne de caractères <s>.
*/

unsigned strlen(char *s)


{
unsigned l = 0;

while (*s++)
l++;

return l;
}

Figure 6.11 – Calcul de la longueur d’une chaîne de caractères

À titre d’exemple, nous donnons le programme de la figure 6.11, qui définit une fonction 4
strlen capable de calculer la longueur d’une chaîne de caractères fournie en argument.

Comme nous venons de l’expliquer, le paramètre s de cette fonction aurait pu être déclaré
char s[] sans que cela n’affecte le programme. Celui-ci énumère tous les caractères qui com-
posent la chaîne de caractères pointée par s, jusqu’à atteindre le caractère terminateur. À chaque
étape, le gardien de boucle prend pour valeur celle du caractère pointé par s, et ce pointeur est
ensuite incrémenté. L’itération incrémente une variable l qui compte le nombre de caractère lus.
La boucle se termine dès que le caractère pointé par s prend une valeur numérique nulle, ce qui
ne se produit que pour le caractère terminateur. Dans le cas particulier d’une chaîne vide, c’est-
à-dire dont le premier caractère est déjà le terminateur, la fonction se termine sans avoir effectué
d’itération de la boucle, et retourne 0 qui est bien la valeur attendue.

Pour terminer, remarquons que dans l’expression

*s++ ,

l’opérateur “++” possède une priorité supérieure à celle de l’opérateur “*” : c’est le pointeur s
qui est incrémenté, et non la donnée pointée par s.

4. Une fonction similaire est implémentée par la bibliothèque standard. Son prototype se trouve dans le fichier
d’en-tête standard string.h.

171
6.3 L’allocation dynamique de mémoire

Lorsqu’on définit un tableau en utilisant les instructions étudiées au chapitre 5, il est né-
cessaire d’en préciser la taille, ce qui oblige que celle-ci soit connue à la compilation du pro-
gramme. Il est cependant fréquent de devoir mémoriser et traiter des données dont la taille ne
devient connue qu’à l’exécution, notamment parce que ces données sont fournies au programme
par l’utilisateur ou l’environnement. Par exemple, quand un navigateur WWW contacte un ser-
veur afin de lui demander de transmettre le contenu d’une page, la taille des données reçues en
réponse est a priori inconnue et peut être très variable d’une page à une autre.

Dans ce chapitre, nous introduisons des mécanismes permettant au cours de l’exécution d’un
programme d’allouer des blocs de mémoire de taille arbitraire, de les redimensionner, et de les
libérer lorsqu’ils ne sont plus utiles.

6.3.1 Principes

L’allocation dynamique de mémoire est implémentée par les fonctions suivantes, dispo-
nibles dans la bibliothèque standard. Leur prototype se trouve dans le fichier d’en-tête standard
stdlib.h.

— La fonction 5
void *malloc(unsigned n)

alloue un nouveau bloc de mémoire composé de n octets consécutifs, et retourne un poin-


teur vers le premier de ces octets. Ce bloc peut être vu comme un tableau d’octets de
taille n, mais peut être utilisé pour y placer des valeurs de n’importe que type, à condition
qu’il soit de taille suffisante. Le fait d’avoir été alloué signifie que ce bloc est maintenant
réservé, et que les emplacements de mémoire qu’il occupe resteront distincts de ceux des
variables et des autres blocs alloués par le programme.
L’opération d’allocation d’un bloc peut échouer, par exemple parce qu’il ne reste plus
suffisamment d’emplacements disponibles dans la mémoire de l’ordinateur. Dans ce cas,
la fonction malloc retourne un pointeur vide pour signaler cette situation d’erreur.
— La fonction
void *realloc(void *p, unsigned n)

redimensionne le bloc de mémoire pointé par p en lui attribuant la nouvelle taille n,


exprimée en octets. Cette opération n’est valide que si p pointe vers un bloc de mémoire

5. Remarquons qu’il ne s’agit pas d’une procédure : son type de retour est void *, c’est-à-dire un pointeur vers
n’importe quelle donnée, et non void.

172
précédemment retourné par un appel à malloc ou realloc. Le redimensionnement peut
conduire à augmenter ou à diminuer la taille du bloc. Dans le cas d’une augmentation, les
données situées dans le bloc sont préservées. Dans le cas d’une diminution, celles situées
dans le préfixe du bloc qui n’est pas affecté par le redimensionnement le sont également.
Dans les deux cas, l’opération peut conduire à déplacer le bloc vers un autre endroit de
la mémoire de l’ordinateur. La fonction retourne un pointeur vers le bloc redimensionné,
qui peut donc potentiellement avoir changé d’adresse, ou un pointeur vide dans le cas
d’une erreur.
— La procédure
void free(void *p)

libère le bloc pointé par p. Tout comme pour la fonction realloc, cette opération n’est
valide que si p correspond à un pointeur précédemment retourné par malloc ou realloc.
Après avoir libéré un bloc, les ressources attribuées à celui-ci sont récupérées par le sys-
tème d’allocation dynamique de mémoire. Cela signifie que ce bloc ne peut plus être
utilisé par le programme.

La taille des blocs alloués par les fonctions malloc et realloc est exprimée en octets. Pour
réserver un bloc destiné à contenir des données d’un type T donné, il est alors nécessaire de
disposer du moyen de calculer le nombre d’octets occupés par une instance de T. On peut pour
cela employer l’opérateur sizeof, applicable à une variable ou à un type, qui retourne le nombre
d’octets occupés en mémoire par cette variable ou par une valeur de ce type. Par exemple, pour
une architecture x86-64, l’évaluation des expressions

sizeof(int)

et

sizeof(int *)

retourne respectivement 4 et 8, car un entier est représenté sur 32 bits et un pointeur sur 64.

Un exemple de programme employant l’allocation dynamique de mémoire est donné à la


figure 6.12. Celui-ci définit une fonction qui alloue un vecteur de n entiers non signés, la valeur
de n étant passée en argument à la fonction. La fonction initialise les éléments de ce vecteur
avec les valeurs de 1 à n, et retourne un pointeur vers le premier d’entre eux. Dans le cas où
l’allocation du vecteur échoue, la fonction retourne un pointeur vide.

173
#include <stdlib.h>

unsigned *creer_vecteur(unsigned n)
{
unsigned *p, i;

p = malloc(n * sizeof(unsigned));
if (!p)
return NULL;

for (i = 1; i <= n; i++)


p[i] = i;

return p;
}

Figure 6.12 – Allocation dynamique d’un vecteur

6.3.2 La copie de blocs en mémoire

Pour manipuler des blocs de mémoire, il est utile de connaître les deux fonctions suivantes de
la bibliothèque standard. Leur prototype se trouve dans le fichier d’en-tête standard string.h.

— La fonction

void *memcpy(void *dest, void *src, unsigned n)

recopie n octets consécutifs à partir de celui pointé par src, vers l’emplacement de la
mémoire désigné par dest. Cette fonction retourne le pointeur dest. Cette fonction ne
peut être utilisée que si le bloc de n octets pointé par src et le bloc de n octets pointé par
dest ne se chevauchent pas, c’est-à-dire s’ils occupent des emplacements entièrement
distincts de la mémoire de l’ordinateur.
— La fonction

void *memmove(void *dest, void *src, unsigned n)

effectue la même opération que memcpy, mais à la différence de cette dernière, elle peut
être employée même si les blocs de n octets pointés par src et dest possèdent une
intersection commune non vide.

Ces fonctions permettent, en particulier, de recopier le contenu d’un tableau dans un autre.
Cette opération est illustrée par le programme de la figure 6.13, qui définit deux vecteurs et
recopie le contenu du premier dans le second.

174
#include <stdio.h>
#include <string.h>

int main()
{
double t[] = { 1.0, 2.0, 3.0, 4.0 };
double u[4];

unsigned i;

memcpy(u, t, 4 * sizeof(double));

for (i = 0; i < 4; i++)


printf(" %lf", u[i]);

printf("\n");

return 0;
}

Figure 6.13 – Copie du contenu d’un vecteur

6.3.3 Application : retournement d’une séquence de valeurs

On emploie l’allocation dynamique de mémoire quand il est impossible ou difficile de prévoir


à la compilation d’un programme la quantité de mémoire qui sera nécessaire à son exécution.
Nous montrons ici un exemple d’une telle situation. Le problème consiste à lire une séquence de
valeurs entières entrées au clavier, de taille arbitraire, et à afficher ensuite ces valeurs en ordre
inverse de leur occurrence dans la séquence. Par exemple, si l’utilisateur introduit successivement
les valeurs 3, 5, 1, 4 et 2, le programme doit afficher 2, 4, 1, 5 et 3.

Pour résoudre ce problème, il est nécessaire de retenir toutes les valeurs qui ont été entrées,
afin de pouvoir les restituer plus tard. On pourrait pour cela employer un vecteur, mais il est im-
possible d’estimer a priori la taille qui doit être allouée pour celui-ci : si l’on surestime le nombre
de valeurs qui seront effectivement fournies par l’utilisateur, alors on monopolise inutilement une
partie de la mémoire de l’ordinateur ; si l’on sous-estime ce nombre, alors le programme ne sera
pas capable de produire un résultat correct.

Nous présentons ici une autre solution, qui consiste à mémoriser la séquence de valeurs
entrée par l’utilisateur à l’aide d’une liste simplement liée. Une liste simplement liée est une
structure de données composée d’un certain nombre p d’éléments e1 , e2 , . . . , e p . Chaque élément
ei prend la forme d’une donnée structurée possédant un premier champ qui retient une valeur,
et un deuxième qui contient un pointeur vers l’élément suivant ei+1 . Le dernier élément e p de la

175
2 4 1 5 3

Figure 6.14 – Exemple de liste simplement liée

liste ne possède pas de successeur, ce qui est représenté par le fait que son pointeur est vide. On
accède à la liste liée par l’intermédiaire d’un pointeur vers son premier élément e1 , qui constitue
la tête de la liste.

Un exemple de liste simplement liée représentant la séquence de valeurs 2, 4, 1, 5 et 3 est


donné à la figure 6.14. Chaque élément de la liste est représenté par un rectangle dont les deux
compartiments correspondent aux champs contenant la valeur associée à cet élément, et le poin-
teur vers son successeur. Le pointeur vide est représenté par une flèche menant à une croix.

L’intérêt de cette approche est que les éléments d’une liste liée peuvent être alloués dynami-
quement, au fur et à mesure de la lecture des valeurs à retenir. On obtient l’algorithme suivant
pour retourner la séquence de valeurs entrée par l’utilisateur.

1. On commence avec une liste vide, c’est-à-dire ne contenant aucun élément. Une telle liste
est représentée par un pointeur ` vide.
2. Pour chaque nouvelle valeur v fournie par l’utilisateur :
2.1 On alloue un nouvel élément e et on attribue la valeur v à son premier champ.
2.2 On fait pointer le second champ de e vers la tête courante de la liste, c’est-à-dire qu’on
le rend égal à `.
2.3 On modifie ` pour le faire pointer vers e, en d’autres termes, l’élément e devient la
nouvelle tête de la liste.
3. À ce stade, la liste contient toutes les valeurs entrées par l’utilisateur, dans l’ordre inverse
où elles ont été fournies. Il reste à parcourir cette liste élément par élément. Tant que `
n’est pas vide :
3.1 On affiche la valeur du premier champ de l’élément pointé par `.
3.2 On remplace ` par le contenu du deuxième champ de l’élément pointé par `, ce qui a
pour effet de retirer de la liste courante l’élément situé à sa tête.

La figure 6.15 montre le contenu successif de la liste liée au cours de l’exécution des étapes 1
et 2 de cet algorithme, pour la séquence de valeurs 3, 5, 1, 4 et 2.

Un programme C implémentant cet algorithme est donné à la figure 6.16. Ce programme


commence par définir un type structuré struct element_liste pour les éléments de la liste

176
`

` 3

` 5 3

` 1 5 3

` 4 1 5 3

` 2 4 1 5 3

Figure 6.15 – Exécution de l’algorithme de retournement

liée. Ce type est composé d’un champ valeur pour la valeur retenue dans l’élément, et d’un
pointeur suivant vers l’élément suivant dans la liste. Remarquons qu’il est permis d’utiliser le
type struct element_liste au sein de sa propre définition.

Le programme définit deux fonctions creer_liste et afficher_et_liberer_liste, dé-


diées respectivement à la construction d’une liste simplement liée à partir de valeurs fournies
par l’utilisateur (correspondant aux étapes 1 et 2 de l’algorithme), et au parcours de cette liste
(étape 3). Ces deux fonctions sont successivement invoquées par la fonction main du programme.

Pour disposer d’un moyen de détecter la fin de la séquence entrée par l’utilisateur, la fonction
creer_liste impose que les valeurs qui font partie de cette séquence soient positives ou nulles ;
de cette façon, la saisie d’une valeur négative termine la séquence. Il est également possible
d’indiquer la fin de la séquence en n’introduisant pas de valeur lorsque le programme le demande,
ce qui conduit alors la fonction scanf à retourner 0.

Pour chaque nouvelle valeur lue, le programme invoque la fonction malloc pour allouer
un élément de la liste, de type type struct element_liste. Cette opération échouera si la
quantité de mémoire disponible est insuffisante. Dans ce cas, l’invocation de malloc retourne un
pointeur vide, ce qui conduit le programme à appeler la fonction exit de la bibliothèque stan-
dard. Celle-ci termine immédiatement l’exécution du programme. Elle admet un argument entier
qui spécifie le code de diagnostic 6 retourné par le programme ; on le fixe ici à −1 pour signaler
qu’une erreur s’est produite. Dans le cas où l’allocation dynamique des éléments s’effectue sans

6. Il s’agit du même code de retour que celui retourné par la fonction main.

177
#include <stdio.h>
#include <stdlib.h>

struct element_liste
{
int valeur;
struct element_liste *suivant;
};

/*
Retourne une liste simplement liée contenant des valeurs lues au clavier, par ordre inverse de
lecture. En cas d’erreur, interrompt l’exécution du programme.
*/

static struct element_liste *creer_liste()


{
struct element_liste *liste, *nouvel_element;
int valeur;

for (liste = NULL;;)


{
printf("Entrez une valeur positive (ou négative pour terminer): ");
if (scanf("%d", &valeur) != 1 || valeur < 0)
return liste;
nouvel_element = malloc(sizeof(struct element_liste));
if (!nouvel_element)
exit(-1);

nouvel_element -> valeur = valeur;


nouvel_element -> suivant = liste;
liste = nouvel_element;
}
}

/*
Affiche à l’écran les valeurs contenues dans la liste simplement liée <l> et libère cette liste.
*/

static void afficher_et_liberer_liste(struct element_liste *l)


{
struct element_liste *e, *e_suivant;

for (e = l; e; e = e_suivant)
{
printf(" %d", e -> valeur);
e_suivant = e -> suivant;
free(e);
}
}

int main()
{
struct element_liste *liste;

liste = creer_liste();

printf("Liste retournée:");
afficher_et_liberer_liste(liste);
printf("\n");

return 0;
}

Figure 6.16 – Programme de retournement d’une séquence de valeurs

178
problème pour la totalité de la séquence, la fonction creer_liste retourne un pointeur vers la
tête de la liste simplement liée qu’elle a créée.

La fonction afficher_et_liberer_liste effectue un parcours de la liste liée qui lui est


passée en argument. Pour chaque élément visité, la fonction affiche la valeur qu’il contient, et
libère ensuite la mémoire correspondant à cet élément. Cette dernière opération n’est pas à stric-
tement parler nécessaire ici, car tous les emplacements de mémoire alloués dynamiquement par
un programme sont automatiquement libérés quand celui-ci se termine. Libérer explicitement
les données allouées dynamiquement lorsqu’elles ne sont plus utiles est cependant une bonne
pratique de programmation.

Notons que le code responsable du parcours de la liste recopie la valeur du champ suivant
d’un élément dans une variable auxiliaire e_suivant avant de libérer cet élément ; il n’est en
effet plus permis d’accéder à un bloc mémoire alloué dynamiquement après l’avoir libéré.

6.3.4 L’allocation dynamique de tableaux

Il est facile d’allouer dynamiquement un tableau unidimensionnel, en invoquant la fonction


malloc avec un argument égal au nombre total d’octets nécessaire pour en mémoriser les élé-
ments. Par exemple, le fragment de code

int n;
double *v;
..
.
/* Calcul de la taille n à donner au vecteur v. */
..
.
v = malloc(n * sizeof(double));

alloue un vecteur v contenant n éléments de type double, où la valeur de n n’est connue qu’à
l’exécution du programme. Rappelons qu’un vecteur est représenté par un pointeur vers son
premier élément ; le fait d’avoir défini la variable v comme un pointeur permet tout à fait d’utiliser
des expressions telles que v[0] , v[1] , . . . pour accéder aux éléments du vecteur.

Nous abordons maintenant le problème consistant à allouer dynamiquement un tableau dont


la dimension d est supérieure à 1. Si t désigne un tel tableau, alors il peut être vu comme un vec-
teur dont les éléments t[0], t[1], . . . contiennent des pointeurs vers des tableaux de dimension
d − 1 représentant les lignes de t. Cette situation est illustrée à la figure 6.17 dans le cas d’un
tableau bidimensionnel comprenant 6 lignes et 12 colonnes.

La procédure permettant d’allouer un tableau t à d dimensions, avec d > 1, est donc la


suivante.

179
t

Figure 6.17 – Tableau bidimensionnel

1. On alloue un vecteur destiné à contenir t[0], t[1], . . . La taille de ce vecteur correspond


à la taille n1 de la première dimension de t, et chacun de ses élément est destiné à contenir
un pointeur vers un sous-tableau.
2. On alloue récursivement n1 sous-tableaux de dimension d−1, correspondant aux lignes de
t, et on place dans les éléments t[0], t[1], . . . de t des pointeurs vers ces sous-tableaux.

Un exemple de programme allouant dynamiquement un tableau bidimensionnel d’entiers est


donné à la figure 6.18. La structure créée par ce programme est particulière : il s’agit d’un tableau
triangulaire, dont la première ligne contient 1 élément, la deuxième ligne 2 éléments, et ainsi de
suite. Un tableau de cette forme est illustré à la figure 6.19. Le nombre de lignes souhaité pour le
tableau, correspondant aussi au nombre de colonnes de sa dernière ligne, est passé en argument
à la fonction creer_tableau_triangulaire.

Dans ce programme, le type employé pour définir la variable t ainsi que pour la valeur de
retour de la fonction creer_tableau_triangulaire est int ** : il désigne un pointeur vers
un pointeur vers un entier. Remarquons aussi que le premier appel à la fonction malloc a pour
but d’allouer un pointeur vers un vecteur dont les éléments sont des pointeurs vers des entiers.
La taille des chaque élément de ce vecteur est donc donnée par

sizeof(int *) .

Enfin, il ne faut pas oublier qu’un appel à malloc peut toujours échouer, dans le cas d’une
mémoire insuffisante. Si la première invocation de cette fonction, chargée d’allouer le vecteur de
lignes, échoue, alors la fonction creer_tableau_triangulaire se termine en retournant elle-
aussi simplement un pointeur vide. Si un appel suivant à malloc échoue, la procédure à suivre
est plus compliquée car il devient alors nécessaire de libérer toutes les lignes précédemment
allouées, ainsi que le vecteur de lignes.

180
#include <stdio.h>
#include <stdlib.h>

/* Alloue un tableau triangulaire t d’entiers, possédant un


* élément t[i][j] pour tout i dans l’intervalle [0, n-1]
* et j tel que 0 <= j <= i. Retourne un pointeur vers le
* tableau créé, ou NULL en cas d’erreur. */

int **creer_tableau_triangulaire(unsigned n)
{
int **t;
unsigned i, k;

if (!(t = malloc(n * sizeof(int *))))


return NULL;

for (i = 0; i < n; i++)


if (!(t[i] = malloc((i + 1) * sizeof(int))))
{
for (k = 0; k < i; k++)
free(t[k]);

free(t);
return NULL;
}

return t;
}

Figure 6.18 – Création d’un tableau triangulaire

181
t

Figure 6.19 – Tableau triangulaire

Un deuxième exemple de programme mettant en œuvre les mécanismes d’allocation de ta-


bleaux multidimensionnels est donné aux figures 6.20, 6.21 et 6.22. Il s’agit d’une ébauche de
bibliothèque permettant de manipuler des matrices rectangulaires, en implémentant des fonc-
tions pour créer et libérer une matrice, ainsi que calculer la somme et le produit de deux matrices
données. À la différence de l’exemple de la section 5.5.3, les matrices considérées peuvent ici
être de n’importe quelle dimension. Cette bibliothèque pourrait être complétée par des fonctions
permettant de consulter et de modifier les éléments des matrices, de créer des matrices spéciales
telles que les matrices d’identité, d’effectuer des opérations comme la réduction vers une forme
échelonnée ou la diagonalisation d’une matrice, . . .

Comme d’habitude, le code source de cette bibliothèque est réparti en deux fichiers source
matrice.h et matrice.c. Le premier contient la définition du type structuré permettant de
représenter une matrice, et le prototype des quatre fonctions de la bibliothèque. Le second fi-
chier fournit l’implémentation de ces fonctions. Comme toujours, le code de chaque fonction est
précédé par un commentaire qui en précise les modalités d’utilisation.

Le type matrice est composé d’une structure qui comprend trois champs. Les champs n et
m sont destinés à contenir respectivement le nombre de lignes et le nombre de colonnes de la
matrice représentée. Le champ el est un pointeur vers un tableau bidimensionnel contenant les
éléments de la matrice, de type double. De façon similaire à l’exemple du tableau triangulaire,
on placera dans el un pointeur vers un vecteur de lignes, dont chaque élément pointera vers un
vecteur de réels. Le type du champ el doit donc correspondre à un pointeur vers un pointeur vers
une valeur réelle, et s’écrit donc double ** .

La fonction creer_matrice est basée sur les mêmes principes que ceux mis en œuvre dans
la fonction creer_tableau_triangulaire de l’exemple précédent ; la difficulté principale est
de ne pas oublier de libérer les données déjà allouées dans le cas où un appel à malloc échoue.
Des opérations de libération similaires sont effectuées dans la fonction liberer_matrice.

182
matrice.c:

#include <stdio.h>
#include <stdlib.h>
#include "matrice.h"

/*
Crée une nouvelle matrice de n lignes et m colonnes, dont les éléments
sont initialement nuls.
Retourne un pointeur vers la matrice créée, ou NULL en cas d’erreur.
*/

matrice *creer_matrice(unsigned n, unsigned m)


{
matrice *r;
unsigned i, j;

r = malloc(sizeof(matrice));
if (!r)
return NULL;

r -> el = malloc(n * sizeof(double *));


if (!(r -> el))
{
free(r);
return NULL;
}

for (i = 0; i < n; i++)


{
r -> el[i] = malloc(m * sizeof(double));
if (!(r -> el[i]))
{
for (j = 0; j < i; j++)
free(r -> el[j]);
free(r -> el);
free(r);
return NULL;
}

for (j = 0; j < m; j++)


r -> el[i][j] = 0.0;
}

r -> n = n;
r -> m = m;

return r;
}

Figure 6.20 – Bibliothèque de manipulation de matrices (1/3)

183
/*
Libère l’espace mémoire occupé par la matrice m.
*/

void liberer_matrice(matrice *m)


{
unsigned i;

for (i = 0; i < m -> n; i++)


free(m -> el[i]);

free(m -> el);


free(m);
}

/*
Calcule la somme de deux matrices a et b. Retourne un pointeur vers
une nouvelle matrice contenant cette somme, ou un pointeur vide en
cas d’erreur.
*/

matrice *somme_matrices(matrice *a, matrice *b)


{
matrice *s;
unsigned i, j;

if (a -> n != b -> n || a -> m != b -> m)


return NULL;

s = creer_matrice(a -> n, a -> m);


if (!s)
return NULL;

for (i = 0; i < a -> n; i++)


for (j = 0; j < a -> m; j++)
s -> el[i][j] = a -> el[i][j] + b -> el[i][j];

return s;
}

Figure 6.21 – Bibliothèque de manipulation de matrices (2/3)

184
/*
Calcule le produit de deux matrices a et b. Retourne un pointeur
vers une nouvelle matrice contenant ce produit, ou un pointeur
vide en cas d’erreur.
*/

matrice *produit_matrices(matrice *a, matrice *b)


{
matrice *p;
unsigned i, j, k;

if (a -> m != b -> n)
return NULL;

p = creer_matrice(a -> n, b -> m);


if (!p)
return NULL;

for (i = 0; i < a -> n; i++)


for (j = 0; j < b -> m; j++)
for (k = 0; k < a -> m; k++)
p -> el[i][j] += a -> el[i][k] * b -> el[k][j];

return p;
}

matrice.h:

/* Représente une matrice réelle de n lignes et m colonnes */

typedef struct
{
double **el;
unsigned n, m;
} matrice;

matrice *creer_matrice(unsigned, unsigned);


void liberer_matrice(matrice *);
matrice *somme_matrices(matrice *, matrice *);
matrice *produit_matrices(matrice *, matrice *);

Figure 6.22 – Bibliothèque de manipulation de matrices (3/3)

185
L’implémentation des fonctions somme_matrices et produit_matrices appelle peu de
commentaires. Ces fonctions commencent par vérifier que les dimensions des deux matrices qui
leur sont fournies en arguments sont compatibles : elles doivent posséder le même nombre de
ligne et le même nombre de colonnes pour une somme, et le nombre de colonnes de la première
matrice doit correspondre au nombre de lignes de la deuxième pour un produit. Si cette condition
n’est pas remplie, les fonctions retournent un pointeur vide. Sinon, elles créent une nouvelle
matrice de taille appropriée, et calculent la valeur à placer dans ses éléments selon les règles du
calcul matriciel.

6.4 Les paramètres de main

Lorsque nous avons introduit la fonction main au chapitre 4, nous avons expliqué que cette
fonction constitue le point d’entrée d’un programme, et qu’elle retourne un code de diagnostic
entier qui sert à indiquer à la fin de son exécution si ce programme s’est terminé normalement
ou bien à la suite d’une erreur.

Il se trouve que cette fonction main possède des paramètres ; ceux-ci lui permettent de rece-
voir des données transmises par le mécanisme qui lance l’exécution d’un programme. On appelle
ces données les arguments d’exécution du programme. Nous étudions ici les deux premiers 7 de
ces paramètres.

— Le premier paramètre de main, appelé par convention argc, est de type int ; il fournit le
nombre d’arguments d’exécution transmis au programme par l’environnement.
— Le deuxième paramètre, habituellement appelé argv, est défini par char *argv[] ; il
s’agit donc d’un tableau de chaînes de caractères. La taille de ce tableau est égale à la
valeur de argc. Chacune des chaînes de caractères qu’il contient fournit un argument
d’exécution.

Quand on lance l’exécution d’un programme en ligne de commande, les arguments d’exé-
cution correspondent aux données qui sont écrites après le nom du programme. Dans la plupart
des environnements, ces données sont précédées par un premier argument qui contient le nom du
programme. Par exemple, dans un environnement UNIX, la ligne de commande

% ./test abc 123456

lance le programme contenu dans le fichier exécutable test situé dans le répertoire courant, en
lui fournissant les deux arguments d’exécution “abc” et “123456”. La fonction main du pro-

7. Plusieurs environnements de programmation en définissent aussi un troisième, que nous ne couvrirons pas
dans ce cours.

186
gramme sera donc invoquée avec argc égal à 3, et argv égal à un pointeur vers un tableau dont
les éléments pointent vers trois chaînes de caractères contenant respectivement “./test”, “abc” et
“123456”. Cette situation est illustrée à la figure 6.23.

Un programme capable d’afficher les arguments d’exécution qui lui sont transmis quand il
est lancé est donné à la figure 6.24. Comme on le voit, la fonction printf de la bibliothèque
standard est capable d’afficher des chaînes de caractères, en employant le marqueur de format
“%s”.

6.5 Exercices
1. Déterminer ce qui sera affiché lors de l’exécution des trois fragments de code C suivants.

(a) int x[2] = { 0, 0 };


int *y;

y = x;
(++y)[0] = 1;
printf("%d %d\n", x[0], x[1]);

(b) int a, b, *c;

a = 1;
b = 2;
c = &a;
a += *c;
c = &b;
*c-- = 3;
printf("%d %d\n", a, b);

(c) struct
{
int val, *adr;
} x, *y;

x.adr = &x.val;
y = &x;
x.val = 41;
y -> val = 42;
printf("%d\n", *(y -> adr));

187
argv argc

. / t e s t \0
a b c \0
1 2 3 4 5 6 \0

Figure 6.23 – Arguments de main

#include <stdio.h>

int main(int argc, char *argv[])


{
int i;

printf("Les arguments du programme sont");

for (i = 0; i < argc; i++)


printf(" [%s]", argv[i]);

printf(".\n");

return 0;
}

Figure 6.24 – Programme affichant ses arguments d’exécution

188
2. Quelle est la valeur affichée lors de l’exécution du fragment de code suivant ? (Justifier
votre réponse.)
int a[] = { 1, 0 };
int *b[2];
int **c;

b[*a] = a + 1;
c = b;

printf("%d\n", **++c);

3. Quelle est la valeur affichée lors de l’exécution du fragment de code suivant ? (Justifier
votre réponse.)
int t[2] = { 1, 2 };
int *a, **b;

a = t + *t;
b = &a;

printf("%d\n", *--*b);

4. Quelle est la valeur affichée lors de l’exécution du fragment de code suivant ? (Justifier
votre réponse.)
int t[2] = { -2, 0 };
int *a, b;

b = *t ? ++*t : --*t;
a = &b;

printf("%d\n", a[*(t + 1)]);

5. Soit une fonction int rand(int min, int max) qui retourne un nombre entier aléa-
toire compris dans l’intervalle [min, max]. On demande d’écrire une fonction qui alloue
un tableau d’entiers de taille donnée et qui remplit celui-ci avec des entiers aléatoires
compris dans un intervalle donné.
La fonction demandée est déclarée de la façon suivante :
int rand_tab(unsigned int n, int min, int max, int **dest);

— n est la taille du tableau,
— min et max sont les bornes inférieure et supérieure des nombres aléatoires à insérer
dans le tableau,

189
— dest la destination du tableau généré.
La fonction retourne une valeur différente de 0 si le tableau a pu être généré et 0 si ce
n’est pas le cas (en particulier, si min > max ou dest == NULL).

6. Soit une fonction int f(int *x) donnée. Cette fonction est capable de produire un
nombre limité et a priori inconnu d’entiers. Lorsque f est appelée, si un entier est produit,
il est stocké dans l’espace pointé par x et f retourne une valeur différente de 0. Sinon, f
retourne 0 et aucun appel suivant ne produira d’entier.
On demande d’écrire un programme qui remplit, au moyen de f, un tableau d’entiers t de
taille n donnée (n ≥ 0) à partir de l’indice 0 et tant que f produit un nombre. À la fin de
l’exécution du programme, la variable nb contiendra le nombre d’entiers que f a produit
(avec nb ≤ n).

7. (a) Écrire un fragment de code C définissant les deux types structurés suivants :
— un type point représentant les coordonnées cartésiennes (x, y) d’un point dans le
plan, où x et y sont des valeurs réelles.
— un type segment représentant un segment de droite caractérisé par son origine et
sa destination (qui sont des points).
(b) Écrire une fonction C prenant en arguments quatre réels x1 , y1 , x2 et y2 définissant les
extrémités (x1 , y1 ) et (x2 , y2 ) d’un segment de droite, et retournant un pointeur vers
une représentation de ce segment, nouvellement allouée.
(c) Écrire une fonction C permettant de libérer une représentation d’un segment créée par
la fonction obtenue au point (b).

8. (a) Écrire un fragment de code C définissant un type structuré capable de retenir le nom
et le prénom d’une personne, tous deux représentés sur au plus 20 caractères.

(b) Écrire une fonction C prenant en argument des pointeurs vers deux instances du type
obtenu à la question précédente, et retournant une valeur booléenne qui indique si
les deux personnes concernées possèdent le même nom et le même prénom. La com-
paraison doit tenir compte des caractères non alphabétiques, mais pas de la casse
(majuscule ou minuscule) des lettres ; par exemple, les noms “le petit” et “Le Petit”
sont considérés égaux, mais différents de “Lepetit”. Il est permis de définir des fonc-
tions auxiliaires de votre choix, mais pas de faire appel à des fonctions issues d’une
bibliothèque.
(a) Écrire un fragment de code C définissant un type structuré capable de représenter un
intervalle [a, b] de nombres entiers, avec a ∈ Z ∪ {−∞} et b ∈ Z ∪ {+∞} tels que a ≤ b.

(b) Écrire une fonction C prenant en argument des pointeurs vers deux instances du type
obtenu à la question précédente, et retournant une valeur booléenne qui indique si

190
le premier intervalle est entièrement inclus dans le second. (Par exemple l’intervalle
[1, 4] est inclus dans [0, +∞], mais pas dans [−2, 3].) Il est permis de définir des
fonctions auxiliaires de votre choix.

191

Vous aimerez peut-être aussi