Cours C

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

Programmation C

(Perfectionnement à la programmation en C)

Samuele Giraudo

Université Gustave Eiffel


LIGM, bureau 4B162
[email protected]
http://igm.univ-mlv.fr/˜giraudo/

1 / 399
Introduction
L’objectif de ce module est d’approfondir les concepts de base et d’approcher certains concepts
plus avancés de la programmation en C.

Celui-ci est organisé en trois axes.

1. Axe 1 : écrire un projet maintenable.


Bonnes habitudes de programmation, documentation, pré-assertions, gestion des erreurs,
programmation modulaire, compilation.

2. Axe 2 : comprendre les mécanismes de base.


Pointeurs, allocation dynamique, types, types structurés, entrées/sorties.

3. Axe 3 : utiliser quelques techniques avancées.


Opérateurs bit à bit, mémoïsation, génération aléatoire, pointeurs de fonction, généricité.

2 / 399
Contenu du cours

Axe 1. Axe 2. Axe 3.


Bases Allocation dynamique Opérateurs

Habitudes Entrées et sorties Pointeurs de fonction

Modules Types Génération aléatoire

Compilation Types structurés Mémoïsation

3 / 399
Pré-requis

Ce cours demande les pré-requis suivants :


I des bases en programmation générale (notion de programme, d’instruction, de
compilation) ;

I des bases en programmation impérative (notion de variable, de structures de contrôle, de


fonction) ;

I des bases en algorithmique (manipulation de structures de données élémentaires comme


les tableaux, les chaînes de caractères, les listes) ;

I des bases en programmation en C (écriture de fonctions, manipulation de tableaux, de


chaînes de caractères, connaissance des types numériques, des types structurés, notion
d’allocation dynamique, de pointeur).

4 / 399
Quelques dates

Le langage C est apparu en 1972 dans les laboratoires Bell. Il a été créé par D. Ritchie et K.
Thompson au même moment que l’apparition des systèmes UNIX.

5 / 399
Quelques dates

Le langage C est apparu en 1972 dans les laboratoires Bell. Il a été créé par D. Ritchie et K.
Thompson au même moment que l’apparition des systèmes UNIX.
Il est influencé par le langage B qui, contrairement au C, ne possédait pas de système de type.

5 / 399
Quelques dates

Le langage C est apparu en 1972 dans les laboratoires Bell. Il a été créé par D. Ritchie et K.
Thompson au même moment que l’apparition des systèmes UNIX.
Il est influencé par le langage B qui, contrairement au C, ne possédait pas de système de type.
Quelques dates sur l’évolution de la spécification du langage :
I 1978 : première description complète du langage (C traditionnel) ;
I 1989 : publication de la norme ANSI C (ou C89) ;
I 1995 : évolution du langage C94/95 ;
I 1999 : évolution du langage C99 ;
I 2011 : nouvelle version du standard C11.

5 / 399
Quelques dates

Le langage C est apparu en 1972 dans les laboratoires Bell. Il a été créé par D. Ritchie et K.
Thompson au même moment que l’apparition des systèmes UNIX.
Il est influencé par le langage B qui, contrairement au C, ne possédait pas de système de type.
Quelques dates sur l’évolution de la spécification du langage :
I 1978 : première description complète du langage (C traditionnel) ;
I 1989 : publication de la norme ANSI C (ou C89) ;
I 1995 : évolution du langage C94/95 ;
I 1999 : évolution du langage C99 ;
I 2011 : nouvelle version du standard C11.

5 / 399
Quelques caractéristiques

Le C est un langage de bas niveau : il offre la possibilité de se placer « proche » de la machine. Il


permet en effet de manipuler finement des données en mémoire, des adresses et des suites de
bits.

6 / 399
Quelques caractéristiques

Le C est un langage de bas niveau : il offre la possibilité de se placer « proche » de la machine. Il


permet en effet de manipuler finement des données en mémoire, des adresses et des suites de
bits.

Ce langage a été initialement pensé pour la conception de systèmes d’exploitation.


Il est cependant suffisamment expressif pour s’adapter à un large éventail d’utilisations.

6 / 399
Quelques caractéristiques

Le C est un langage de bas niveau : il offre la possibilité de se placer « proche » de la machine. Il


permet en effet de manipuler finement des données en mémoire, des adresses et des suites de
bits.

Ce langage a été initialement pensé pour la conception de systèmes d’exploitation.


Il est cependant suffisamment expressif pour s’adapter à un large éventail d’utilisations.

Il se situe à la croisée des chemins du monde des langages de programmation : beaucoup de


langages modernes sont traduits en C pour être compilés.

6 / 399
Quelques caractéristiques

Le C est un langage de bas niveau : il offre la possibilité de se placer « proche » de la machine. Il


permet en effet de manipuler finement des données en mémoire, des adresses et des suites de
bits.

Ce langage a été initialement pensé pour la conception de systèmes d’exploitation.


Il est cependant suffisamment expressif pour s’adapter à un large éventail d’utilisations.

Il se situe à la croisée des chemins du monde des langages de programmation : beaucoup de


langages modernes sont traduits en C pour être compilés.

L’un de ses compilateurs, gcc, fruit de nombreuses optimisations, fait que le C est un langage
d’une efficacité extrême.

6 / 399
Axe 1 : écrire un projet maintenable
Bases

Habitudes

Modules

Compilation

7 / 399
Plan

Bases
Généralités
Expressions et instructions
Constructions syntaxiques
Variables
Fonctions et pile
Commandes préprocesseur

8 / 399
Plan

Bases
Généralités
Expressions et instructions
Constructions syntaxiques
Variables
Fonctions et pile
Commandes préprocesseur

9 / 399
La notion de programme

Un programme en C est avant tout un texte (contenu dans un fichier) qui suit certaines règles
(dites de syntaxe).

10 / 399
La notion de programme

Un programme en C est avant tout un texte (contenu dans un fichier) qui suit certaines règles
(dites de syntaxe).

C’est une collection de déclarations et de définitions de fonctions, de types, de variables globales,


assorties de commandes pré-processeur.

10 / 399
La notion de programme

Un programme en C est avant tout un texte (contenu dans un fichier) qui suit certaines règles
(dites de syntaxe).

C’est une collection de déclarations et de définitions de fonctions, de types, de variables globales,


assorties de commandes pré-processeur.

Tout programme possède une fonction principale nommée main . C’est par elle que commence
l’exécution du programme. On appelle ceci le point d’entrée du programme.

10 / 399
Compiler un programme
Compiler un programme signifie traduire le fichier le contenant en un langage compréhensible
et exécutable par la machine cible.

11 / 399
Compiler un programme
Compiler un programme signifie traduire le fichier le contenant en un langage compréhensible
et exécutable par la machine cible.
On compile un fichier Prog.c par la commande

gcc -o NOM Prog.c

Ceci produit un exécutable nommé NOM .

11 / 399
Compiler un programme
Compiler un programme signifie traduire le fichier le contenant en un langage compréhensible
et exécutable par la machine cible.
On compile un fichier Prog.c par la commande

gcc -o NOM Prog.c

Ceci produit un exécutable nommé NOM .

On compilera obligatoirement avec les options -ansi , -pedantic et -Wall au moyen de la


commande
gcc -o NOM -ansi -pedantic -Wall Prog.c

11 / 399
Compiler un programme
Compiler un programme signifie traduire le fichier le contenant en un langage compréhensible
et exécutable par la machine cible.
On compile un fichier Prog.c par la commande

gcc -o NOM Prog.c

Ceci produit un exécutable nommé NOM .

On compilera obligatoirement avec les options -ansi , -pedantic et -Wall au moyen de la


commande
gcc -o NOM -ansi -pedantic -Wall Prog.c

I -ansi -pedantic : empêche un programme non compatible avec la norme ANSI C de


compiler ;

11 / 399
Compiler un programme
Compiler un programme signifie traduire le fichier le contenant en un langage compréhensible
et exécutable par la machine cible.
On compile un fichier Prog.c par la commande

gcc -o NOM Prog.c

Ceci produit un exécutable nommé NOM .

On compilera obligatoirement avec les options -ansi , -pedantic et -Wall au moyen de la


commande
gcc -o NOM -ansi -pedantic -Wall Prog.c

I -ansi -pedantic : empêche un programme non compatible avec la norme ANSI C de


compiler ;
I -Wall : active tous les messages d’avertissement.

11 / 399
Plan

Bases
Généralités
Expressions et instructions
Constructions syntaxiques
Variables
Fonctions et pile
Commandes préprocesseur

12 / 399
Expressions

Une expression est définie récursivement comme étant soit


1. une constante ;

13 / 399
Expressions

Une expression est définie récursivement comme étant soit


1. une constante ;
2. une variable ;

13 / 399
Expressions

Une expression est définie récursivement comme étant soit


1. une constante ;
2. une variable ;
3. une combinaison d’expressions et d’opérateurs ;

13 / 399
Expressions

Une expression est définie récursivement comme étant soit


1. une constante ;
2. une variable ;
3. une combinaison d’expressions et d’opérateurs ;
4. un appel de fonction qui renvoie une valeur, c.-à-d., de type de retour autre que void .

13 / 399
Expressions

Une expression est définie récursivement comme étant soit


1. une constante ;
2. une variable ;
3. une combinaison d’expressions et d’opérateurs ;
4. un appel de fonction qui renvoie une valeur, c.-à-d., de type de retour autre que void .
Une expression n’est donc rien d’autre qu’un assemblage de symboles qui vérifie des règles
syntaxiques et sémantiques.

13 / 399
Expressions

Une expression est définie récursivement comme étant soit


1. une constante ;
2. une variable ;
3. une combinaison d’expressions et d’opérateurs ;
4. un appel de fonction qui renvoie une valeur, c.-à-d., de type de retour autre que void .
Une expression n’est donc rien d’autre qu’un assemblage de symboles qui vérifie des règles
syntaxiques et sémantiques.

Toute expression possède une valeur et un type. Le processus qui consiste à déterminer la valeur
d’une expression se nomme l’évaluation.

13 / 399
Expressions
P.ex., les entités suivantes sont des expressions :

I 0

Valeur : 0 , type : int

14 / 399
Expressions
P.ex., les entités suivantes sont des expressions :

I 0

Valeur : 0 , type : int

I ’a’

Valeur : 97 , type : int

14 / 399
Expressions
P.ex., les entités suivantes sont des expressions :

I 0

Valeur : 0 , type : int

I ’a’

Valeur : 97 , type : int

I "abc"

Valeur : "abc" , type : char *

14 / 399
Expressions
P.ex., les entités suivantes sont des expressions :

I 0

Valeur : 0 , type : int

I ’a’

Valeur : 97 , type : int

I "abc"

Valeur : "abc" , type : char *

I x

Valeur : x , type : le type de la variable x

14 / 399
Expressions
P.ex., les entités suivantes sont des expressions :

I 0

Valeur : 0 , type : int

I ’a’

Valeur : 97 , type : int

I "abc"

Valeur : "abc" , type : char *

I x

Valeur : x , type : le type de la variable x

I a == 2

Valeur : 0 ou 1 , type : int

14 / 399
Expressions
P.ex., les entités suivantes sont des expressions :

I 0 I b = 3

Valeur : 0 , type : int Valeur : 3 , type : int

I ’a’

Valeur : 97 , type : int

I "abc"

Valeur : "abc" , type : char *

I x

Valeur : x , type : le type de la variable x

I a == 2

Valeur : 0 ou 1 , type : int

14 / 399
Expressions
P.ex., les entités suivantes sont des expressions :

I 0 I b = 3

Valeur : 0 , type : int Valeur : 3 , type : int

I f(5)
I ’a’

Valeur : la valeur renvoyée par f(5) , type : le type


Valeur : 97 , type : int
de retour de f
I "abc"

Valeur : "abc" , type : char *

I x

Valeur : x , type : le type de la variable x

I a == 2

Valeur : 0 ou 1 , type : int

14 / 399
Expressions
P.ex., les entités suivantes sont des expressions :

I 0 I b = 3

Valeur : 0 , type : int Valeur : 3 , type : int

I f(5)
I ’a’

Valeur : la valeur renvoyée par f(5) , type : le type


Valeur : 97 , type : int
de retour de f
I "abc"
I (a == 0) && (b >= 3)
Valeur : "abc" , type : char *
Valeur : 0 ou 1 , type : int
I x

Valeur : x , type : le type de la variable x

I a == 2

Valeur : 0 ou 1 , type : int

14 / 399
Expressions
P.ex., les entités suivantes sont des expressions :

I 0 I b = 3

Valeur : 0 , type : int Valeur : 3 , type : int

I f(5)
I ’a’

Valeur : la valeur renvoyée par f(5) , type : le type


Valeur : 97 , type : int
de retour de f
I "abc"
I (a == 0) && (b >= 3)
Valeur : "abc" , type : char *
Valeur : 0 ou 1 , type : int
I x
I a > ’a’ ? a : -a
Valeur : x , type : le type de la variable x Valeur : a ou -a , type : le type de a
I a == 2

Valeur : 0 ou 1 , type : int

14 / 399
Expressions
P.ex., les entités suivantes sont des expressions :

I 0 I b = 3

Valeur : 0 , type : int Valeur : 3 , type : int

I f(5)
I ’a’

Valeur : la valeur renvoyée par f(5) , type : le type


Valeur : 97 , type : int
de retour de f
I "abc"
I (a == 0) && (b >= 3)
Valeur : "abc" , type : char *
Valeur : 0 ou 1 , type : int
I x
I a > ’a’ ? a : -a
Valeur : x , type : le type de la variable x Valeur : a ou -a , type : le type de a
I a == 2 I 1. + 7 * 2

Valeur : 0 ou 1 , type : int Valeur : 15. , type : float

14 / 399
Instructions
Une instruction est définie récursivement comme étant soit :
1. une expression E; terminée par un point-virgule ;

2. un bloc {I1 I2 ...Ik} où I1 , I2 , . . ., Ik sont des instructions ;

3. une conditionnelle if (E) I1 else I2 , où E est une expression et I1 et I2 sont des


instructions ;
4. toute autre construction similaire ( switch , while , do while , for );

5. une déclaration T X; de variable, où T est un type et X est un identificateur.

15 / 399
Instructions
Une instruction est définie récursivement comme étant soit :
1. une expression E; terminée par un point-virgule ;

2. un bloc {I1 I2 ...Ik} où I1 , I2 , . . ., Ik sont des instructions ;

3. une conditionnelle if (E) I1 else I2 , où E est une expression et I1 et I2 sont des


instructions ;
4. toute autre construction similaire ( switch , while , do while , for );

5. une déclaration T X; de variable, où T est un type et X est un identificateur.

À la différence des expressions, c’est souvent l’effet d’une instruction qui est sa raison d’être (et
non plus uniquement sa valeur ou son type).

15 / 399
Instructions
Une instruction est définie récursivement comme étant soit :
1. une expression E; terminée par un point-virgule ;

2. un bloc {I1 I2 ...Ik} où I1 , I2 , . . ., Ik sont des instructions ;

3. une conditionnelle if (E) I1 else I2 , où E est une expression et I1 et I2 sont des


instructions ;
4. toute autre construction similaire ( switch , while , do while , for );

5. une déclaration T X; de variable, où T est un type et X est un identificateur.

À la différence des expressions, c’est souvent l’effet d’une instruction qui est sa raison d’être (et
non plus uniquement sa valeur ou son type).
En effet, une instruction peut p.ex. afficher un élément, allouer une zone mémoire, lire dans un
fichier, etc. La valeur de l’expression sous-jacente à l’instruction est d’importance secondaire.

15 / 399
Instructions
P.ex., les entités suivantes sont des instructions :

I ; (instruction vide) I malloc(64);

I 1; I int a;

I a = b; I int tab[64];

I while (1) a += 1; I tab[3] = 9;

I printf("abc\n"); I tab[3];

I a++; I return 31;

16 / 399
Expressions à effet de bord
Une expression est à effet de bord (ou encore à effet secondaire) si son évaluation modifie la
mémoire.

17 / 399
Expressions à effet de bord
Une expression est à effet de bord (ou encore à effet secondaire) si son évaluation modifie la
mémoire.

P.ex.,

I 0 , I (a + 21) < < 3 ,


I ’c’ ,
I tab[8] ,

sont des expressions qui ne sont pas à effet de bord.

17 / 399
Expressions à effet de bord
Une expression est à effet de bord (ou encore à effet secondaire) si son évaluation modifie la
mémoire.

P.ex.,

I 0 , I (a + 21) < < 3 ,


I ’c’ ,
I tab[8] ,

sont des expressions qui ne sont pas à effet de bord.

En revanche,

I a = 31 , I char c ,
I printf("abc\n") ,
I malloc(64) ,

sont des expressions à effet de bord (la déclaration de variable n’est pas une expression mais une
instruction).
17 / 399
Expressions à effet de bord

En règle générale, ce sont les éléments suivants dans les expressions qui produisent des effets de
bord :
I les affectations ;
I les allocations de mémoire ;
I la sollicitation au système de fichiers.

18 / 399
Expressions à effet de bord

En règle générale, ce sont les éléments suivants dans les expressions qui produisent des effets de
bord :
I les affectations ;
I les allocations de mémoire ;
I la sollicitation au système de fichiers.

En revanche, les éléments suivants dans les expressions ne produisent pas d’effet de bord :
I lectures de constantes et de variables ;
I calculs arithmétiques, logiques ou bit à bit ;
I comparaisons.

18 / 399
Plan

Bases
Généralités
Expressions et instructions
Constructions syntaxiques
Variables
Fonctions et pile
Commandes préprocesseur

19 / 399
Blocs

Un bloc est une suite d’instructions délimitée par des accolades. Il est constitué d’une partie
consacrée à la déclaration de variables et d’une partie consacrée aux instructions.

20 / 399
Blocs

Un bloc est une suite d’instructions délimitée par des accolades. Il est constitué d’une partie
consacrée à la déclaration de variables et d’une partie consacrée aux instructions.

{ Ici, D est une section de déclarations et I


D
I est une section d’instructions.
}

20 / 399
Blocs

Un bloc est une suite d’instructions délimitée par des accolades. Il est constitué d’une partie
consacrée à la déclaration de variables et d’une partie consacrée aux instructions.

{ Ici, D est une section de déclarations et I


D
I est une section d’instructions.
}

Les parties D et/ou I peuvent être vides.

20 / 399
Blocs

Un bloc est une suite d’instructions délimitée par des accolades. Il est constitué d’une partie
consacrée à la déclaration de variables et d’une partie consacrée aux instructions.

{ Ici, D est une section de déclarations et I


D
I est une section d’instructions.
}

Les parties D et/ou I peuvent être vides.

Sachant qu’un bloc est une suite d’instructions, il est possible de placer un bloc dans la section
d’instructions d’un bloc et ainsi d’imbriquer plusieurs blocs.

20 / 399
Blocs
int main() { La partie grise est un bloc au sein d’une
int b;
scanf("%d", &b); fonction main .
{
int a;
a = 1 + b;
}
Il y a des règles concernant la visibilité des
} variables (que nous verrons plus loin).

21 / 399
Blocs
int main() { La partie grise est un bloc au sein d’une
int b;
scanf("%d", &b); fonction main .
{
int a;
a = 1 + b;
}
Il y a des règles concernant la visibilité des
} variables (que nous verrons plus loin).

{ Ceci est un bloc constitué d’une section de


int a;
int b; déclarations et d’une section d’instructions.
scanf(" %d", &a);
{
printf("%d\n", a); Cette dernière partie contient un bloc (en gris)
} dont la section de déclarations est vide.
scanf(" %d", &b);
}

21 / 399
Opérateur de test if
L’opérateur de test se décline en deux versions :

if (E) Ici, E est une expression booléenne : elle est


B
considérée comme fausse si elle s’évalue en
if (E)
zéro et comme vraie sinon. De plus, B , B_1 et
B_1 B_2 sont des blocs d’instructions.
else
B_2

22 / 399
Opérateur de test if
L’opérateur de test se décline en deux versions :

if (E) Ici, E est une expression booléenne : elle est


B
considérée comme fausse si elle s’évalue en
if (E)
zéro et comme vraie sinon. De plus, B , B_1 et
B_1 B_2 sont des blocs d’instructions.
else
B_2

Diagrammes d’exécution :
Debut Debut

E E
6= 0 =0
6= 0
=0
B B_1 B_2

Fin

Fin

22 / 399
Instruction de branchement switch

L’instruction de branchement admet la syntaxe

switch (E) { Ici, E , E_1 , E_2 , . . ., E_N sont des expressions


case E_1 : I_1
case E_2 : I_2 qui s’évaluent en des entiers.
...
case E_N : I_N Les I_1 , I_2 , . . ., I_N et I_D sont des suites
default : I_D
} d’instructions.

23 / 399
Instruction de branchement switch

L’instruction de branchement admet la syntaxe

switch (E) { Ici, E , E_1 , E_2 , . . ., E_N sont des expressions


case E_1 : I_1
case E_2 : I_2 qui s’évaluent en des entiers.
...
case E_N : I_N Les I_1 , I_2 , . . ., I_N et I_D sont des suites
default : I_D
} d’instructions.

L’exécution de cette instruction se passe ainsi. Soit i le plus petit entier (s’il existe) tel que les
évaluations de E et E_i sont égales. Les instructions I_i , . . ., I_N ainsi que I_D sont exécutées.

23 / 399
Instruction de branchement switch

L’instruction de branchement admet la syntaxe

switch (E) { Ici, E , E_1 , E_2 , . . ., E_N sont des expressions


case E_1 : I_1
case E_2 : I_2 qui s’évaluent en des entiers.
...
case E_N : I_N Les I_1 , I_2 , . . ., I_N et I_D sont des suites
default : I_D
} d’instructions.

L’exécution de cette instruction se passe ainsi. Soit i le plus petit entier (s’il existe) tel que les
évaluations de E et E_i sont égales. Les instructions I_i , . . ., I_N ainsi que I_D sont exécutées.

La ligne default : I_D est facultative. Si elle est présente, I_D est toujours exécutée.

23 / 399
Instruction de branchement switch

On s’impose de terminer chaque I_j et I_D par le mot-clé break .

Ceci permet de n’exécuter que le I_i tel que les évaluations de E et E_i sont égales. Dans ce
cas, I_D n’est exécuté que si aucun des E_i ne l’a été.

On obtient ainsi le diagramme d’exécution


Debut

E
= E_1 6= E_i , 1 6 i 6 N

= E_2 = E_N

I_1 I_2 ... I_N I_D

Fin

24 / 399
Instruction itérative while

L’instruction itérative while admet la syntaxe

while (E) Ici, E est une expression booléenne et B est


B
un bloc d’instructions.

25 / 399
Instruction itérative while

L’instruction itérative while admet la syntaxe

while (E) Ici, E est une expression booléenne et B est


B
un bloc d’instructions.

Diagramme d’exécution :

Debut

6= 0 E =0

B Fin

25 / 399
Instruction itérative do while

L’instruction itérative do while admet la syntaxe

do Ici, E est une expression booléenne et B est


B
while (E); un bloc d’instructions.

26 / 399
Instruction itérative do while

L’instruction itérative do while admet la syntaxe

do Ici, E est une expression booléenne et B est


B
while (E); un bloc d’instructions.

Diagramme d’exécution :

Debut

6= 0

=0
Fin

26 / 399
Instruction itérative for
L’instruction itérative for admet la syntaxe

for (E_1 ; E_2 ; E_3) Ici, E_2 est une expression booléenne (le test), E_1
B
(l’initialisation) et E_3 (l’incrémentation) sont des
expressions et B est un bloc d’instructions.

27 / 399
Instruction itérative for
L’instruction itérative for admet la syntaxe

for (E_1 ; E_2 ; E_3) Ici, E_2 est une expression booléenne (le test), E_1
B
(l’initialisation) et E_3 (l’incrémentation) sont des
expressions et B est un bloc d’instructions.

Diagramme d’exécution :
Debut

E_1

E_2

6= 0
=0
B

Fin E_3

27 / 399
Instruction itérative for

for (i = 0 ; i < 3 ; ++i) { Affiche trois occurrences du caractère ’a’ .


printf("a");
}

28 / 399
Instruction itérative for

for (i = 0 ; i < 3 ; ++i) { Affiche trois occurrences du caractère ’a’ .


printf("a");
}

for (x = lst ; x != NULL ; Parcours une liste simplement chaînée et l’affiche.


x = x->suiv) {
afficher(x->elem);
La variable x est un pointeur sur la cellule
} courante.

28 / 399
Instruction itérative for

for (i = 0 ; i < 3 ; ++i) { Affiche trois occurrences du caractère ’a’ .


printf("a");
}

for (x = lst ; x != NULL ; Parcours une liste simplement chaînée et l’affiche.


x = x->suiv) {
afficher(x->elem);
La variable x est un pointeur sur la cellule
} courante.

for ( ; *str != ’\0’; str++)) { Affiche la chaîne de caractères str . Il n’y a pas
putchar(*str);
} d’initialisation. L’incrémentation fait pointer str sur le
prochain caractère.

28 / 399
Instruction itérative for

for (i = 0 ; i < 3 ; ++i) { Affiche trois occurrences du caractère ’a’ .


printf("a");
}

for (x = lst ; x != NULL ; Parcours une liste simplement chaînée et l’affiche.


x = x->suiv) {
afficher(x->elem);
La variable x est un pointeur sur la cellule
} courante.

for ( ; *str != ’\0’; str++)) { Affiche la chaîne de caractères str . Il n’y a pas
putchar(*str);
} d’initialisation. L’incrémentation fait pointer str sur le
prochain caractère.

for ( ; *str != ’\0’; Affiche la chaîne de caractères str . Il n’y a pas


putchar(*str++)) {
/* Rien. */ d’initialisation. L’affichage est réalisé comme effet
} de bord de l’incrémentation.

28 / 399
Instructions de court-circuit

Les instructions de court-circuit sont break et continue .

29 / 399
Instructions de court-circuit

Les instructions de court-circuit sont break et continue .

L’instruction break permet à l’exécution de sortir d’une structure switch , while , do while ou
for . L’exécution continue alors aux instructions qui suivent cette structure.

29 / 399
Instructions de court-circuit

Les instructions de court-circuit sont break et continue .

L’instruction break permet à l’exécution de sortir d’une structure switch , while , do while ou
for . L’exécution continue alors aux instructions qui suivent cette structure.

L’instruction continue permet à l’exécution de sauter à la fin du bloc B d’une structure while ,
do while ou for . L’exécution continue alors à l’évaluation de l’expression test.

29 / 399
Instructions de court-circuit
Dans une boucle for , l’instruction continue fait que l’expression d’incrémentation est tout de
même évaluée.

30 / 399
Instructions de court-circuit
Dans une boucle for , l’instruction continue fait que l’expression d’incrémentation est tout de
même évaluée.

for (i = 1 ; i <= 7 ; ++i) { L’exécution de ces instructions produit sept tours de


if (i == 4)
continue; boucle et l’affichage 1 2 3 5 6 7 . Lorsque i == 4 ,
printf("%d ", i);
le continue relance l’exécution à l’incrémentation
}
du for .

30 / 399
Instructions de court-circuit
Dans une boucle for , l’instruction continue fait que l’expression d’incrémentation est tout de
même évaluée.

for (i = 1 ; i <= 7 ; ++i) { L’exécution de ces instructions produit sept tours de


if (i == 4)
continue; boucle et l’affichage 1 2 3 5 6 7 . Lorsque i == 4 ,
printf("%d ", i);
le continue relance l’exécution à l’incrémentation
}
du for .

Ce n’est pas le cas pour le break .

30 / 399
Instructions de court-circuit
Dans une boucle for , l’instruction continue fait que l’expression d’incrémentation est tout de
même évaluée.

for (i = 1 ; i <= 7 ; ++i) { L’exécution de ces instructions produit sept tours de


if (i == 4)
continue; boucle et l’affichage 1 2 3 5 6 7 . Lorsque i == 4 ,
printf("%d ", i);
le continue relance l’exécution à l’incrémentation
}
du for .

Ce n’est pas le cas pour le break .

for (i = 1 ; i <= 7 ; ++i) { L’exécution de ces instructions produit quatre tours


if (i == 4)
break; de boucle et l’affichage 1 2 3 . Lorsque i == 4 , le
printf("%d ", i);
break fait sortir de la boucle et i vaut 4 .
}

30 / 399
Plan

Bases
Généralités
Expressions et instructions
Constructions syntaxiques
Variables
Fonctions et pile
Commandes préprocesseur

31 / 399
Variables

Une variable est une entité constituée des cinq éléments suivants :

32 / 399
Variables

Une variable est une entité constituée des cinq éléments suivants :

1. un identificateur ;

Identificateur

32 / 399
Variables

Une variable est une entité constituée des cinq éléments suivants :

1. un identificateur ;
2. un type ;
Type
Identificateur

32 / 399
Variables

Une variable est une entité constituée des cinq éléments suivants :

1. un identificateur ;
2. un type ;
Valeur
3. une valeur ; Type
Identificateur

32 / 399
Variables

Une variable est une entité constituée des cinq éléments suivants :

1. un identificateur ;
2. un type ;
Valeur
3. une valeur ; Adresse Type
4. une adresse ; Identificateur

32 / 399
Variables

Une variable est une entité constituée des cinq éléments suivants :

1. un identificateur ; 5. une portée lexicale.


2. un type ;
Valeur
3. une valeur ; Adresse Type E
4. une adresse ; Identificateur

32 / 399
Variables

Une variable est une entité constituée des cinq éléments suivants :

1. un identificateur ; 5. une portée lexicale.


2. un type ;
Valeur
3. une valeur ; Adresse Type E
4. une adresse ; Identificateur

Intuitivement, c’est une boîte qui peut contenir un objet (valeur) et qui dispose d’un nom
(identificateur).
Une boîte ne peut contenir que des objets d’une certaine sorte (type).
Elle se situe de plus à un endroit bien précis dans la mémoire (adresse) et elle n’est visible qu’à
partir de certains endroits du code (portée lexicale).

32 / 399
Identificateurs de variable

L’identificateur d’une variable est un mot commençant par une lettre ou bien ’_’ , suivi par un
nombre arbitraire de lettres, chiffres ou ’_’ .

De plus, aucun identificateur ne peut-être un mot réservé du langage. En voici la liste complète :
auto break case char const continue default do

double else enum extern float for goto if

int long register return short signed sizeof static

struct switch typedef union unsigned void volatile while

33 / 399
Identificateurs de variable

L’identificateur d’une variable est un mot commençant par une lettre ou bien ’_’ , suivi par un
nombre arbitraire de lettres, chiffres ou ’_’ .

De plus, aucun identificateur ne peut-être un mot réservé du langage. En voici la liste complète :
auto break case char const continue default do

double else enum extern float for goto if

int long register return short signed sizeof static

struct switch typedef union unsigned void volatile while

L’identificateur d’une variable est attribué à sa déclaration.

33 / 399
Valeur d’une variable
La valeur d’une variable est la raison pour laquelle celle-ci existe. Le rôle premier d’une variable
étant en effet de contenir une valeur.

34 / 399
Valeur d’une variable
La valeur d’une variable est la raison pour laquelle celle-ci existe. Le rôle premier d’une variable
étant en effet de contenir une valeur.

La valeur d’une variable n’est pas attribuée à sa déclaration (elle contient à ce moment là une
valeur mais il ne faut rien supposer dessus).

34 / 399
Valeur d’une variable
La valeur d’une variable est la raison pour laquelle celle-ci existe. Le rôle premier d’une variable
étant en effet de contenir une valeur.

La valeur d’une variable n’est pas attribuée à sa déclaration (elle contient à ce moment là une
valeur mais il ne faut rien supposer dessus).
On accède à la valeur d’une variable par son identificateur.

34 / 399
Valeur d’une variable
La valeur d’une variable est la raison pour laquelle celle-ci existe. Le rôle premier d’une variable
étant en effet de contenir une valeur.

La valeur d’une variable n’est pas attribuée à sa déclaration (elle contient à ce moment là une
valeur mais il ne faut rien supposer dessus).
On accède à la valeur d’une variable par son identificateur.

On modifie une variable par une affectation. L’occurrence de l’identificateur de la variable se


trouve dans ce cas à gauche de l’opérateur = .

34 / 399
Valeur d’une variable
La valeur d’une variable est la raison pour laquelle celle-ci existe. Le rôle premier d’une variable
étant en effet de contenir une valeur.

La valeur d’une variable n’est pas attribuée à sa déclaration (elle contient à ce moment là une
valeur mais il ne faut rien supposer dessus).
On accède à la valeur d’une variable par son identificateur.

On modifie une variable par une affectation. L’occurrence de l’identificateur de la variable se


trouve dans ce cas à gauche de l’opérateur = .

int num; L’occurrence de num en l. 2 est située à gauche du


num = 23; = : il s’agit d’une affectation. Il y en a deux en ligne
num = num + 32;
3 : la 1re permet de modifier et la 2 de lire sa valeur.

34 / 399
L-values et R-values
Nous rencontrons une subtilité : un identificateur x de variable peut désigner soit :

1. la valeur de la variable x , p.ex., dans x + 16 ;

35 / 399
L-values et R-values
Nous rencontrons une subtilité : un identificateur x de variable peut désigner soit :

1. la valeur de la variable x , p.ex., dans x + 16 ;

2. soit la variable x elle-même, p.ex., dans x += 8 .

35 / 399
L-values et R-values
Nous rencontrons une subtilité : un identificateur x de variable peut désigner soit :

1. la valeur de la variable x , p.ex., dans x + 16 ;

2. soit la variable x elle-même, p.ex., dans x += 8 .


La terminologie de « L-value » (valeur gauche) et « R-value » (valeur droite) permet de mettre en
évidence cette différence.

35 / 399
L-values et R-values
Nous rencontrons une subtilité : un identificateur x de variable peut désigner soit :

1. la valeur de la variable x , p.ex., dans x + 16 ;

2. soit la variable x elle-même, p.ex., dans x += 8 .


La terminologie de « L-value » (valeur gauche) et « R-value » (valeur droite) permet de mettre en
évidence cette différence.

Une L-value est une expression qui peut se situer dans le membre gauche d’une affectation
(l’expression peut recevoir une valeur).

35 / 399
L-values et R-values
Nous rencontrons une subtilité : un identificateur x de variable peut désigner soit :

1. la valeur de la variable x , p.ex., dans x + 16 ;

2. soit la variable x elle-même, p.ex., dans x += 8 .


La terminologie de « L-value » (valeur gauche) et « R-value » (valeur droite) permet de mettre en
évidence cette différence.

Une L-value est une expression qui peut se situer dans le membre gauche d’une affectation
(l’expression peut recevoir une valeur).

Une R-value est une expression qui peut se situer dans le membre droit d’une affectation (une
valeur peut être lue depuis l’expression).

35 / 399
L-values et R-values
Nous rencontrons une subtilité : un identificateur x de variable peut désigner soit :

1. la valeur de la variable x , p.ex., dans x + 16 ;

2. soit la variable x elle-même, p.ex., dans x += 8 .


La terminologie de « L-value » (valeur gauche) et « R-value » (valeur droite) permet de mettre en
évidence cette différence.

Une L-value est une expression qui peut se situer dans le membre gauche d’une affectation
(l’expression peut recevoir une valeur).

Une R-value est une expression qui peut se situer dans le membre droit d’une affectation (une
valeur peut être lue depuis l’expression).

Note 1 : c’est le contexte qui permet de dire si une expression est une L-value ou une R-value.

35 / 399
L-values et R-values
Nous rencontrons une subtilité : un identificateur x de variable peut désigner soit :

1. la valeur de la variable x , p.ex., dans x + 16 ;

2. soit la variable x elle-même, p.ex., dans x += 8 .


La terminologie de « L-value » (valeur gauche) et « R-value » (valeur droite) permet de mettre en
évidence cette différence.

Une L-value est une expression qui peut se situer dans le membre gauche d’une affectation
(l’expression peut recevoir une valeur).

Une R-value est une expression qui peut se situer dans le membre droit d’une affectation (une
valeur peut être lue depuis l’expression).

Note 1 : c’est le contexte qui permet de dire si une expression est une L-value ou une R-value.
Note 2 : toute L-value peut-être une R-value (pour un contexte différent), mais pas l’inverse.

35 / 399
Adresse d’une variable

L’adresse d’une variable est une valeur entière spécifiant la position de la variable en mémoire.

36 / 399
Adresse d’une variable

L’adresse d’une variable est une valeur entière spécifiant la position de la variable en mémoire.

L’adresse d’une variable est attribuée à sa déclaration par le système à l’exécution. Elle ne peut
pas être choisie par le programmeur ni être modifiée.

36 / 399
Adresse d’une variable

L’adresse d’une variable est une valeur entière spécifiant la position de la variable en mémoire.

L’adresse d’une variable est attribuée à sa déclaration par le système à l’exécution. Elle ne peut
pas être choisie par le programmeur ni être modifiée.

On accède à l’adresse d’une variable par son identificateur précédé de l’opérateur & .

36 / 399
Adresse d’une variable

L’adresse d’une variable est une valeur entière spécifiant la position de la variable en mémoire.

L’adresse d’une variable est attribuée à sa déclaration par le système à l’exécution. Elle ne peut
pas être choisie par le programmeur ni être modifiée.

On accède à l’adresse d’une variable par son identificateur précédé de l’opérateur & .

int num; Une 1re exécution de ces instructions affiche


printf("%p\n", &num);
0x7fff6a3014fc . Une 2e affiche 0x7fffbdc357dc .

L’adresse de num varie d’une exécution à


l’autre.

36 / 399
Portée lexicale d’une variable et variables locales
La portée lexicale d’une variable désigne la zone du programme dans laquelle la variable peut
être utilisée.

37 / 399
Portée lexicale d’une variable et variables locales
La portée lexicale d’une variable désigne la zone du programme dans laquelle la variable peut
être utilisée.

Elle dépend de l’endroit dans lequel elle a été déclarée.

37 / 399
Portée lexicale d’une variable et variables locales
La portée lexicale d’une variable désigne la zone du programme dans laquelle la variable peut
être utilisée.

Elle dépend de l’endroit dans lequel elle a été déclarée.

Sa portée lexicale s’étend aux instructions qui sont situées après sa déclaration dans le plus petit
bloc d’instructions qui la contient.

37 / 399
Portée lexicale d’une variable et variables locales
La portée lexicale d’une variable désigne la zone du programme dans laquelle la variable peut
être utilisée.

Elle dépend de l’endroit dans lequel elle a été déclarée.

Sa portée lexicale s’étend aux instructions qui sont situées après sa déclaration dans le plus petit
bloc d’instructions qui la contient.

void f(int x) { La portée lexicale des variables a et b s’étend aux


int a, b;
... instructions du corps de la fonction f . Elle ne
}
s’étend pas aux instructions du corps de g . Les
variables a et b sont des variables locales à la
int g(int y, int z) {
int c; fonction f et invisibles ailleurs.
...
}

37 / 399
Portée lexicale d’une variable et variables locales
La portée lexicale d’une variable désigne la zone du programme dans laquelle la variable peut
être utilisée.

Elle dépend de l’endroit dans lequel elle a été déclarée.

Sa portée lexicale s’étend aux instructions qui sont situées après sa déclaration dans le plus petit
bloc d’instructions qui la contient.

void f(int x) { La portée lexicale des variables a et b s’étend aux


int a, b;
... instructions du corps de la fonction f . Elle ne
}
s’étend pas aux instructions du corps de g . Les
variables a et b sont des variables locales à la
int g(int y, int z) {
int c; fonction f et invisibles ailleurs.
...
} Le paramètre x de f a pour portée lexicale
uniquement le corps de f .

37 / 399
Portée lexicale d’une variable et variables locales

... La portée lexicale de la variable taille s’étend


int f() {...}
... à tout ce qui suit sa déclaration dans le
int taille = 31; programme. Elle est donc visible dans les
...
int g() {...} fonctions g et main mais pas dans f . Étant
...
int main() {...}
donné qu’elle est déclarée en dehors de toute
fonction, elle est qualifiée de variable globale.

38 / 399
Portée lexicale d’une variable et variables locales

... La portée lexicale de la variable taille s’étend


int f() {...}
... à tout ce qui suit sa déclaration dans le
int taille = 31; programme. Elle est donc visible dans les
...
int g() {...} fonctions g et main mais pas dans f . Étant
...
int main() {...}
donné qu’elle est déclarée en dehors de toute
fonction, elle est qualifiée de variable globale.

Attention : l’utilisation de variables globales n’est ni élégante ni indispensable. Elle est


également source d’erreurs car il est souvent difficile de comprendre un programme les utilisant.
Elle est bannie pour ces raisons.

On préfère utiliser des définitions préprocesseur pour représenter leur valeur.

38 / 399
Portée lexicale d’une variable et blocs d’instructions

{ Il y a erreur de compilation : la portée lexicale


int a;
a = 15; de la variable a s’étend de la l. 2 à la l. 4. Elle
printf("%d", a); n’est pas visible à la l. 6. Il n’existe pas de
}
printf("%d", a); variable identifiée par a lorsque la l. 7 est
évaluée.

39 / 399
Portée lexicale d’une variable et blocs d’instructions

{ Il y a erreur de compilation : la portée lexicale


int a;
a = 15; de la variable a s’étend de la l. 2 à la l. 4. Elle
printf("%d", a); n’est pas visible à la l. 6. Il n’existe pas de
}
printf("%d", a); variable identifiée par a lorsque la l. 7 est
évaluée.

{ De la même manière que dans l’exemple


int a;
a = 15; précédent, l’occurrence du symbole a dans le
printf("%d", a); second bloc (l. 7) n’est pas résolue. Elle se situe
}
{ dans un bloc qui n’est pas contenu par celui où
printf("%d", a); le symbole a est déclaré.
}

39 / 399
Portée lexicale d’une variable et blocs d’instructions

int a; Ces instructions produisent l’affichage 10 10 .


a = 10;
{ En effet, la variable a est visible dans le bloc
printf("%d ", a);
} d’instructions dans lequel elle est définie, ainsi
printf("%d\n", a); que dans les blocs d’instructions qui se
trouvent à l’intérieur.

40 / 399
Portée lexicale d’une variable et blocs d’instructions

int a; Ces instructions produisent l’affichage 10 10 .


a = 10;
{ En effet, la variable a est visible dans le bloc
printf("%d ", a);
} d’instructions dans lequel elle est définie, ainsi
printf("%d\n", a); que dans les blocs d’instructions qui se
trouvent à l’intérieur.

int a; Ces instructions produisent l’affichage 20 10 .


a = 10;
{ La variable identifiée par a dans le bloc
int a;
a = 20; d’instructions de la l. 3 à la l. 7 est celle déclarée
printf("%d ", a); en l. 4. La variable identifiée par a hors de ce
}
printf("%d\n", a); bloc d’instructions est celle déclarée en l. 1.

40 / 399
Portée lexicale d’une variable et blocs d’instructions

Comparons les instructions

int a; int a;
a = 10; a = 10;
{ {
int a; a = 20;
a = 20; printf("%d ", a);
printf("%d ", a); }
} printf("%d\n", a);
printf("%d\n", a);

41 / 399
Portée lexicale d’une variable et blocs d’instructions

Comparons les instructions

int a; int a;
a = 10; a = 10;
{ {
int a; a = 20;
a = 20; printf("%d ", a);
printf("%d ", a); }
} printf("%d\n", a);
printf("%d\n", a);

Dans le cas de gauche (déjà vu), l’affectation a = 20 n’a d’effet que sur la variable a déclarée à
l’intérieur du bloc. Ceci affiche 20 10 .

41 / 399
Portée lexicale d’une variable et blocs d’instructions

Comparons les instructions

int a; int a;
a = 10; a = 10;
{ {
int a; a = 20;
a = 20; printf("%d ", a);
printf("%d ", a); }
} printf("%d\n", a);
printf("%d\n", a);

Dans le cas de gauche (déjà vu), l’affectation a = 20 n’a d’effet que sur la variable a déclarée à
l’intérieur du bloc. Ceci affiche 20 10 .

En revanche, les instructions de droite affichent 20 20 car il n’y a pas de déclaration de a dans
le bloc. L’affectation a = 20 modifie la variable a déclarée en l. 1.

41 / 399
Plan

Bases
Généralités
Expressions et instructions
Constructions syntaxiques
Variables
Fonctions et pile
Commandes préprocesseur

42 / 399
Fonctions
Une fonction est constituée
1. d’un identificateur (qui suit les mêmes contraintes que ceux des variables) ;
2. d’une signature (la liste de ses paramètres et de leurs types) ;
3. d’un type de retour (le type de la valeur renvoyée par la fonction) ;
4. d’instructions (qui forment le corps de la fonction).

43 / 399
Fonctions
Une fonction est constituée
1. d’un identificateur (qui suit les mêmes contraintes que ceux des variables) ;
2. d’une signature (la liste de ses paramètres et de leurs types) ;
3. d’un type de retour (le type de la valeur renvoyée par la fonction) ;
4. d’instructions (qui forment le corps de la fonction).

La ligne constituée du type de retour, de l’identificateur et de la signature d’une fonction est son
prototype.

43 / 399
Fonctions
Une fonction est constituée
1. d’un identificateur (qui suit les mêmes contraintes que ceux des variables) ;
2. d’une signature (la liste de ses paramètres et de leurs types) ;
3. d’un type de retour (le type de la valeur renvoyée par la fonction) ;
4. d’instructions (qui forment le corps de la fonction).

La ligne constituée du type de retour, de l’identificateur et de la signature d’une fonction est son
prototype.

P.ex.,
int produit(int a, int b, int c) {
return a * b * c;
}

est une fonction d’identificateur produit , de signature (int a, int b, int c) et de type de retour
int .

43 / 399
Définition vs déclaration

La définition d’une fonction consiste à fournir tous ses constituants.

44 / 399
Définition vs déclaration

La définition d’une fonction consiste à fournir tous ses constituants.


La déclaration d’une fonction consiste à fournir son prototype.

44 / 399
Définition vs déclaration

La définition d’une fonction consiste à fournir tous ses constituants.


La déclaration d’une fonction consiste à fournir son prototype.
Déclarer une fonction est utile si l’on souhaite s’en servir avant de l’avoir définie.

44 / 399
Définition vs déclaration

La définition d’une fonction consiste à fournir tous ses constituants.


La déclaration d’une fonction consiste à fournir son prototype.
Déclarer une fonction est utile si l’on souhaite s’en servir avant de l’avoir définie.
Voici un exemple :

#include <stdio.h> void flip(int nb) { void flop(int nb) {


if (nb >= 1) { if (nb >= 1) {
⁄* Declarations. *⁄ printf("flip\n"); printf("flop\n");
void flop(int nb); flop(nb - 1); flip(nb - 1);
void flip(int nb); } }
} }
⁄* Definitions. *⁄
int main() {
flip(10);
return 0;
}

44 / 399
Paramètres vs arguments
Il faut faire attention à bien distinguer les notions de paramètre et d’argument qui sont deux
choses différentes.

45 / 399
Paramètres vs arguments
Il faut faire attention à bien distinguer les notions de paramètre et d’argument qui sont deux
choses différentes.

On considère la fonction
int produit(int a, int b, int c) {
return a * b * c;
}

45 / 399
Paramètres vs arguments
Il faut faire attention à bien distinguer les notions de paramètre et d’argument qui sont deux
choses différentes.

On considère la fonction
int produit(int a, int b, int c) {
return a * b * c;
}

Les symboles a , b et c de son prototype sont ses paramètres.

45 / 399
Paramètres vs arguments
Il faut faire attention à bien distinguer les notions de paramètre et d’argument qui sont deux
choses différentes.

On considère la fonction
int produit(int a, int b, int c) {
return a * b * c;
}

Les symboles a , b et c de son prototype sont ses paramètres.

Lors de l’appel
produit(15, num, -3);

les expressions 15 , num et -3 sont les arguments de l’appel.

45 / 399
Paramètres vs arguments
Il faut faire attention à bien distinguer les notions de paramètre et d’argument qui sont deux
choses différentes.

On considère la fonction
int produit(int a, int b, int c) {
return a * b * c;
}

Les symboles a , b et c de son prototype sont ses paramètres.

Lors de l’appel
produit(15, num, -3);

les expressions 15 , num et -3 sont les arguments de l’appel.

Aide-mémoire : paramètre ↔ prototype ; argument ↔ appel.

45 / 399
Portée lexicale des paramètres
Les variables locales d’une fonction ont pour portée lexicale la fonction elle-même (déjà
mentionné).

46 / 399
Portée lexicale des paramètres
Les variables locales d’une fonction ont pour portée lexicale la fonction elle-même (déjà
mentionné).

Il en est de même pour ses paramètres : leur portée lexicale est la fonction elle-même. On peut
voir la déclaration des paramètres d’une fonction dans son en-tête comme une déclaration de
variable.

46 / 399
Portée lexicale des paramètres
Les variables locales d’une fonction ont pour portée lexicale la fonction elle-même (déjà
mentionné).

Il en est de même pour ses paramètres : leur portée lexicale est la fonction elle-même. On peut
voir la déclaration des paramètres d’une fonction dans son en-tête comme une déclaration de
variable.

int doubl(int a) { Il y plusieurs occurrences du symbole a .


return 2 * a;
} Celui déclaré dans l’en-tête de doubl a une
int main() { portée lexicale qui s’étend de la l. 1 à la l. 2.
int a;
a = 10; Celui déclaré dans le main a pour portée
a = doubl(a + 1);
return 0; lexicale le main tout entier.
}
Ce sont des variables différentes.

46 / 399
Pile
Lors de l’appel d’une fonction, les valeurs de ses arguments sont recopiées dans une zone de la
mémoire appelée pile.

Conséquence très importante : toute modification des paramètres dans une fonction ne
modifie pas les valeurs des arguments avec lesquels elle a été appelée.

P.ex.,

void incr(int a) { L’appel à f en l. 14 produit les configurations


a = a + 1;
} de pile
int f() { a (l. 1, val. 3 ) a (l. 1, val. 4 )
int b;
b (l. 6, val. 3 ) b (l. 6, val. 3 ) b (l. 6, val. 3 )
b = 3;
incr(b);
printf("%d\n", b); l. 8 l. 1 l. 2
return 0;
}
b 3 0
... (l. 6, val. ) (l. 11, val. ret.)

f();
l. 10 l. 11
47 / 399
Pile

Les variables locales d’une fonction (c.-à-d. les variables déclarées dans le corps de la fonction) se
situent dans la pile.
De plus, la valeur renvoyée (si son type de retour n’est pas void ) se situe dans la pile.

48 / 399
Pile

Les variables locales d’une fonction (c.-à-d. les variables déclarées dans le corps de la fonction) se
situent dans la pile.
De plus, la valeur renvoyée (si son type de retour n’est pas void ) se situe dans la pile.
Après avoir appelé une fonction, c.-à-d. juste après avoir renvoyé la valeur de retour, la pile se
trouve dans le même état qu’avant l’appel.

48 / 399
Pile

Les variables locales d’une fonction (c.-à-d. les variables déclarées dans le corps de la fonction) se
situent dans la pile.
De plus, la valeur renvoyée (si son type de retour n’est pas void ) se situe dans la pile.
Après avoir appelé une fonction, c.-à-d. juste après avoir renvoyé la valeur de retour, la pile se
trouve dans le même état qu’avant l’appel.

Conséquence très importante : toute variable locale à une fonction est non seulement invisible
mais n’existe plus en mémoire hors de la fonction et après son appel.

48 / 399
Pile

P.ex.,
void fct_1(int a) { void fct_2() {
int b, c; int x;
b = 1; x = 16;
fct_2();
c = 2; fct_1(x);
a = b + c; printf("%d\n", x);
} }

Configurations de pile :
c (l. 2, val. ? ) c (l. 2, val. 2 )

b (l. 2, val. ? ) b (l. 2, val. ? ) b (l. 2, val. 1 )

a (l. 1, val. 16 ) a (l. 1, val. 16 ) a (l. 1, val. 16 ) a (l. 1, val. 3 )

x (l. 10, val. 16 ) x (l. 10, val. 16 ) x (l. 10, val. 16 ) x (l. 10, val. 16 ) x (l. 10, val. 16 ) x (l. 10, val. 16 )
∅ ∅
l. 15 l. 11 l. 2 l. 3 l. 3 l. 6 l. 13 l. 17

49 / 399
Pile et fonctions récursives
Soit la fonction et la suite d’instructions
int fibo(int a) { int x;
if (a <= 1) x = fibo(4);
return a;
return fibo(a - 1) + fibo(a - 2);
}

50 / 399
Pile et fonctions récursives
Soit la fonction et la suite d’instructions
int fibo(int a) { int x;
if (a <= 1) x = fibo(4);
return a;
return fibo(a - 1) + fibo(a - 2);
}

On représente son exécution par un arbre des appels :

fibo(4)

fibo(3) fibo(2)

fibo(2) fibo(1) fibo(1) fibo(0)

fibo(1) fibo(0) 1 1 0

1 0

50 / 399
Fonctions à effet de bord

Une fonction est à effet de bord s’il existe au moins un jeu d’arguments qui fait que l’évaluation
de l’appel à la fonction sur ce jeu d’arguments modifie la mémoire par rapport à son état d’avant
l’appel.

51 / 399
Fonctions à effet de bord

Une fonction est à effet de bord s’il existe au moins un jeu d’arguments qui fait que l’évaluation
de l’appel à la fonction sur ce jeu d’arguments modifie la mémoire par rapport à son état d’avant
l’appel.

int f(int a, int b) { Cette fonction n’est pas à effet de bord. Elle
return 21 * a + b;
} renvoie une valeur sans modifier la mémoire.

51 / 399
Fonctions à effet de bord

Une fonction est à effet de bord s’il existe au moins un jeu d’arguments qui fait que l’évaluation
de l’appel à la fonction sur ce jeu d’arguments modifie la mémoire par rapport à son état d’avant
l’appel.

int f(int a, int b) { Cette fonction n’est pas à effet de bord. Elle
return 21 * a + b;
} renvoie une valeur sans modifier la mémoire.

float double_val(float *x) { Cette fonction est à effet de bord puisqu’elle


*x = 2 * (*x);
return *x; modifie une zone de la mémoire (celle à
} l’adresse spécifiée par son argument).

51 / 399
Fonctions à effet de bord

int g(char c) {
int b;
b = 5;
return b + c;
}

52 / 399
Fonctions à effet de bord

int g(char c) { Cette fonction n’est pas à effet de bord. La


int b;
b = 5; déclaration (et l’affectation) de b reste locale à
return b + c;
la fonction. Après tout appel à g , la variable
}
b n’existe plus.

52 / 399
Fonctions à effet de bord

int g(char c) { Cette fonction n’est pas à effet de bord. La


int b;
b = 5; déclaration (et l’affectation) de b reste locale à
return b + c;
la fonction. Après tout appel à g , la variable
}
b n’existe plus.

char *allouer(int n) {
char *res;
res = (char *)
malloc(sizeof(char) * n);
return res;
}

52 / 399
Fonctions à effet de bord

int g(char c) { Cette fonction n’est pas à effet de bord. La


int b;
b = 5; déclaration (et l’affectation) de b reste locale à
return b + c;
la fonction. Après tout appel à g , la variable
}
b n’existe plus.

char *allouer(int n) { Cette fonction est à effet de bord. Elle réserve


char *res;
res = (char *) en effet, par l’appel interne à la fonction
malloc(sizeof(char) * n); malloc , une zone de la mémoire, ce qui modifie
return res;
} son état.

52 / 399
Fonctions à effet de bord

int g(char c) { Cette fonction n’est pas à effet de bord. La


int b;
b = 5; déclaration (et l’affectation) de b reste locale à
return b + c;
la fonction. Après tout appel à g , la variable
}
b n’existe plus.

char *allouer(int n) { Cette fonction est à effet de bord. Elle réserve


char *res;
res = (char *) en effet, par l’appel interne à la fonction
malloc(sizeof(char) * n); malloc , une zone de la mémoire, ce qui modifie
return res;
} son état.

int h(int a, int b) {


if (a * b == 0)
printf("z\n");
return a - b;
}

52 / 399
Fonctions à effet de bord

int g(char c) { Cette fonction n’est pas à effet de bord. La


int b;
b = 5; déclaration (et l’affectation) de b reste locale à
return b + c;
la fonction. Après tout appel à g , la variable
}
b n’existe plus.

char *allouer(int n) { Cette fonction est à effet de bord. Elle réserve


char *res;
res = (char *) en effet, par l’appel interne à la fonction
malloc(sizeof(char) * n); malloc , une zone de la mémoire, ce qui modifie
return res;
} son état.

int h(int a, int b) { Cette fonction est à effet de bord. En effet,


if (a * b == 0)
printf("z\n"); l’appel à h avec, p.ex., les arguments 1 et 0
return a - b; provoque un affichage sur la sortie standard,
}
modifiant l’état de la mémoire.

52 / 399
Plan

Bases
Généralités
Expressions et instructions
Constructions syntaxiques
Variables
Fonctions et pile
Commandes préprocesseur

53 / 399
Commandes préprocesseur

Une commande préprocesseur (ou directive préprocesseur) est une ligne qui commence par # .

54 / 399
Commandes préprocesseur

Une commande préprocesseur (ou directive préprocesseur) est une ligne qui commence par # .

Le préprocesseur est une unité qui intervient lors de la compilation. Son rôle est de traiter les
commandes préprocesseur.
Il fonctionne en construisant une nouvelle version du programme en remplaçant chaque
commande préprocesseur par des expressions en C adéquates.

54 / 399
Commandes préprocesseur

Une commande préprocesseur (ou directive préprocesseur) est une ligne qui commence par # .

Le préprocesseur est une unité qui intervient lors de la compilation. Son rôle est de traiter les
commandes préprocesseur.
Il fonctionne en construisant une nouvelle version du programme en remplaçant chaque
commande préprocesseur par des expressions en C adéquates.

Il existe plusieurs sortes de commandes préprocesseur :


I les inclusions de fichiers ;
I les définitions de symboles ;
I les macro-instructions à paramètres ;
I les macro-instructions de contrôle de compilation.

54 / 399
Inclusions de fichiers

La commande préprocesseur
#include <NOM.h>

permet d’inclure le fichier NOM.h dans le programme pour bénéficier des fonctionnalités qu’il
apporte.

55 / 399
Inclusions de fichiers

La commande préprocesseur
#include <NOM.h>

permet d’inclure le fichier NOM.h dans le programme pour bénéficier des fonctionnalités qu’il
apporte.
Le préprocesseur résout cette commande en recopiant le contenu de NOM.h à l’endroit où elle est
invoquée.

55 / 399
Inclusions de fichiers

La commande préprocesseur
#include <NOM.h>

permet d’inclure le fichier NOM.h dans le programme pour bénéficier des fonctionnalités qu’il
apporte.
Le préprocesseur résout cette commande en recopiant le contenu de NOM.h à l’endroit où elle est
invoquée.

Il est possible d’enchaîner les inclusions :


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

55 / 399
Inclusions de fichiers

La commande préprocesseur
#include <NOM.h>

permet d’inclure le fichier NOM.h dans le programme pour bénéficier des fonctionnalités qu’il
apporte.
Le préprocesseur résout cette commande en recopiant le contenu de NOM.h à l’endroit où elle est
invoquée.

Il est possible d’enchaîner les inclusions :


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

Habituellement, les inclusions sont réalisées au début du programme.

55 / 399
Définitions de symboles
La commande préprocesseur
#define SYMB EXP

permet de définir un alias SYMB pour l’expression EXP . Ceci autorise à faire référence à
l’expression EXP par l’intermédiaire du symbole SYMB .

56 / 399
Définitions de symboles
La commande préprocesseur
#define SYMB EXP

permet de définir un alias SYMB pour l’expression EXP . Ceci autorise à faire référence à
l’expression EXP par l’intermédiaire du symbole SYMB .

Le préprocesseur résout tout invocation SYMB en la remplaçant par EXP .

56 / 399
Définitions de symboles
La commande préprocesseur
#define SYMB EXP

permet de définir un alias SYMB pour l’expression EXP . Ceci autorise à faire référence à
l’expression EXP par l’intermédiaire du symbole SYMB .

Le préprocesseur résout tout invocation SYMB en la remplaçant par EXP .

À gauche (resp. à droite), des instructions avant (resp. après) le passage du préprocesseur :

#define NB 5 ⁄* Rien. *⁄
#define CHAINE "cba\n" ⁄* Rien. *⁄
... ...
for (i = 1 ; i <= NB ; ++i) { for (i = 1 ; i <= 5 ; ++i) {
printf("%s", CHAINE); printf("%s", "cba\n");
} }

56 / 399
Définitions de symboles
La commande préprocesseur
#define SYMB EXP

permet de définir un alias SYMB pour l’expression EXP . Ceci autorise à faire référence à
l’expression EXP par l’intermédiaire du symbole SYMB .

Le préprocesseur résout tout invocation SYMB en la remplaçant par EXP .

À gauche (resp. à droite), des instructions avant (resp. après) le passage du préprocesseur :

#define NB 5 ⁄* Rien. *⁄
#define CHAINE "cba\n" ⁄* Rien. *⁄
... ...
for (i = 1 ; i <= NB ; ++i) { for (i = 1 ; i <= 5 ; ++i) {
printf("%s", CHAINE); printf("%s", "cba\n");
} }

Par convention, tout alias est constitué de lettres majuscules, de chiffres ou de tirets bas.
56 / 399
Macro-instructions à paramètres
La commande préprocesseur
#define SYMB(P1, P2, ..., Pn) EXP

permet de définir une macro-instruction à paramètres SYMB . Ceci autorise à faire référence à
l’expression EXP par l’intermédiaire du symbole SYMB paramétrable par des paramètres P1 , P2 ,
. . ., Pn .

57 / 399
Macro-instructions à paramètres
La commande préprocesseur
#define SYMB(P1, P2, ..., Pn) EXP

permet de définir une macro-instruction à paramètres SYMB . Ceci autorise à faire référence à
l’expression EXP par l’intermédiaire du symbole SYMB paramétrable par des paramètres P1 , P2 ,
. . ., Pn .

Le préprocesseur résout toute invocation SYMB(A1, A2, ..., An) en la remplaçant par l’expression
obtenue en substituant Ai à toute occurrence du paramètre Pi dans EXP .

57 / 399
Macro-instructions à paramètres
La commande préprocesseur
#define SYMB(P1, P2, ..., Pn) EXP

permet de définir une macro-instruction à paramètres SYMB . Ceci autorise à faire référence à
l’expression EXP par l’intermédiaire du symbole SYMB paramétrable par des paramètres P1 , P2 ,
. . ., Pn .

Le préprocesseur résout toute invocation SYMB(A1, A2, ..., An) en la remplaçant par l’expression
obtenue en substituant Ai à toute occurrence du paramètre Pi dans EXP .

À gauche (resp. à droite), des instructions avant (resp. après) le passage du préprocesseur :

#define MAX(a, b) a > b ? a : b ⁄* Rien. *⁄


... ...
int x; int x;
x = MAX(10, 14); x = 10 > 14 ? 10 : 14;

57 / 399
Macro-instructions à paramètres

Problème : étudions comment le préprocesseur transforme les instructions suivantes :


#define CARRE(a) a * a
...
x = 2;
y = 3;
z = CARRE(x + y);

58 / 399
Macro-instructions à paramètres

Problème : étudions comment le préprocesseur transforme les instructions suivantes :


#define CARRE(a) a * a
...
x = 2;
y = 3;
z = CARRE(x + y);

La l. 5 est remplacée par z = x + y * x + y; .


Ainsi, la valeur 2 + 3 * 2 + 3 = 11 est affectée à z au lieu de 25 comme attendu.

58 / 399
Macro-instructions à paramètres

Problème : étudions comment le préprocesseur transforme les instructions suivantes :


#define CARRE(a) a * a
...
x = 2;
y = 3;
z = CARRE(x + y);

La l. 5 est remplacée par z = x + y * x + y; .


Ainsi, la valeur 2 + 3 * 2 + 3 = 11 est affectée à z au lieu de 25 comme attendu.

Solution : il faut placer des parenthèses autour des paramètres des macro-instructions à
paramètres :
#define CARRE(a) (a) * (a)

58 / 399
Macro-instructions à paramètres

Problème : étudions comment le préprocesseur transforme les instructions suivantes :


#define CARRE(a) a * a
...
x = 2;
y = 3;
z = CARRE(x + y);

La l. 5 est remplacée par z = x + y * x + y; .


Ainsi, la valeur 2 + 3 * 2 + 3 = 11 est affectée à z au lieu de 25 comme attendu.

Solution : il faut placer des parenthèses autour des paramètres des macro-instructions à
paramètres :
#define CARRE(a) (a) * (a)

De cette façon, CARRE(x + y) est remplacée par (x + y) * (x + y) comme désiré.

58 / 399
Macro-instructions à paramètres

Problème : étudions comment le préprocesseur transforme les instructions suivantes :


#define DOUBLE(a) (a) + (a)
...
x = 3;
z = 5 * DOUBLE(x);

59 / 399
Macro-instructions à paramètres

Problème : étudions comment le préprocesseur transforme les instructions suivantes :


#define DOUBLE(a) (a) + (a)
...
x = 3;
z = 5 * DOUBLE(x);

La l. 4 est remplacée par z = 5 * (x) + (x); .


Ainsi, la valeur 5 * 3 + 3 = 18 est affectée à z au lieu de 30 comme attendu.

59 / 399
Macro-instructions à paramètres

Problème : étudions comment le préprocesseur transforme les instructions suivantes :


#define DOUBLE(a) (a) + (a)
...
x = 3;
z = 5 * DOUBLE(x);

La l. 4 est remplacée par z = 5 * (x) + (x); .


Ainsi, la valeur 5 * 3 + 3 = 18 est affectée à z au lieu de 30 comme attendu.

Solution : il faut placer des parenthèses autour de l’expression toute entière :


#define DOUBLE(a) ((a) + (a))

59 / 399
Macro-instructions à paramètres

Problème : étudions comment le préprocesseur transforme les instructions suivantes :


#define DOUBLE(a) (a) + (a)
...
x = 3;
z = 5 * DOUBLE(x);

La l. 4 est remplacée par z = 5 * (x) + (x); .


Ainsi, la valeur 5 * 3 + 3 = 18 est affectée à z au lieu de 30 comme attendu.

Solution : il faut placer des parenthèses autour de l’expression toute entière :


#define DOUBLE(a) ((a) + (a))

De cette façon, 5 * DOUBLE(x) est remplacée par 5 * ((x) + (x)) comme désiré.

59 / 399
Macro-instructions à paramètres
Attention, l’usage de macro-instructions à paramètres peut provoquer des évaluations
multiples d’une même expression.

60 / 399
Macro-instructions à paramètres
Attention, l’usage de macro-instructions à paramètres peut provoquer des évaluations
multiples d’une même expression.
Considérons par exemple
#define MAX(a, b) ((a) > (b) ? (a) : (b))

60 / 399
Macro-instructions à paramètres
Attention, l’usage de macro-instructions à paramètres peut provoquer des évaluations
multiples d’une même expression.
Considérons par exemple
#define MAX(a, b) ((a) > (b) ? (a) : (b))

et son emploi dans


res = MAX(f(t1), f(t2));

où res est une variable entière, f est une fonction qui renvoie un entier et t1 et t2 sont des
arguments acceptés par f .

60 / 399
Macro-instructions à paramètres
Attention, l’usage de macro-instructions à paramètres peut provoquer des évaluations
multiples d’une même expression.
Considérons par exemple
#define MAX(a, b) ((a) > (b) ? (a) : (b))

et son emploi dans


res = MAX(f(t1), f(t2));

où res est une variable entière, f est une fonction qui renvoie un entier et t1 et t2 sont des
arguments acceptés par f .
Ceci est remplacé par le préprocesseur en
res = ((f(t1)) > (f(t2)) ? (f(t1)) : (f(t2)));

60 / 399
Macro-instructions à paramètres
Attention, l’usage de macro-instructions à paramètres peut provoquer des évaluations
multiples d’une même expression.
Considérons par exemple
#define MAX(a, b) ((a) > (b) ? (a) : (b))

et son emploi dans


res = MAX(f(t1), f(t2));

où res est une variable entière, f est une fonction qui renvoie un entier et t1 et t2 sont des
arguments acceptés par f .
Ceci est remplacé par le préprocesseur en
res = ((f(t1)) > (f(t2)) ? (f(t1)) : (f(t2)));

Problème : ceci réalise un appel


I à f avec l’argument t1 (ce qui est normal) ;
I à f avec l’argument t2 (ce qui est normal) ;
I à f avec l’argument t1 ou t2 (ce qui est de trop).
60 / 399
Macro-instructions de contrôle de compilation

Les macro-instructions de contrôle de compilation permettent d’ignorer, lors de la compilation,


une partie du programme.
Ceci est utile pour sélectionner les parties à prendre en compte dans un programme, sans avoir
à les (dé)commenter.

61 / 399
Macro-instructions de contrôle de compilation

Les macro-instructions de contrôle de compilation permettent d’ignorer, lors de la compilation,


une partie du programme.
Ceci est utile pour sélectionner les parties à prendre en compte dans un programme, sans avoir
à les (dé)commenter.

On dispose ainsi des constructions

#ifdef SYMB #ifndef SYMB #ifdef SYMB


... ... ...
#endif #endif #else
...
#endif

À gauche, le code ... n’est considéré que si l’alias SYMB est défini.

Au centre, le code ... n’est considéré que si l’alias SYMB n’est pas défini.

61 / 399
Macro-instructions de contrôle de compilation

#include <stdio.h>
#else
#define GAUCHE_DROITE
int rechercher(char *tab,
int main() {
int n, char x) {
#ifdef GAUCHE_DROITE int res;
int i;
char tab[] = "chaine de test";
for (i = n - 1 ; i >= 0 ; --i)
int rechercher(char *tab,
if (tab[i] == x)
int n, char x) { res = rechercher(tab, 14, ’t’);
return i;
int i; printf("%d\n", res);
return -1;
for (i = 0 ; i < n ; ++i) }
}
if (tab[i] == x)
return i;
#endif
return -1;
}

Ici, on donne deux algorithmes pour localiser la première occurrence d’une lettre dans un
tableau : de la gauche vers la droite, ou bien de la droite vers la gauche.
Ce programme affiche 10 ; si on renomme GAUCHE_DROITE (en l. 4), il affiche 13 .

62 / 399
Plan

Habitudes
Mise en page
Gestion d’erreurs
Assertions d’entrée

63 / 399
Plan

Habitudes
Mise en page
Gestion d’erreurs
Assertions d’entrée

64 / 399
Mise en page d’un programme

Pour écrire un programme de valeur, il faut soigner les points suivants :

1. l’indentation ;

2. l’organisation des espaces autour des caractères ;

3. le choix des identificateurs ;

4. la documentation.

65 / 399
Indentation
L’indentation consiste à disposer des caractères blancs au début de certaines lignes d’un
programme.
Contrairement au Python, celle-ci ne modifie pas le comportement d’un programme.
L’objectif est d’augmenter sa lisibilité.

66 / 399
Indentation
L’indentation consiste à disposer des caractères blancs au début de certaines lignes d’un
programme.
Contrairement au Python, celle-ci ne modifie pas le comportement d’un programme.
L’objectif est d’augmenter sa lisibilité.
Règle : on place quatre espaces (pas de tabulation) avant chaque instruction d’un bloc et on
incrémente cet espacement de quatre en quatre en fonction de la profondeur des blocs.

66 / 399
Indentation
L’indentation consiste à disposer des caractères blancs au début de certaines lignes d’un
programme.
Contrairement au Python, celle-ci ne modifie pas le comportement d’un programme.
L’objectif est d’augmenter sa lisibilité.
Règle : on place quatre espaces (pas de tabulation) avant chaque instruction d’un bloc et on
incrémente cet espacement de quatre en quatre en fonction de la profondeur des blocs.

⁄* Correct. *⁄
a␣=␣8;
if␣(b␣>=␣0)␣{
␣␣␣␣printf("%d\n",␣a);
␣␣␣␣a␣=␣0;
␣␣␣␣for␣(i␣=␣0␣;␣i␣<=␣b␣;␣++i)
␣␣␣␣␣␣␣␣a␣/=␣2;
}

66 / 399
Indentation
L’indentation consiste à disposer des caractères blancs au début de certaines lignes d’un
programme.
Contrairement au Python, celle-ci ne modifie pas le comportement d’un programme.
L’objectif est d’augmenter sa lisibilité.
Règle : on place quatre espaces (pas de tabulation) avant chaque instruction d’un bloc et on
incrémente cet espacement de quatre en quatre en fonction de la profondeur des blocs.

⁄* Correct. *⁄ ⁄* Incorrect. *⁄
a␣=␣8; a␣=␣8;
if␣(b␣>=␣0)␣{ if␣(b␣>=␣0)␣{
␣␣␣␣printf("%d\n",␣a); ␣␣printf("%d\n",␣a);
␣␣␣␣a␣=␣0; ␣␣a␣=␣0;
␣␣␣␣for␣(i␣=␣0␣;␣i␣<=␣b␣;␣++i) ␣␣for␣(i␣=␣0␣;␣i␣<=␣b␣;␣++i)
␣␣␣␣␣␣␣␣a␣/=␣2; ␣␣␣␣␣a␣/=␣2;
} }

66 / 399
Organisation des blocs
Nous avons vu qu’un bloc des une suite d’instructions délimitée par des accolades. Il se trouve
attaché à une instruction de branchement ou de boucle. Il peut aussi être indépendant.
On revient à la ligne après une accolade ouvrante.
L’accolade fermante se trouve horizontalement au niveau du début de la ligne qui contient
l’accolade ouvrante.

67 / 399
Organisation des blocs
Nous avons vu qu’un bloc des une suite d’instructions délimitée par des accolades. Il se trouve
attaché à une instruction de branchement ou de boucle. Il peut aussi être indépendant.
On revient à la ligne après une accolade ouvrante.
L’accolade fermante se trouve horizontalement au niveau du début de la ligne qui contient
l’accolade ouvrante.

⁄* Correct. *⁄
if (valeur >= 1) {
valeur -= 1;
}

67 / 399
Organisation des blocs
Nous avons vu qu’un bloc des une suite d’instructions délimitée par des accolades. Il se trouve
attaché à une instruction de branchement ou de boucle. Il peut aussi être indépendant.
On revient à la ligne après une accolade ouvrante.
L’accolade fermante se trouve horizontalement au niveau du début de la ligne qui contient
l’accolade ouvrante.

⁄* Correct. *⁄ ⁄* Incorrect. *⁄
if (valeur >= 1) { if (valeur >= 1)
valeur -= 1; {
} valeur -= 1;
}

67 / 399
Organisation des blocs
Nous avons vu qu’un bloc des une suite d’instructions délimitée par des accolades. Il se trouve
attaché à une instruction de branchement ou de boucle. Il peut aussi être indépendant.
On revient à la ligne après une accolade ouvrante.
L’accolade fermante se trouve horizontalement au niveau du début de la ligne qui contient
l’accolade ouvrante.

⁄* Correct. *⁄ ⁄* Incorrect. *⁄
if (valeur >= 1) { if (valeur >= 1)
valeur -= 1; {
} valeur -= 1;
}

⁄* Correct. *⁄
valeur = 1;
{
int a;
valeur = 10;
}

67 / 399
Organisation des blocs
Nous avons vu qu’un bloc des une suite d’instructions délimitée par des accolades. Il se trouve
attaché à une instruction de branchement ou de boucle. Il peut aussi être indépendant.
On revient à la ligne après une accolade ouvrante.
L’accolade fermante se trouve horizontalement au niveau du début de la ligne qui contient
l’accolade ouvrante.

⁄* Correct. *⁄ ⁄* Incorrect. *⁄
if (valeur >= 1) { if (valeur >= 1)
valeur -= 1; {
} valeur -= 1;
}

⁄* Correct. *⁄ ⁄* Incorrect. *⁄
valeur = 1; valeur = 1; {
{ int a;
int a; valeur = 10;
valeur = 10; }
}

67 / 399
Organisation des espaces

On place une espace avant et après chaque opérateur.

⁄* Correct. *⁄
a␣=␣b␣*␣2␣+␣5;

68 / 399
Organisation des espaces

On place une espace avant et après chaque opérateur.

⁄* Correct. *⁄ ⁄* Incorrect. *⁄
a␣=␣b␣*␣2␣+␣5; a␣=␣b*2␣+␣5;

68 / 399
Organisation des espaces

On place une espace avant et après chaque opérateur.

⁄* Correct. *⁄ ⁄* Incorrect. *⁄
a␣=␣b␣*␣2␣+␣5; a␣=␣b*2␣+␣5;

On utilise les règles habituelles de typographie pour l’usage des virgules.

⁄* Correct. *⁄
f(a,␣b,␣c,␣16);

68 / 399
Organisation des espaces

On place une espace avant et après chaque opérateur.

⁄* Correct. *⁄ ⁄* Incorrect. *⁄
a␣=␣b␣*␣2␣+␣5; a␣=␣b*2␣+␣5;

On utilise les règles habituelles de typographie pour l’usage des virgules.

⁄* Correct. *⁄ ⁄* Incorrect. *⁄
f(a,␣b,␣c,␣16); f(a,b,c,16);

68 / 399
Organisation des espaces

On place une espace avant et après chaque opérateur.

⁄* Correct. *⁄ ⁄* Incorrect. *⁄
a␣=␣b␣*␣2␣+␣5; a␣=␣b*2␣+␣5;

On utilise les règles habituelles de typographie pour l’usage des virgules.

⁄* Correct. *⁄ ⁄* Incorrect. *⁄
f(a,␣b,␣c,␣16); f(a,b,c,16);

On ne place pas d’espace après une parenthèse ouvrante ou avant une parenthèse fermante.

⁄* Correct. *⁄
a␣=␣(f(a,␣3)␣+␣a)␣*␣2;

68 / 399
Organisation des espaces

On place une espace avant et après chaque opérateur.

⁄* Correct. *⁄ ⁄* Incorrect. *⁄
a␣=␣b␣*␣2␣+␣5; a␣=␣b*2␣+␣5;

On utilise les règles habituelles de typographie pour l’usage des virgules.

⁄* Correct. *⁄ ⁄* Incorrect. *⁄
f(a,␣b,␣c,␣16); f(a,b,c,16);

On ne place pas d’espace après une parenthèse ouvrante ou avant une parenthèse fermante.

⁄* Correct. *⁄ ⁄* Incorrect. *⁄
a␣=␣(f(a,␣3)␣+␣a)␣*␣2; a␣=␣(f(␣a,␣3)␣+␣a␣)␣*␣2;

68 / 399
Choix des identificateurs

Les identificateurs doivent à la fois renseigner sur le rôle des entités auxquelles ils
appartiennent (variables, paramètres, fonctions, modules, etc.) et être concis.

69 / 399
Choix des identificateurs

Les identificateurs doivent à la fois renseigner sur le rôle des entités auxquelles ils
appartiennent (variables, paramètres, fonctions, modules, etc.) et être concis.

⁄* Identificateur non explicite. *⁄


v

69 / 399
Choix des identificateurs

Les identificateurs doivent à la fois renseigner sur le rôle des entités auxquelles ils
appartiennent (variables, paramètres, fonctions, modules, etc.) et être concis.

⁄* Identificateur non explicite. *⁄


v

⁄* Identificateur trop long. *⁄


valeur_choisie_pour_le_nombre_parties

69 / 399
Choix des identificateurs

Les identificateurs doivent à la fois renseigner sur le rôle des entités auxquelles ils
appartiennent (variables, paramètres, fonctions, modules, etc.) et être concis.

⁄* Identificateur non explicite. *⁄


v

⁄* Identificateur trop long. *⁄


valeur_choisie_pour_le_nombre_parties

⁄* Identificateur acceptable. *⁄
nb_parties

69 / 399
Choix des identificateurs

Les identificateurs doivent à la fois renseigner sur le rôle des entités auxquelles ils
appartiennent (variables, paramètres, fonctions, modules, etc.) et être concis.

⁄* Identificateur non explicite. *⁄


v

⁄* Identificateur trop long. *⁄


valeur_choisie_pour_le_nombre_parties

⁄* Identificateur acceptable. *⁄
nb_parties

On fixe la langue au français pour leur construction.

69 / 399
Choix des identificateurs
Identificateurs de variables :
Les seuls identificateurs d’une lettre autorisés sont i , j , k , etc., pour les indices de boucles.

70 / 399
Choix des identificateurs
Identificateurs de variables :
Les seuls identificateurs d’une lettre autorisés sont i , j , k , etc., pour les indices de boucles.

Les majuscules sont interdites plusieurs mots, ces derniers sont ⁄* Correct. *⁄
nb_parties
dans les identificateurs. Dans un séparés par des sous-tirets.
identificateur composé de ⁄* Incorrect. *⁄
nbParties

70 / 399
Choix des identificateurs
Identificateurs de variables :
Les seuls identificateurs d’une lettre autorisés sont i , j , k , etc., pour les indices de boucles.

Les majuscules sont interdites plusieurs mots, ces derniers sont ⁄* Correct. *⁄
nb_parties
dans les identificateurs. Dans un séparés par des sous-tirets.
identificateur composé de ⁄* Incorrect. *⁄
nbParties

Identificateurs de fonctions. Mêmes règles que pour les identificateurs de variables.

70 / 399
Choix des identificateurs
Identificateurs de variables :
Les seuls identificateurs d’une lettre autorisés sont i , j , k , etc., pour les indices de boucles.

Les majuscules sont interdites plusieurs mots, ces derniers sont ⁄* Correct. *⁄
nb_parties
dans les identificateurs. Dans un séparés par des sous-tirets.
identificateur composé de ⁄* Incorrect. *⁄
nbParties

Identificateurs de fonctions. Mêmes règles que pour les identificateurs de variables.


Identificateurs des macro-instructions.

Ils sont écrits exclusivement en majuscules et, s’ils ⁄* Correct. *⁄


#define TAILLE_MAX 1024
sont composés de plusieurs mots, ils sont séparés #define DEBUG
par des sous-tirets.

70 / 399
Choix des identificateurs
Identificateurs de variables :
Les seuls identificateurs d’une lettre autorisés sont i , j , k , etc., pour les indices de boucles.

Les majuscules sont interdites plusieurs mots, ces derniers sont ⁄* Correct. *⁄
nb_parties
dans les identificateurs. Dans un séparés par des sous-tirets.
identificateur composé de ⁄* Incorrect. *⁄
nbParties

Identificateurs de fonctions. Mêmes règles que pour les identificateurs de variables.


Identificateurs des macro-instructions.

Ils sont écrits exclusivement en majuscules et, s’ils ⁄* Correct. *⁄


#define TAILLE_MAX 1024
sont composés de plusieurs mots, ils sont séparés #define DEBUG
par des sous-tirets.

Identificateurs de types.

La première lettre commence par une majuscule et, majuscule.


s’ils sont composés de plusieurs mots, chaque typedef ... Liste;
première lettre de chaque mot commence par une typedef ... ArbreBinaire;
70 / 399
Documentation
Un programme est documenté par des commentaires. Ce sont des phrases, placées entre ⁄* et
*⁄ .

71 / 399
Documentation
Un programme est documenté par des commentaires. Ce sont des phrases, placées entre ⁄* et
*⁄ .
On documente chaque fichier de programme, au tout début, par
I les prénoms et noms des auteurs ;
I la date de création (au format jour-mois-année) ;
I la date de modification (au format précédent).
⁄* Auteur : A. M. Turing
* Creation : 23-06-1912
* Modification : 07-06-1954 *⁄

71 / 399
Documentation
Un programme est documenté par des commentaires. Ce sont des phrases, placées entre ⁄* et
*⁄ .
On documente chaque fichier de programme, au tout début, par
I les prénoms et noms des auteurs ;
I la date de création (au format jour-mois-année) ;
I la date de modification (au format précédent).
⁄* Auteur : A. M. Turing
* Creation : 23-06-1912
* Modification : 07-06-1954 *⁄

On commentera le moins possible les instructions.

71 / 399
Documentation
Un programme est documenté par des commentaires. Ce sont des phrases, placées entre ⁄* et
*⁄ .
On documente chaque fichier de programme, au tout début, par
I les prénoms et noms des auteurs ;
I la date de création (au format jour-mois-année) ;
I la date de modification (au format précédent).
⁄* Auteur : A. M. Turing
* Creation : 23-06-1912
* Modification : 07-06-1954 *⁄

On commentera le moins possible les instructions.


Il faut éviter les commentaires inutiles.
⁄* Incorrect. *⁄
⁄* Affiche la valeur de ‘a‘. *⁄
printf("%d\n", a);

71 / 399
Documentation des fonctions
On documente la plupart des fonctions par des commentaires situés avant leur déclaration (ou
leur définition).

72 / 399
Documentation des fonctions
On documente la plupart des fonctions par des commentaires situés avant leur déclaration (ou
leur définition).
Un commentaire de fonction explique
I le rôle de chaque paramètre ;
I ce que renvoie la fonction ;
I l’effet produit par la fonction.

72 / 399
Documentation des fonctions
On documente la plupart des fonctions par des commentaires situés avant leur déclaration (ou
leur définition).
Un commentaire de fonction explique
I le rôle de chaque paramètre ;
I ce que renvoie la fonction ;
I l’effet produit par la fonction.

⁄* Correct. *⁄
⁄* Renvoie le plus grand entier
* parmi ‘a‘ et ‘b‘. *⁄
int max(int a, int b) {
if (a >= b)
return a;
return b;
}

72 / 399
Documentation des fonctions
On documente la plupart des fonctions par des commentaires situés avant leur déclaration (ou
leur définition).
Un commentaire de fonction explique
I le rôle de chaque paramètre ;
I ce que renvoie la fonction ;
I l’effet produit par la fonction.

⁄* Correct. *⁄ ⁄* Incorrect. *⁄
⁄* Renvoie le plus grand entier ⁄* Calcule le maximum de deux
* parmi ‘a‘ et ‘b‘. *⁄ * entiers. *⁄
int max(int a, int b) { int max(int a, int b) {
if (a >= b) if (a >= b)
return a; return a;
return b; return b;
} }

72 / 399
Plan

Habitudes
Mise en page
Gestion d’erreurs
Assertions d’entrée

73 / 399
Détecter les erreurs

Certains appels à des fonctions peuvent mal se passer.

74 / 399
Détecter les erreurs

Certains appels à des fonctions peuvent mal se passer.


Ceci peut provoquer une erreur provenant principalement
I du système (p.ex. lorsqu’il ne parvient pas à satisfaire un malloc pour cause de la
configuration de la mémoire) ;

74 / 399
Détecter les erreurs

Certains appels à des fonctions peuvent mal se passer.


Ceci peut provoquer une erreur provenant principalement
I du système (p.ex. lorsqu’il ne parvient pas à satisfaire un malloc pour cause de la
configuration de la mémoire) ;
I de l’utilisateur (p.ex. lorsqu’il communique au programme des données inattendues) ;

74 / 399
Détecter les erreurs

Certains appels à des fonctions peuvent mal se passer.


Ceci peut provoquer une erreur provenant principalement
I du système (p.ex. lorsqu’il ne parvient pas à satisfaire un malloc pour cause de la
configuration de la mémoire) ;
I de l’utilisateur (p.ex. lorsqu’il communique au programme des données inattendues) ;
I du programmeur (p.ex. lorsqu’il appelle une fonction avec des arguments inadéquats qui
rendent le calcul impraticable).

74 / 399
Détecter les erreurs

Certains appels à des fonctions peuvent mal se passer.


Ceci peut provoquer une erreur provenant principalement
I du système (p.ex. lorsqu’il ne parvient pas à satisfaire un malloc pour cause de la
configuration de la mémoire) ;
I de l’utilisateur (p.ex. lorsqu’il communique au programme des données inattendues) ;
I du programmeur (p.ex. lorsqu’il appelle une fonction avec des arguments inadéquats qui
rendent le calcul impraticable).
Il est nécessaire de
1. pouvoir détecter les erreurs ;

74 / 399
Détecter les erreurs

Certains appels à des fonctions peuvent mal se passer.


Ceci peut provoquer une erreur provenant principalement
I du système (p.ex. lorsqu’il ne parvient pas à satisfaire un malloc pour cause de la
configuration de la mémoire) ;
I de l’utilisateur (p.ex. lorsqu’il communique au programme des données inattendues) ;
I du programmeur (p.ex. lorsqu’il appelle une fonction avec des arguments inadéquats qui
rendent le calcul impraticable).
Il est nécessaire de
1. pouvoir détecter les erreurs ;
2. pouvoir remédier aux erreurs quand elles surviennent.

74 / 399
Détecter les erreurs

Certains appels à des fonctions peuvent mal se passer.


Ceci peut provoquer une erreur provenant principalement
I du système (p.ex. lorsqu’il ne parvient pas à satisfaire un malloc pour cause de la
configuration de la mémoire) ;
I de l’utilisateur (p.ex. lorsqu’il communique au programme des données inattendues) ;
I du programmeur (p.ex. lorsqu’il appelle une fonction avec des arguments inadéquats qui
rendent le calcul impraticable).
Il est nécessaire de
1. pouvoir détecter les erreurs ;
2. pouvoir remédier aux erreurs quand elles surviennent.
Pour cela, nous allons augmenter l’écriture de fonctions de mécanismes de gestion d’erreur.

74 / 399
Mécanisme de gestion d’erreurs
On écrira la plupart des fonctions selon le schéma suivant :
I le type de retour est int et la valeur de retour, le code d’erreur, renseigne si l’exécution de la
fonction s’est bien déroulée ;

75 / 399
Mécanisme de gestion d’erreurs
On écrira la plupart des fonctions selon le schéma suivant :
I le type de retour est int et la valeur de retour, le code d’erreur, renseigne si l’exécution de la
fonction s’est bien déroulée ;
I la (ou les) valeur(s) « renvoyée(s) » par la fonction se fait par un (des) passage(s) par adresse.

75 / 399
Mécanisme de gestion d’erreurs
On écrira la plupart des fonctions selon le schéma suivant :
I le type de retour est int et la valeur de retour, le code d’erreur, renseigne si l’exécution de la
fonction s’est bien déroulée ;
I la (ou les) valeur(s) « renvoyée(s) » par la fonction se fait par un (des) passage(s) par adresse.

Schématiquement, une telle fonction admet ainsi le prototype


int FCT(T1 E1, ..., TN EN, U1 S1, ..., UK SK);

dit prototype standard

75 / 399
Mécanisme de gestion d’erreurs
On écrira la plupart des fonctions selon le schéma suivant :
I le type de retour est int et la valeur de retour, le code d’erreur, renseigne si l’exécution de la
fonction s’est bien déroulée ;
I la (ou les) valeur(s) « renvoyée(s) » par la fonction se fait par un (des) passage(s) par adresse.

Schématiquement, une telle fonction admet ainsi le prototype


int FCT(T1 E1, ..., TN EN, U1 S1, ..., UK SK);

dit prototype standard où


I les Ei sont les paramètres d’entrée ;

75 / 399
Mécanisme de gestion d’erreurs
On écrira la plupart des fonctions selon le schéma suivant :
I le type de retour est int et la valeur de retour, le code d’erreur, renseigne si l’exécution de la
fonction s’est bien déroulée ;
I la (ou les) valeur(s) « renvoyée(s) » par la fonction se fait par un (des) passage(s) par adresse.

Schématiquement, une telle fonction admet ainsi le prototype


int FCT(T1 E1, ..., TN EN, U1 S1, ..., UK SK);

dit prototype standard où


I les Ei sont les paramètres d’entrée ;

I les Ti sont des types (potentiellement des adresses) ;

75 / 399
Mécanisme de gestion d’erreurs
On écrira la plupart des fonctions selon le schéma suivant :
I le type de retour est int et la valeur de retour, le code d’erreur, renseigne si l’exécution de la
fonction s’est bien déroulée ;
I la (ou les) valeur(s) « renvoyée(s) » par la fonction se fait par un (des) passage(s) par adresse.

Schématiquement, une telle fonction admet ainsi le prototype


int FCT(T1 E1, ..., TN EN, U1 S1, ..., UK SK);

dit prototype standard où


I les Ei sont les paramètres d’entrée ;

I les Ti sont des types (potentiellement des adresses) ;


I les Sj sont les paramètres de sortie ;

75 / 399
Mécanisme de gestion d’erreurs
On écrira la plupart des fonctions selon le schéma suivant :
I le type de retour est int et la valeur de retour, le code d’erreur, renseigne si l’exécution de la
fonction s’est bien déroulée ;
I la (ou les) valeur(s) « renvoyée(s) » par la fonction se fait par un (des) passage(s) par adresse.

Schématiquement, une telle fonction admet ainsi le prototype


int FCT(T1 E1, ..., TN EN, U1 S1, ..., UK SK);

dit prototype standard où


I les Ei sont les paramètres d’entrée ;

I les Ti sont des types (potentiellement des adresses) ;


I les Sj sont les paramètres de sortie ;
I les Uj sont des types (potentiellement des adresses).

75 / 399
Mécanisme de gestion d’erreurs

Le code d’erreur d’une fonction ayant un prototype standard suit la spécification suivante :

76 / 399
Mécanisme de gestion d’erreurs

Le code d’erreur d’une fonction ayant un prototype standard suit la spécification suivante :
I une valeur négative ou nulle renseigne qu’une erreur s’est produite. En général, la fonction renvoie
0 dans ce cas. On peut être plus précis et exprimer une erreur particulière en renvoyant des autres
valeurs négatives.

76 / 399
Mécanisme de gestion d’erreurs

Le code d’erreur d’une fonction ayant un prototype standard suit la spécification suivante :
I une valeur négative ou nulle renseigne qu’une erreur s’est produite. En général, la fonction renvoie
0 dans ce cas. On peut être plus précis et exprimer une erreur particulière en renvoyant des autres
valeurs négatives.
I une valeur strictement positive signifie que l’exécution de la fonction s’est bien déroulée. En
général, la fonction renvoie 1 dans ce cas. On peut être plus précis et exprimer une information
particulière en renvoyant des autres valeurs strictement positives.

76 / 399
Mécanisme de gestion d’erreurs

Le code d’erreur d’une fonction ayant un prototype standard suit la spécification suivante :
I une valeur négative ou nulle renseigne qu’une erreur s’est produite. En général, la fonction renvoie
0 dans ce cas. On peut être plus précis et exprimer une erreur particulière en renvoyant des autres
valeurs négatives.
I une valeur strictement positive signifie que l’exécution de la fonction s’est bien déroulée. En
général, la fonction renvoie 1 dans ce cas. On peut être plus précis et exprimer une information
particulière en renvoyant des autres valeurs strictement positives.

Attention : il y a des fonctions de la librairie standard qui ne suivent pas cette spécification.

76 / 399
Mécanisme de gestion d’erreurs

Le code d’erreur d’une fonction ayant un prototype standard suit la spécification suivante :
I une valeur négative ou nulle renseigne qu’une erreur s’est produite. En général, la fonction renvoie
0 dans ce cas. On peut être plus précis et exprimer une erreur particulière en renvoyant des autres
valeurs négatives.
I une valeur strictement positive signifie que l’exécution de la fonction s’est bien déroulée. En
général, la fonction renvoie 1 dans ce cas. On peut être plus précis et exprimer une information
particulière en renvoyant des autres valeurs strictement positives.

Attention : il y a des fonctions de la librairie standard qui ne suivent pas cette spécification.
De notre côté, nous allons la suivre à la lettre dans les fonctions que nous écrirons.

76 / 399
Mécanisme de gestion d’erreurs

Le code d’erreur d’une fonction ayant un prototype standard suit la spécification suivante :
I une valeur négative ou nulle renseigne qu’une erreur s’est produite. En général, la fonction renvoie
0 dans ce cas. On peut être plus précis et exprimer une erreur particulière en renvoyant des autres
valeurs négatives.
I une valeur strictement positive signifie que l’exécution de la fonction s’est bien déroulée. En
général, la fonction renvoie 1 dans ce cas. On peut être plus précis et exprimer une information
particulière en renvoyant des autres valeurs strictement positives.

Attention : il y a des fonctions de la librairie standard qui ne suivent pas cette spécification.
De notre côté, nous allons la suivre à la lettre dans les fonctions que nous écrirons.

Remarque : le fonctionnement des codes d’erreurs de chaque fonction écrite doit être spécifié
dans sa documentation.

76 / 399
Exemple 1 de gestion d’erreur

Considérons la fonction
int division(float x, float y, float *res) {
if (y == 0)
return 0;
*res = x / y;
return 1;
}

77 / 399
Exemple 1 de gestion d’erreur

Considérons la fonction
int division(float x, float y, float *res) {
if (y == 0)
return 0;
*res = x / y;
return 1;
}

Les entrées sont les flottants x et y . La sortie est res ; c’est une adresse qui pointera sur le
résultat de la division de x par y .

77 / 399
Exemple 1 de gestion d’erreur

Considérons la fonction
int division(float x, float y, float *res) {
if (y == 0)
return 0;
*res = x / y;
return 1;
}

Les entrées sont les flottants x et y . La sortie est res ; c’est une adresse qui pointera sur le
résultat de la division de x par y .

La valeur de retour est un code d’erreur : il vaut 0 lorsque la division ne peut pas être calculée
( y nul) et vaut 1 sinon.

77 / 399
Exemple 1 de gestion d’erreur

Considérons la fonction
int division(float x, float y, float *res) {
if (y == 0)
return 0;
*res = x / y;
return 1;
}

Les entrées sont les flottants x et y . La sortie est res ; c’est une adresse qui pointera sur le
résultat de la division de x par y .

La valeur de retour est un code d’erreur : il vaut 0 lorsque la division ne peut pas être calculée
( y nul) et vaut 1 sinon.

On remarque que l’on ne modifie pas *res lorsque le calcul ne peut pas être réalisé.

77 / 399
Exemple 2 de gestion d’erreur
Considérons la fonction
int nb_min_maj(char *chaine, int *res_min, int *res_maj) {
int i;
*res_min = 0;
*res_maj = 0;
i = 0;
while (chaine[i] != ’\0’) {
if ((’a’ <= chaine[i]) && (chaine[i] <= ’z’))
*res_min += 1;
else if ((’A’ <= chaine[i]) && (chaine[i] <= ’Z’))
*res_maj += 1;
else
return 0;
i += 1;
}
return i;
}

78 / 399
Exemple 2 de gestion d’erreur
Considérons la fonction
int nb_min_maj(char *chaine, int *res_min, int *res_maj) {
int i;
*res_min = 0;
*res_maj = 0;
i = 0;
while (chaine[i] != ’\0’) {
if ((’a’ <= chaine[i]) && (chaine[i] <= ’z’))
*res_min += 1;
else if ((’A’ <= chaine[i]) && (chaine[i] <= ’Z’))
*res_maj += 1;
else
return 0;
i += 1;
}
return i;
}

L’entrée est la chaîne de caractères chaine . Les sorties sont res_min et res_maj ; ces adresses pointeront
sur le nombre de minuscules et de majuscules dans chaine .

78 / 399
Exemple 2 de gestion d’erreur
Considérons la fonction
int nb_min_maj(char *chaine, int *res_min, int *res_maj) {
int i;
*res_min = 0;
*res_maj = 0;
i = 0;
while (chaine[i] != ’\0’) {
if ((’a’ <= chaine[i]) && (chaine[i] <= ’z’))
*res_min += 1;
else if ((’A’ <= chaine[i]) && (chaine[i] <= ’Z’))
*res_maj += 1;
else
return 0;
i += 1;
}
return i;
}

L’entrée est la chaîne de caractères chaine . Les sorties sont res_min et res_maj ; ces adresses pointeront
sur le nombre de minuscules et de majuscules dans chaine .

La valeur de retour est un code d’erreur : il vaut 0 si un caractère non alphabétique apparaît dans chaine

et vaut la longueur de chaine sinon.


78 / 399
Fonctions classiques à gestion d’erreurs
La librairie standard du C contient beaucoup de fonctions à gestion d’erreurs. Par exemple :

79 / 399
Fonctions classiques à gestion d’erreurs
La librairie standard du C contient beaucoup de fonctions à gestion d’erreurs. Par exemple :
I printf renvoie le nombre de caractères écrits (sans compter ’\0’ );

79 / 399
Fonctions classiques à gestion d’erreurs
La librairie standard du C contient beaucoup de fonctions à gestion d’erreurs. Par exemple :
I printf renvoie le nombre de caractères écrits (sans compter ’\0’ );

I scanf renvoie le nombre d’affectations réalisées lors de la lecture. La valeur EOF est renvoyée si une
erreur de lecture a lieu ;

79 / 399
Fonctions classiques à gestion d’erreurs
La librairie standard du C contient beaucoup de fonctions à gestion d’erreurs. Par exemple :
I printf renvoie le nombre de caractères écrits (sans compter ’\0’ );

I scanf renvoie le nombre d’affectations réalisées lors de la lecture. La valeur EOF est renvoyée si une
erreur de lecture a lieu ;
I malloc renvoie un pointeur vers la zone de la mémoire allouée. Lorsque l’allocation échoue, la valeur
NULL est renvoyée ;

79 / 399
Fonctions classiques à gestion d’erreurs
La librairie standard du C contient beaucoup de fonctions à gestion d’erreurs. Par exemple :
I printf renvoie le nombre de caractères écrits (sans compter ’\0’ );

I scanf renvoie le nombre d’affectations réalisées lors de la lecture. La valeur EOF est renvoyée si une
erreur de lecture a lieu ;
I malloc renvoie un pointeur vers la zone de la mémoire allouée. Lorsque l’allocation échoue, la valeur
NULL est renvoyée ;

I fopen renvoie un pointeur sur le fichier ouvert. Lorsque l’ouverture échoue, la valeur NULL est
renvoyée ;

79 / 399
Fonctions classiques à gestion d’erreurs
La librairie standard du C contient beaucoup de fonctions à gestion d’erreurs. Par exemple :
I printf renvoie le nombre de caractères écrits (sans compter ’\0’ );

I scanf renvoie le nombre d’affectations réalisées lors de la lecture. La valeur EOF est renvoyée si une
erreur de lecture a lieu ;
I malloc renvoie un pointeur vers la zone de la mémoire allouée. Lorsque l’allocation échoue, la valeur
NULL est renvoyée ;

I fopen renvoie un pointeur sur le fichier ouvert. Lorsque l’ouverture échoue, la valeur NULL est
renvoyée ;
I fclose renvoie 0 si la fermeture du fichier s’est bien déroulée (attention à ce cas particulier). Lorsque
la fermeture échoue, la valeur EOF est renvoyée.

79 / 399
Fonctions classiques à gestion d’erreurs
La librairie standard du C contient beaucoup de fonctions à gestion d’erreurs. Par exemple :
I printf renvoie le nombre de caractères écrits (sans compter ’\0’ );

I scanf renvoie le nombre d’affectations réalisées lors de la lecture. La valeur EOF est renvoyée si une
erreur de lecture a lieu ;
I malloc renvoie un pointeur vers la zone de la mémoire allouée. Lorsque l’allocation échoue, la valeur
NULL est renvoyée ;

I fopen renvoie un pointeur sur le fichier ouvert. Lorsque l’ouverture échoue, la valeur NULL est
renvoyée ;
I fclose renvoie 0 si la fermeture du fichier s’est bien déroulée (attention à ce cas particulier). Lorsque
la fermeture échoue, la valeur EOF est renvoyée.

Remarque : certaines de ces fonctions ont une gestion d’erreurs encore plus sophistiquée et
modifient des variables globales comme errno (de l’en-tête errno.h ) pour renseigner précisément
sur l’erreur survenue.
79 / 399
Emploi des fonctions à gestion d’erreurs
Les fonctions à gestion d’erreur renvoient des entiers. De ce fait, leur valeur de retour est une
expression booléenne.

80 / 399
Emploi des fonctions à gestion d’erreurs
Les fonctions à gestion d’erreur renvoient des entiers. De ce fait, leur valeur de retour est une
expression booléenne.
Nous pouvons donc combiner l’appel d’une fonction à gestion d’erreurs avec un test pour traiter
l’erreur éventuelle.

80 / 399
Emploi des fonctions à gestion d’erreurs
Les fonctions à gestion d’erreur renvoient des entiers. De ce fait, leur valeur de retour est une
expression booléenne.
Nous pouvons donc combiner l’appel d’une fonction à gestion d’erreurs avec un test pour traiter
l’erreur éventuelle.
Voici quelques exemples avec les deux fonctions précédentes :
if (division(8, a, &b) == 0) {
⁄* Traitement de l’erreur lors
* de la division par zero. *⁄
}
⁄* Instructions suivantes. *⁄

80 / 399
Emploi des fonctions à gestion d’erreurs
Les fonctions à gestion d’erreur renvoient des entiers. De ce fait, leur valeur de retour est une
expression booléenne.
Nous pouvons donc combiner l’appel d’une fonction à gestion d’erreurs avec un test pour traiter
l’erreur éventuelle.
Voici quelques exemples avec les deux fonctions précédentes :
if (division(8, a, &b) == 0) {
⁄* Traitement de l’erreur lors
* de la division par zero. *⁄
}
⁄* Instructions suivantes. *⁄

if (nb_min_maj("UnDeuxTrois", &a, &b) == 0) {


⁄* Traitement de l’erreur lorsque
* la chaine de caracteres contient
* des caracteres non alphabetiques. *⁄
}
⁄* Instructions suivantes. *⁄

80 / 399
Interruption de l’exécution
Dans certains cas où une erreur survient, celle-ci peut être irrécupérable. Il faut donc interrompre
l’exécution du programme. On utilise pour cela la fonction
void exit(int status);

de stdlib.h , appelée avec l’argument EXIT_FAILURE (constante qui vaut 1 ).

81 / 399
Interruption de l’exécution
Dans certains cas où une erreur survient, celle-ci peut être irrécupérable. Il faut donc interrompre
l’exécution du programme. On utilise pour cela la fonction
void exit(int status);

de stdlib.h , appelée avec l’argument EXIT_FAILURE (constante qui vaut 1 ).

P.ex.,
⁄* Allocation dynamique. *⁄
tab = (int *) malloc(sizeof(int) * 1024);

⁄* Verification de son succes. *⁄


if (NULL == tab)
⁄* Sur son echec, on interrompt
* l’execution immediatement. *⁄
exit(EXIT_FAILURE);
⁄* Instructions suivantes. *⁄

81 / 399
Interruption de l’exécution
Dans certains cas où une erreur survient, celle-ci peut être irrécupérable. Il faut donc interrompre
l’exécution du programme. On utilise pour cela la fonction
void exit(int status);

de stdlib.h , appelée avec l’argument EXIT_FAILURE (constante qui vaut 1 ).

P.ex.,
⁄* Allocation dynamique. *⁄
tab = (int *) malloc(sizeof(int) * 1024);

⁄* Verification de son succes. *⁄


if (NULL == tab)
⁄* Sur son echec, on interrompt
* l’execution immediatement. *⁄
exit(EXIT_FAILURE);
⁄* Instructions suivantes. *⁄

Important : on utilisera ce mécanisme d’arrêt principalement dans la fonction main . Ailleurs, il


faut préférer renvoyer un code d’erreur plutôt que d’interrompre ainsi l’exécution.
81 / 399
Plan

Habitudes
Mise en page
Gestion d’erreurs
Assertions d’entrée

82 / 399
Assertions d’entrée
Lors d’un appel à une fonction, certains arguments peuvent être dans un état incohérent.

83 / 399
Assertions d’entrée
Lors d’un appel à une fonction, certains arguments peuvent être dans un état incohérent.
Au lieu de gérer ces cas de figure par l’usage de codes d’erreur, il est possible de tester l’état des
arguments.

83 / 399
Assertions d’entrée
Lors d’un appel à une fonction, certains arguments peuvent être dans un état incohérent.
Au lieu de gérer ces cas de figure par l’usage de codes d’erreur, il est possible de tester l’état des
arguments.
Une pré-assertion (ou assertion d’entrée) est un test réalisé dans une fonction pour vérifier si elle
est appelée avec des arguments adéquats.

83 / 399
Assertions d’entrée
Lors d’un appel à une fonction, certains arguments peuvent être dans un état incohérent.
Au lieu de gérer ces cas de figure par l’usage de codes d’erreur, il est possible de tester l’état des
arguments.
Une pré-assertion (ou assertion d’entrée) est un test réalisé dans une fonction pour vérifier si elle
est appelée avec des arguments adéquats.

On utilise la fonction
void assert(int a);

du fichier d’en-tête assert.h .

83 / 399
Assertions d’entrée
Lors d’un appel à une fonction, certains arguments peuvent être dans un état incohérent.
Au lieu de gérer ces cas de figure par l’usage de codes d’erreur, il est possible de tester l’état des
arguments.
Une pré-assertion (ou assertion d’entrée) est un test réalisé dans une fonction pour vérifier si elle
est appelée avec des arguments adéquats.

On utilise la fonction
void assert(int a);

du fichier d’en-tête assert.h . Elle fonctionne de la manière suivante :


I lorsque l’assertion a est fausse, l’exécution du programme est interrompue et diverses
informations utiles sont affichées ;

83 / 399
Assertions d’entrée
Lors d’un appel à une fonction, certains arguments peuvent être dans un état incohérent.
Au lieu de gérer ces cas de figure par l’usage de codes d’erreur, il est possible de tester l’état des
arguments.
Une pré-assertion (ou assertion d’entrée) est un test réalisé dans une fonction pour vérifier si elle
est appelée avec des arguments adéquats.

On utilise la fonction
void assert(int a);

du fichier d’en-tête assert.h . Elle fonctionne de la manière suivante :


I lorsque l’assertion a est fausse, l’exécution du programme est interrompue et diverses
informations utiles sont affichées ;
I lorsque a est vraie, l’exécution continue.

83 / 399
Exemple 1 de fonction avec pré-assertions

Considérons la fonction
void afficher_tab(int tab[], int nb) {
int i;
assert(tab != NULL);
assert(nb >= 0);
for (i = 0 ; i < nb ; ++i)
printf("%d\n", tab[i]);
}

84 / 399
Exemple 1 de fonction avec pré-assertions

Considérons la fonction
void afficher_tab(int tab[], int nb) {
int i;
assert(tab != NULL);
assert(nb >= 0);
for (i = 0 ; i < nb ; ++i)
printf("%d\n", tab[i]);
}

Elle possède deux pré-assertions :


1. la première teste si le tableau tab est bien un pointeur valide (différent de NULL );

84 / 399
Exemple 1 de fonction avec pré-assertions

Considérons la fonction
void afficher_tab(int tab[], int nb) {
int i;
assert(tab != NULL);
assert(nb >= 0);
for (i = 0 ; i < nb ; ++i)
printf("%d\n", tab[i]);
}

Elle possède deux pré-assertions :


1. la première teste si le tableau tab est bien un pointeur valide (différent de NULL );

2. la seconde teste si la taille nb donnée est bien positive.

84 / 399
Conception de pré-assertions

Il est important de munir ses fonctions de pré-assertions les plus précises et complètes possibles.
Quelques règles :

85 / 399
Conception de pré-assertions

Il est important de munir ses fonctions de pré-assertions les plus précises et complètes possibles.
Quelques règles :
I la condition testée ne doit dépendre que des arguments d’une fonction (elle ne dépend pas
de données apprises à l’exécution) ;

85 / 399
Conception de pré-assertions

Il est important de munir ses fonctions de pré-assertions les plus précises et complètes possibles.
Quelques règles :
I la condition testée ne doit dépendre que des arguments d’une fonction (elle ne dépend pas
de données apprises à l’exécution) ;
I la condition testée doit être la plus atomique possible.

⁄* Correct. *⁄ ⁄* Incorrect. *⁄
assert(nb >= 0); assert((0<=nb) && (nb<=1024));
assert(nb <= 1024);

85 / 399
Conception de pré-assertions

Il est important de munir ses fonctions de pré-assertions les plus précises et complètes possibles.
Quelques règles :
I la condition testée ne doit dépendre que des arguments d’une fonction (elle ne dépend pas
de données apprises à l’exécution) ;
I la condition testée doit être la plus atomique possible.

⁄* Correct. *⁄ ⁄* Incorrect. *⁄
assert(nb >= 0); assert((0<=nb) && (nb<=1024));
assert(nb <= 1024);

I Pour les concevoir, il faut imaginer les pires cas possibles à capturer qui peuvent survenir
(p.ex., pointeurs nuls, quantités négatives, chaînes de caractères vides, etc.).

85 / 399
Conception de pré-assertions

Il est important de munir ses fonctions de pré-assertions les plus précises et complètes possibles.
Quelques règles :
I la condition testée ne doit dépendre que des arguments d’une fonction (elle ne dépend pas
de données apprises à l’exécution) ;
I la condition testée doit être la plus atomique possible.

⁄* Correct. *⁄ ⁄* Incorrect. *⁄
assert(nb >= 0); assert((0<=nb) && (nb<=1024));
assert(nb <= 1024);

I Pour les concevoir, il faut imaginer les pires cas possibles à capturer qui peuvent survenir
(p.ex., pointeurs nuls, quantités négatives, chaînes de caractères vides, etc.).
I Elles sont situées juste après les déclarations de variables dans le corps d’une fonction.

85 / 399
Redondance nécessaire des pré-assertions
Considérons les fonctions

int div(int a, int b) { int somme_div(int a, int b) {


assert(b != 0); return div(a, b)
return a / b; + div(b, a + 1);
} }

86 / 399
Redondance nécessaire des pré-assertions
Considérons les fonctions

int div(int a, int b) { int somme_div(int a, int b) {


assert(b != 0); return div(a, b)
return a / b; + div(b, a + 1);
} }

Raisonnement : il n’y a pas de pré-assertion dans somme_div mais cela n’est pas grave car les cas
problématiques sont capturés par div qui contient une pré-assertion.

86 / 399
Redondance nécessaire des pré-assertions
Considérons les fonctions

int div(int a, int b) { int somme_div(int a, int b) {


assert(b != 0); return div(a, b)
return a / b; + div(b, a + 1);
} }

Raisonnement : il n’y a pas de pré-assertion dans somme_div mais cela n’est pas grave car les cas
problématiques sont capturés par div qui contient une pré-assertion.
Ceci est une fausse bonne idée : chaque fonction doit faire ses propres pré-assertions. Toute
erreur doit être capturée le plus en amont possible.

86 / 399
Redondance nécessaire des pré-assertions
Considérons les fonctions

int div(int a, int b) { int somme_div(int a, int b) {


assert(b != 0); return div(a, b)
return a / b; + div(b, a + 1);
} }

Raisonnement : il n’y a pas de pré-assertion dans somme_div mais cela n’est pas grave car les cas
problématiques sont capturés par div qui contient une pré-assertion.
Ceci est une fausse bonne idée : chaque fonction doit faire ses propres pré-assertions. Toute
erreur doit être capturée le plus en amont possible.

La bonne version de somme_div consiste à int somme_div(int a, int b) {


assert(b != 0);
capturer les mauvaises valeurs possibles de ses assert(a + 1 != 0);
arguments de la manière suivante : return div(a, b)
+ div(b, a + 1);
}

86 / 399
Exemple 2 de fonction avec pré-assertion

La fonction nb_min_maj , à code d’erreur, doit être pourvue de pré-assertions :

int nb_min_maj(char *chaine, int *res_min, int *res_maj) {


int i;

assert(chaine != NULL);
assert(res_min != NULL);
assert(res_maj != NULL);

⁄* Suite inchangee. *⁄
}

87 / 399
Exemple 2 de fonction avec pré-assertion

La fonction nb_min_maj , à code d’erreur, doit être pourvue de pré-assertions :

int nb_min_maj(char *chaine, int *res_min, int *res_maj) {


int i;

assert(chaine != NULL);
assert(res_min != NULL);
assert(res_maj != NULL);

⁄* Suite inchangee. *⁄
}

On observe que le mécanisme de gestion d’erreurs par valeur de retour teste des
comportements incohérents complexes qui se déroulent à l’exécution, tandis que le mécanisme
de pré-assertion permet de capturer des erreurs évidentes lisibles directement sur les arguments.

87 / 399
Les pré-assertions pour corriger un programme

Considérons le programme

#include <stdio.h>
#include <assert.h>

int div(int a, int b) {


assert(b != 0);

return a / b;
}

int main() {
int a;

a = div(17, 0);
printf("%d\n", a);

return 0;
}

88 / 399
Les pré-assertions pour corriger un programme

Considérons le programme

#include <stdio.h> La compilation gcc -ansi -pedantic -Wall Prgm.c


#include <assert.h>
donne l’exécutable a.out .
int div(int a, int b) {
assert(b != 0);

return a / b;
}

int main() {
int a;

a = div(17, 0);
printf("%d\n", a);

return 0;
}

88 / 399
Les pré-assertions pour corriger un programme

Considérons le programme

#include <stdio.h> La compilation gcc -ansi -pedantic -Wall Prgm.c


#include <assert.h>
donne l’exécutable a.out .
int div(int a, int b) {
assert(b != 0);
Son exécution ./a.out est interrompue en l.5. Elle
return a / b;
} produit la réponse
int main() { a.out: Prgm.c:5: div: Assertion ‘b != 0’ failed.
int a; Aborted (core dumped)

a = div(17, 0);
printf("%d\n", a); On récolte la précieuse information sur le numéro
return 0;
de ligne de la pré-assertion non satisfaite qui
} produit l’arrêt précipité de l’exécution.

88 / 399
Plan

Modules
Notion de modularité
Découpage d’un projet
Fichiers sources / d’en-tête
Création de modules
Graphes d’inclusions
Erreurs courantes et bonnes habitudes

89 / 399
Plan

Modules
Notion de modularité
Découpage d’un projet
Fichiers sources / d’en-tête
Création de modules
Graphes d’inclusions
Erreurs courantes et bonnes habitudes

90 / 399
Modules

Modulariser un projet signifie le découper de manière cohérente en plusieurs parties plus petites.

91 / 399
Modules

Modulariser un projet signifie le découper de manière cohérente en plusieurs parties plus petites.
Un module est un ensemble de données et d’instructions qui permettent de gérer une partie bien
ciblée d’un projet.

91 / 399
Modules

Modulariser un projet signifie le découper de manière cohérente en plusieurs parties plus petites.
Un module est un ensemble de données et d’instructions qui permettent de gérer une partie bien
ciblée d’un projet.

Il existe deux manières de concevoir un projet :


1. programmer dans un unique fichier contenant tout le code nécessaire ;

91 / 399
Modules

Modulariser un projet signifie le découper de manière cohérente en plusieurs parties plus petites.
Un module est un ensemble de données et d’instructions qui permettent de gérer une partie bien
ciblée d’un projet.

Il existe deux manières de concevoir un projet :


1. programmer dans un unique fichier contenant tout le code nécessaire ;
2. programmer dans divers fichiers qui fractionnent le projet en plusieurs sous-parties.

91 / 399
Modules

Modulariser un projet signifie le découper de manière cohérente en plusieurs parties plus petites.
Un module est un ensemble de données et d’instructions qui permettent de gérer une partie bien
ciblée d’un projet.

Il existe deux manières de concevoir un projet :


1. programmer dans un unique fichier contenant tout le code nécessaire ;
2. programmer dans divers fichiers qui fractionnent le projet en plusieurs sous-parties.
À partir de maintenant, on adoptera la 2e manière.

91 / 399
Modules

Modulariser un projet signifie le découper de manière cohérente en plusieurs parties plus petites.
Un module est un ensemble de données et d’instructions qui permettent de gérer une partie bien
ciblée d’un projet.

Il existe deux manières de concevoir un projet :


1. programmer dans un unique fichier contenant tout le code nécessaire ;
2. programmer dans divers fichiers qui fractionnent le projet en plusieurs sous-parties.
À partir de maintenant, on adoptera la 2e manière.

Il reste à savoir comment découper un projet de manière cohérente et comment utiliser les
outils offerts par le langage pour gérer ce découpage.

91 / 399
Avantages offerts par la modularité
La modularité, illustration du principe stratégique universel
« diviser pour régner »,
offre les avantages suivants.

92 / 399
Avantages offerts par la modularité
La modularité, illustration du principe stratégique universel
« diviser pour régner »,
offre les avantages suivants.

1. La lisibilité du code est accrue, ainsi que la facilité de son entretien.

92 / 399
Avantages offerts par la modularité
La modularité, illustration du principe stratégique universel
« diviser pour régner »,
offre les avantages suivants.

1. La lisibilité du code est accrue, ainsi que la facilité de son entretien.

2. Permet de regrouper les types et les fonctions selon leurs objectifs.

92 / 399
Avantages offerts par la modularité
La modularité, illustration du principe stratégique universel
« diviser pour régner »,
offre les avantages suivants.

1. La lisibilité du code est accrue, ainsi que la facilité de son entretien.

2. Permet de regrouper les types et les fonctions selon leurs objectifs.

3. Il devient possible de réutiliser dans un nouveau projet un module créé dans un projet
antérieur.

92 / 399
Avantages offerts par la modularité
La modularité, illustration du principe stratégique universel
« diviser pour régner »,
offre les avantages suivants.

1. La lisibilité du code est accrue, ainsi que la facilité de son entretien.

2. Permet de regrouper les types et les fonctions selon leurs objectifs.

3. Il devient possible de réutiliser dans un nouveau projet un module créé dans un projet
antérieur.

4. Permet de cacher des fonctions (notion de fonctions privées).

92 / 399
Avantages offerts par la modularité
La modularité, illustration du principe stratégique universel
« diviser pour régner »,
offre les avantages suivants.

1. La lisibilité du code est accrue, ainsi que la facilité de son entretien.

2. Permet de regrouper les types et les fonctions selon leurs objectifs.

3. Il devient possible de réutiliser dans un nouveau projet un module créé dans un projet
antérieur.

4. Permet de cacher des fonctions (notion de fonctions privées).

5. Facilite le travail par équipe.

92 / 399
Avantages offerts par la modularité
La modularité, illustration du principe stratégique universel
« diviser pour régner »,
offre les avantages suivants.

1. La lisibilité du code est accrue, ainsi que la facilité de son entretien.

2. Permet de regrouper les types et les fonctions selon leurs objectifs.

3. Il devient possible de réutiliser dans un nouveau projet un module créé dans un projet
antérieur.

4. Permet de cacher des fonctions (notion de fonctions privées).

5. Facilite le travail par équipe.

6. Permet de rendre la compilation localisée (compilation module par module).

92 / 399
Plan

Modules
Notion de modularité
Découpage d’un projet
Fichiers sources / d’en-tête
Création de modules
Graphes d’inclusions
Erreurs courantes et bonnes habitudes

93 / 399
Spécification d’un projet

Considérons le projet spécifié de la manière suivante :

94 / 399
Spécification d’un projet

Considérons le projet spécifié de la manière suivante :


I le but est de fournir un programme qui permet de décider si des formules logiques sans
quantificateur sont valides ou contradictoires.

94 / 399
Spécification d’un projet

Considérons le projet spécifié de la manière suivante :


I le but est de fournir un programme qui permet de décider si des formules logiques sans
quantificateur sont valides ou contradictoires.
I La syntaxe d’une formule est la suivante : on dispose du jeu de formules atomiques a, b, . . .,
z et on écrit les formules de manière infixe et totalement parenthésée. Par exemple, la
formule (A → (B ∨ ¬C)) ∧ A s’écrit ((a IMP (b OU (NON c))) ET a) .

94 / 399
Spécification d’un projet

Considérons le projet spécifié de la manière suivante :


I le but est de fournir un programme qui permet de décider si des formules logiques sans
quantificateur sont valides ou contradictoires.
I La syntaxe d’une formule est la suivante : on dispose du jeu de formules atomiques a, b, . . .,
z et on écrit les formules de manière infixe et totalement parenthésée. Par exemple, la
formule (A → (B ∨ ¬C)) ∧ A s’écrit ((a IMP (b OU (NON c))) ET a) .

I L’interaction utilisateur / programme se fait de la manière suivante :


1. l’utilisateur fournit un fichier en entrée contenant une formule par ligne ;
2. le programme produit un fichier en sortie contenant, ligne par ligne, la réponse erreur

si la formule correspondante est syntaxiquement erronée ou bien valide ,


contradictoire ou rien selon la nature de la formule.

94 / 399
Découpage du projet
Il y a deux parties bien distinctes dans ce projet :
1. la représentation des formules et le test de validité / contradiction ;
2. la gestion syntaxique des formules (lecture / écriture d’une formule dans un fichier).

95 / 399
Découpage du projet
Il y a deux parties bien distinctes dans ce projet :
1. la représentation des formules et le test de validité / contradiction ;
2. la gestion syntaxique des formules (lecture / écriture d’une formule dans un fichier).
Ces deux parties dictent le découpage suivant :
– test syntaxique
– type formule
d’une formule
– test validité
– chaîne vers formule
– test contradiction
– formule vers chaîne
Formule Parseur

– lecture fichier de
formules et écriture
des résultats
Main

Toute flèche A B signifie que le module A dépend du module B.


95 / 399
Plan

Modules
Notion de modularité
Découpage d’un projet
Fichiers sources / d’en-tête
Création de modules
Graphes d’inclusions
Erreurs courantes et bonnes habitudes

96 / 399
Composition d’un module
Un module est composé de deux fichiers :
1. un fichier d’en-tête d’extension .h ;
2. un fichier source d’extension .c .

97 / 399
Composition d’un module
Un module est composé de deux fichiers :
1. un fichier d’en-tête d’extension .h ;
2. un fichier source d’extension .c .
Les noms de ces deux fichiers sont les mêmes (extension mise à part).

– ... – ...
– ... – ...
A.h A.c

97 / 399
Composition d’un module
Un module est composé de deux fichiers :
1. un fichier d’en-tête d’extension .h ;
2. un fichier source d’extension .c .
Les noms de ces deux fichiers sont les mêmes (extension mise à part).

– ... – ...
– ... – ...
A.h A.c

Seul le module principal est constitué d’un seul fichier source Main.c .

– ...
– ...
Main.c

97 / 399
Composition d’un module
Un module est composé de deux fichiers :
1. un fichier d’en-tête d’extension .h ;
2. un fichier source d’extension .c .
Les noms de ces deux fichiers sont les mêmes (extension mise à part).

– ... – ...
– ... – ...
A.h A.c

Seul le module principal est constitué d’un seul fichier source Main.c .

– ...
– ...
Main.c

Par exemple, le projet précédent est constitué des fichiers


Formule.h , Formule.c , Parseur.h , Parseur.c et Main.c .

97 / 399
Fichiers d’en-tête

Un fichier d’en-tête contient des déclarations de types et de fonctions (prototypes).

98 / 399
Fichiers d’en-tête

Un fichier d’en-tête contient des déclarations de types et de fonctions (prototypes).

Les prototypes qui y figurent sont ceux des fonctions que l’on souhaite rendre visibles
(utilisables) par d’autres modules.

98 / 399
Fichiers d’en-tête

Un fichier d’en-tête contient des déclarations de types et de fonctions (prototypes).

Les prototypes qui y figurent sont ceux des fonctions que l’on souhaite rendre visibles
(utilisables) par d’autres modules.

Par exemple, un en-tête possible du module Formule est

⁄* Formule.h *⁄

typedef struct {
...
} Form;

int est_valide(Form *f);


int est_contra(Form *f);

98 / 399
Fichiers d’en-tête
Les fichiers d’en-tête sont ceux que le programmeur regarde en premier pour connaître le rôle
d’un type ou d’une fonction.

99 / 399
Fichiers d’en-tête
Les fichiers d’en-tête sont ceux que le programmeur regarde en premier pour connaître le rôle
d’un type ou d’une fonction.
De ce fait, c’est dans les fichiers d’en-tête que l’on commente chaque type et fonction pour
préciser leur rôle.

99 / 399
Fichiers d’en-tête
Les fichiers d’en-tête sont ceux que le programmeur regarde en premier pour connaître le rôle
d’un type ou d’une fonction.
De ce fait, c’est dans les fichiers d’en-tête que l’on commente chaque type et fonction pour
préciser leur rôle.
Par exemple, l’en-tête du module Formule devrait être de la forme

⁄* Formule.h *⁄

⁄* Representation des formules logiques sans


* quantificateur. *⁄
typedef struct {
...
} Form;

⁄* Renvoie ‘1‘ si la formule pointee par ‘f‘


* est valide et ‘0‘ sinon. *⁄
int est_valide(Form *f);
...

99 / 399
Fichiers sources

Un fichier source contient une implantation de l’en-tête du module auquel il appartient.

100 / 399
Fichiers sources

Un fichier source contient une implantation de l’en-tête du module auquel il appartient.


Il contient de ce fait les définitions de fonctions déclarées dans l’en-tête.

100 / 399
Fichiers sources

Un fichier source contient une implantation de l’en-tête du module auquel il appartient.


Il contient de ce fait les définitions de fonctions déclarées dans l’en-tête.
Par exemple, une implantation possible du module Formule est

⁄* Formule.c *⁄ L’ordre de définition des fonctions n’est pas


... important. Nous en verrons la raison dans la
int est_valide(Form *f) { suite.
int i, j;
assert(f != NULL);
...
}

int est_contra(Form *f) {


...
}

100 / 399
Fichiers sources

Il faut impérativement que toutes les fonctions déclarées dans le fichier d’en-tête du module
soient définies dans le fichier source correspondant.

101 / 399
Fichiers sources

Il faut impérativement que toutes les fonctions déclarées dans le fichier d’en-tête du module
soient définies dans le fichier source correspondant.

Il est en revanche possible de définir dans un fichier source des fonctions qui ne sont pas
déclarées dans l’en-tête correspondant.

101 / 399
Fichiers sources

Il faut impérativement que toutes les fonctions déclarées dans le fichier d’en-tête du module
soient définies dans le fichier source correspondant.

Il est en revanche possible de définir dans un fichier source des fonctions qui ne sont pas
déclarées dans l’en-tête correspondant.

Ainsi,
I une fonction définie dans un fichier source mais pas dans l’en-tête correspondant s’appelle
fonction privée ;

101 / 399
Fichiers sources

Il faut impérativement que toutes les fonctions déclarées dans le fichier d’en-tête du module
soient définies dans le fichier source correspondant.

Il est en revanche possible de définir dans un fichier source des fonctions qui ne sont pas
déclarées dans l’en-tête correspondant.

Ainsi,
I une fonction définie dans un fichier source mais pas dans l’en-tête correspondant s’appelle
fonction privée ;
I l’intérêt des fonctions privées est d’être des fonctions outils dont le champ d’application
est local au module dans lequel elles sont définies.
On ne souhaite pas les rendre utilisables en dehors.

101 / 399
Fichiers sources et fonctions privées

Une fonction privée se définit avec le mot clé static (à ne pas confondre avec le static pour la
déclaration de variables).

102 / 399
Fichiers sources et fonctions privées

Une fonction privée se définit avec le mot clé static (à ne pas confondre avec le static pour la
déclaration de variables).

Par exemple, on peut avoir besoin d’une fonction privée appartenant au module Formule qui
permet de compter le nombre d’occurrences d’un atome dans une formule.

102 / 399
Fichiers sources et fonctions privées

Une fonction privée se définit avec le mot clé static (à ne pas confondre avec le static pour la
déclaration de variables).

Par exemple, on peut avoir besoin d’une fonction privée appartenant au module Formule qui
permet de compter le nombre d’occurrences d’un atome dans une formule.
On la définit alors dans Formule.c par
static int nb_occ(Form *f, char atome) {
...
assert(f != NULL);
assert((’a’ <= atome) && (atome <= ’z’));
...
}

102 / 399
Fichiers sources et fonctions privées

Une fonction privée se définit avec le mot clé static (à ne pas confondre avec le static pour la
déclaration de variables).

Par exemple, on peut avoir besoin d’une fonction privée appartenant au module Formule qui
permet de compter le nombre d’occurrences d’un atome dans une formule.
On la définit alors dans Formule.c par
static int nb_occ(Form *f, char atome) {
...
assert(f != NULL);
assert((’a’ <= atome) && (atome <= ’z’));
...
}

La portée lexicale de cette fonction s’étend à tout ce qui suit sa définition dans le fichier
Formule.c . Elle est invisible ailleurs.

102 / 399
Fichiers sources et fonctions privées

C’est un contresens que de définir une fonction dans un fichier source sans le mot clé static et
sans l’avoir déclarée dans le fichier d’en-tête.

103 / 399
Fichiers sources et fonctions privées

C’est un contresens que de définir une fonction dans un fichier source sans le mot clé static et
sans l’avoir déclarée dans le fichier d’en-tête.

C’est aussi un contresens que de définir dans un fichier source une fonction déclarée dans le
fichier d’en-tête avec static .

103 / 399
Fichiers sources et fonctions privées

C’est un contresens que de définir une fonction dans un fichier source sans le mot clé static et
sans l’avoir déclarée dans le fichier d’en-tête.

C’est aussi un contresens que de définir dans un fichier source une fonction déclarée dans le
fichier d’en-tête avec static .

Ainsi, pour résumer, toute fonction définie dans un fichier source est
1. soit déclarée dans le fichier d’en-tête ;

103 / 399
Fichiers sources et fonctions privées

C’est un contresens que de définir une fonction dans un fichier source sans le mot clé static et
sans l’avoir déclarée dans le fichier d’en-tête.

C’est aussi un contresens que de définir dans un fichier source une fonction déclarée dans le
fichier d’en-tête avec static .

Ainsi, pour résumer, toute fonction définie dans un fichier source est
1. soit déclarée dans le fichier d’en-tête ;
2. soit non déclarée dans le fichier d’en-tête mais définie par static .

103 / 399
Allure d’un projet

Il y a deux manières d’organiser un projet en termes de fichiers et de répertoires :

104 / 399
Allure d’un projet

Il y a deux manières d’organiser un projet en termes de fichiers et de répertoires :

1. la 1re consiste à regrouper l’ensemble des fichiers d’en-tête et des fichiers sources dans un
même répertoire. Il y figure donc un nombre impair de fichiers (le module principal et les
paires en-tête / source pour chaque module) ;

104 / 399
Allure d’un projet

Il y a deux manières d’organiser un projet en termes de fichiers et de répertoires :

1. la 1re consiste à regrouper l’ensemble des fichiers d’en-tête et des fichiers sources dans un
même répertoire. Il y figure donc un nombre impair de fichiers (le module principal et les
paires en-tête / source pour chaque module) ;

2. la 2e consiste à séparer les fichiers du projet en deux répertoires frères, include et src , le
premier contenant les fichiers d’en-tête et l’autre, les fichiers sources et le module principal
du projet.

104 / 399
Plan

Modules
Notion de modularité
Découpage d’un projet
Fichiers sources / d’en-tête
Création de modules
Graphes d’inclusions
Erreurs courantes et bonnes habitudes

105 / 399
Inclusion de modules

Pour utiliser un module Module dans un fichier F , on doit l’y inclure.

106 / 399
Inclusion de modules

Pour utiliser un module Module dans un fichier F , on doit l’y inclure.

On utilise pour cela dans F la commande pré-processeur

#include "Module.h"

106 / 399
Inclusion de modules

Pour utiliser un module Module dans un fichier F , on doit l’y inclure.

On utilise pour cela dans F la commande pré-processeur

#include "Module.h"

Celle-ci sera remplacée par le pré-processeur par le contenu de Module.h .

106 / 399
Inclusion de modules

Pour utiliser un module Module dans un fichier F , on doit l’y inclure.

On utilise pour cela dans F la commande pré-processeur

#include "Module.h"

Celle-ci sera remplacée par le pré-processeur par le contenu de Module.h .

Cette commande peut se trouver


I dans un fichier source pour bénéficier des fonctions définies et des types déclarés par le
module ;

106 / 399
Inclusion de modules

Pour utiliser un module Module dans un fichier F , on doit l’y inclure.

On utilise pour cela dans F la commande pré-processeur

#include "Module.h"

Celle-ci sera remplacée par le pré-processeur par le contenu de Module.h .

Cette commande peut se trouver


I dans un fichier source pour bénéficier des fonctions définies et des types déclarés par le
module ;
I dans un fichier d’en-tête pour bénéficier des types déclarés par le module.

106 / 399
Inclusion de modules
Par exemple, si A est un module définissant une fonction f , pour utiliser f dans un fichier B.c

situé dans le même répertoire que A.c et A.h , on écrit

⁄* B.c *⁄

#include "A.h"
...

107 / 399
Inclusion de modules
Par exemple, si A est un module définissant une fonction f , pour utiliser f dans un fichier B.c

situé dans le même répertoire que A.c et A.h , on écrit

⁄* B.c *⁄

#include "A.h"
...

Il est possible d’inclure à la suite plusieurs modules dans un même fichier :

⁄* Fichier.c ou Fichier.h *⁄

#include "A.h"
#include "B.h"
#include "C.h"
...

107 / 399
Inclusion de modules
Par exemple, si A est un module définissant une fonction f , pour utiliser f dans un fichier B.c

situé dans le même répertoire que A.c et A.h , on écrit

⁄* B.c *⁄

#include "A.h"
...

Il est possible d’inclure à la suite plusieurs modules dans un même fichier :

⁄* Fichier.c ou Fichier.h *⁄ de tout ce qui est déclaré et défini dans les modules
A , B et C .
#include "A.h"
#include "B.h"
#include "C.h"
... Attention : les modules inclus ne doivent pas
déclarer / définir des éléments d’un identificateur
De cette manière, Fichier.c ou Fichier.h bénéficie commun.

107 / 399
Inclusion de modules

Le fichier incluant n’a accès qu’au fichier d’en-tête du module, et donc qu’aux déclarations
effectuées.

108 / 399
Inclusion de modules

Le fichier incluant n’a accès qu’au fichier d’en-tête du module, et donc qu’aux déclarations
effectuées.
Le fichier incluant n’a pas besoin de connaître l’implantation du module.

108 / 399
Inclusion de modules

Le fichier incluant n’a accès qu’au fichier d’en-tête du module, et donc qu’aux déclarations
effectuées.
Le fichier incluant n’a pas besoin de connaître l’implantation du module.

Considérons par exemple le module Couple défini par

⁄* Couple.h *⁄ ⁄* Couple.c *⁄
...
typedef int Couple[2]; int est_zero(Couple c) {
return (c[0] == 0) && (c[1]== 0);
int est_zero(Couple c); }

void afficher(Couple c); void afficher(Couple c) {


printf("(%d, %d)", c[0], c[1]);
}

108 / 399
Inclusion de modules
Supposons que l’on ait besoin d’inclure le module Couple dans un fichier Fichier.c .

109 / 399
Inclusion de modules
Supposons que l’on ait besoin d’inclure le module Couple dans un fichier Fichier.c .

Le pré-processeur aura donc l’effet suivant sur Fichier.c :

⁄* Fichier.c avant la passe du


* pre-processeur *⁄

#include "Couple.h"
...

if (!est_zero(c))
afficher(c);
...

109 / 399
Inclusion de modules
Supposons que l’on ait besoin d’inclure le module Couple dans un fichier Fichier.c .

Le pré-processeur aura donc l’effet suivant sur Fichier.c :

⁄* Fichier.c avant la passe du ⁄* Fichier.c apres la passe


* pre-processeur *⁄ * du pre-processeur *⁄

#include "Couple.h" typedef int Couple[2];


... int est_zero(Couple c);
void afficher(Couple c);
if (!est_zero(c)) ...
afficher(c);
... if (!est_zero(c))
afficher(c);
...

109 / 399
Inclusion de modules
Supposons que l’on ait besoin d’inclure le module Couple dans un fichier Fichier.c .

Le pré-processeur aura donc l’effet suivant sur Fichier.c :

⁄* Fichier.c avant la passe du ⁄* Fichier.c apres la passe


* pre-processeur *⁄ * du pre-processeur *⁄

#include "Couple.h" typedef int Couple[2];


... int est_zero(Couple c);
void afficher(Couple c);
if (!est_zero(c)) ...
afficher(c);
... if (!est_zero(c))
afficher(c);
...

Fichier.c a besoin uniquement de connaître les types de retour et les signatures des fonctions
qu’il invoque (connus à leur déclaration). Il n’a à ce stade pas besoin de connaître les définitions
de ces fonctions.

109 / 399
Création complète d’un module

Pour créer un module A , il faut inclure son fichier d’en-tête A.h dans son fichier source A.c .

⁄* A.h *⁄ ⁄* A.c *⁄

... #include "A.h"


...

110 / 399
Création complète d’un module

Pour créer un module A , il faut inclure son fichier d’en-tête A.h dans son fichier source A.c .

⁄* A.h *⁄ ⁄* A.c *⁄

... #include "A.h"


...

De cette manière,
I d’une part, A.c a accès aux types et aux prototypes de fonctions déclarés dans A.h ;

110 / 399
Création complète d’un module

Pour créer un module A , il faut inclure son fichier d’en-tête A.h dans son fichier source A.c .

⁄* A.h *⁄ ⁄* A.c *⁄

... #include "A.h"


...

De cette manière,
I d’une part, A.c a accès aux types et aux prototypes de fonctions déclarés dans A.h ;
I d’autre part, cela permet d’implanter les fonctions déclarées dans A.h sans contrainte
d’ordre.

110 / 399
Création complète d’un module

Considérons la situation suivante :


/* A.h */ /* A.c */ /* B.h */ /* B.c */ /* C.h */ /* C.c */ /* D.c */
... ... ...
#include "A.h"
#include "C.h" #include "A.h" #include "C.h" #include "B.h" int f(); #include "C.h" #include "B.h"
... ... ... ... ... ... ...

111 / 399
Création complète d’un module

Considérons la situation suivante :


/* A.h */ /* A.c */ /* B.h */ /* B.c */ /* C.h */ /* C.c */ /* D.c */
... ... ...
#include "A.h"
#include "C.h" #include "A.h" #include "C.h" #include "B.h" int f(); #include "C.h" #include "B.h"
... ... ... ... ... ... ...

Le pré-processeur transforme ces fichiers en


/* D.c */
/* A.h */ /* A.c */ /* B.h */ /* B.c */ /* C.h */ /* C.c */
... ... ...
int f();
int f(); /* Copie A.h */ int f(); /* Copie B.h */ int f(); /* Copie C.h */
int f();
... ... ... ... ... ...
...

111 / 399
Création complète d’un module

Considérons la situation suivante :


/* A.h */ /* A.c */ /* B.h */ /* B.c */ /* C.h */ /* C.c */ /* D.c */
... ... ...
#include "A.h"
#include "C.h" #include "A.h" #include "C.h" #include "B.h" int f(); #include "C.h" #include "B.h"
... ... ... ... ... ... ...

Le pré-processeur transforme ces fichiers en


/* D.c */
/* A.h */ /* A.c */ /* B.h */ /* B.c */ /* C.h */ /* C.c */
... ... ...
int f();
int f(); /* Copie A.h */ int f(); /* Copie B.h */ int f(); /* Copie C.h */
int f();
... ... ... ... ... ...
...

Problème : le contenu de C.h est copié deux fois dans D.c . Ceci n’est pas accepté par le
compilateur car il y a multiple déclaration d’un même symbole ( f ici).

111 / 399
Création complète d’un module
La parade consiste à inclure un fichier d’en-tête de manière conditionnelle : on procède à
l’inclusion que s’il n’a pas déjà été inclus.
On utilise pour cela les macro-instructions de contrôle de compilation #ifndef et #endif ainsi
que #define .
Le schéma général est
Ainsi, lors d’une inclusion de A.h , le pré-
/* A.h */
processeur vérifie si la macro __A__n’existe
#ifndef __A__
#define __A__
pas.
I Si elle n’existe pas, alors on la définit
/* Declaration de types */
... ( #define __A__ ) et le contenu du
/* Declaration de fonctions */
module est pris en compte ;
... I sinon, cela signifie que le contenu a
#endif déjà été pris en compte. Celui-ci n’est
pas repris en compte une 2e fois.

112 / 399
Squelette d’un module

Pour résumer, tous les modules doivent avoir le squelette suivant :


/* A.h */

#ifndef __A__
/* A.c */
#define __A__
#include "A.h"
/* Inclusions eventuelles de modules */
...
/* Inclusions eventuelles de modules */
...
/* Definitions eventuelles de macros */
...
/* Definitions eventuelles de fonctions privees */
...
/* Declarations eventuelles de types */
...
/* Definitions de toutes les fonctions declarees
* dans le fichier d’en-tete */
/* Declarations eventuelles de fonctions */
...
...

#endif

113 / 399
Exemple du module Parseur

/* Parseur.h */
/* Parseur.c */
#ifndef __PARSEUR__
#include "Parseur.h"
#define __PARSEUR__
#include <stdio.h>
#include "Formule.h"
#include <stdlib.h>
#include <assert.h>
/* Convertit la chaine de caracteres ‘ch‘ sensee
* representer une formule en une variable de type
int chaine_vers_form(Form *f, char *ch) {
* ‘Form‘, qui va etre ecrite dans ‘f‘. Renvoie ‘1‘
...
* si ‘ch‘ represente bien une formule et ‘0‘ sinon. */
assert(f != NULL);
int chaine_vers_form(Form *f, char *ch);
...
}
#endif

114 / 399
Plan

Modules
Notion de modularité
Découpage d’un projet
Fichiers sources / d’en-tête
Création de modules
Graphes d’inclusions
Erreurs courantes et bonnes habitudes

115 / 399
Graphes d’inclusions
On rappelle qu’un module A dépend d’un module B si le fichier d’en-tête de A inclut B .

116 / 399
Graphes d’inclusions
On rappelle qu’un module A dépend d’un module B si le fichier d’en-tête de A inclut B .
Pour visualiser l’allure d’un projet, on trace son graphe d’inclusions. On représente pour cela
chacun des modules qui le composent dans des cercles (sommets) et on trace des flèches (arcs)
de A vers B pour tout module A dépendant de B .

116 / 399
Graphes d’inclusions
On rappelle qu’un module A dépend d’un module B si le fichier d’en-tête de A inclut B .
Pour visualiser l’allure d’un projet, on trace son graphe d’inclusions. On représente pour cela
chacun des modules qui le composent dans des cercles (sommets) et on trace des flèches (arcs)
de A vers B pour tout module A dépendant de B .
Par exemple, le graphe d’inclusions

signifie que
A B
I A.h inclut B.h et D.h ;
I B.h inclut D.h ;
C D I C.h inclut D.h ;
I D.h n’inclut rien.

116 / 399
Graphes d’inclusions
On rappelle qu’un module A dépend d’un module B si le fichier d’en-tête de A inclut B .
Pour visualiser l’allure d’un projet, on trace son graphe d’inclusions. On représente pour cela
chacun des modules qui le composent dans des cercles (sommets) et on trace des flèches (arcs)
de A vers B pour tout module A dépendant de B .
Par exemple, le graphe d’inclusions

signifie que
A B
I A.h inclut B.h et D.h ;
I B.h inclut D.h ;
C D I C.h inclut D.h ;
I D.h n’inclut rien.

On ne mentionne pas dans les graphes d’inclusions les inclusions aux fichiers d’en-tête standards
( stdio.h , stdlib.h , assert.h , etc.).

116 / 399
Graphe d’inclusions et fichier principal
Le graphe d’inclusions d’un projet consistant à faire jouer l’ordinateur aux échecs contre un
humain peut être le suivant :

Piece IA

Position Main

Case IGraph

Tout projet contient un fichier source Main.c , le fichier principal du projet, où figure la fonction
main (le point d’entrée de l’exécution du programme). Celui-ci apparaît dans le graphe
d’inclusions.
117 / 399
Inclusions circulaires

Les graphes d’inclusions permettent d’avoir une vision globale de l’architecture d’un projet.

118 / 399
Inclusions circulaires

Les graphes d’inclusions permettent d’avoir une vision globale de l’architecture d’un projet.

Ils permettent aussi de mettre en évidence des problèmes de conception et notamment les
problèmes d’inclusion circulaire. Ce type de problème s’observe par la présence d’un cycle dans
le graphe d’inclusions :

A B

C D

118 / 399
Inclusions circulaires

Les graphes d’inclusions permettent d’avoir une vision globale de l’architecture d’un projet.

Ils permettent aussi de mettre en évidence des problèmes de conception et notamment les
problèmes d’inclusion circulaire. Ce type de problème s’observe par la présence d’un cycle dans
le graphe d’inclusions :

A B

C D

Règle importante : il ne doit jamais y avoir de cycle dans le graphe d’inclusions d’un projet. S’il
y a un cycle, c’est que le projet est mal découpé en modules.

118 / 399
Limiter les inclusions

La plupart des inclusions circulaires peuvent être évitées en réduisant au maximum les inclusions
de modules dans les fichiers d’en-tête en les faisant plutôt si possible dans les fichiers sources.

Par exemple, supposons que l’on dispose d’un module Tri qui permet de trier des tableaux
génériques (nous aborderons plus loin ce concept de généricité). Il est de la forme :
/* Tri.h */
/* Tri.c */
#ifndef __TRI__
#include "Tri.h"
#define __TRI__
...
void trier_tab(void **t, int n,
void trier_tab(void **t, int n,
int (*est_inf)(void *, void *)) {
int (*est_inf)(void *, void *));
...
...
}
...
#endif

119 / 399
Limiter les inclusions
On souhaite maintenant écrire un module TabInt pour gérer des tableaux d’entiers.
On n’écrira pas
/* TabInt.h */
#ifndef __TAB_INT__
#define __TAB_INT__ /* TabInt.c */
#include "TabInt.h"
#include "Tri.h"
int trier_tab_int(TabInt *t) {
typedef struct {int n; int *tab;} TabInt; /* Util. de ‘trier_tab‘ */
int trier_tab_int(TabInt *t); }

#endif
mais plutôt
/* TabInt.h */ /* TabInt.c */
#ifndef __TAB_INT__ #include "TabInt.h"
#define __TAB_INT__
#include "Tri.h"
typedef struct int n; int *tab; TabInt;
int trier_tab_int(TabInt *t); int trier_tab_int(TabInt *t) {
/* Util. de ‘trier_tab‘ */
#endif }

120 / 399
Dépendances étendues

Nous serons amenés dans la suite (dans le cadre de la compilation séparée) — étant donné un
fichier A.c — à considérer l’ensemble des fichiers d’en-tête qu’il inclut.

121 / 399
Dépendances étendues

Nous serons amenés dans la suite (dans le cadre de la compilation séparée) — étant donné un
fichier A.c — à considérer l’ensemble des fichiers d’en-tête qu’il inclut.

Si A.c inclut un fichier d’en-tête B.h , alors le module A dépend de manière étendue au module
B .

121 / 399
Dépendances étendues

Nous serons amenés dans la suite (dans le cadre de la compilation séparée) — étant donné un
fichier A.c — à considérer l’ensemble des fichiers d’en-tête qu’il inclut.

Si A.c inclut un fichier d’en-tête B.h , alors le module A dépend de manière étendue au module
B .

Le graphe d’inclusions étendu d’un projet consiste en le graphe d’inclusions du projet dans
lequel sont ajoutées des flèches en pointillés pour symboliser les dépendances étendues.

121 / 399
Dépendances étendues

Nous serons amenés dans la suite (dans le cadre de la compilation séparée) — étant donné un
fichier A.c — à considérer l’ensemble des fichiers d’en-tête qu’il inclut.

Si A.c inclut un fichier d’en-tête B.h , alors le module A dépend de manière étendue au module
B .

Le graphe d’inclusions étendu d’un projet consiste en le graphe d’inclusions du projet dans
lequel sont ajoutées des flèches en pointillés pour symboliser les dépendances étendues.

Note 1. : les flèches qui partent du module principal Main représentent des inclusions étendues.

121 / 399
Dépendances étendues

Nous serons amenés dans la suite (dans le cadre de la compilation séparée) — étant donné un
fichier A.c — à considérer l’ensemble des fichiers d’en-tête qu’il inclut.

Si A.c inclut un fichier d’en-tête B.h , alors le module A dépend de manière étendue au module
B .

Le graphe d’inclusions étendu d’un projet consiste en le graphe d’inclusions du projet dans
lequel sont ajoutées des flèches en pointillés pour symboliser les dépendances étendues.

Note 1. : les flèches qui partent du module principal Main représentent des inclusions étendues.

Note 2. : les cycles dans lesquels intervient au moins une flèche en pointillés ne posent pas de
problème de structure du projet.

121 / 399
Dépendances étendues
Par exemple, le graphe d’inclusions étendu

A B

F E

C D

nous renseigne sur le fait que

I A.h n’inclut rien ; I C.h inclut A.h ; I E.h inclut B.h et D.h

I A.c inclut B.h ; I C.c n’inclut rien ; I E.c n’inclut rien ;

I B.h n’inclut rien ; I D.h inclut C.h ; I F.h inclut C.h ;

I B.c inclut C.h ; I D.c n’inclut rien ; I F.c inclut A.h .

On observe que ce projet n’est pas mal structuré car il ne possède pas de cycle formé uniquement
par des flèches de dépendance.
122 / 399
Plan

Modules
Notion de modularité
Découpage d’un projet
Fichiers sources / d’en-tête
Création de modules
Graphes d’inclusions
Erreurs courantes et bonnes habitudes

123 / 399
Erreur : le fichier d’en-tête général

Une erreur consiste, pour un projet donné, à développer un fichier Types.h et plusieurs fichiers
source F1.c , F2.c , . . ., Fn.c .

124 / 399
Erreur : le fichier d’en-tête général

Une erreur consiste, pour un projet donné, à développer un fichier Types.h et plusieurs fichiers
source F1.c , F2.c , . . ., Fn.c .

Ici le fichier d’en-tête Types.h contient les déclarations de tous les types et fonctions nécessaires
au projet et les fichiers sources Fi.c implantent chacun un sous-ensemble des fonctions
déclarées.

124 / 399
Erreur : le fichier d’en-tête général

Une erreur consiste, pour un projet donné, à développer un fichier Types.h et plusieurs fichiers
source F1.c , F2.c , . . ., Fn.c .

Ici le fichier d’en-tête Types.h contient les déclarations de tous les types et fonctions nécessaires
au projet et les fichiers sources Fi.c implantent chacun un sous-ensemble des fonctions
déclarées.
Cette conception est erronée puisque :
1. il n’y a plus de notion de module (et donc tous leurs avantages relatifs sont absents) ;

2. il est impossible de réutiliser du code du projet pour un nouveau (il faudrait copier / coller
les types et fonctions importantes, ce qui n’est pas abordable) ;

3. le fichier Types.h peut contenir des déclarations de types et de fonctions qui n’ont pas grand
chose à voir.

124 / 399
Erreur : économie d’inclusions
Une erreur consiste à éviter volontairement de réaliser des inclusions de modules dans d’autres si
l’inclusion est déjà réalisée de manière transitive.

125 / 399
Erreur : économie d’inclusions
Une erreur consiste à éviter volontairement de réaliser des inclusions de modules dans d’autres si
l’inclusion est déjà réalisée de manière transitive.
Plus explicitement, soient trois modules A , B et C tels que B inclut C , A inclut B et A inclut
C :

A B C

125 / 399
Erreur : économie d’inclusions
Une erreur consiste à éviter volontairement de réaliser des inclusions de modules dans d’autres si
l’inclusion est déjà réalisée de manière transitive.
Plus explicitement, soient trois modules A , B et C tels que B inclut C , A inclut B et A inclut
C :

A B C

On peut être tenté de n’inclure que B dans A et C dans B car — par transitivité — ceci
entraîne que C est inclut dans A . Ceci fonctionne en pratique.

125 / 399
Erreur : économie d’inclusions
Une erreur consiste à éviter volontairement de réaliser des inclusions de modules dans d’autres si
l’inclusion est déjà réalisée de manière transitive.
Plus explicitement, soient trois modules A , B et C tels que B inclut C , A inclut B et A inclut
C :

A B C

On peut être tenté de n’inclure que B dans A et C dans B car — par transitivité — ceci
entraîne que C est inclut dans A . Ceci fonctionne en pratique.
Cette conception est cependant erronée puisque :
1. savoir de quels modules dépend A simplement en lisant son fichier d’en-tête, sans avoir de surprise
sur les modules qui peuvent être inclus de manière cachée par transitivité, est un avantage ;

2. le jour où l’on modifie B de sorte qu’il n’ait plus besoin de dépendre de C provoque le fait que C

n’est plus inclut dans A , ce qui est problématique.


125 / 399
Habitude : un module par type
Une bonne façon de faire par défaut consiste à créer un module pour chaque type nécessaire à
l’écriture d’un projet.

126 / 399
Habitude : un module par type
Une bonne façon de faire par défaut consiste à créer un module pour chaque type nécessaire à
l’écriture d’un projet.
Avec ce point de vue, il y a dans chaque A.h une déclaration de type unique (dont le nom est A ,
celui du module) et des déclarations de fonctions qui agissent sur des éléments de type A .

126 / 399
Habitude : un module par type
Une bonne façon de faire par défaut consiste à créer un module pour chaque type nécessaire à
l’écriture d’un projet.
Avec ce point de vue, il y a dans chaque A.h une déclaration de type unique (dont le nom est A ,
celui du module) et des déclarations de fonctions qui agissent sur des éléments de type A .
Cette conception est correcte mais un peu limitée car
1. elle dispense d’une reflexion approfondie sur un bon découpage en modules du projet ;

2. des « types de travail » ne méritent pas d’appartenir à un module dédié ;

3. un projet compterait ainsi trop de modules.

126 / 399
Habitude : un module par type
Une bonne façon de faire par défaut consiste à créer un module pour chaque type nécessaire à
l’écriture d’un projet.
Avec ce point de vue, il y a dans chaque A.h une déclaration de type unique (dont le nom est A ,
celui du module) et des déclarations de fonctions qui agissent sur des éléments de type A .
Cette conception est correcte mais un peu limitée car
1. elle dispense d’une reflexion approfondie sur un bon découpage en modules du projet ;

2. des « types de travail » ne méritent pas d’appartenir à un module dédié ;

3. un projet compterait ainsi trop de modules.


En pratique, commencer la réflexion d’un découpage en modules d’un projet en se posant la
question

« De quels types ai-je besoin ? »


fournit un point de départ efficace, à raffiner ensuite.
126 / 399
Plan

Compilation
Étapes de compilation
Compilation séparée
Makefile simples
Makefile avancés
Bibliothèques

127 / 399
Plan

Compilation
Étapes de compilation
Compilation séparée
Makefile simples
Makefile avancés
Bibliothèques

128 / 399
Compilation d’un projet d’un fichier

La compilation d’un projet constitué d’un unique fichier Fichier.c contenant la fonction
principale main se fait par la commande

gcc Fichier.c

Elle permet d’obtenir finalement un fichier exécutable.

129 / 399
Compilation d’un projet d’un fichier

La compilation d’un projet constitué d’un unique fichier Fichier.c contenant la fonction
principale main se fait par la commande

gcc Fichier.c

Cette commande réalise à la suite les étapes suivantes :


1. traitement préliminaire par le pré-processeur ;

Elle permet d’obtenir finalement un fichier exécutable.

129 / 399
Compilation d’un projet d’un fichier

La compilation d’un projet constitué d’un unique fichier Fichier.c contenant la fonction
principale main se fait par la commande

gcc Fichier.c

Cette commande réalise à la suite les étapes suivantes :


1. traitement préliminaire par le pré-processeur ;
2. compilation en langage assembleur ;

Elle permet d’obtenir finalement un fichier exécutable.

129 / 399
Compilation d’un projet d’un fichier

La compilation d’un projet constitué d’un unique fichier Fichier.c contenant la fonction
principale main se fait par la commande

gcc Fichier.c

Cette commande réalise à la suite les étapes suivantes :


1. traitement préliminaire par le pré-processeur ;
2. compilation en langage assembleur ;
3. traduction du langage assembleur en langage machine ;

Elle permet d’obtenir finalement un fichier exécutable.

129 / 399
Compilation d’un projet d’un fichier

La compilation d’un projet constitué d’un unique fichier Fichier.c contenant la fonction
principale main se fait par la commande

gcc Fichier.c

Cette commande réalise à la suite les étapes suivantes :


1. traitement préliminaire par le pré-processeur ;
2. compilation en langage assembleur ;
3. traduction du langage assembleur en langage machine ;
4. édition des liens.

Elle permet d’obtenir finalement un fichier exécutable.

129 / 399
Compilation d’un projet d’un fichier
Fichier.c

pré-processeur

Fichier.i Fichier.s Fichier.o


assembleur langage machine

exécutable

a.out

130 / 399
Pré-processeur
Le pré-processeur réalise un pré-traitement du fichier source pour le rendre traduisible en
langage machine.

131 / 399
Pré-processeur
Le pré-processeur réalise un pré-traitement du fichier source pour le rendre traduisible en
langage machine.

Il procède en
1. supprimant les commentaires ;

131 / 399
Pré-processeur
Le pré-processeur réalise un pré-traitement du fichier source pour le rendre traduisible en
langage machine.

Il procède en
1. supprimant les commentaires ;
2. incluant les fichiers d’en-tête (copie / colle les fichiers .h inclus) ;

131 / 399
Pré-processeur
Le pré-processeur réalise un pré-traitement du fichier source pour le rendre traduisible en
langage machine.

Il procède en
1. supprimant les commentaires ;
2. incluant les fichiers d’en-tête (copie / colle les fichiers .h inclus) ;

3. traitant les définitions de symboles par un mécanisme de substitution ( #define );

131 / 399
Pré-processeur
Le pré-processeur réalise un pré-traitement du fichier source pour le rendre traduisible en
langage machine.

Il procède en
1. supprimant les commentaires ;
2. incluant les fichiers d’en-tête (copie / colle les fichiers .h inclus) ;

3. traitant les définitions de symboles par un mécanisme de substitution ( #define );

4. traitant les macro-instructions de contrôle de compilation ( #ifndef , #endif , etc.).

131 / 399
Pré-processeur
Le pré-processeur réalise un pré-traitement du fichier source pour le rendre traduisible en
langage machine.

Il procède en
1. supprimant les commentaires ;
2. incluant les fichiers d’en-tête (copie / colle les fichiers .h inclus) ;

3. traitant les définitions de symboles par un mécanisme de substitution ( #define );

4. traitant les macro-instructions de contrôle de compilation ( #ifndef , #endif , etc.).

Il est possible de récupérer le fichier d’extension .i ainsi obtenu par la commande

gcc -E Fichier.c > > Fichier.i

131 / 399
Compilation en assembleur

Après avoir été traité par le pré-processeur, le fichier Fichier.i est traduit en assembleur.

132 / 399
Compilation en assembleur

Après avoir été traité par le pré-processeur, le fichier Fichier.i est traduit en assembleur.

Il est possible de récupérer le fichier d’extension .s ainsi obtenu par la commande

gcc -S Fichier.c

132 / 399
Compilation en assembleur

Après avoir été traité par le pré-processeur, le fichier Fichier.i est traduit en assembleur.

Il est possible de récupérer le fichier d’extension .s ainsi obtenu par la commande

gcc -S Fichier.c

L’assembleur est un langage très proche de la machine. Il peut se traduire assez facilement en un
langage directement exécutable par le procésseur.
Il existe plusieurs langages d’assemblage différents : au moins un par architecture.

132 / 399
Compilation en assembleur
Par exemple, avec le fichier Fichier.c suivant :
/* Fichier.c */

#include <stdio.h>

int main() {
printf("Bonjour");
return 0;
}

on obtient le fichier assembleur Fichier.s suivant :


.file "Fichier.c"
.cfi_offset 6, -16
.section .rodata
movq %rsp, %rbp
.LC0:
.cfi_def_cfa_register 6
.string "Bonjour"
movl $.LC0, %edi .LFE0:
.text
movl $0, %eax .size main, .-main
.globl main
call printf .ident "GCC: (Ubuntu/Linaro 4.8.1-10
.type main, @function
movl $0, %eax ubuntu9) 4.8.1"
main:
popq %rbp .section .note.GNU-stack,"",@progbits
.LFB0:
.cfi_def_cfa 7, 8
.cfi_startproc
ret
pushq %rbp
.cfi_endproc
.cfi_def_cfa_offset 16

133 / 399
Traduction en langage machine

Le code assembleur Fichier.s est traduit en langage machine.

134 / 399
Traduction en langage machine

Le code assembleur Fichier.s est traduit en langage machine.

On obtient ce fichier d’extension .o par la commande

gcc -c Fichier.c

134 / 399
Traduction en langage machine

Le code assembleur Fichier.s est traduit en langage machine.

On obtient ce fichier d’extension .o par la commande

gcc -c Fichier.c

Ce fichier s’appelle fichier objet. Il est illisible pour un humain mais peut cependant être affiché
au moyen de la commande
od -x Fichier.o ou bien od -a Fichier.o

134 / 399
Traduction en langage machine

Le code assembleur Fichier.s est traduit en langage machine.

On obtient ce fichier d’extension .o par la commande

gcc -c Fichier.c

Ce fichier s’appelle fichier objet. Il est illisible pour un humain mais peut cependant être affiché
au moyen de la commande
od -x Fichier.o ou bien od -a Fichier.o

Le langage machine est directement compris par le processeur qui peut de ce fait exécuter
directement les instructions qu’il contient.

134 / 399
Traduction en langage machine
Par exemple, avec le programme précédent, le contenu de Fichier.o est

0000000 del E L F stx soh soh nul nul nul nul nul nul nul nul nul
0000020 soh nul > nul soh nul nul nul nul nul nul nul nul nul nul nul
0000040 nul nul nul nul nul nul nul nul 0 soh nul nul nul nul nul nul
0000060 nul nul nul nul @ nul nul nul nul nul @ nul cr nul nl nul
0000100 U H ht e ? nul nul nul nul 8 nul nul nul nul h nul
0000120 nul nul nul 8 nul nul nul nul ] C B o n j o u
0000140 r nul nul G C C : sp ( U b u n t u /
0000160 L i n a r o sp 4 . 8 . 1 - 1 0 u
0000200 b u n t u 9 ) sp 4 . 8 . 1 nul nul nul
0000220 dc4 nul nul nul nul nul nul nul soh z R nul soh x dle soh
0000240 esc ff bel bs dle soh nul nul fs nul nul nul fs nul nul nul
0000260 nul nul nul nul sub nul nul nul nul A so dle ack stx C cr
0000300 ack U ff bel bs nul nul nul nul . s y m t a b
0000320 nul . s t r t a b nul . s h s t r t
0000340 a b nul . r e l a . t e x t nul . d
0000360 a t a nul . b s s nul . r o d a t a
135 / 399
Traduction en langage machine
0000400 nul . c o m m e n t nul . n o t e .
0000420 G N U - s t a c k nul . r e l a .
0000440 e h _ f r a m e nul nul nul nul nul nul nul nul
0000460 nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
*
0000560 sp nul nul nul soh nul nul nul ack nul nul nul nul nul nul nul
0000600 nul nul nul nul nul nul nul nul @ nul nul nul nul nul nul nul
0000620 sub nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
0000640 soh nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
0000660 esc nul nul nul eot nul nul nul nul nul nul nul nul nul nul nul
0000700 nul nul nul nul nul nul nul nul dle enq nul nul nul nul nul nul
0000720 0 nul nul nul nul nul nul nul vt nul nul nul soh nul nul nul
0000740 bs nul nul nul nul nul nul nul can nul nul nul nul nul nul nul
0000760 & nul nul nul soh nul nul nul etx nul nul nul nul nul nul nul
0001000 nul nul nul nul nul nul nul nul Z nul nul nul nul nul nul nul
0001020 nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
0001040 soh nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul

136 / 399
Traduction en langage machine
0001060 , nul nul nul bs nul nul nul etx nul nul nul nul nul nul nul
0001100 nul nul nul nul nul nul nul nul Z nul nul nul nul nul nul nul
0001120 nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
0001140 soh nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
0001160 1 nul nul nul soh nul nul nul stx nul nul nul nul nul nul nul
0001200 nul nul nul nul nul nul nul nul Z nul nul nul nul nul nul nul
0001220 bs nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
0001240 soh nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
0001260 9 nul nul nul soh nul nul nul 0 nul nul nul nul nul nul nul
0001300 nul nul nul nul nul nul nul nul b nul nul nul nul nul nul nul
0001320 , nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
0001340 soh nul nul nul nul nul nul nul soh nul nul nul nul nul nul nul
0001360 B nul nul nul soh nul nul nul nul nul nul nul nul nul nul nul
0001400 nul nul nul nul nul nul nul nul so nul nul nul nul nul nul nul
0001420 nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
0001440 soh nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
0001460 W nul nul nul soh nul nul nul stx nul nul nul nul nul nul nul

137 / 399
Traduction en langage machine
0001500 nul nul nul nul nul nul nul nul dle nul nul nul nul nul nul nul
0001520 8 nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
0001540 bs nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
0001560 R nul nul nul eot nul nul nul nul nul nul nul nul nul nul nul
0001600 nul nul nul nul nul nul nul nul @ enq nul nul nul nul nul nul
0001620 can nul nul nul nul nul nul nul vt nul nul nul bs nul nul nul
0001640 bs nul nul nul nul nul nul nul can nul nul nul nul nul nul nul
0001660 dc1 nul nul nul etx nul nul nul nul nul nul nul nul nul nul nul
0001700 nul nul nul nul nul nul nul nul H nul nul nul nul nul nul nul
0001720 a nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
0001740 soh nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
0001760 soh nul nul nul stx nul nul nul nul nul nul nul nul nul nul nul
0002000 nul nul nul nul nul nul nul nul p eot nul nul nul nul nul nul
0002020 bs soh nul nul nul nul nul nul ff nul nul nul ht nul nul nul
0002040 bs nul nul nul nul nul nul nul can nul nul nul nul nul nul nul
0002060 ht nul nul nul etx nul nul nul nul nul nul nul nul nul nul nul
0002100 nul nul nul nul nul nul nul nul x enq nul nul nul nul nul nul

138 / 399
Traduction en langage machine
0002120 etb nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
0002140 soh nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
0002160 nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
0002200 nul nul nul nul nul nul nul nul soh nul nul nul eot nul q del
0002220 nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
0002240 nul nul nul nul etx nul soh nul nul nul nul nul nul nul nul nul
0002260 nul nul nul nul nul nul nul nul nul nul nul nul etx nul etx nul
0002300 nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
0002320 nul nul nul nul etx nul eot nul nul nul nul nul nul nul nul nul
0002340 nul nul nul nul nul nul nul nul nul nul nul nul etx nul enq nul
0002360 nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
0002400 nul nul nul nul etx nul bel nul nul nul nul nul nul nul nul nul
0002420 nul nul nul nul nul nul nul nul nul nul nul nul etx nul bs nul
0002440 nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul nul
0002460 nul nul nul nul etx nul ack nul nul nul nul nul nul nul nul nul
0002500 nul nul nul nul nul nul nul nul vt nul nul nul dc2 nul soh nul
0002520 nul nul nul nul nul nul nul nul sub nul nul nul nul nul nul nul

139 / 399
Traduction en langage machine

0002540 dle nul nul nul dle nul nul nul nul nul nul nul nul nul nul nul
0002560 nul nul nul nul nul nul nul nul nul F i c h i e r
0002600 . c nul m a i n nul p r i n t f nul nul
0002620 enq nul nul nul nul nul nul nul nl nul nul nul enq nul nul nul
0002640 nul nul nul nul nul nul nul nul si nul nul nul nul nul nul nul
0002660 stx nul nul nul nl nul nul nul | del del del del del del del
0002700 sp nul nul nul nul nul nul nul stx nul nul nul stx nul nul nul
0002720 nul nul nul nul nul nul nul nul
0002730

140 / 399
Édition des liens

L’édition des liens réunit le fichier objet et le code propre aux fonctions et types de la librairie
standard utilisés (comme printf , scanf , etc.) pour produire l’exécutable complet.

141 / 399
Édition des liens

L’édition des liens réunit le fichier objet et le code propre aux fonctions et types de la librairie
standard utilisés (comme printf , scanf , etc.) pour produire l’exécutable complet.

Avant l’édition des liens, seuls les prototypes des fonctions sont connus du compilateur. Cela lui
permet de vérifier que les types sont bien respectés.

141 / 399
Édition des liens

L’édition des liens réunit le fichier objet et le code propre aux fonctions et types de la librairie
standard utilisés (comme printf , scanf , etc.) pour produire l’exécutable complet.

Avant l’édition des liens, seuls les prototypes des fonctions sont connus du compilateur. Cela lui
permet de vérifier que les types sont bien respectés.

Cependant, pour l’obtention finale de l’exécutable qui va suivre, il est nécessaire de connaître le
comportement des fonctions (c.-à-d. leur définition).

141 / 399
Édition des liens

L’édition des liens réunit le fichier objet et le code propre aux fonctions et types de la librairie
standard utilisés (comme printf , scanf , etc.) pour produire l’exécutable complet.

Avant l’édition des liens, seuls les prototypes des fonctions sont connus du compilateur. Cela lui
permet de vérifier que les types sont bien respectés.

Cependant, pour l’obtention finale de l’exécutable qui va suivre, il est nécessaire de connaître le
comportement des fonctions (c.-à-d. leur définition).

C’est précisément dans cette phase de la compilation que la résolution des symboles a lieu. C’est
l’étape qui consiste à associer aux identificateurs de fonctions leur implantation.

141 / 399
Compilation d’un projet de plusieurs fichiers
On suppose que l’on travaille sur un projet constitué de trois modules A , B et C et d’un fichier
principal Main.c contenant la fonction main .

A.h
A.c

B.h
B.c

C.h
C.c

Main.c

142 / 399
Compilation d’un projet de plusieurs fichiers
On suppose que l’on travaille sur un projet constitué de trois modules A , B et C et d’un fichier
principal Main.c contenant la fonction main .
La compilation de ce projet se réalise au moyen des étapes suivantes :
1. obtenir les fichiers objets de chaque module ;

A.h gcc -c A.c A.h


A.o
A.c

B.h gcc -c B.c B.h


B.o
B.c

C.h gcc -c C.c C.h


C.o
C.c

Main.c

142 / 399
Compilation d’un projet de plusieurs fichiers
On suppose que l’on travaille sur un projet constitué de trois modules A , B et C et d’un fichier
principal Main.c contenant la fonction main .
La compilation de ce projet se réalise au moyen des étapes suivantes :
1. obtenir les fichiers objets de chaque module ;
2. obtenir le fichier objet de Main.c ;

A.h gcc -c A.c A.h


A.o
A.c

B.h gcc -c B.c B.h


B.o
B.c

C.h gcc -c C.c C.h


C.o
C.c

gcc -c Main.c
Main.c Main.o

142 / 399
Compilation d’un projet de plusieurs fichiers
On suppose que l’on travaille sur un projet constitué de trois modules A , B et C et d’un fichier
principal Main.c contenant la fonction main .
La compilation de ce projet se réalise au moyen des étapes suivantes :
1. obtenir les fichiers objets de chaque module ;
2. obtenir le fichier objet de Main.c ;
3. lier les fichiers objets ainsi obtenus en un exécutable.

A.h gcc -c A.c A.h


A.o
A.c

B.h gcc -c B.c B.h


B.o
B.c gcc Main.o A.o B.o C.o
a.out
C.h gcc -c C.c C.h
C.o
C.c

gcc -c Main.c
Main.c Main.o

142 / 399
Création des fichiers objets

Pour compiler le projet, on commence par créer un fichier objet pour chaque module M . On
utilise pour cela la commande
gcc -c M.c M.h

143 / 399
Création des fichiers objets

Pour compiler le projet, on commence par créer un fichier objet pour chaque module M . On
utilise pour cela la commande
gcc -c M.c M.h

Cette commande est équivalente à


gcc -c M.c

car le fichier source M.c inclut le fichier d’en-tête M.h .

143 / 399
Création des fichiers objets

Pour compiler le projet, on commence par créer un fichier objet pour chaque module M . On
utilise pour cela la commande
gcc -c M.c M.h

Cette commande est équivalente à


gcc -c M.c

car le fichier source M.c inclut le fichier d’en-tête M.h .

On utilisera donc de préférence cette 2e commande.

143 / 399
Création des fichiers objets

Pour compiler le projet, on commence par créer un fichier objet pour chaque module M . On
utilise pour cela la commande
gcc -c M.c M.h

Cette commande est équivalente à


gcc -c M.c

car le fichier source M.c inclut le fichier d’en-tête M.h .

On utilisera donc de préférence cette 2e commande.

Chaque module est ainsi compilé séparément et dans un ordre quelconque.

143 / 399
Symboles non résolus

Rappel : pour compiler un module A , il n’est pas nécessaire que A ait connaissance des
définitions des symboles qu’il utilise.
Seules leurs déclarations sont suffisantes. Celles-ci se trouvent dans les fichiers d’en-tête
inclus dans A .

144 / 399
Symboles non résolus

Rappel : pour compiler un module A , il n’est pas nécessaire que A ait connaissance des
définitions des symboles qu’il utilise.
Seules leurs déclarations sont suffisantes. Celles-ci se trouvent dans les fichiers d’en-tête
inclus dans A .

On dit qu’un symbole n’est pas résolu à un stade donné de la compilation si sa définition n’est
pas encore connue.

144 / 399
Symboles non résolus

Rappel : pour compiler un module A , il n’est pas nécessaire que A ait connaissance des
définitions des symboles qu’il utilise.
Seules leurs déclarations sont suffisantes. Celles-ci se trouvent dans les fichiers d’en-tête
inclus dans A .

On dit qu’un symbole n’est pas résolu à un stade donné de la compilation si sa définition n’est
pas encore connue.

/* Fichier.c */ Par exemple, ce fichier permet de produire un fichier objet sur la com-
mande gcc -c Fichier.c même si le symbole g est non résolu pour le
int g(int x);
moment.
int f(int x) {
return g(x); Sa déclaration (dans le fichier lui-même ou dans un fichier inclus) est ce-
} pendant nécessaire (pour que le compilateur connaisse son prototype).

144 / 399
Résolution des symboles

Lors de l’édition des liens, un exécutable est créé. On utilise pour cela la commande
gcc Main.o A1.o ... An.o

dans le cadre d’un projet constitué des modules A1 , . . ., An et du module principal Main .

145 / 399
Résolution des symboles

Lors de l’édition des liens, un exécutable est créé. On utilise pour cela la commande
gcc Main.o A1.o ... An.o

dans le cadre d’un projet constitué des modules A1 , . . ., An et du module principal Main .

Cette étape lie à chaque symbole sa définition.

145 / 399
Résolution des symboles

Lors de l’édition des liens, un exécutable est créé. On utilise pour cela la commande
gcc Main.o A1.o ... An.o

dans le cadre d’un projet constitué des modules A1 , . . ., An et du module principal Main .

Cette étape lie à chaque symbole sa définition.

Tous les symboles utilisés dans le projet doivent être résolus (sinon, un message d’erreur est
produit et l’exécutable ne peut pas être construit).

145 / 399
Résolution des symboles — exemple

/∗ B. c ∗/ /∗ Main.c ∗/
/∗ A.h ∗/ /∗ A.c ∗/ /∗ B.h ∗/
#include "B.h" #include "B.h"
#ifndef __A__ #include "A.h" #ifndef __B__
#include "A.h" int main() {
#define __A__ int f(int x) { #define __B__
int g(int x) { g(5);
int f(int x); return x * x; int g(int x);
return f(x); return 0;
#endif } #endif
} }

146 / 399
Résolution des symboles — exemple

/∗ B. c ∗/ /∗ Main.c ∗/
/∗ A.h ∗/ /∗ A.c ∗/ /∗ B.h ∗/
#include "B.h" #include "B.h"
#ifndef __A__ #include "A.h" #ifndef __B__
#include "A.h" int main() {
#define __A__ int f(int x) { #define __B__
int g(int x) { g(5);
int f(int x); return x * x; int g(int x);
return f(x); return 0;
#endif } #endif
} }

Pour compiler ce projet, on emploie les commandes


gcc -c A.c

gcc -c B.c

gcc -c Main.c

gcc Main.o A.o B.o

146 / 399
Résolution des symboles — exemple

/∗ B. c ∗/ /∗ Main.c ∗/
/∗ A.h ∗/ /∗ A.c ∗/ /∗ B.h ∗/
#include "B.h" #include "B.h"
#ifndef __A__ #include "A.h" #ifndef __B__
#include "A.h" int main() {
#define __A__ int f(int x) { #define __B__
int g(int x) { g(5);
int f(int x); return x * x; int g(int x);
return f(x); return 0;
#endif } #endif
} }

Pour compiler ce projet, on emploie les commandes


gcc -c A.c

gcc -c B.c

gcc -c Main.c

gcc Main.o A.o B.o

L’ordre d’exécution des trois 1res commandes n’a aucune incidence sur le résultat produit.

146 / 399
Résolution des symboles — exemple

/∗ B. c ∗/ /∗ Main.c ∗/
/∗ A.h ∗/ /∗ A.c ∗/ /∗ B.h ∗/
#include "B.h" #include "B.h"
#ifndef __A__ #include "A.h" #ifndef __B__
#include "A.h" int main() {
#define __A__ int f(int x) { #define __B__
int g(int x) { g(5);
int f(int x); return x * x; int g(int x);
return f(x); return 0;
#endif } #endif
} }

Lors de la création de B.o , le compilateur ignore ce que fait le symbole f . Il sait seulement
(grâce à l’inclusion de A dans B ) que f est un symbole de fonction paramétrée par un entier et
renvoyant un entier et peut donc vérifier la correspondance des types.
C’est au moment de l’édition des liens que le compilateur va chercher l’implantation du
symbole f pour créer l’exécutable de la bonne manière.

Observation importante : la compilation d’un projet à plusieurs fichiers ne dépend pas de la


manière dont ses modules sont inclus les uns dans les autres. Le schéma de compilation est
toujours le même.

147 / 399
Plan

Compilation
Étapes de compilation
Compilation séparée
Makefile simples
Makefile avancés
Bibliothèques

148 / 399
Compilation séparée — intuition
Fait 1. : pour compiler un module A , il n’est pas nécessaire d’avoir les fichiers objets des autres
modules du projet dont A ne dépend pas (de manière étendue ou non).

149 / 399
Compilation séparée — intuition
Fait 1. : pour compiler un module A , il n’est pas nécessaire d’avoir les fichiers objets des autres
modules du projet dont A ne dépend pas (de manière étendue ou non).

Conséquence : si un module B est modifié, il n’est nécessaire de recompiler que B et


l’ensemble des modules qui dépendent (de manière étendue) à B .

149 / 399
Compilation séparée — intuition
Fait 1. : pour compiler un module A , il n’est pas nécessaire d’avoir les fichiers objets des autres
modules du projet dont A ne dépend pas (de manière étendue ou non).

Conséquence : si un module B est modifié, il n’est nécessaire de recompiler que B et


l’ensemble des modules qui dépendent (de manière étendue) à B .

Fait 2. : si A dépend de B et seul le fichier source de B a été modifié, il n’est pas nécessaire de
recompiler A .

149 / 399
Compilation séparée — intuition
Fait 1. : pour compiler un module A , il n’est pas nécessaire d’avoir les fichiers objets des autres
modules du projet dont A ne dépend pas (de manière étendue ou non).

Conséquence : si un module B est modifié, il n’est nécessaire de recompiler que B et


l’ensemble des modules qui dépendent (de manière étendue) à B .

Fait 2. : si A dépend de B et seul le fichier source de B a été modifié, il n’est pas nécessaire de
recompiler A .

Conséquence : on ne recompile A que si B.h a été modifié.

149 / 399
Compilation séparée — intuition
Fait 1. : pour compiler un module A , il n’est pas nécessaire d’avoir les fichiers objets des autres
modules du projet dont A ne dépend pas (de manière étendue ou non).

Conséquence : si un module B est modifié, il n’est nécessaire de recompiler que B et


l’ensemble des modules qui dépendent (de manière étendue) à B .

Fait 2. : si A dépend de B et seul le fichier source de B a été modifié, il n’est pas nécessaire de
recompiler A .

Conséquence : on ne recompile A que si B.h a été modifié.

Attention à ne pas oublier de recompiler le module principal Main si celui-ci dépend de manière
étendue à des modules modifiés.

149 / 399
Compilation séparée — exemple introductif
Considérons par exemple le projet suivant :

A gcc -c Main.c

gcc -c A.c
B Main
gcc -c B.c

C
gcc -c C.c

gcc Main.o A.o B.o C.o


On le compile pour la 1re fois par

150 / 399
Compilation séparée — exemple introductif
Considérons par exemple le projet suivant :

A gcc -c Main.c

gcc -c A.c
B Main
gcc -c B.c

C
gcc -c C.c

gcc Main.o A.o B.o C.o


On le compile pour la 1re fois par

Si on modifie par la suite le module A , il suffit d’exécuter les commandes

gcc -c A.c gcc Main.o A.o B.o C.o

gcc -c B.c pour mettre à jour l’exécutable du projet.


gcc -c Main.c Note : les 3 premières lignes commutent.

Il est inutile de recompiler C car il ne dépend pas (de manière étendue) à A .


150 / 399
Schéma opérationnel de la compilation d’un projet

À l’appui de cette observation, la compilation d’un projet s’organise de la manière suivante.

151 / 399
Schéma opérationnel de la compilation d’un projet

À l’appui de cette observation, la compilation d’un projet s’organise de la manière suivante.

(1) Pour chaque module A du projet :

151 / 399
Schéma opérationnel de la compilation d’un projet

À l’appui de cette observation, la compilation d’un projet s’organise de la manière suivante.

(1) Pour chaque module A du projet :


(a) compiler A si au moins l’une des conditions suivante est vérifiée :

151 / 399
Schéma opérationnel de la compilation d’un projet

À l’appui de cette observation, la compilation d’un projet s’organise de la manière suivante.

(1) Pour chaque module A du projet :


(a) compiler A si au moins l’une des conditions suivante est vérifiée :
I A.o n’existe pas ;

151 / 399
Schéma opérationnel de la compilation d’un projet

À l’appui de cette observation, la compilation d’un projet s’organise de la manière suivante.

(1) Pour chaque module A du projet :


(a) compiler A si au moins l’une des conditions suivante est vérifiée :
I A.o n’existe pas ;
I A.c ou A.h ont été modifiés après A.o ;

151 / 399
Schéma opérationnel de la compilation d’un projet

À l’appui de cette observation, la compilation d’un projet s’organise de la manière suivante.

(1) Pour chaque module A du projet :


(a) compiler A si au moins l’une des conditions suivante est vérifiée :
I A.o n’existe pas ;
I A.c ou A.h ont été modifiés après A.o ;
I il existe un module B dont A dépend (de manière étendue) et tel que B.h a été
modifié après A.o ;

151 / 399
Schéma opérationnel de la compilation d’un projet

À l’appui de cette observation, la compilation d’un projet s’organise de la manière suivante.

(1) Pour chaque module A du projet :


(a) compiler A si au moins l’une des conditions suivante est vérifiée :
I A.o n’existe pas ;
I A.c ou A.h ont été modifiés après A.o ;
I il existe un module B dont A dépend (de manière étendue) et tel que B.h a été
modifié après A.o ;
(2) si au moins un module du projet a été (re)compilé, reconstruire l’exécutable.

151 / 399
Schéma opérationnel de la compilation d’un projet

À l’appui de cette observation, la compilation d’un projet s’organise de la manière suivante.

(1) Pour chaque module A du projet :


(a) compiler A si au moins l’une des conditions suivante est vérifiée :
I A.o n’existe pas ;
I A.c ou A.h ont été modifiés après A.o ;
I il existe un module B dont A dépend (de manière étendue) et tel que B.h a été
modifié après A.o ;
(2) si au moins un module du projet a été (re)compilé, reconstruire l’exécutable.

Nous allons utiliser l’utilitaire make et les fichiers Makefile pour rendre cette procédure
automatique.

151 / 399
Plan

Compilation
Étapes de compilation
Compilation séparée
Makefile simples
Makefile avancés
Bibliothèques

152 / 399
Fichiers Makefile
L’utilitaire make est paramétré par un fichier dont le nom est imposé :

« Makefile » ou « makefile ».

Il doit se trouver dans le répertoire de travail (là où se trouvent les autres fichiers du projet ou au
niveau des répertoires include et src ).
Ce fichier fait partie intégrante du projet.

Tout fichier Makefile est constitué de règles. Elles sont de la forme

CIBLE: DEPENDANCES
→ COMMANDE
.
.
.
→ COMMANDE

Le symbole « → » désigne une tabulation.

153 / 399
Fichiers Makefile et fonctionnement
Considérons le projet et le Makefile suivants.

A 1 Prog: A.o B.o Main.o


2 gcc -o Prog Main.o A.o B.o
3 Main.o: Main.c A.h B.h
Main
4 gcc -c Main.c
5 A.o: A.c A.h
B 6 gcc -c A.c
7 B.o: B.c B.h
8 gcc -c B.c

Lorsque l’on exécute la commande make , make tente de résoudre la 1re règle (l. 1). Pour cela, il
doit résoudre ses dépendances. Ensuite, il exécute les commandes de la règle si la cible n’est pas à
jour.
Concrètement, pour pouvoir créer l’exécutable Prog , il est nécessaire que A.o , B.o et Main.o
soient à jour. Une fois qu’ils le sont, il suffit de procéder à l’édition des liens (l. 2).

154 / 399
Fichiers Makefile et fonctionnement

Pour savoir si une cible est à jour, make regarde les dépendances de la règle et :

155 / 399
Fichiers Makefile et fonctionnement

Pour savoir si une cible est à jour, make regarde les dépendances de la règle et :
I si la dépendance est la cible d’une autre règle, make procède récursivement à sa résolution ;

155 / 399
Fichiers Makefile et fonctionnement

Pour savoir si une cible est à jour, make regarde les dépendances de la règle et :
I si la dépendance est la cible d’une autre règle, make procède récursivement à sa résolution ;

I si la dépendance est le nom d’un fichier, make compare la date de dernière modification
de la cible par rapport à celle du fichier.

155 / 399
Fichiers Makefile et fonctionnement

Pour savoir si une cible est à jour, make regarde les dépendances de la règle et :
I si la dépendance est la cible d’une autre règle, make procède récursivement à sa résolution ;

I si la dépendance est le nom d’un fichier, make compare la date de dernière modification
de la cible par rapport à celle du fichier.

S’il y a au moins un fichier dans les dépendances avec une date supérieure à celle de la cible, les
commandes de la règle sont exécutées.

155 / 399
Fichiers Makefile et fonctionnement

Pour savoir si une cible est à jour, make regarde les dépendances de la règle et :
I si la dépendance est la cible d’une autre règle, make procède récursivement à sa résolution ;

I si la dépendance est le nom d’un fichier, make compare la date de dernière modification
de la cible par rapport à celle du fichier.

S’il y a au moins un fichier dans les dépendances avec une date supérieure à celle de la cible, les
commandes de la règle sont exécutées.

On peut imposer à make de commencer par la résolution de la règle dont la cible est CIBLE par

make CIBLE

155 / 399
Déclarations de types et dépendances
Soient A et B deux modules tels que B dépend (de manière étendue) à A , qu’un type T soit
déclaré dans A et que B utilise ce type.

Toute modification de A.h doit être suivie d’une nouvelle compilation de B . En effet, si la
déclaration de T a été modifiée, B doit être recompilé pour la prendre en compte.

En conséquence, dans le Makefile du projet doit figurer la règle

1 B.o: B.c B.h A.h pour déclarer que la construction de B.o


2 gcc -c B.c
dépend aussi de A.h .

Attention : ceci ne s’applique pas aux modifications de l’implantation des fonctions de A dans
A.c (comme nous l’avons déjà vu). La cible B.o ne dépend ainsi pas de A.c . Elle ne dépend que
des déclarations du module A (et donc de A.h ).

156 / 399
Exemple complet de Makefile
Piece IA
Le Makefile du projet dont le graphe
d’inclusions (étendues) est donné ci-contre est
Position Main

Case IGraph

1 Echecs: Main.o Piece.o Case.o Position.o IA.o IGraph.o


2 gcc -o Echecs Main.o Piece.o Case.o Position.o IA.o IGraph.o
3 Main.o: Main.c IA.h IGraph.h
4 gcc -c Main.c
5 Piece.o: Piece.c Piece.h
6 gcc -c Piece.c
7 Case.o: Case.c Case.h
8 gcc -c Case.c
9 Position.o: Position.c Position.h Piece.h Case.h
10 gcc -c Position.c
11 IA.o: IA.c IA.h Piece.h Case.h Position.h
12 gcc -c IA.c
13 IGraph.o: IGraph.c IGraph.h Piece.h Case.h Position.h
14 gcc -c IGraph.c
157 / 399
Écriture de Makefile simples — résumé
Le Makefile d’un projet contenant des modules A1 , . . ., An et un module principal Main est
génériquement de la forme
1 NOM: Main.o A1.o ... An.o
2 gcc -o NOM Main.o A1.o ... An.o
3
4 Main.o: Main.c EXTRAmain
5 gcc -c Main.c
6
7 A1.o: A1.c A1.h EXTRA1
8 gcc -c A1.c
9
10 ...
11
12 An.o: An.c An.h EXTRAn
13 gcc -c An.c

où EXTRAmain est la suite des noms des fichiers .h que Main.c inclut et pour tout 16k6n ,
EXTRAk est la suite des noms des fichiers .h dont le module Ak dépend (de manière étendue).

158 / 399
Plan

Compilation
Étapes de compilation
Compilation séparée
Makefile simples
Makefile avancés
Bibliothèques

159 / 399
Variables dans les Makefile

Il est possible de définir des variables dans un Makefile par

ID=VAL

Ceci définit une variable identifiée par ID . Sa valeur est la chaîne de caractères VAL .

On accède à la valeur d’une variable identifiée par ID par

$(ID)

Ceci substitue à l’occurrence de $(ID) la chaîne de caractères qui lui a été attribuée lors de sa
définition.

160 / 399
Variables dans les Makefile — exemple
Les variables permettent de factoriser les règles d’un Makefile :

1 Main: Main.o A.o


2 gcc -o Main Main.o A.o -ansi -pedantic -Wall
3
4 Main.o: Main.c
5 gcc -c Main.c -ansi -pedantic -Wall
6
7 A.o: A.c A.h
8 gcc -c A.c -ansi -pedantic -Wall

s’écrit plus simplement par


1 CFLAGS=-ansi -pedantic -Wall
2
3 Main: Main.o A.o
4 gcc -o Main Main.o A.o $(CFLAGS)
5
6 Main.o: Main.c
7 gcc -c Main.c $(CFLAGS)
8
9 A.o: A.c A.h
10 gcc -c A.c $(CFLAGS)
161 / 399
Variables dans les Makefile
On utilise en général les noms de variable suivants :
I CFLAGS pour les options de compilation, p.ex.,
CFLAGS=-ansi -pedantic -Wall

I LDFLAGS pour l’inclusion de bibliothèques, p.ex.,


LDFLAGS=-lm -lMLV

I CC pour le compilateur utilisé, p.ex.,


CC=gcc ou bien CC=colorgcc

I OPT pour les option d’optimisation de code


OPT=-O1 ou bien OPT=-O2 ou encore OPT=-O3

162 / 399
Règles courantes

Observation : la plupart des règles des Makefile sont sous l’une de ces deux formes :

M.o: M.c DEP2... DEPn


1. → gcc -c M.c

EXEC: DEP1 ... DEPn


2. → gcc -o EXEC DEP1 ... DEPn

La 1re forme de règle a pour but de construire le fichier objet d’un module M . Dans ce cas, DEP2 ,
. . ., DEPn sont les .h dont le module M dépend.

La 2e forme de règle a pour but de construire l’exécutable EXEC du projet. Les dépendances DEP1 ,
. . ., DEPn sont dans ce cas les fichiers .o du projet.

163 / 399
Variables internes dans les Makefile
Il est possible de simplifier l’écriture de ces règles courantes au moyen des variables internes. Il y
en a trois principales et deux secondaires :
Symbole Ce qu’il désigne
$@ Nom de la cible
$< Nom de la 1re dép.
$^ Noms de toutes les dép.
$? Noms des dép. plus récentes que la cible
$* Nom de la cible sans extension
En les utilisant, les deux règles précédentes deviennent
M.o: M.c DEP2 ... DEPn
M.o: M.c DEP2 ... DEPn
−→ gcc -c $<
gcc -c M.c

EXEC: DEP1 ... DEPn


EXEC: DEP1 ... DEPn
−→ gcc -o $@ $^
gcc -o EXEC DEP1 ... DEPn

164 / 399
Variables internes dans les Makefile — exemple
Piece IA L’utilisation des variables et variables internes
Position Main
permet de simplifier les Makefile .

Case IGraph

1 CC=colorgcc 16 Case.o: Case.c Case.h


2 CFLAGS=-ansi -pedantic -Wall 17 $(CC) -c $< $(CFLAGS)
3 OBJ=Main.o Piece.o Case.o 18
4 Position.o IA.o IGraph.o 19 Position.o: Position.c Position.h
5 20 Piece.h Case.h
6 Echecs: $(OBJ) 21 $(CC) -c $< $(CFLAGS)
7 $(CC) -o $@ $^ $(CFLAGS) 22
8 23 IA.o: IA.c IA.h
9 24 Piece.h Case.h Position.h
10 Main.o: Main.c IA.h IGraph.h 25 $(CC) -c $< $(CFLAGS)
11 $(CC) -c $< $(CFLAGS) 26
12 27 IGraph.o: IGraph.c IGraph.h
13 Piece.o: Piece.c Piece.h 28 Piece.h Case.h Position.h
14 $(CC) -c $< $(CFLAGS) 29 $(CC) -c $< $(CFLAGS)
15

165 / 399
Règles génériques
Il est possible de simplifier encore d’avantage l’écriture des Makefile au moyen des règles
génériques.
Ce sont des règles de la forme
%.o: %.c
→ COMMANDE

où COMMANDE est une commande.


Cette syntaxe simule une règle
M.o: M.c
→ COMMANDE

pour chaque fichier M.c du projet.

Intérêt principal : l’unique règle


%.o: %.c
gcc -c $<

permet de construire chaque fichier objet du projet.


166 / 399
Règles génériques

Attention : la règle
%.o: %.c
gcc -c $<

ne prend pas en compte des dépendances des modules aux fichiers .h concernés.

Il faut les mentionner explicitement de la manière suivante :


M.o: M.c M.h DEP1 ... DEPn

pour chaque module M du projet. DEP1 , . . ., DEPn sont les .h dont le module M dépend.

167 / 399
Règles génériques — exemple
Piece IA L’utilisation des règles génériques permet de
Position Main
simplifier encore les Makefile .

Case IGraph

1 CC=colorgcc 15
2 CFLAGS=-ansi -pedantic -Wall 16 Position.o: Position.c Position.h
3 OBJ=Main.o Piece.o Case.o 17 Piece.h Case.h
4 Position.o IA.o IGraph.o 18
5 19 IA.o: IA.c IA.h
6 Echecs: $(OBJ) 20 Piece.h Case.h Position.h
7 $(CC) -o $@ $^ $(CFLAGS) 21
8 22 IGraph.o: IGraph.c IGraph.h
9 23 Piece.h Case.h Position.h
10 Main.o: Main.c IA.h IGraph.h 24
11 25 %.o: %.c
12 Piece.o: Piece.c Piece.h 26 $(CC) -c $< $(CFLAGS)
13
14 Case.o: Case.c Case.h

168 / 399
Règles de nettoyage

Règle de nettoyage :

1 clean: commande make clean , de supprimer les


2 rm -f *.o
fichiers .o du répertoire courant.
Cette règle permet, lorsque l’on saisit la

Règle de nettoyage total :

1 mrproper: clean fichiers regénérables (c.-à-d. les fichiers .o


2 rm -f EXEC
et l’exécutable) à partir des fichiers .c et .h
Cette règle, où EXEC est le nom de l’exécutable du projet.
du projet, permet de supprimer tous les

169 / 399
Règles d’installation / désinstallation

Règle d’installation :

1 install: EXEC Cette règle permet de compiler le projet, de


2 mkdir ../bin placer son exécutable EXEC dans un répertoire
3 mv EXEC ../bin/EXEC
4 make mrproper bin et de supprimer les fichiers regénérables.

Règle de désinstallation :

1 uninstall: mrproper Cette règle permet de supprimer les fichiers


2 rm -f ../bin/EXEC regénérables, l’exécutable du projet, ainsi que
3 rm -rf ../bin
le répertoire bin le contenant.

170 / 399
Plan

Compilation
Étapes de compilation
Compilation séparée
Makefile simples
Makefile avancés
Bibliothèques

171 / 399
Principes généraux

Une bibliothèque est un regroupement de modules offrant des fonctionnalités allant vers un
même objectif.

172 / 399
Principes généraux

Une bibliothèque est un regroupement de modules offrant des fonctionnalités allant vers un
même objectif.

Par exemple, la bibliothèque graphique MLV est un ensemble de plusieurs modules ( MLV_audio ,
MLV_keyboard , MLV_image , etc.) réunis dans le but d’offrir des fonctions d’affichage graphique et de
gérer les entrées / sorties (son, clavier, souris, etc.).

172 / 399
Principes généraux

Une bibliothèque est un regroupement de modules offrant des fonctionnalités allant vers un
même objectif.

Par exemple, la bibliothèque graphique MLV est un ensemble de plusieurs modules ( MLV_audio ,
MLV_keyboard , MLV_image , etc.) réunis dans le but d’offrir des fonctions d’affichage graphique et de
gérer les entrées / sorties (son, clavier, souris, etc.).

L’intérêt des bibliothèques est double :


1. il suffit de réaliser une inclusion d’un seul en-tête pour bénéficier des fonctionnalités d’une
bibliothèque, plutôt que des inclusions de plusieurs en-têtes sans être sûr du fichier précis à
inclure ;

172 / 399
Principes généraux

Une bibliothèque est un regroupement de modules offrant des fonctionnalités allant vers un
même objectif.

Par exemple, la bibliothèque graphique MLV est un ensemble de plusieurs modules ( MLV_audio ,
MLV_keyboard , MLV_image , etc.) réunis dans le but d’offrir des fonctions d’affichage graphique et de
gérer les entrées / sorties (son, clavier, souris, etc.).

L’intérêt des bibliothèques est double :


1. il suffit de réaliser une inclusion d’un seul en-tête pour bénéficier des fonctionnalités d’une
bibliothèque, plutôt que des inclusions de plusieurs en-têtes sans être sûr du fichier précis à
inclure ;
2. sous réserve de savoir comment créer des bibliothèques, il est possible de partager et
réutiliser son propre code entre plusieurs de ses projets, sans avoir à le recompiler.

172 / 399
Bibliothèques statiques

Une bibliothèque statique est un fichier d’extension .a .

173 / 399
Bibliothèques statiques

Une bibliothèque statique est un fichier d’extension .a .

Lors de son utilisation, son code est inclus dans l’exécutable pendant l’édition des liens.

173 / 399
Bibliothèques statiques

Une bibliothèque statique est un fichier d’extension .a .

Lors de son utilisation, son code est inclus dans l’exécutable pendant l’édition des liens.

I Avantage : tout projet qui utilise une bibliothèque statique peut être exécuté sur une
machine où la bibliothèque n’est pas installée.

173 / 399
Bibliothèques statiques

Une bibliothèque statique est un fichier d’extension .a .

Lors de son utilisation, son code est inclus dans l’exécutable pendant l’édition des liens.

I Avantage : tout projet qui utilise une bibliothèque statique peut être exécuté sur une
machine où la bibliothèque n’est pas installée.
I Inconvénient : l’exécutable est plus volumineux.

173 / 399
Bibliothèques dynamiques

Une bibliothèque dynamique est un fichier d’extension .so .

174 / 399
Bibliothèques dynamiques

Une bibliothèque dynamique est un fichier d’extension .so .

Lors de son utilisation, son code n’est pas inclus dans l’exécutable. C’est lors de l’exécution que
les symboles provenant de la bibliothèque sont résolus au moyen de l’éditeur de liens dynamique.

174 / 399
Bibliothèques dynamiques

Une bibliothèque dynamique est un fichier d’extension .so .

Lors de son utilisation, son code n’est pas inclus dans l’exécutable. C’est lors de l’exécution que
les symboles provenant de la bibliothèque sont résolus au moyen de l’éditeur de liens dynamique.

I Avantages : l’exécutable est moins volumineux. Il n’y a pas de duplication du code de la


bibliothèque sur un même système si plusieurs projets l’utilisent.

174 / 399
Bibliothèques dynamiques

Une bibliothèque dynamique est un fichier d’extension .so .

Lors de son utilisation, son code n’est pas inclus dans l’exécutable. C’est lors de l’exécution que
les symboles provenant de la bibliothèque sont résolus au moyen de l’éditeur de liens dynamique.

I Avantages : l’exécutable est moins volumineux. Il n’y a pas de duplication du code de la


bibliothèque sur un même système si plusieurs projets l’utilisent.
I Inconvénient : tout projet qui utilise une bibliothèque dynamique ne peut être exécuté que
sur un système où cette dernière est installée.

174 / 399
Compilation d’un projet utilisant des bibliothèques
On suppose que l’on travaille sur un projet constitué de deux modules A et B et d’un fichier
principal Main.c . Ce projet utilise deux bibliothèques statiques : libm.a et libMLV.a .

175 / 399
Compilation d’un projet utilisant des bibliothèques
On suppose que l’on travaille sur un projet constitué de deux modules A et B et d’un fichier
principal Main.c . Ce projet utilise deux bibliothèques statiques : libm.a et libMLV.a .
La compilation de ce projet se réalise de manière habituelle. La différence porte sur l’étape
d’édition des liens dans laquelle il est nécessaire de signaler les bibliothèques utilisées.

175 / 399
Compilation d’un projet utilisant des bibliothèques
On suppose que l’on travaille sur un projet constitué de deux modules A et B et d’un fichier
principal Main.c . Ce projet utilise deux bibliothèques statiques : libm.a et libMLV.a .
La compilation de ce projet se réalise de manière habituelle. La différence porte sur l’étape
d’édition des liens dans laquelle il est nécessaire de signaler les bibliothèques utilisées.
libm.a libMLV.a
A.h gcc -c A.c
A.o
A.c

B.h gcc -c B.c


B.o gcc Main.o A.o B.o -lm -lMLV a.out
B.c

gcc -c Main.c
Main.c Main.o

175 / 399
Compilation d’un projet utilisant des bibliothèques
On suppose que l’on travaille sur un projet constitué de deux modules A et B et d’un fichier
principal Main.c . Ce projet utilise deux bibliothèques statiques : libm.a et libMLV.a .
La compilation de ce projet se réalise de manière habituelle. La différence porte sur l’étape
d’édition des liens dans laquelle il est nécessaire de signaler les bibliothèques utilisées.
libm.a libMLV.a
A.h gcc -c A.c
A.o
A.c

B.h gcc -c B.c


B.o gcc Main.o A.o B.o -lm -lMLV a.out
B.c

gcc -c Main.c
Main.c Main.o

Pour utiliser des bibliothèques qui ne sont pas situées dans le répertoire standard des
bibliothèques, on précisera leur chemin chem lors de l’édition des liens au moyen de l’option
-Lchem .

175 / 399
Compilation d’un projet utilisant des bibliothèques
Le nom d’une bibliothèque commence par lib . On signale l’utilisation d’une bibliothèque à
l’éditeur de liens par -lNOM où libNOM est le nom de la bibliothèque.

En supposant que le graphe d’inclusions (étendues) du A

Main
projet précédent est celui ci-contre, un Makefile possible B
est
1 CC=colorgcc 18 clean:
2 CFLAGS=-ansi -pedantic -Wall 19 rm -f *.o
3 LDFLAGS=-lm -lncurses 20
4 OBJ=Main.o A.o B.o 21 mrproper: clean
5 22 rm -f Projet
6 Projet: $(OBJ) 23
7 $(CC) -o $@ $^ $(CFLAGS) $(LDFLAGS) 24 install: Projet
8 25 mkdir ../bin
9 Main.o: Main.c A.h B.h 26 mv Projet ../bin/Projet
10 27 make mrproper
11 A.o: A.c A.h 28
12 29 uninstall: mrproper
13 B.o: B.c B.h A.h 30 rm -f ../bin/Projet
14 31 rm -rf ../bin
15 %.o: %.c
16 $(CC) -c $^ $(CFLAGS)
17

176 / 399
La bibliothèque standard
La bibliothèque standard libc.a ( libc.so ) regroupe les vingt-quatre modules

assert , complex , ctype , errno ,


fenv , float , inttypes , iso646 ,
limits , locale , math , setjmp ,
signal , stdarg , stdbool , stddef ,
stdint , stdio , stdlib , string ,
tgmath , time , wchar , wctype .

177 / 399
La bibliothèque standard
La bibliothèque standard libc.a ( libc.so ) regroupe les vingt-quatre modules

assert , complex , ctype , errno ,


fenv , float , inttypes , iso646 ,
limits , locale , math , setjmp ,
signal , stdarg , stdbool , stddef ,
stdint , stdio , stdlib , string ,
tgmath , time , wchar , wctype .

Cette bibliothèque est liée implicitement lors de toute édition des liens.

177 / 399
La bibliothèque standard
La bibliothèque standard libc.a ( libc.so ) regroupe les vingt-quatre modules

assert , complex , ctype , errno ,


fenv , float , inttypes , iso646 ,
limits , locale , math , setjmp ,
signal , stdarg , stdbool , stddef ,
stdint , stdio , stdlib , string ,
tgmath , time , wchar , wctype .

Cette bibliothèque est liée implicitement lors de toute édition des liens.
Il est donc possible d’utiliser certains des modules de la bibliothèque standard simplement en les
incluant ( #include <NOM.h> ), sans avoir à utiliser l’option -lc .

177 / 399
La bibliothèque standard
La bibliothèque standard libc.a ( libc.so ) regroupe les vingt-quatre modules

assert , complex , ctype , errno ,


fenv , float , inttypes , iso646 ,
limits , locale , math , setjmp ,
signal , stdarg , stdbool , stddef ,
stdint , stdio , stdlib , string ,
tgmath , time , wchar , wctype .

Cette bibliothèque est liée implicitement lors de toute édition des liens.
Il est donc possible d’utiliser certains des modules de la bibliothèque standard simplement en les
incluant ( #include <NOM.h> ), sans avoir à utiliser l’option -lc .

L’utilisation de certains modules doit cependant s’accompagner de l’option -lX (comme -lm

pour utiliser math.h ).

177 / 399
Création de bibliothèques statiques
Pour créer une bibliothèque statique X , on suit les étapes suivantes :
1. écriture des modules M1 , . . ., Mn qui vont constituer la bibliothèque ;

2. compilation des modules et création des fichiers objets M1.o , . . ., Mn.o ;

3. création de l’archive libX.a par


ar r libX.a M1.o ... Mn.o

4. génération de l’index de la bibliothèque par


ranlib libX.a

5. (étape facultative) écriture d’un fichier d’en-tête global


/∗ X.h ∗/
#ifndef __X__
#define __X__

#include "M1.h"
...
#include "Mn.h"

#endif
178 / 399
Création de bibliothèques statiques — exemple

On suppose que l’on a créé trois modules :


1. Liste pour la représentation des listes chaînées ;

2. Arbre pour la représentation des arbres binaires de recherche ;

3. Tri pour l’implantation d’algorithmes de tris de tableaux.

179 / 399
Création de bibliothèques statiques — exemple

On suppose que l’on a créé trois modules :


1. Liste pour la représentation des listes chaînées ;

2. Arbre pour la représentation des arbres binaires de recherche ;

3. Tri pour l’implantation d’algorithmes de tris de tableaux.

On souhaite regrouper ces modules en une bibliothèque nommée algo .

179 / 399
Création de bibliothèques statiques — exemple

On suppose que l’on a créé trois modules :


1. Liste pour la représentation des listes chaînées ;

2. Arbre pour la représentation des arbres binaires de recherche ;

3. Tri pour l’implantation d’algorithmes de tris de tableaux.

On souhaite regrouper ces modules en une bibliothèque nommée algo .

Celle-ci sera constituée de deux fichiers ;


1. libalgo.a , l’implantation de la bibliothèque ;

2. Algo.h , l’en-tête de la bibliothèque.

179 / 399
Création de bibliothèques statiques — exemple
Pour créer de la bibliothèque algo , on saisit les commandes
gcc -c Liste.c ; gcc -c Arbre.c ; gcc -c Tri.c

ar r libalgo.a Liste.o Arbre.o Tri.o

ranlib libalgo.a

et on écrit l’en-tête global

/∗ Algo . h ∗/
#ifndef __ALGO__
#define __ALGO__

#include "Liste.h"
#include "Arbre.h"
#include "Tri.h"

#endif

Pour utiliser la bibliothèque algo dans un fichier F d’un projet P , il faut inclure dans F son
en-tête, réaliser l’édition des liens de P avec l’option -lalgo et indiquer son chemin chem avec
l’option -Lchem .
180 / 399
Index
Il est possible de consulter l’index d’une bibliothèque statique LIB par la commande

nm -s libLIB.a

/∗ B.h ∗/ /∗ B. c ∗/
#ifndef __B__ #include "B.h"
/∗ A.h ∗/ /∗ A.c ∗/
#define __B__ int f(S s) {
#ifndef __A__ #include "A.h"
typedef int S; return s;
#define __A__ char h(int a) {
}
char h(int a); return a % 256;
int f(S s); char g(int a) {
#endif }
char g(int a); return a % 64;
#endif }

Création :
Indexe de l’archive:
gcc -c A.c ; gcc -c B.c h in A.o
f in B.o
g in B.o
ar r libAB.a A.o B.o
A.o:
ranlib libAB.a 0000000000000000 T h

B.o:
Affichage : 0000000000000000 T f
000000000000000c T g
nm -s libAB.a

181 / 399
Résumé de l’axe 1

Nous avons étudié plusieurs outils et posé des conventions pour le développement de projets.

Tout ce qui a été étudié dans cet axe est à appliquer dans la pratique.

182 / 399
Résumé de l’axe 1

Nous avons étudié plusieurs outils et posé des conventions pour le développement de projets.
1. Au niveau de la présentation du code :
I indentation ;
I choix des identificateurs ;
I documentation.

Tout ce qui a été étudié dans cet axe est à appliquer dans la pratique.

182 / 399
Résumé de l’axe 1

Nous avons étudié plusieurs outils et posé des conventions pour le développement de projets.
1. Au niveau de la présentation du code :
I indentation ;
I choix des identificateurs ;
I documentation.

2. Au niveau de l’écriture de fonctions :


I pré-assertions ;
I mécanismes de gestion d’erreurs.

Tout ce qui a été étudié dans cet axe est à appliquer dans la pratique.

182 / 399
Résumé de l’axe 1

Nous avons étudié plusieurs outils et posé des conventions pour le développement de projets.
1. Au niveau de la présentation du code :
I indentation ;
I choix des identificateurs ;
I documentation.

2. Au niveau de l’écriture de fonctions :


I pré-assertions ;
I mécanismes de gestion d’erreurs.

3. Au niveau de la conception de projets :


I analyse d’une spécification ;
I découpage en modules ;
I compilation séparée.

Tout ce qui a été étudié dans cet axe est à appliquer dans la pratique.

182 / 399
Axe 2 : comprendre les mécanismes de base
Allocation dynamique

Entrées et sorties

Types

Types structurés

183 / 399
Plan

Allocation dynamique
Pointeurs
Passage par adresse
Allocation dynamique
Tableaux dynamiques

184 / 399
Plan

Allocation dynamique
Pointeurs
Passage par adresse
Allocation dynamique
Tableaux dynamiques

185 / 399
La mémoire
Lors de l’exécution d’un programme, une portion (de taille variable) de la mémoire lui est dédiée.
Cette zone lui est exclusive. On l’appelle la mémoire.

186 / 399
La mémoire
Lors de l’exécution d’un programme, une portion (de taille variable) de la mémoire lui est dédiée.
Cette zone lui est exclusive. On l’appelle la mémoire.
On peut voir la mémoire comme un très grand tableau d’octets.

186 / 399
La mémoire
Lors de l’exécution d’un programme, une portion (de taille variable) de la mémoire lui est dédiée.
Cette zone lui est exclusive. On l’appelle la mémoire.
On peut voir la mémoire comme un très grand tableau d’octets.

La mémoire est segmentée en plusieurs parties :


Zone statique

I la zone statique qui contient le code et les


Tas
données statiques ;

I le tas, de taille variable au fil de l’exécution ; ...

I la pile, de taille variable au fil de l’exécution. Pile

Autre
Il y a d’autres zones (non repr. ici).

186 / 399
La mémoire
Lors de l’exécution d’un programme, une portion (de taille variable) de la mémoire lui est dédiée.
Cette zone lui est exclusive. On l’appelle la mémoire.
On peut voir la mémoire comme un très grand tableau d’octets.

La mémoire est segmentée en plusieurs parties :


Zone statique

I la zone statique qui contient le code et les


Tas
données statiques ;

I le tas, de taille variable au fil de l’exécution ; ...

I la pile, de taille variable au fil de l’exécution. Pile

Autre
Il y a d’autres zones (non repr. ici).

Le tas contient les variables allouées dynamiquement.


La pile contient les variables locales lors des appels aux fonctions.
186 / 399
Pointeurs

Nous avons vu que chaque variable possède une adresse.

Connaître l’adresse d’une variable est suffisant pour la manipuler (c.-à-d., lire et modifier sa
valeur). L’objet qui permet de représenter et de manipuler des adresses est le pointeur.

187 / 399
Pointeurs

Nous avons vu que chaque variable possède une adresse.

Connaître l’adresse d’une variable est suffisant pour la manipuler (c.-à-d., lire et modifier sa
valeur). L’objet qui permet de représenter et de manipuler des adresses est le pointeur.

Un pointeur est une entité constituée des deux éléments suivants :


1. une adresse vers une zone de la mémoire ;
2. un type.

187 / 399
Pointeurs
Intuitivement, un pointeur est une flèche munie d’un mode d’emploi, le tout pointant vers une
zone de la mémoire.
Le mode d’emploi décrit le type de la zone de la mémoire ainsi adressée en renseignant sur la
taille de la zone.

188 / 399
Pointeurs
Intuitivement, un pointeur est une flèche munie d’un mode d’emploi, le tout pointant vers une
zone de la mémoire.
Le mode d’emploi décrit le type de la zone de la mémoire ainsi adressée en renseignant sur la
taille de la zone.

Si ptr_1 est un pointeur sur une donnée de


type int (4 octets) :

1000

1001

1002

ptr_1 1003
valeur : 1002
type : int * 1004

1005

1006

188 / 399
Pointeurs
Intuitivement, un pointeur est une flèche munie d’un mode d’emploi, le tout pointant vers une
zone de la mémoire.
Le mode d’emploi décrit le type de la zone de la mémoire ainsi adressée en renseignant sur la
taille de la zone.

Si ptr_1 est un pointeur sur une donnée de Si ptr_2 est un pointeur sur une donnée de
type int (4 octets) : type short (2 octets) :

1000 1000

1001 1001

1002 1002

ptr_1 1003 ptr_2 1003


valeur : 1002 valeur : 1002
type : int * 1004 type : short * 1004

1005 1005

1006 1006

188 / 399
Déclaration de pointeurs

On déclare un pointeur sur une zone de la mémoire de type T par

T *ptr;

189 / 399
Déclaration de pointeurs

On déclare un pointeur sur une zone de la mémoire de type T par

T *ptr;

On accède à la valeur de la zone mémoire pointée par ptr par

*ptr

189 / 399
Déclaration de pointeurs

On déclare un pointeur sur une zone de la mémoire de type T par

T *ptr;

On accède à la valeur de la zone mémoire pointée par ptr par

*ptr

On accède à l’adresse de la zone mémoire pointée par ptr par

ptr

189 / 399
Déclaration de pointeurs

On déclare un pointeur sur une zone de la mémoire de type T par

T *ptr;

On accède à la valeur de la zone mémoire pointée par ptr par

*ptr

On accède à l’adresse de la zone mémoire pointée par ptr par

ptr

Attention : le même symbole * est utilisé pour la déclaration d’un pointeur et pour accéder à la
valeur pointée. C’est une imperfection du langage C qui peut porter à confusion.

189 / 399
Manipulation de pointeurs
On suppose que ptr est un pointeur pointant sur une zone de la mémoire de type T .

190 / 399
Manipulation de pointeurs
On suppose que ptr est un pointeur pointant sur une zone de la mémoire de type T .

Il est possible de changer l’endroit où ptr pointe par

ptr = ADR;

où ADR est une adresse de la mémoire de type T .

190 / 399
Manipulation de pointeurs
On suppose que ptr est un pointeur pointant sur une zone de la mémoire de type T .

Il est possible de changer l’endroit où ptr pointe par

ptr = ADR;

où ADR est une adresse de la mémoire de type T .

Il est possible d’affecter une valeur VAL de type T à *ptr par

*ptr = VAL;

De cette manière, la zone mémoire d’adresse ptr est modifiée et devient de valeur VAL .

190 / 399
Manipulation de pointeurs — exemple

× ×
1 /∗ (1) ∗/
2 int num1, num2; 10 20 ? ?
3 int *ptr1, *ptr2;
4 num1 = 10; num_1 num_2 ptr_1 ptr_2

5 num2 = 20;
6
7 /∗ (2) ∗/
8 ptr1 = &num1; 100 200 &num_1 &num_2

9 ptr2 = &num2;
10 *ptr1 = 100; num_1 num_2 ptr_1 ptr_2

11 *ptr2 = 200;
12
13 /∗ (3) ∗/
14 ptr2 = ptr1;
15 *ptr1 = 300; 400 200 &num_1 &num_1

16 *ptr2 = 400;
num_1 num_2 ptr_1 ptr_2

191 / 399
Pointeurs et tableaux statiques
Un tableau est un pointeur vers le 1er élément qui le constitue. Les autres éléments du tableau
sont contigus en mémoire et situés à des adresses plus élevées.

192 / 399
Pointeurs et tableaux statiques
Un tableau est un pointeur vers le 1er élément qui le constitue. Les autres éléments du tableau
sont contigus en mémoire et situés à des adresses plus élevées.
On déclare un tableau statique de taille N de valeurs de type T par

T tab[N];

1 octet

tab ... ... ... ... ...

tab[0] tab[1] tab[2] tab[N - 1]

192 / 399
Pointeurs et tableaux statiques
Un tableau est un pointeur vers le 1er élément qui le constitue. Les autres éléments du tableau
sont contigus en mémoire et situés à des adresses plus élevées.
On déclare un tableau statique de taille N de valeurs de type T par

T tab[N];

1 octet

tab ... ... ... ... ...

tab[0] tab[1] tab[2] tab[N - 1]

On accède à la valeur du i e élément de tab par

tab[i]

192 / 399
Pointeurs et tableaux statiques
Un tableau est un pointeur vers le 1er élément qui le constitue. Les autres éléments du tableau
sont contigus en mémoire et situés à des adresses plus élevées.
On déclare un tableau statique de taille N de valeurs de type T par

T tab[N];

1 octet

tab ... ... ... ... ...

tab[0] tab[1] tab[2] tab[N - 1]

On accède à la valeur du i e élément de tab par

tab[i]

On affecte une valeur VAL de type T en i e position dans tab par

tab[i] = VAL;

192 / 399
Pointeurs et tableaux statiques

Sachant qu’un tableau tab est un pointeur et que ses éléments sont contigus en mémoire, la
syntaxe
tab[i]

est équivalente à
*(tab + i)

193 / 399
Pointeurs et tableaux statiques

Sachant qu’un tableau tab est un pointeur et que ses éléments sont contigus en mémoire, la
syntaxe
tab[i]

est équivalente à
*(tab + i)

Le type du pointeur tab permet de faire un décalage correct en fonction de la taille en mémoire
de ses éléments.

193 / 399
Pointeurs et tableaux statiques

Sachant qu’un tableau tab est un pointeur et que ses éléments sont contigus en mémoire, la
syntaxe
tab[i]

est équivalente à
*(tab + i)

Le type du pointeur tab permet de faire un décalage correct en fonction de la taille en mémoire
de ses éléments.
En effet, si ptr est un pointeur pointant sur une zone mémoire de type T , la valeur de
l’expression ptr + i dépend de la taille nécessaire pour représenter une valeur de type T (c.-à-d.
de sizeof(T) ).

193 / 399
Pointeurs et tableaux statiques
Considérons les instructions suivantes :

int tab[2];
char *ptr;

tab[0] = 300;
tab[1] = 60;

printf("%p %p\n",
tab + 0, tab + 1);
printf("%d %d\n",
tab[0], tab[1]);

ptr = (char *) tab;


printf("%p %p\n",
ptr + 0, ptr + 1);
printf("%d %d\n",
ptr[0], ptr[1]);

194 / 399
Pointeurs et tableaux statiques
Considérons les instructions suivantes :

int tab[2]; Elles affichent


char *ptr; 0x7fffc38b5d60 0x7fffc38b5d64
300 60
0x7fffc38b5d60 0x7fffc38b5d61
tab[0] = 300; 44 1
tab[1] = 60;

printf("%p %p\n",
tab + 0, tab + 1);
printf("%d %d\n",
tab[0], tab[1]);

ptr = (char *) tab;


printf("%p %p\n",
ptr + 0, ptr + 1);
printf("%d %d\n",
ptr[0], ptr[1]);

194 / 399
Pointeurs et tableaux statiques
Considérons les instructions suivantes :

int tab[2]; Elles affichent


char *ptr; 0x7fffc38b5d60 0x7fffc38b5d64
300 60
0x7fffc38b5d60 0x7fffc38b5d61
tab[0] = 300; 44 1
tab[1] = 60;
Le pointeur ptr interprète les éléments du tableau tab
printf("%p %p\n",
comme des valeurs de taille 1 octet (= sizeof(char) ).
tab + 0, tab + 1);
printf("%d %d\n",
tab[0], tab[1]);
ptr + 0 ptr + 1

ptr = (char *) tab; tab + 0 tab + 1 1 octet


printf("%p %p\n",
ptr + 0, ptr + 1);
printf("%d %d\n",
tab[0] tab[1]
ptr[0], ptr[1]);

194 / 399
Plan

Allocation dynamique
Pointeurs
Passage par adresse
Allocation dynamique
Tableaux dynamiques

195 / 399
Passage par valeur — rappel
Nous savons qu’il est impossible de modifier la valeur d’un argument passé à une fonction car
celle-ci travaille sur une copie de sa valeur et non pas sur l’éventuelle variable figurant en
argument.

196 / 399
Passage par valeur — rappel
Nous savons qu’il est impossible de modifier la valeur d’un argument passé à une fonction car
celle-ci travaille sur une copie de sa valeur et non pas sur l’éventuelle variable figurant en
argument.

void incrementer(int nb) { printf("%d\n", num);


nb = nb + 1;
} Ces instructions affichent 5 .
...
num = 5;
incrementer(num);

196 / 399
Passage par valeur — rappel
Nous savons qu’il est impossible de modifier la valeur d’un argument passé à une fonction car
celle-ci travaille sur une copie de sa valeur et non pas sur l’éventuelle variable figurant en
argument.

void incrementer(int nb) { printf("%d\n", num);


nb = nb + 1;
} Ces instructions affichent 5 .
...
num = 5; En ligne 6, c’est la valeur de num qui est
incrementer(num); transmise et non pas la variable elle-même.

196 / 399
Passage par valeur — rappel
Nous savons qu’il est impossible de modifier la valeur d’un argument passé à une fonction car
celle-ci travaille sur une copie de sa valeur et non pas sur l’éventuelle variable figurant en
argument.

void incrementer(int nb) { printf("%d\n", num);


nb = nb + 1;
} Ces instructions affichent 5 .
...
num = 5; En ligne 6, c’est la valeur de num qui est
incrementer(num); transmise et non pas la variable elle-même.

Ceci est encore plus clair quand il s’agit de l’appel


incrementer(2 * num + 1)

qui est considéré. La fonction travaille en effet avec la valeur issue de l’évaluation de
l’expression 2 * num + 1 qui est recopiée dans la variable locale (paramètre) nb.
196 / 399
Passage par adresse

Cependant, si l’on donne l’adresse et le type d’une zone mémoire (ce qui revient à donner un
pointeur vers la zone mémoire considérée), il devient possible de la modifier.

197 / 399
Passage par adresse

Cependant, si l’on donne l’adresse et le type d’une zone mémoire (ce qui revient à donner un
pointeur vers la zone mémoire considérée), il devient possible de la modifier.

void incrementer(int *ptr_nb) { Ces instructions affichent 6 .


*ptr_nb = *ptr_nb + 1;
}
...
num = 5;
incrementer(&num);
printf("%d\n", num);

197 / 399
Passage par adresse

Cependant, si l’on donne l’adresse et le type d’une zone mémoire (ce qui revient à donner un
pointeur vers la zone mémoire considérée), il devient possible de la modifier.

void incrementer(int *ptr_nb) { Ces instructions affichent 6 .


*ptr_nb = *ptr_nb + 1;
} En ligne 6, c’est (la valeur de) l’adresse de num
... qui est transmise. Celle-ci universelle (visible
num = 5; partout).
incrementer(&num);
printf("%d\n", num);

197 / 399
Passage par adresse

Cependant, si l’on donne l’adresse et le type d’une zone mémoire (ce qui revient à donner un
pointeur vers la zone mémoire considérée), il devient possible de la modifier.

void incrementer(int *ptr_nb) { Ces instructions affichent 6 .


*ptr_nb = *ptr_nb + 1;
} En ligne 6, c’est (la valeur de) l’adresse de num
... qui est transmise. Celle-ci universelle (visible
num = 5; partout).
incrementer(&num);
printf("%d\n", num);

C’est un passage par adresse.

197 / 399
Qualificateur const

Dans certains cas, un passage par adresse n’est pas fait pour modifier la valeur située à l’adresse
spécifiée.

198 / 399
Qualificateur const

Dans certains cas, un passage par adresse n’est pas fait pour modifier la valeur située à l’adresse
spécifiée.

P.ex., cette fonction affiche, sans la modifier, la void eto( char *chaine, char c) {
chaîne de caractères chaine en substituant des int i = 0;
while (chaine[i] != ’\0’) {
étoiles ’*’ aux caractères c . if (chaine[i] == c)
putchar(’*’);
else
putchar(chaine[i]);
i += 1;
}
}

198 / 399
Qualificateur const

Dans certains cas, un passage par adresse n’est pas fait pour modifier la valeur située à l’adresse
spécifiée.

P.ex., cette fonction affiche, sans la modifier, la void eto( char *chaine, char c) {
chaîne de caractères chaine en substituant des int i = 0;
while (chaine[i] != ’\0’) {
étoiles ’*’ aux caractères c . if (chaine[i] == c)
putchar(’*’);
else
putchar(chaine[i]);
i += 1;
On souhaite protéger chaine de toute }
modification sur son contenu. }

198 / 399
Qualificateur const

Dans certains cas, un passage par adresse n’est pas fait pour modifier la valeur située à l’adresse
spécifiée.

P.ex., cette fonction affiche, sans la modifier, la void eto(const char *chaine, char c) {
chaîne de caractères chaine en substituant des int i = 0;
while (chaine[i] != ’\0’) {
étoiles ’*’ aux caractères c . if (chaine[i] == c)
putchar(’*’);
else
putchar(chaine[i]);
i += 1;
On souhaite protéger chaine de toute }
modification sur son contenu. }

Pour cela, on place le qualificateur const devant la déclaration de chaine .

198 / 399
Qualificateur const
Ainsi, plus généralement,
const T *ID

déclare un paramètre ID , pointeur sur une zone mémoire de type T , dont le contenu est protégé
en écriture.

199 / 399
Qualificateur const
Ainsi, plus généralement,
const T *ID

déclare un paramètre ID , pointeur sur une zone mémoire de type T , dont le contenu est protégé
en écriture.

void exemple_1(const int *x) { Cette tentative directe pour modifier la valeur à
*x = 40; l’adresse x est sanctionnée par le compilateur par
} une erreur.

199 / 399
Qualificateur const
Ainsi, plus généralement,
const T *ID

déclare un paramètre ID , pointeur sur une zone mémoire de type T , dont le contenu est protégé
en écriture.

void exemple_1(const int *x) { Cette tentative directe pour modifier la valeur à
*x = 40; l’adresse x est sanctionnée par le compilateur par
} une erreur.

void exemple_2(const int *x) { Cette tentative détournée pour modifier la valeur à
int *tmp; l’adresse x est sanctionnée par le compilateur par
tmp = x; un avertissement. Il est ainsi possible de modifier
*tmp = 40; une valeur protégée.
}

199 / 399
Qualificateur const
Ainsi, plus généralement,
const T *ID

déclare un paramètre ID , pointeur sur une zone mémoire de type T , dont le contenu est protégé
en écriture.

void exemple_1(const int *x) { Cette tentative directe pour modifier la valeur à
*x = 40; l’adresse x est sanctionnée par le compilateur par
} une erreur.

void exemple_2(const int *x) { Cette tentative détournée pour modifier la valeur à
int *tmp; l’adresse x est sanctionnée par le compilateur par
tmp = x; un avertissement. Il est ainsi possible de modifier
*tmp = 40; une valeur protégée.
}

Le qualificateur const est avant tout une aide pour le développeur : il informe d’un
comportement à adopter.
199 / 399
Qualificateur const
Le qualificateur const permet quelques subtilités :
T *const ID

déclare un paramètre ID , pointeur fixe sur une zone mémoire de type T

(il n’est pas possible de faire pointer ID ailleurs).

200 / 399
Qualificateur const
Le qualificateur const permet quelques subtilités :
T *const ID

déclare un paramètre ID , pointeur fixe sur une zone mémoire de type T

(il n’est pas possible de faire pointer ID ailleurs). De plus,


const T *const ID

déclare un paramètre ID , pointeur fixe sur une zone mémoire de type T , dont le contenu est
protégé en écriture.

200 / 399
Qualificateur const
Le qualificateur const permet quelques subtilités :
T *const ID

déclare un paramètre ID , pointeur fixe sur une zone mémoire de type T

(il n’est pas possible de faire pointer ID ailleurs). De plus,


const T *const ID

déclare un paramètre ID , pointeur fixe sur une zone mémoire de type T , dont le contenu est
protégé en écriture.
Voici un exemple qui illustre les quatre cas de figure :

void exemple_3(int *a, const int *b,


int *const c,
const int *const d) {

int e;

} 200 / 399
Qualificateur const
Le qualificateur const permet quelques subtilités :
T *const ID

déclare un paramètre ID , pointeur fixe sur une zone mémoire de type T

(il n’est pas possible de faire pointer ID ailleurs). De plus,


const T *const ID

déclare un paramètre ID , pointeur fixe sur une zone mémoire de type T , dont le contenu est
protégé en écriture.
Voici un exemple qui illustre les quatre cas de figure :

void exemple_3(int *a, const int *b,


int *const c,
const int *const d) {

int e;

a = &e; /* Autorise */
*a = 3; /* Autorise */
} 200 / 399
Qualificateur const
Le qualificateur const permet quelques subtilités :
T *const ID

déclare un paramètre ID , pointeur fixe sur une zone mémoire de type T

(il n’est pas possible de faire pointer ID ailleurs). De plus,


const T *const ID

déclare un paramètre ID , pointeur fixe sur une zone mémoire de type T , dont le contenu est
protégé en écriture.
Voici un exemple qui illustre les quatre cas de figure :

void exemple_3(int *a, const int *b, b = &e; /* Autorise */


int *const c, *b = 3; /* Interdit */
const int *const d) {

int e;

a = &e; /* Autorise */
*a = 3; /* Autorise */
} 200 / 399
Qualificateur const
Le qualificateur const permet quelques subtilités :
T *const ID

déclare un paramètre ID , pointeur fixe sur une zone mémoire de type T

(il n’est pas possible de faire pointer ID ailleurs). De plus,


const T *const ID

déclare un paramètre ID , pointeur fixe sur une zone mémoire de type T , dont le contenu est
protégé en écriture.
Voici un exemple qui illustre les quatre cas de figure :

void exemple_3(int *a, const int *b, b = &e; /* Autorise */


int *const c, *b = 3; /* Interdit */
const int *const d) {
c = &e; /* Interdit */
int e; *c = 3; /* Autorise */

a = &e; /* Autorise */
*a = 3; /* Autorise */
} 200 / 399
Qualificateur const
Le qualificateur const permet quelques subtilités :
T *const ID

déclare un paramètre ID , pointeur fixe sur une zone mémoire de type T

(il n’est pas possible de faire pointer ID ailleurs). De plus,


const T *const ID

déclare un paramètre ID , pointeur fixe sur une zone mémoire de type T , dont le contenu est
protégé en écriture.
Voici un exemple qui illustre les quatre cas de figure :

void exemple_3(int *a, const int *b, b = &e; /* Autorise */


int *const c, *b = 3; /* Interdit */
const int *const d) {
c = &e; /* Interdit */
int e; *c = 3; /* Autorise */

a = &e; /* Autorise */ d = &e; /* Interdit */


*a = 3; /* Autorise */ *d = 3; /* Interdit */
} 200 / 399
Plan

Allocation dynamique
Pointeurs
Passage par adresse
Allocation dynamique
Tableaux dynamiques

201 / 399
Données persistantes en mémoire

Nous savons que toutes les variables locales à une fonction n’existent plus après son appel.

202 / 399
Données persistantes en mémoire

Nous savons que toutes les variables locales à une fonction n’existent plus après son appel.
Pour créer des données persistantes dans une fonction, il faut écrire dans la mémoire ailleurs que
dans la pile.

202 / 399
Données persistantes en mémoire

Nous savons que toutes les variables locales à une fonction n’existent plus après son appel.
Pour créer des données persistantes dans une fonction, il faut écrire dans la mémoire ailleurs que
dans la pile.

On écrit pour cela dans le tas. Cela se fait en deux temps :

202 / 399
Données persistantes en mémoire

Nous savons que toutes les variables locales à une fonction n’existent plus après son appel.
Pour créer des données persistantes dans une fonction, il faut écrire dans la mémoire ailleurs que
dans la pile.

On écrit pour cela dans le tas. Cela se fait en deux temps :


1. on demande au système d’allouer (de réserver) une certaine portion du tas ;

202 / 399
Données persistantes en mémoire

Nous savons que toutes les variables locales à une fonction n’existent plus après son appel.
Pour créer des données persistantes dans une fonction, il faut écrire dans la mémoire ailleurs que
dans la pile.

On écrit pour cela dans le tas. Cela se fait en deux temps :


1. on demande au système d’allouer (de réserver) une certaine portion du tas ;
2. on écrit ensuite dans cette zone en lui affectant la valeur souhaitée.

202 / 399
Données persistantes en mémoire

Nous savons que toutes les variables locales à une fonction n’existent plus après son appel.
Pour créer des données persistantes dans une fonction, il faut écrire dans la mémoire ailleurs que
dans la pile.

On écrit pour cela dans le tas. Cela se fait en deux temps :


1. on demande au système d’allouer (de réserver) une certaine portion du tas ;
2. on écrit ensuite dans cette zone en lui affectant la valeur souhaitée.

Pour allouer une portion du tas, on utilise la fonction


void *malloc(size_t size);

202 / 399
Données persistantes en mémoire

Nous savons que toutes les variables locales à une fonction n’existent plus après son appel.
Pour créer des données persistantes dans une fonction, il faut écrire dans la mémoire ailleurs que
dans la pile.

On écrit pour cela dans le tas. Cela se fait en deux temps :


1. on demande au système d’allouer (de réserver) une certaine portion du tas ;
2. on écrit ensuite dans cette zone en lui affectant la valeur souhaitée.

Pour allouer une portion du tas, on utilise la fonction


void *malloc(size_t size);

Elle renvoie un pointeur de type générique void * sur une donnée en mémoire de taille size
octets.

202 / 399
Utilisation de malloc
Pour allouer une zone de la mémoire pouvant accueillir N valeurs d’un type T , on utilise
l’instruction
ptr = (T *) malloc(sizeof(T) * N);

où ptr est un pointeur pointant sur une zone de la mémoire de type T .

203 / 399
Utilisation de malloc
Pour allouer une zone de la mémoire pouvant accueillir N valeurs d’un type T , on utilise
l’instruction
ptr = (T *) malloc(sizeof(T) * N);

où ptr est un pointeur pointant sur une zone de la mémoire de type T .

Explications :
I (T *) sert à préciser que la zone de la mémoire à allouer est de type T . Cette précision est
nécessaire car, par défaut, malloc renvoie un pointeur sur une zone non typée ( void * );

203 / 399
Utilisation de malloc
Pour allouer une zone de la mémoire pouvant accueillir N valeurs d’un type T , on utilise
l’instruction
ptr = (T *) malloc(sizeof(T) * N);

où ptr est un pointeur pointant sur une zone de la mémoire de type T .

Explications :
I (T *) sert à préciser que la zone de la mémoire à allouer est de type T . Cette précision est
nécessaire car, par défaut, malloc renvoie un pointeur sur une zone non typée ( void * );
I l’argument sizeof(T) * N permet de demander à allouer une zone mémoire de taille
sizeof(T) * N octets. Celle-ci pourra donc accueillir N valeurs de type T .

203 / 399
Utilisation de malloc
Pour allouer une zone de la mémoire pouvant accueillir N valeurs d’un type T , on utilise
l’instruction
ptr = (T *) malloc(sizeof(T) * N);

où ptr est un pointeur pointant sur une zone de la mémoire de type T .

Explications :
I (T *) sert à préciser que la zone de la mémoire à allouer est de type T . Cette précision est
nécessaire car, par défaut, malloc renvoie un pointeur sur une zone non typée ( void * );
I l’argument sizeof(T) * N permet de demander à allouer une zone mémoire de taille
sizeof(T) * N octets. Celle-ci pourra donc accueillir N valeurs de type T .

Après exécution de cette instruction, ptr pointe sur le début d’une zone de la mémoire de taille
sizeof(T) * N octets.

203 / 399
Tests d’erreurs et malloc
C’est le système d’exploitation qui, lors de l’appel à malloc se charge de fournir une adresse pour
la zone mémoire à allouer.

204 / 399
Tests d’erreurs et malloc
C’est le système d’exploitation qui, lors de l’appel à malloc se charge de fournir une adresse pour
la zone mémoire à allouer.
Il se peut que pour une raison ou une autre, il ne soit pas possible d’allouer la zone mémoire
demandée. Dans ce cas, malloc renvoie une valeur spéciale : NULL . Cette valeur vaut 0 et est une
constante définie dans stdlib.h .

204 / 399
Tests d’erreurs et malloc
C’est le système d’exploitation qui, lors de l’appel à malloc se charge de fournir une adresse pour
la zone mémoire à allouer.
Il se peut que pour une raison ou une autre, il ne soit pas possible d’allouer la zone mémoire
demandée. Dans ce cas, malloc renvoie une valeur spéciale : NULL . Cette valeur vaut 0 et est une
constante définie dans stdlib.h .

Il est impératif de tester, pour toute allocation dynamique réalisée, son bon déroulement. On
procède de la manière suivante :
char *ptr;
ptr = (char *) malloc(sizeof(char) * 1);
if (NULL == ptr)
ACTION

204 / 399
Tests d’erreurs et malloc
C’est le système d’exploitation qui, lors de l’appel à malloc se charge de fournir une adresse pour
la zone mémoire à allouer.
Il se peut que pour une raison ou une autre, il ne soit pas possible d’allouer la zone mémoire
demandée. Dans ce cas, malloc renvoie une valeur spéciale : NULL . Cette valeur vaut 0 et est une
constante définie dans stdlib.h .

Il est impératif de tester, pour toute allocation dynamique réalisée, son bon déroulement. On
procède de la manière suivante :
char *ptr;
ptr = (char *) malloc(sizeof(char) * 1);
if (NULL == ptr)
ACTION

De cette manière, si l’allocation s’est mal passée, on prend en charge cette erreur par les
instructions ACTION .

204 / 399
Tests d’erreurs et malloc
C’est le système d’exploitation qui, lors de l’appel à malloc se charge de fournir une adresse pour
la zone mémoire à allouer.
Il se peut que pour une raison ou une autre, il ne soit pas possible d’allouer la zone mémoire
demandée. Dans ce cas, malloc renvoie une valeur spéciale : NULL . Cette valeur vaut 0 et est une
constante définie dans stdlib.h .

Il est impératif de tester, pour toute allocation dynamique réalisée, son bon déroulement. On
procède de la manière suivante :
char *ptr;
ptr = (char *) malloc(sizeof(char) * 1);
if (NULL == ptr)
ACTION

De cette manière, si l’allocation s’est mal passée, on prend en charge cette erreur par les
instructions ACTION .
ACTION peut être un exit(EXIT_FAILURE); si l’on se trouve dans le main ou un return NULL; si l’on se
trouve dans une fonction.
204 / 399
Libération de mémoire
Pour désallouer une zone de la mémoire, on utilise la fonction
void free(void *ptr);

205 / 399
Libération de mémoire
Pour désallouer une zone de la mémoire, on utilise la fonction
void free(void *ptr);

On l’utilise de la manière suivante :


free(ptr);

ptr = NULL;

où ptr est un pointeur. La 2e ligne sert à ne plus conserver l’accès sur la zone libéré (non
indispensable mais peut éviter des erreurs).

205 / 399
Libération de mémoire
Pour désallouer une zone de la mémoire, on utilise la fonction
void free(void *ptr);

On l’utilise de la manière suivante :


free(ptr);

ptr = NULL;

où ptr est un pointeur. La 2e ligne sert à ne plus conserver l’accès sur la zone libéré (non
indispensable mais peut éviter des erreurs).

short *ptr; La l. 2 alloue une zone de la mémoire pouvant


ptr = (short *) accueillir 15 valeurs de type short . En l. 3,
malloc(sizeof(short) * 15); cette zone est libérée. Il est d’ores impossible
free(ptr); d’y accéder.
ptr = NULL;

205 / 399
Libération de mémoire
Pour désallouer une zone de la mémoire, on utilise la fonction
void free(void *ptr);

On l’utilise de la manière suivante :


free(ptr);

ptr = NULL;

où ptr est un pointeur. La 2e ligne sert à ne plus conserver l’accès sur la zone libéré (non
indispensable mais peut éviter des erreurs).

short *ptr; La l. 2 alloue une zone de la mémoire pouvant


ptr = (short *) accueillir 15 valeurs de type short . En l. 3,
malloc(sizeof(short) * 15); cette zone est libérée. Il est d’ores impossible
free(ptr); d’y accéder.
ptr = NULL;

Important : pour éviter les fuites mémoire, il faut désallouer toute zone de la mémoire qui n’est
plus utilisée.
205 / 399
La fonction calloc

La fonction
void *calloc(size_t nmemb, size_t size);

alloue et renvoie un pointeur sur une zone de la mémoire de nmemb * size octets, tous initialisés
avec des 0 .

206 / 399
La fonction calloc

La fonction
void *calloc(size_t nmemb, size_t size);

alloue et renvoie un pointeur sur une zone de la mémoire de nmemb * size octets, tous initialisés
avec des 0 .

P.ex.,
long *ptr;
ptr = (long *) calloc(13, sizeof(long));

alloue une zone de la mémoire pouvant accueillir 13 valeurs de type long , initialisées à 0 .

206 / 399
Plan

Allocation dynamique
Pointeurs
Passage par adresse
Allocation dynamique
Tableaux dynamiques

207 / 399
Tableaux dynamiques
Un tableau dynamique de N valeurs de type T est un pointeur pointant vers une zone de la
mémoire de sizeof(T) * N octets.

208 / 399
Tableaux dynamiques
Un tableau dynamique de N valeurs de type T est un pointeur pointant vers une zone de la
mémoire de sizeof(T) * N octets.
P.ex.,
int *tab;
tab = (int *) malloc(sizeof(int) * 66);

déclare un tableau dynamique de valeurs de type int de taille 66 .

208 / 399
Tableaux dynamiques
Un tableau dynamique de N valeurs de type T est un pointeur pointant vers une zone de la
mémoire de sizeof(T) * N octets.
P.ex.,
int *tab;
tab = (int *) malloc(sizeof(int) * 66);

déclare un tableau dynamique de valeurs de type int de taille 66 .

On lit et écrit dans un tableau dynamique de la même manière que dans un tableau statique. En
effet, tab pointe vers la première case du tableau, et pour tout 0 6 i < 66 , (tab + i) pointe
vers la case d’indice i .

208 / 399
Tableaux dynamiques
Un tableau dynamique de N valeurs de type T est un pointeur pointant vers une zone de la
mémoire de sizeof(T) * N octets.
P.ex.,
int *tab;
tab = (int *) malloc(sizeof(int) * 66);

déclare un tableau dynamique de valeurs de type int de taille 66 .

On lit et écrit dans un tableau dynamique de la même manière que dans un tableau statique. En
effet, tab pointe vers la première case du tableau, et pour tout 0 6 i < 66 , (tab + i) pointe
vers la case d’indice i .

La fonction free vue précédemment permet de désallouer un tableau. On utilise donc


free(tab);

pour libérer la zone mémoire occupée par tab (c.-à-d. les 66 entiers situés à partir de l’adresse
tab ).
208 / 399
Création d’un tableau dynamique à deux dimensions
Un tableau à deux dimensions est un tableau dont chaque case est elle-même un tableau.

209 / 399
Création d’un tableau dynamique à deux dimensions
Un tableau à deux dimensions est un tableau dont chaque case est elle-même un tableau. Un
tableau dynamique à deux dimensions est donc un pointeur sur un pointeur.

209 / 399
Création d’un tableau dynamique à deux dimensions
Un tableau à deux dimensions est un tableau dont chaque case est elle-même un tableau. Un
tableau dynamique à deux dimensions est donc un pointeur sur un pointeur. Ceci se
généralise dans le cas des tableaux à plus de deux dimensions.

209 / 399
Création d’un tableau dynamique à deux dimensions
Un tableau à deux dimensions est un tableau dont chaque case est elle-même un tableau. Un
tableau dynamique à deux dimensions est donc un pointeur sur un pointeur. Ceci se
généralise dans le cas des tableaux à plus de deux dimensions.
Les instructions
int i, j, **tab;
tab = (int **) malloc(sizeof(int *) * 4);
if (tab == NULL) exit(EXIT_FAILURE);
for (i = 0 ; i < 4 ; ++i) {
tab[i] = (int *) malloc(sizeof(int) * 3);
if (tab[i] == NULL) exit(EXIT_FAILURE);
for (j = 0 ; j < 3 ; ++j)
tab[i][j] = 0;
}

permettent de créer un tableau dynamique à deux dimensions de taille


4 × 3 de valeurs de type int initialisées à 0 .

209 / 399
Création d’un tableau dynamique à deux dimensions
tab × Initialement, tab est pointeur vers une zone
indéterminée de la mémoire.

210 / 399
Création d’un tableau dynamique à deux dimensions
tab × Initialement, tab est pointeur vers une zone
indéterminée de la mémoire.
tab
Juste après la 1re allocation dynamique, tab est
tab[0] ×
dans cet état. L’espace de mémoire de 4
tab[1] ×
pointeurs sur des entiers a été réservé.
tab[2] ×

tab[3] ×

210 / 399
Création d’un tableau dynamique à deux dimensions
tab × Initialement, tab est pointeur vers une zone
indéterminée de la mémoire.
tab
Juste après la 1re allocation dynamique, tab est
tab[0] ×
dans cet état. L’espace de mémoire de 4
tab[1] ×
pointeurs sur des entiers a été réservé.
tab[2] ×

tab[3] ×

tab[0][0] tab[0][2]

0 0 0
Après les allocations dynamiques des tableaux
tab à une dimension de taille 3 et des
tab[0]
initialisations des cases d’entiers, tab est dans
0 0 0

tab[1]
cet état.
tab[2][1]
tab[2]

0 0 0

tab[3]

0 0 0

210 / 399
Destruction d’un tableau dynamique à deux dimensions
Pour libérer l’espace occupé par un tableau à deux dimensions, on utilise plusieurs fois la
fonction free .

211 / 399
Destruction d’un tableau dynamique à deux dimensions
Pour libérer l’espace occupé par un tableau à deux dimensions, on utilise plusieurs fois la
fonction free .

Les instructions
for (i = 0 ; i < 4 ; ++i) {
free(tab[i]);
tab[i] = NULL;
}
free(tab);
tab = NULL;

permettent de libérer l’espace mémoire occupé par un tableau tab à deux dimensions de taille 4

× N de valeurs de type T où N est un entier strictement positif quelconque et T est un type.

211 / 399
Destruction d’un tableau dynamique à deux dimensions
Pour libérer l’espace occupé par un tableau à deux dimensions, on utilise plusieurs fois la
fonction free .

Les instructions
for (i = 0 ; i < 4 ; ++i) {
free(tab[i]);
tab[i] = NULL;
}
free(tab);
tab = NULL;

permettent de libérer l’espace mémoire occupé par un tableau tab à deux dimensions de taille 4

× N de valeurs de type T où N est un entier strictement positif quelconque et T est un type.

Remarque : la connaissance de la seconde dimension du tableau ( N ) n’est pas utile et


n’intervient pas dans la suite d’instructions ci-dessus.

211 / 399
La fonction realloc
Il est possible de modifier la taille d’un tableau dynamique via la fonction
void *realloc(void *ptr, size_t size);

212 / 399
La fonction realloc
Il est possible de modifier la taille d’un tableau dynamique via la fonction
void *realloc(void *ptr, size_t size);

Celle-ci renvoie un pointeur sur une zone de la mémoire de taille size octets. Le contenu de cette
zone mémoire est le même que celui de la zone pointée par ptr .

212 / 399
La fonction realloc
Il est possible de modifier la taille d’un tableau dynamique via la fonction
void *realloc(void *ptr, size_t size);

Celle-ci renvoie un pointeur sur une zone de la mémoire de taille size octets. Le contenu de cette
zone mémoire est le même que celui de la zone pointée par ptr .
Attention : l’adresse de la zone de la mémoire réallouée peut être différente de son adresse
d’origine.

212 / 399
La fonction realloc
Il est possible de modifier la taille d’un tableau dynamique via la fonction
void *realloc(void *ptr, size_t size);

Celle-ci renvoie un pointeur sur une zone de la mémoire de taille size octets. Le contenu de cette
zone mémoire est le même que celui de la zone pointée par ptr .
Attention : l’adresse de la zone de la mémoire réallouée peut être différente de son adresse
d’origine. En effet,
int *tab;
tab = (int *) malloc(sizeof(int) * 150);
printf("%p\n", tab);
tab = (int *) realloc(tab, sizeof(int) * 250000);
printf("%p\n", tab);

affiche
0x1205010
0x7fb123f9d010
.

212 / 399
Exemple d’utilisation de realloc
Construction d’une chaîne de caractères, caractère par caractère, dans un tableau dynamique. La
lecture s’arrête sur lecture de ’!’ .

213 / 399
Exemple d’utilisation de realloc
Construction d’une chaîne de caractères, caractère par caractère, dans un tableau dynamique. La
lecture s’arrête sur lecture de ’!’ .

char car, *chaine;


int t_max, t_reelle;

213 / 399
Exemple d’utilisation de realloc
Construction d’une chaîne de caractères, caractère par caractère, dans un tableau dynamique. La
lecture s’arrête sur lecture de ’!’ .

char car, *chaine;


int t_max, t_reelle;
t_reelle = 0;

213 / 399
Exemple d’utilisation de realloc
Construction d’une chaîne de caractères, caractère par caractère, dans un tableau dynamique. La
lecture s’arrête sur lecture de ’!’ .

char car, *chaine;


int t_max, t_reelle;
t_reelle = 0;
t_max = 2;

213 / 399
Exemple d’utilisation de realloc
Construction d’une chaîne de caractères, caractère par caractère, dans un tableau dynamique. La
lecture s’arrête sur lecture de ’!’ .

char car, *chaine;


int t_max, t_reelle;
t_reelle = 0;
t_max = 2;
chaine = (char *) malloc(sizeof(char) * t_max); /* Allocation initiale. */

213 / 399
Exemple d’utilisation de realloc
Construction d’une chaîne de caractères, caractère par caractère, dans un tableau dynamique. La
lecture s’arrête sur lecture de ’!’ .

char car, *chaine;


int t_max, t_reelle;
t_reelle = 0;
t_max = 2;
chaine = (char *) malloc(sizeof(char) * t_max); /* Allocation initiale. */
scanf(" %c", &car);

213 / 399
Exemple d’utilisation de realloc
Construction d’une chaîne de caractères, caractère par caractère, dans un tableau dynamique. La
lecture s’arrête sur lecture de ’!’ .

char car, *chaine;


int t_max, t_reelle;
t_reelle = 0;
t_max = 2;
chaine = (char *) malloc(sizeof(char) * t_max); /* Allocation initiale. */
scanf(" %c", &car);
while (car != ’!’) {

213 / 399
Exemple d’utilisation de realloc
Construction d’une chaîne de caractères, caractère par caractère, dans un tableau dynamique. La
lecture s’arrête sur lecture de ’!’ .

char car, *chaine;


int t_max, t_reelle;
t_reelle = 0;
t_max = 2;
chaine = (char *) malloc(sizeof(char) * t_max); /* Allocation initiale. */
scanf(" %c", &car);
while (car != ’!’) {
if (t_reelle + 1 >= t_max) {

213 / 399
Exemple d’utilisation de realloc
Construction d’une chaîne de caractères, caractère par caractère, dans un tableau dynamique. La
lecture s’arrête sur lecture de ’!’ .

char car, *chaine;


int t_max, t_reelle;
t_reelle = 0;
t_max = 2;
chaine = (char *) malloc(sizeof(char) * t_max); /* Allocation initiale. */
scanf(" %c", &car);
while (car != ’!’) {
if (t_reelle + 1 >= t_max) {
t_max *= 2;

213 / 399
Exemple d’utilisation de realloc
Construction d’une chaîne de caractères, caractère par caractère, dans un tableau dynamique. La
lecture s’arrête sur lecture de ’!’ .

char car, *chaine;


int t_max, t_reelle;
t_reelle = 0;
t_max = 2;
chaine = (char *) malloc(sizeof(char) * t_max); /* Allocation initiale. */
scanf(" %c", &car);
while (car != ’!’) {
if (t_reelle + 1 >= t_max) {
t_max *= 2;
chaine = (char *) realloc(chaine, t_max); /* Augmentation. */
}

213 / 399
Exemple d’utilisation de realloc
Construction d’une chaîne de caractères, caractère par caractère, dans un tableau dynamique. La
lecture s’arrête sur lecture de ’!’ .

char car, *chaine;


int t_max, t_reelle;
t_reelle = 0;
t_max = 2;
chaine = (char *) malloc(sizeof(char) * t_max); /* Allocation initiale. */
scanf(" %c", &car);
while (car != ’!’) {
if (t_reelle + 1 >= t_max) {
t_max *= 2;
chaine = (char *) realloc(chaine, t_max); /* Augmentation. */
}
chaine[t_reelle] = car;

213 / 399
Exemple d’utilisation de realloc
Construction d’une chaîne de caractères, caractère par caractère, dans un tableau dynamique. La
lecture s’arrête sur lecture de ’!’ .

char car, *chaine;


int t_max, t_reelle;
t_reelle = 0;
t_max = 2;
chaine = (char *) malloc(sizeof(char) * t_max); /* Allocation initiale. */
scanf(" %c", &car);
while (car != ’!’) {
if (t_reelle + 1 >= t_max) {
t_max *= 2;
chaine = (char *) realloc(chaine, t_max); /* Augmentation. */
}
chaine[t_reelle] = car;
t_reelle += 1;

213 / 399
Exemple d’utilisation de realloc
Construction d’une chaîne de caractères, caractère par caractère, dans un tableau dynamique. La
lecture s’arrête sur lecture de ’!’ .

char car, *chaine;


int t_max, t_reelle;
t_reelle = 0;
t_max = 2;
chaine = (char *) malloc(sizeof(char) * t_max); /* Allocation initiale. */
scanf(" %c", &car);
while (car != ’!’) {
if (t_reelle + 1 >= t_max) {
t_max *= 2;
chaine = (char *) realloc(chaine, t_max); /* Augmentation. */
}
chaine[t_reelle] = car;
t_reelle += 1;
scanf(" %c", &car);
}

213 / 399
Exemple d’utilisation de realloc
Construction d’une chaîne de caractères, caractère par caractère, dans un tableau dynamique. La
lecture s’arrête sur lecture de ’!’ .

char car, *chaine;


int t_max, t_reelle;
t_reelle = 0;
t_max = 2;
chaine = (char *) malloc(sizeof(char) * t_max); /* Allocation initiale. */
scanf(" %c", &car);
while (car != ’!’) {
if (t_reelle + 1 >= t_max) {
t_max *= 2;
chaine = (char *) realloc(chaine, t_max); /* Augmentation. */
}
chaine[t_reelle] = car;
t_reelle += 1;
scanf(" %c", &car);
}
chaine[t_reelle] = ’\0’;

213 / 399
Exemple d’utilisation de realloc
Construction d’une chaîne de caractères, caractère par caractère, dans un tableau dynamique. La
lecture s’arrête sur lecture de ’!’ .

char car, *chaine;


int t_max, t_reelle;
t_reelle = 0;
t_max = 2;
chaine = (char *) malloc(sizeof(char) * t_max); /* Allocation initiale. */
scanf(" %c", &car);
while (car != ’!’) {
if (t_reelle + 1 >= t_max) {
t_max *= 2;
chaine = (char *) realloc(chaine, t_max); /* Augmentation. */
}
chaine[t_reelle] = car;
t_reelle += 1;
scanf(" %c", &car);
}
chaine[t_reelle] = ’\0’;
chaine = (char *) realloc(chaine, t_reelle + 1); /* Diminution. */

213 / 399
Exemple d’utilisation de realloc
Construction d’une chaîne de caractères, caractère par caractère, dans un tableau dynamique. La
lecture s’arrête sur lecture de ’!’ .

char car, *chaine;


int t_max, t_reelle;
t_reelle = 0;
t_max = 2;
chaine = (char *) malloc(sizeof(char) * t_max); /* Allocation initiale. */
scanf(" %c", &car);
while (car != ’!’) {
if (t_reelle + 1 >= t_max) {
t_max *= 2;
chaine = (char *) realloc(chaine, t_max); /* Augmentation. */
}
chaine[t_reelle] = car;
t_reelle += 1;
scanf(" %c", &car);
}
chaine[t_reelle] = ’\0’;
chaine = (char *) realloc(chaine, t_reelle + 1); /* Diminution. */
printf("%s\n", chaine);

213 / 399
Plan

Entrées et sorties
Sortie
Entrée
Fichiers
Fichiers binaires

214 / 399
Plan

Entrées et sorties
Sortie
Entrée
Fichiers
Fichiers binaires

215 / 399
Écriture formatée

La fonction
int printf(char *format, V_1, ..., V_N);

permet de réaliser une écriture formatée sur la sortie standard stdout .

216 / 399
Écriture formatée

La fonction
int printf(char *format, V_1, ..., V_N);

permet de réaliser une écriture formatée sur la sortie standard stdout .


Rappel : cette fonction renvoie le nombre de caractères affichés. Si une erreur a lieu, elle renvoie
un entier négatif.

216 / 399
Écriture formatée

La fonction
int printf(char *format, V_1, ..., V_N);

permet de réaliser une écriture formatée sur la sortie standard stdout .


Rappel : cette fonction renvoie le nombre de caractères affichés. Si une erreur a lieu, elle renvoie
un entier négatif.

int num; Ces instructions affichent


num = printf("%s+%d\n", test+200
9
"test", 200);
printf("%d\n", num);

216 / 399
Indicateurs de conversion
On utilise les indicateurs de conversion suivants pour interpréter chaque valeur à écrire (ou à lire,
voir la partie suivante) de manière adéquate :

Indicateur de conversion Affichage


d , i Entier en base dix
u Entier non signé en base dix
x , X Entier en hexadécimal
c Caractère
e , E Flottant en notation scientifique
f , g Flottant
s Chaîne de caractères
p Pointeur

217 / 399
Caractères spéciaux

Certains caractères ne s’affichent pas mais produisent un effet sur la sortie. Ce sont des
caractères spéciaux.

Caractère spécial Rôle


\n Passage à la ligne suivante
\b Retour en arrière d’un caractère
\f Passage à la ligne suivante avec alinéa
\r Retour chariot
\t Tabulation horizontale
\v Tabulation verticale

218 / 399
Caractères d’attribut

Il est possible de faire suivre le % d’un indicateur de conversion de caractères d’attribut pour
réaliser un formatage avancé.

Caractère d’attribut Rôle


0N Affichage du nombre sur N chiffres (ajout de « 0 »)
N Affichage du nombre sur N chiffres (ajout d’espaces)
+ Force l’affichage du signe d’un nombre
- Justifie à gauche un nombre (à droite par défaut)

219 / 399
Caractères d’attribut

Il est possible de faire suivre le % d’un indicateur de conversion de caractères d’attribut pour
réaliser un formatage avancé.

Caractère d’attribut Rôle


0N Affichage du nombre sur N chiffres (ajout de « 0 »)
N Affichage du nombre sur N chiffres (ajout d’espaces)
+ Force l’affichage du signe d’un nombre
- Justifie à gauche un nombre (à droite par défaut)

P.ex.,
printf("%+5d\n", 23);
affiche ␣␣+23

219 / 399
Caractères d’attribut

Il est possible de faire suivre le % d’un indicateur de conversion de caractères d’attribut pour
réaliser un formatage avancé.

Caractère d’attribut Rôle


0N Affichage du nombre sur N chiffres (ajout de « 0 »)
N Affichage du nombre sur N chiffres (ajout d’espaces)
+ Force l’affichage du signe d’un nombre
- Justifie à gauche un nombre (à droite par défaut)

P.ex.,
printf("%+5d\n", 23); printf("%+-5d %d\n", 23, 5);
affiche ␣␣+23 et affiche +23␣␣␣5 .

219 / 399
Écriture caractère par caractère

La fonction
int putchar(int c);

permet d’afficher un caractère sur la sortie standard.

220 / 399
Écriture caractère par caractère

La fonction
int putchar(int c);

permet d’afficher un caractère sur la sortie standard.


Cette fonction renvoie le caractère écrit (converti en un int ). Elle renvoie la constante EOF
(end-of-file) si une erreur a lieu.

220 / 399
Écriture caractère par caractère

La fonction
int putchar(int c);

permet d’afficher un caractère sur la sortie standard.


Cette fonction renvoie le caractère écrit (converti en un int ). Elle renvoie la constante EOF
(end-of-file) si une erreur a lieu.

int ret; Ces instructions affichent a . Un test est


ret = putchar(’a’); réalisé pour détecter une erreur éventuelle.
if (ret == EOF)
exit(EXIT_FAILURE);

220 / 399
Plan

Entrées et sorties
Sortie
Entrée
Fichiers
Fichiers binaires

221 / 399
Lecture formatée
La fonction
int scanf (char *format, PTR_1, ..., PTR_N);

permet de réaliser une lecture formatée sur l’entrée standard stdin .

222 / 399
Lecture formatée
La fonction
int scanf (char *format, PTR_1, ..., PTR_N);

permet de réaliser une lecture formatée sur l’entrée standard stdin .

Cette fonction renvoie le nombre d’éléments lus correctement assignés.

222 / 399
Lecture formatée
La fonction
int scanf (char *format, PTR_1, ..., PTR_N);

permet de réaliser une lecture formatée sur l’entrée standard stdin .

Cette fonction renvoie le nombre d’éléments lus correctement assignés.

int num, ret;


char chaine[128];
chaine[0] = ’\0’;
ret = scanf("%d W %s",
&num, chaine);
printf("%d %d %s\n",
ret, num, chaine);
Ces instructions lisent sur l’entrée standard un
entier, un caractère ’W’ , puis une chaîne de
caractères.
222 / 399
Lecture formatée
La fonction
int scanf (char *format, PTR_1, ..., PTR_N);

permet de réaliser une lecture formatée sur l’entrée standard stdin .

Cette fonction renvoie le nombre d’éléments lus correctement assignés.

int num, ret; 1. 25␣W␣abc\n


char chaine[128]; → 2 25 abc
chaine[0] = ’\0’;
ret = scanf("%d W %s",
&num, chaine);
printf("%d %d %s\n",
ret, num, chaine);
Ces instructions lisent sur l’entrée standard un
entier, un caractère ’W’ , puis une chaîne de
caractères.
222 / 399
Lecture formatée
La fonction
int scanf (char *format, PTR_1, ..., PTR_N);

permet de réaliser une lecture formatée sur l’entrée standard stdin .

Cette fonction renvoie le nombre d’éléments lus correctement assignés.

int num, ret; 1. 25␣W␣abc\n


char chaine[128]; → 2 25 abc
chaine[0] = ’\0’;
ret = scanf("%d W %s", 2. 25␣W␣␣abc\n

&num, chaine); → 2 25 abc


printf("%d %d %s\n",
ret, num, chaine);
Ces instructions lisent sur l’entrée standard un
entier, un caractère ’W’ , puis une chaîne de
caractères.
222 / 399
Lecture formatée
La fonction
int scanf (char *format, PTR_1, ..., PTR_N);

permet de réaliser une lecture formatée sur l’entrée standard stdin .

Cette fonction renvoie le nombre d’éléments lus correctement assignés.

int num, ret; 1. 25␣W␣abc\n


char chaine[128]; → 2 25 abc
chaine[0] = ’\0’;
ret = scanf("%d W %s", 2. 25␣W␣␣abc\n

&num, chaine); → 2 25 abc


printf("%d %d %s\n",
3. 25Wabc\n
ret, num, chaine);
→ 2 25 abc
Ces instructions lisent sur l’entrée standard un
entier, un caractère ’W’ , puis une chaîne de
caractères.
222 / 399
Lecture formatée
La fonction
int scanf (char *format, PTR_1, ..., PTR_N);

permet de réaliser une lecture formatée sur l’entrée standard stdin .

Cette fonction renvoie le nombre d’éléments lus correctement assignés.

int num, ret; 1. 25␣W␣abc\n 4. 25␣abc\n


char chaine[128]; → 2 25 abc → 1 25
chaine[0] = ’\0’;
ret = scanf("%d W %s", 2. 25␣W␣␣abc\n

&num, chaine); → 2 25 abc


printf("%d %d %s\n",
3. 25Wabc\n
ret, num, chaine);
→ 2 25 abc
Ces instructions lisent sur l’entrée standard un
entier, un caractère ’W’ , puis une chaîne de
caractères.
222 / 399
Lecture formatée
La fonction
int scanf (char *format, PTR_1, ..., PTR_N);

permet de réaliser une lecture formatée sur l’entrée standard stdin .

Cette fonction renvoie le nombre d’éléments lus correctement assignés.

int num, ret; 1. 25␣W␣abc\n 4. 25␣abc\n


char chaine[128]; → 2 25 abc → 1 25
chaine[0] = ’\0’;
ret = scanf("%d W %s", 2. 25␣W␣␣abc\n 5. xy␣W␣abc\n
&num, chaine); → 2 25 abc → 0 ?
printf("%d %d %s\n",
3. 25Wabc\n
ret, num, chaine);
→ 2 25 abc
Ces instructions lisent sur l’entrée standard un
entier, un caractère ’W’ , puis une chaîne de
caractères.
222 / 399
Lecture formatée
La fonction
int scanf (char *format, PTR_1, ..., PTR_N);

permet de réaliser une lecture formatée sur l’entrée standard stdin .

Cette fonction renvoie le nombre d’éléments lus correctement assignés.

int num, ret; 1. 25␣W␣abc\n 4. 25␣abc\n


char chaine[128]; → 2 25 abc → 1 25
chaine[0] = ’\0’;
ret = scanf("%d W %s", 2. 25␣W␣␣abc\n 5. xy␣W␣abc\n
&num, chaine); → 2 25 abc → 0 ?
printf("%d %d %s\n",
3. 25Wabc\n 6. 25␣w␣abc\n
ret, num, chaine);
→ 2 25 abc → 1 25
Ces instructions lisent sur l’entrée standard un
entier, un caractère ’W’ , puis une chaîne de
caractères.
222 / 399
Exemple d’utilisation de scanf
Ce programme lit sur l’entrée standard une chaîne d’au plus sept caractères (format %Ns ), une
espace, puis un entier.

223 / 399
Exemple d’utilisation de scanf
Ce programme lit sur l’entrée standard une chaîne d’au plus sept caractères (format %Ns ), une
espace, puis un entier.

ret = scanf("%7s %d", prenom, &age);

223 / 399
Exemple d’utilisation de scanf
Ce programme lit sur l’entrée standard une chaîne d’au plus sept caractères (format %Ns ), une
espace, puis un entier.

ret = scanf("%7s %d", prenom, &age);


while (ret != 2) {
printf("%lu\n", strlen(prenom));
ret = scanf("%7s %d", prenom, &age);
}

223 / 399
Exemple d’utilisation de scanf
Ce programme lit sur l’entrée standard une chaîne d’au plus sept caractères (format %Ns ), une
espace, puis un entier.
#include <stdio.h>
#include <string.h>

int main() {
char prenom[8];
int age, ret;

ret = scanf("%7s %d", prenom, &age);


while (ret != 2) {
printf("%lu\n", strlen(prenom));
ret = scanf("%7s %d", prenom, &age);
}
return 0;
}

223 / 399
Lecture caractère par caractère

La fonction
int getchar();

permet de lire un caractère sur l’entrée standard.

224 / 399
Lecture caractère par caractère

La fonction
int getchar();

permet de lire un caractère sur l’entrée standard.


Cette fonction renvoie le caractère lu (converti en un int ). Elle renvoie la constante EOF
(end-of-file) lorsque la fin de fichier est détectée (touche Ctrl + D).

224 / 399
Lecture caractère par caractère

La fonction
int getchar();

permet de lire un caractère sur l’entrée standard.


Cette fonction renvoie le caractère lu (converti en un int ). Elle renvoie la constante EOF
(end-of-file) lorsque la fin de fichier est détectée (touche Ctrl + D).

char c; Ces instructions affichent chaque caractère lu


while ((c = getchar()) != EOF) en entrée, tant que EOF n’est pas rencontré.
printf("%c", c);

224 / 399
Le tampon d’entrée
Considérons les instructions
char c;
while ((c = getchar()) != EOF)
printf("%c", c);

225 / 399
Le tampon d’entrée
Considérons les instructions
char c;
while ((c = getchar()) != EOF)
printf("%c", c);

Il est possible de saisir plusieurs caractères avant le ’\n’ (touche Entrée).

225 / 399
Le tampon d’entrée
Considérons les instructions
char c;
while ((c = getchar()) != EOF)
printf("%c", c);

Il est possible de saisir plusieurs caractères avant le ’\n’ (touche Entrée).

Avant d’être lus par le getchar de la boucle, ils sont situés temporairement dans le tampon
d’entrée.

225 / 399
Le tampon d’entrée
Considérons les instructions
char c;
while ((c = getchar()) != EOF)
printf("%c", c);

Il est possible de saisir plusieurs caractères avant le ’\n’ (touche Entrée).

Avant d’être lus par le getchar de la boucle, ils sont situés temporairement dans le tampon
d’entrée.

Le tampon d’entrée se comporte comme un tableau de caractères.

225 / 399
Le tampon d’entrée
Considérons les instructions
char c;
while ((c = getchar()) != EOF)
printf("%c", c);

Il est possible de saisir plusieurs caractères avant le ’\n’ (touche Entrée).

Avant d’être lus par le getchar de la boucle, ils sont situés temporairement dans le tampon
d’entrée.

Le tampon d’entrée se comporte comme un tableau de caractères.

Chaque appel à getchar considère le tampon d’entrée et :


I s’il est vide, on attend la saisie d’un caractère de l’entrée standard ;
I s’il contient au moins un élément, il le considère et le supprime du tampon d’entrée sans
mettre en pause l’exécution.
225 / 399
Plan

Entrées et sorties
Sortie
Entrée
Fichiers
Fichiers binaires

226 / 399
Descripteurs de fichiers et tête de lecture/écriture

Tout fichier est manipulé par un pointeur sur une variable de type FILE .

C’est un type structuré déclaré dans stdio.h qui contient diverses informations nécessaires et
relatives au fichier qu’il adresse.
Toute variable de type FILE * est un descripteur de fichier.

227 / 399
Descripteurs de fichiers et tête de lecture/écriture

Tout fichier est manipulé par un pointeur sur une variable de type FILE .

C’est un type structuré déclaré dans stdio.h qui contient diverses informations nécessaires et
relatives au fichier qu’il adresse.
Toute variable de type FILE * est un descripteur de fichier.

La lecture/écriture dans un fichier se fait par l’intermédiaire d’une tête de lecture.


Celle-ci désigne un caractère du fichier considéré comme le caractère observé à un instant
donné.
Les fonctions de lecture/écriture ont pour effet (en particulier) de mettre à jour cette tête de
lecture en avançant dans son positionnement dans le fichier.

227 / 399
Tête de lecture/écriture
Il existe trois fonctions de stdio.h pour déplacer manuellement la tête de lecture ou obtenir des
informations à son sujet :
I int ftell(FILE *f); qui renvoie l’indice de la tête de lecture du fichier pointé par f .
Cette fonction est sans effet de bord ;

228 / 399
Tête de lecture/écriture
Il existe trois fonctions de stdio.h pour déplacer manuellement la tête de lecture ou obtenir des
informations à son sujet :
I int ftell(FILE *f); qui renvoie l’indice de la tête de lecture du fichier pointé par f .
Cette fonction est sans effet de bord ;
I int fseek(FILE *f, int decalage, int mode); qui décale la tête de lecture du fichier pointé par
f de decalage caractères selon le mode mode .
Ce paramètre explique à quoi le décalage est relatif ( SEEK_SET : début du fichier, SEEK_CUR :
position courante, SEEK_END : fin du fichier).
Cette fonction renvoie 0 si elle s’est bien exécutée et -1 sinon ;

228 / 399
Tête de lecture/écriture
Il existe trois fonctions de stdio.h pour déplacer manuellement la tête de lecture ou obtenir des
informations à son sujet :
I int ftell(FILE *f); qui renvoie l’indice de la tête de lecture du fichier pointé par f .
Cette fonction est sans effet de bord ;
I int fseek(FILE *f, int decalage, int mode); qui décale la tête de lecture du fichier pointé par
f de decalage caractères selon le mode mode .
Ce paramètre explique à quoi le décalage est relatif ( SEEK_SET : début du fichier, SEEK_CUR :
position courante, SEEK_END : fin du fichier).
Cette fonction renvoie 0 si elle s’est bien exécutée et -1 sinon ;

I void rewind(FILE *f) ; place la tête de lecture au début du fichier. L’appel rewind(f) est ainsi
équivalent à fseek(f, O, SEEK_SET) .

228 / 399
Ouverture de fichiers

La fonction
FILE *fopen(const char *chemin, const char *mode);

permet d’ouvrir un fichier.

229 / 399
Ouverture de fichiers

La fonction
FILE *fopen(const char *chemin, const char *mode);

permet d’ouvrir un fichier.


Cette fonction renvoie un descripteur de fichier sur le fichier de chemin chemin (relatif par
rapport à l’exécutable).

229 / 399
Ouverture de fichiers

La fonction
FILE *fopen(const char *chemin, const char *mode);

permet d’ouvrir un fichier.


Cette fonction renvoie un descripteur de fichier sur le fichier de chemin chemin (relatif par
rapport à l’exécutable).
La tête de lecture est positionnée sur son 1er caractère.

229 / 399
Ouverture de fichiers

La fonction
FILE *fopen(const char *chemin, const char *mode);

permet d’ouvrir un fichier.


Cette fonction renvoie un descripteur de fichier sur le fichier de chemin chemin (relatif par
rapport à l’exécutable).
La tête de lecture est positionnée sur son 1er caractère.
Le paramètre mode désigne le mode d’ouverture désiré.

229 / 399
Ouverture de fichiers

La fonction
FILE *fopen(const char *chemin, const char *mode);

permet d’ouvrir un fichier.


Cette fonction renvoie un descripteur de fichier sur le fichier de chemin chemin (relatif par
rapport à l’exécutable).
La tête de lecture est positionnée sur son 1er caractère.
Le paramètre mode désigne le mode d’ouverture désiré.

Attention : fopen renvoie NULL si l’ouverture s’est mal passée. Il faut donc toujours tester la
valeur de retour de fopen .

229 / 399
Modes d’ouverture
Il existe plusieurs modes d’ouverture. Chacun répond à un besoin particulier :
I "r" : lecture seule.

I "w" : écriture seule. Si le fichier n’existe pas, il est créé.

I "a" : écriture en ajout. Permet d’écrire dans le fichier en partant de la fin. Si le fichier
n’existe pas, il est créé.
I "r+" : lecture et écriture.

I "w+" : lecture et écriture avec suppression préalable du contenu du fichier. Si le fichier


n’existe pas, il est créé.
I "a+" : lecture et écriture en ajout. Permet de lire et d’écrire dans le fichier en partant de la
fin. Si le fichier n’existe pas, il est créé.

230 / 399
Fermeture de fichiers

La fonction
int fclose(FILE *f);

permet de fermer un fichier.

231 / 399
Fermeture de fichiers

La fonction
int fclose(FILE *f);

permet de fermer un fichier.

Cette fonction permet de mettre à jour le fichier pointé par f de sorte que toutes les
modifications effectuées soient prisent en compte. Le pointeur f n’est alors plus utilisable pour
accéder au fichier.

231 / 399
Fermeture de fichiers

La fonction
int fclose(FILE *f);

permet de fermer un fichier.

Cette fonction permet de mettre à jour le fichier pointé par f de sorte que toutes les
modifications effectuées soient prisent en compte. Le pointeur f n’est alors plus utilisable pour
accéder au fichier.

Cette fonction renvoie 0 si la fermeture s’est bien déroulée et EOF dans le cas contraire.

231 / 399
Fermeture de fichiers

La fonction
int fclose(FILE *f);

permet de fermer un fichier.

Cette fonction permet de mettre à jour le fichier pointé par f de sorte que toutes les
modifications effectuées soient prisent en compte. Le pointeur f n’est alors plus utilisable pour
accéder au fichier.

Cette fonction renvoie 0 si la fermeture s’est bien déroulée et EOF dans le cas contraire.

Attention : toute ouverture d’un fichier lors de l’exécution d’un programme doit s’accompagner
tôt ou tard de la fermeture future du fichier en question.

231 / 399
Écriture dans un fichier
La fonction
int fprintf(FILE *f, char *format, V_1, ..., V_N);

permet de réaliser une écriture formatée dans le fichier pointé par f .

232 / 399
Écriture dans un fichier
La fonction
int fprintf(FILE *f, char *format, V_1, ..., V_N);

permet de réaliser une écriture formatée dans le fichier pointé par f .

Cette fonction se comporte comme printf , à la différence que cette dernière travaille sur le
fichier stdout .

232 / 399
Écriture dans un fichier
La fonction
int fprintf(FILE *f, char *format, V_1, ..., V_N);

permet de réaliser une écriture formatée dans le fichier pointé par f .

Cette fonction se comporte comme printf , à la différence que cette dernière travaille sur le
fichier stdout .

FILE *f; if (ret == EOF)


int ret; exit(EXIT_FAILURE);
f = fopen("fic.txt", "w");
if (f == NULL)
Ces instructions déclarent un pointeur sur le
exit(EXIT_FAILURE);
fprintf(f, "abc\n"); fichier fic.txt et écrivent "abc\n" dans
ret = fclose(f); fic.txt .

232 / 399
Lecture dans un fichier

La fonction
int fscanf(FILE *f, char *format, V_1, ..., V_N);

permet de réaliser une lecture formatée depuis le fichier pointé par f .

233 / 399
Lecture dans un fichier

La fonction
int fscanf(FILE *f, char *format, V_1, ..., V_N);

permet de réaliser une lecture formatée depuis le fichier pointé par f .

Cette fonction se comporte comme scanf , à la différence que cette dernière travaille sur le fichier
stdin .

233 / 399
Plan

Entrées et sorties
Sortie
Entrée
Fichiers
Fichiers binaires

234 / 399
Fichiers binaires — introduction
À la différence des fichiers textes qui sont constitués de caractères (et sont donc potentiellement
lisibles par un être humain directement), les fichiers binaires contiennent des suites de bits non
formatées.

235 / 399
Fichiers binaires — introduction
À la différence des fichiers textes qui sont constitués de caractères (et sont donc potentiellement
lisibles par un être humain directement), les fichiers binaires contiennent des suites de bits non
formatées.
Par exemple, si ft pointe vers un fichier texte, l’instruction
fprintf(ft, "%d", 261)

place les caractères ’2’ , ’6’ et ’1’ dans le fichier en question.

235 / 399
Fichiers binaires — introduction
À la différence des fichiers textes qui sont constitués de caractères (et sont donc potentiellement
lisibles par un être humain directement), les fichiers binaires contiennent des suites de bits non
formatées.
Par exemple, si ft pointe vers un fichier texte, l’instruction
fprintf(ft, "%d", 261)

place les caractères ’2’ , ’6’ et ’1’ dans le fichier en question. Ceci écrit donc les trois octets
00110010 , 00110110 et 00110001 , obtenus en interprétant en base deux les valeurs ASCII de ces
caractères décimaux.

235 / 399
Fichiers binaires — introduction
À la différence des fichiers textes qui sont constitués de caractères (et sont donc potentiellement
lisibles par un être humain directement), les fichiers binaires contiennent des suites de bits non
formatées.
Par exemple, si ft pointe vers un fichier texte, l’instruction
fprintf(ft, "%d", 261)

place les caractères ’2’ , ’6’ et ’1’ dans le fichier en question. Ceci écrit donc les trois octets
00110010 , 00110110 et 00110001 , obtenus en interprétant en base deux les valeurs ASCII de ces
caractères décimaux.
En revanche, si fb pointe vers un fichier binaire, on emploiera l’instruction d’écriture fwrite et,
si v est une variable entière de valeur 261 ,
fwrite(&v, sizeof(int), 1, fb)

écrit dans le fichier les 4 (= sizeof(int) ) octets 00000000 , 00000000 , 00000001 , 00000101 à la suite
(développement en base deux de la valeur).

235 / 399
Fichiers binaires — introduction
À la différence des fichiers textes qui sont constitués de caractères (et sont donc potentiellement
lisibles par un être humain directement), les fichiers binaires contiennent des suites de bits non
formatées.
Par exemple, si ft pointe vers un fichier texte, l’instruction
fprintf(ft, "%d", 261)

place les caractères ’2’ , ’6’ et ’1’ dans le fichier en question. Ceci écrit donc les trois octets
00110010 , 00110110 et 00110001 , obtenus en interprétant en base deux les valeurs ASCII de ces
caractères décimaux.
En revanche, si fb pointe vers un fichier binaire, on emploiera l’instruction d’écriture fwrite et,
si v est une variable entière de valeur 261 ,
fwrite(&v, sizeof(int), 1, fb)

écrit dans le fichier les 4 (= sizeof(int) ) octets 00000000 , 00000000 , 00000001 , 00000101 à la suite
(développement en base deux de la valeur).
Ainsi, on écrit/lit dans/depuis un fichier binaire directement des valeurs, sans se soucier du
format à adopter. 235 / 399
Ouverture de fichiers en mode binaire
On utilise les modes d’ouverture habituels avec un b en plus pour signaler l’ouverture en binaire.
I "rb" : lecture binaire seule.

I "wb" : écriture binaire seule. Si le fichier n’existe pas, il est créé.

I "ab" : écriture binaire en ajout. Permet d’écrire dans le fichier en partant de la fin. Si le
fichier n’existe pas, il est créé.
I "rb+" : lecture binaire et écriture binaire .

I "wb+" : lecture binaire et écriture binaire avec suppression préalable du contenu du fichier.
Si le fichier n’existe pas, il est créé.
I "ab+" : lecture binaire et écriture binaire en ajout. Permet de lire et d’écrire dans le fichier
en partant de la fin. Si le fichier n’existe pas, il est créé.

236 / 399
Écriture dans un fichier binaire

On utilise la fonction
int fwrite(void *ptr, int taille, int nb, FILE *f);

pour écrire dans un fichier pointé par f ouvert en mode binaire.

237 / 399
Écriture dans un fichier binaire

On utilise la fonction
int fwrite(void *ptr, int taille, int nb, FILE *f);

pour écrire dans un fichier pointé par f ouvert en mode binaire.

Cette fonction écrit les nb valeurs de taille taille octets pointées par le pointeur ptr dans le
fichier pointé par f .

237 / 399
Écriture dans un fichier binaire

On utilise la fonction
int fwrite(void *ptr, int taille, int nb, FILE *f);

pour écrire dans un fichier pointé par f ouvert en mode binaire.

Cette fonction écrit les nb valeurs de taille taille octets pointées par le pointeur ptr dans le
fichier pointé par f .

Cette fonction renvoie le nombre de valeurs écrites.

237 / 399
Exemple d’utilisation de fwrite
Ce programme écrit en mode binaire dans le fichier fic la valeur d’une variable d’un type
structuré.

238 / 399
Exemple d’utilisation de fwrite
Ce programme écrit en mode binaire dans le fichier fic la valeur d’une variable d’un type
structuré.

typedef struct {
unsigned char rouge;
unsigned char bleu;
unsigned char vert;
} Couleur;

238 / 399
Exemple d’utilisation de fwrite
Ce programme écrit en mode binaire dans le fichier fic la valeur d’une variable d’un type
structuré.

#include <stdio.h>

typedef struct {
unsigned char rouge;
unsigned char bleu;
unsigned char vert;
} Couleur;

int main() {
FILE *f;
Couleur coul;
coul.rouge = 120; coul.bleu = 200; coul.vert = 12;

return 0;
}

238 / 399
Exemple d’utilisation de fwrite
Ce programme écrit en mode binaire dans le fichier fic la valeur d’une variable d’un type
structuré.

#include <stdio.h>

typedef struct {
unsigned char rouge;
unsigned char bleu;
unsigned char vert;
} Couleur;

int main() {
FILE *f;
Couleur coul;
coul.rouge = 120; coul.bleu = 200; coul.vert = 12;

f = fopen("fic", "wb");

return 0;
}

238 / 399
Exemple d’utilisation de fwrite
Ce programme écrit en mode binaire dans le fichier fic la valeur d’une variable d’un type
structuré.

#include <stdio.h>

typedef struct {
unsigned char rouge;
unsigned char bleu;
unsigned char vert;
} Couleur;

int main() {
FILE *f;
Couleur coul;
coul.rouge = 120; coul.bleu = 200; coul.vert = 12;

f = fopen("fic", "wb");
fwrite(&coul, sizeof(Couleur), 1, f);

return 0;
}

238 / 399
Exemple d’utilisation de fwrite
Ce programme écrit en mode binaire dans le fichier fic la valeur d’une variable d’un type
structuré.

#include <stdio.h>

typedef struct {
unsigned char rouge;
unsigned char bleu;
unsigned char vert;
} Couleur;

int main() {
FILE *f;
Couleur coul;
coul.rouge = 120; coul.bleu = 200; coul.vert = 12;

f = fopen("fic", "wb");
fwrite(&coul, sizeof(Couleur), 1, f);
fclose(f);

return 0;
}

238 / 399
Exemple d’utilisation de fwrite
Ce programme écrit en mode binaire dans le fichier fic la valeur d’une variable d’un type
structuré.

#include <stdio.h>

typedef struct {
unsigned char rouge;
unsigned char bleu;
unsigned char vert;
} Couleur;

int main() {
FILE *f;
Couleur coul;
coul.rouge = 120; coul.bleu = 200; coul.vert = 12;

f = fopen("fic", "wb");
fwrite(&coul, sizeof(Couleur), 1, f);
fclose(f);

return 0;
}

Remarque : il n’a pas été nécessaire ici de définir un format d’écriture (ce qui aurait été imposé
en cas d’écriture dans un fichier texte).
238 / 399
Lecture dans un fichier binaire

On utilise la fonction
int fread(void *ptr, int taille, int nb, FILE *f);

pour lire dans un fichier pointé par f ouvert en mode binaire.

239 / 399
Lecture dans un fichier binaire

On utilise la fonction
int fread(void *ptr, int taille, int nb, FILE *f);

pour lire dans un fichier pointé par f ouvert en mode binaire.

Cette fonction lit nb valeurs de taille taille octets dans le fichier pointé par f et les place à
l’adresse pointée par le pointeur ptr .

239 / 399
Lecture dans un fichier binaire

On utilise la fonction
int fread(void *ptr, int taille, int nb, FILE *f);

pour lire dans un fichier pointé par f ouvert en mode binaire.

Cette fonction lit nb valeurs de taille taille octets dans le fichier pointé par f et les place à
l’adresse pointée par le pointeur ptr .

Cette fonction renvoie le nombre de valeurs lues.

239 / 399
Exemple d’utilisation de fread
Ce programme écrit en mode binaire dans le fichier fic un tableau d’entiers et le lit.

240 / 399
Exemple d’utilisation de fread
Ce programme écrit en mode binaire dans le fichier fic un tableau d’entiers et le lit.

#include <stdio.h>

int main() {
FILE *f;
int i, tab_1[12], tab_2[12];

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


tab_1[i] = i;

return 0;
}

240 / 399
Exemple d’utilisation de fread
Ce programme écrit en mode binaire dans le fichier fic un tableau d’entiers et le lit.

#include <stdio.h>

int main() {
FILE *f;
int i, tab_1[12], tab_2[12];

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


tab_1[i] = i;

f = fopen("fic", "wb");

return 0;
}

240 / 399
Exemple d’utilisation de fread
Ce programme écrit en mode binaire dans le fichier fic un tableau d’entiers et le lit.

#include <stdio.h>

int main() {
FILE *f;
int i, tab_1[12], tab_2[12];

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


tab_1[i] = i;

f = fopen("fic", "wb");
fwrite(tab_1, sizeof(int), 12, f);

return 0;
}

240 / 399
Exemple d’utilisation de fread
Ce programme écrit en mode binaire dans le fichier fic un tableau d’entiers et le lit.

#include <stdio.h>

int main() {
FILE *f;
int i, tab_1[12], tab_2[12];

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


tab_1[i] = i;

f = fopen("fic", "wb");
fwrite(tab_1, sizeof(int), 12, f);
fclose(f);

return 0;
}

240 / 399
Exemple d’utilisation de fread
Ce programme écrit en mode binaire dans le fichier fic un tableau d’entiers et le lit.

#include <stdio.h>

int main() {
FILE *f;
int i, tab_1[12], tab_2[12];

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


tab_1[i] = i;

f = fopen("fic", "wb");
fwrite(tab_1, sizeof(int), 12, f);
fclose(f);

f = fopen("fic", "rb");

return 0;
}

240 / 399
Exemple d’utilisation de fread
Ce programme écrit en mode binaire dans le fichier fic un tableau d’entiers et le lit.

#include <stdio.h>

int main() {
FILE *f;
int i, tab_1[12], tab_2[12];

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


tab_1[i] = i;

f = fopen("fic", "wb");
fwrite(tab_1, sizeof(int), 12, f);
fclose(f);

f = fopen("fic", "rb");
fread(tab_2, sizeof(int), 12, f);

return 0;
}

240 / 399
Exemple d’utilisation de fread
Ce programme écrit en mode binaire dans le fichier fic un tableau d’entiers et le lit.

#include <stdio.h>

int main() {
FILE *f;
int i, tab_1[12], tab_2[12];

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


tab_1[i] = i;

f = fopen("fic", "wb");
fwrite(tab_1, sizeof(int), 12, f);
fclose(f);

f = fopen("fic", "rb");
fread(tab_2, sizeof(int), 12, f);
fclose(f);

return 0;
}

240 / 399
Exemple d’utilisation de fread
Ce programme écrit en mode binaire dans le fichier fic un tableau d’entiers et le lit.

#include <stdio.h>

int main() {
FILE *f;
int i, tab_1[12], tab_2[12];

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


tab_1[i] = i;

f = fopen("fic", "wb");
fwrite(tab_1, sizeof(int), 12, f);
fclose(f);

f = fopen("fic", "rb");
fread(tab_2, sizeof(int), 12, f);
fclose(f);

return 0;
}

Remarque : il n’a pas été nécessaire de se baser sur un format pour la lecture (ce qui aurait été
imposé en cas de lecture depuis un fichier texte).
240 / 399
Plan

Types
Notion de type
Types scalaires
Types construits

241 / 399
Plan

Types
Notion de type
Types scalaires
Types construits

242 / 399
Types

Un type peut être vu comme un ensemble (fini ou infini) de valeurs.

243 / 399
Types

Un type peut être vu comme un ensemble (fini ou infini) de valeurs.

Dire qu’une variable x est de type T signifie que la valeur de x est dans T .

243 / 399
Types

Un type peut être vu comme un ensemble (fini ou infini) de valeurs.

Dire qu’une variable x est de type T signifie que la valeur de x est dans T .

Il existe deux sortes de types :


1. les types scalaires, qui sont des types atomiques et définis à l’avance dans le langage ;

243 / 399
Types

Un type peut être vu comme un ensemble (fini ou infini) de valeurs.

Dire qu’une variable x est de type T signifie que la valeur de x est dans T .

Il existe deux sortes de types :


1. les types scalaires, qui sont des types atomiques et définis à l’avance dans le langage ;
2. les types composites, qui sont des assemblages de types scalaires ou de types composites
par le biais des constructions struct , enum ou tableau.

243 / 399
Type d’une variable
Le type d’une variable indique comment interpréter la zone mémoire qui lui est attribuée ainsi
que sa taille.

244 / 399
Type d’une variable
Le type d’une variable indique comment interpréter la zone mémoire qui lui est attribuée ainsi
que sa taille.

L’opérateur sizeof permet de connaître la taille en octets d’un type. On peut aussi l’appliquer à
une valeur. P.ex., sizeof(int) et sizeof(33) valent 4 .

244 / 399
Type d’une variable
Le type d’une variable indique comment interpréter la zone mémoire qui lui est attribuée ainsi
que sa taille.

L’opérateur sizeof permet de connaître la taille en octets d’un type. On peut aussi l’appliquer à
une valeur. P.ex., sizeof(int) et sizeof(33) valent 4 .

int t; printf("%d\n", sizeof(’a’));


char c;
Ceci affiche 4 4 4 1 1 4 .
printf("%d ", sizeof(int));
printf("%d ", sizeof(t)); L’expression sizeof(’a’) vaut 4 . En effet,
printf("%d ", sizeof(31));
même si ’a’ est un caractère, c’est avant tout
printf("%d ", sizeof(char)); un entier.
printf("%d ", sizeof(c));
La conversion est implicite.

244 / 399
Type d’une variable
Le type d’une variable indique comment interpréter la zone mémoire qui lui est attribuée ainsi
que sa taille.

L’opérateur sizeof permet de connaître la taille en octets d’un type. On peut aussi l’appliquer à
une valeur. P.ex., sizeof(int) et sizeof(33) valent 4 .

int t; printf("%d\n", sizeof(’a’));


char c;
Ceci affiche 4 4 4 1 1 4 .
printf("%d ", sizeof(int));
printf("%d ", sizeof(t)); L’expression sizeof(’a’) vaut 4 . En effet,
printf("%d ", sizeof(31));
même si ’a’ est un caractère, c’est avant tout
printf("%d ", sizeof(char)); un entier.
printf("%d ", sizeof(c));
La conversion est implicite.

Le type d’une variable est attribué à sa déclaration et ne peut pas être modifié.
244 / 399
Plan

Types
Notion de type
Types scalaires
Types construits

245 / 399
Types entier
On se place sur une machine 64 bits.

Nom Taille (octets) Plage


char 1 −128 à 127
short 2 −32768 à 32767
int 4 −231 à 231 − 1
long 8 −263 à 263 − 1

246 / 399
Types entier
On se place sur une machine 64 bits.

Nom Taille (octets) Plage


char 1 −128 à 127
short 2 −32768 à 32767
int 4 −231 à 231 − 1
long 8 −263 à 263 − 1
Chacun de ces types peut être précédé de unsigned pour faire en sorte de ne représenter que des
entiers positifs. On a ainsi les plages suivantes :
Nom Plage
unsigned char 0 à 255
unsigned short 0 à 65535
unsigned int 0 à 232 − 1
unsigned long 0 à 264 − 1
246 / 399
Entiers non signés
Dans le cas où l’on a besoin de représenter uniquement des valeurs entières positives, on utilisera
les versions non signées des types entiers.

247 / 399
Entiers non signés
Dans le cas où l’on a besoin de représenter uniquement des valeurs entières positives, on utilisera
les versions non signées des types entiers.
Quelques avantages de ce procédé :
1. possibilité de représenter des entiers plus grands ;
2. gain de lisibilité du programme.

247 / 399
Entiers non signés
Dans le cas où l’on a besoin de représenter uniquement des valeurs entières positives, on utilisera
les versions non signées des types entiers.
Quelques avantages de ce procédé :
1. possibilité de représenter des entiers plus grands ;
2. gain de lisibilité du programme.

Attention : les instructions


unsigned int i;
for (i = 8 ; i >= 0 ; --i) {
...
}

produisent une boucle infinie. En effet, i étant non signé, il est toujours positif et donc la
condition i >= 0 est toujours vraie.

247 / 399
Constantes entières
Il existe plusieurs manières d’exprimer des constantes entières :
I en base dix : 0 , 29 , -322 , ...

248 / 399
Constantes entières
Il existe plusieurs manières d’exprimer des constantes entières :
I en base dix : 0 , 29 , -322 , ...

I en octal : 01 , 0145 , -01234567 , ...

248 / 399
Constantes entières
Il existe plusieurs manières d’exprimer des constantes entières :
I en base dix : 0 , 29 , -322 , ...

I en octal : 01 , 0145 , -01234567 , ...

I en hexadécimal : 0x1 , 0x5555FFFF , -0x98879AFA , ...

248 / 399
Constantes entières
Il existe plusieurs manières d’exprimer des constantes entières :
I en base dix : 0 , 29 , -322 , ...

I en octal : 01 , 0145 , -01234567 , ...

I en hexadécimal : 0x1 , 0x5555FFFF , -0x98879AFA , ...

I par un caractère : ’a’ , ’9’ , ’*’ , ’\n’ , ...

248 / 399
Constantes entières
Il existe plusieurs manières d’exprimer des constantes entières :
I en base dix : 0 , 29 , -322 , ...

I en octal : 01 , 0145 , -01234567 , ...

I en hexadécimal : 0x1 , 0x5555FFFF , -0x98879AFA , ...

I par un caractère : ’a’ , ’9’ , ’*’ , ’\n’ , ...

Un entier peut être représenté par un caractère car tout caractère est représenté par son code
ASCII (qui est un entier compris entre 0 et 127 ).

248 / 399
Constantes entières
Il existe plusieurs manières d’exprimer des constantes entières :
I en base dix : 0 , 29 , -322 , ...

I en octal : 01 , 0145 , -01234567 , ...

I en hexadécimal : 0x1 , 0x5555FFFF , -0x98879AFA , ...

I par un caractère : ’a’ , ’9’ , ’*’ , ’\n’ , ...

Un entier peut être représenté par un caractère car tout caractère est représenté par son code
ASCII (qui est un entier compris entre 0 et 127 ).

Attention : ne pas confondre les caractères chiffres avec les entiers (l’entier ’1’ vaut 49 et non
pas 1 ).

248 / 399
Types flottant

On se place sur une machine 64 bits.

Nom Taille (octets) Valeur absolue maximale


float 4 3.40282 × 1038
double 8 1.79769 × 10308
long double 16 1.18973 × 104932

Le fichier d’en-tête float.h contient des constantes donnant d’autres renseignements sur les
types flottant.

249 / 399
Danger des types flottant

float x = 10000001.0; Ces instructions affichent, de manière


printf("%f\n", x); attendue, 10000001.000000 .

250 / 399
Danger des types flottant

float x = 10000001.0; Ces instructions affichent, de manière


printf("%f\n", x); attendue, 10000001.000000 .

float x = 100000001.0; En revanche, ces instructions affichent, de


printf("%f\n", x); manière inattendue, 100000000.000000 .

250 / 399
Danger des types flottant

float x = 10000001.0; Ces instructions affichent, de manière


printf("%f\n", x); attendue, 10000001.000000 .

float x = 100000001.0; En revanche, ces instructions affichent, de


printf("%f\n", x); manière inattendue, 100000000.000000 .

Les nombres flottants sont représentés de manière approchée.


Comme ces exemples le montrent, même certains entiers, représentables de manière exacte par
des types entier, ne le sont pas par des types flottant.

250 / 399
Danger des types flottant : une solution partielle

Les types flottant présentent divers désavantages par rapport aux types entier :
1. représentation non exacte des nombres ;
2. opérations arithmétiques beaucoup moins efficaces.

251 / 399
Danger des types flottant : une solution partielle

Les types flottant présentent divers désavantages par rapport aux types entier :
1. représentation non exacte des nombres ;
2. opérations arithmétiques beaucoup moins efficaces.

Pour ces raisons, il est recommandé de ne jamais utiliser de types flottant.

251 / 399
Danger des types flottant : une solution partielle

Les types flottant présentent divers désavantages par rapport aux types entier :
1. représentation non exacte des nombres ;
2. opérations arithmétiques beaucoup moins efficaces.

Pour ces raisons, il est recommandé de ne jamais utiliser de types flottant.

Solution partielle : on représente par l’entier x × 10k tout nombre x qui dispose de k > 0
chiffres (en base dix) après la virgule, k étant fixé.

251 / 399
Danger des types flottant : une solution partielle

Les types flottant présentent divers désavantages par rapport aux types entier :
1. représentation non exacte des nombres ;
2. opérations arithmétiques beaucoup moins efficaces.

Pour ces raisons, il est recommandé de ne jamais utiliser de types flottant.

Solution partielle : on représente par l’entier x × 10k tout nombre x qui dispose de k > 0
chiffres (en base dix) après la virgule, k étant fixé.
P.ex., si l’on a besoin de manipuler des nombres à k := 2 chiffres après la virgule, les nombres
0.15 et 331.9 sont respectivement représentés par les entiers 15 et 33190.

251 / 399
Opérations sur les types scalaires

Les valeurs d’un type scalaire (entier ou flottant) forment un ensemble totalement ordonné :
étant donné deux valeurs, il est toujours possible de les comparer. On utilise pour cela les
opérateurs relationnels
== , != , <= , >= , < , > .

252 / 399
Opérations sur les types scalaires

Les valeurs d’un type scalaire (entier ou flottant) forment un ensemble totalement ordonné :
étant donné deux valeurs, il est toujours possible de les comparer. On utilise pour cela les
opérateurs relationnels
== , != , <= , >= , < , > .

Il est possible de mélanger des comparaisons de valeurs de types entier et de types flottant. Dans
ce cas, les entiers sont convertis implicitement en une valeur de type flottant avant d’effectuer la
comparaison.

252 / 399
Opérations sur les types scalaires

Les valeurs d’un type scalaire (entier ou flottant) forment un ensemble totalement ordonné :
étant donné deux valeurs, il est toujours possible de les comparer. On utilise pour cela les
opérateurs relationnels
== , != , <= , >= , < , > .

Il est possible de mélanger des comparaisons de valeurs de types entier et de types flottant. Dans
ce cas, les entiers sont convertis implicitement en une valeur de type flottant avant d’effectuer la
comparaison.

Sur des variables de type scalaire sont définis les opérateurs arithmétiques
+ , - , * , / , ++ , -- .

Les opérateurs ++ et -- servent à additionner ou à retrancher de 1 la valeur des variables sur


lesquels ils sont appliqués.

252 / 399
Plan

Types
Notion de type
Types scalaires
Types construits

253 / 399
Types structurés
La syntaxe
typedef struct ALIAS {

TYPE_1 CHAMP_1;

TYPE_2 CHAMP_2;

...

} NOM;

permet de déclarer un type structuré NOM , constitué des champs CHAMP_1 , CHAMP_2 , . . .. L’alias
ALIAS est facultatif.

254 / 399
Types structurés
La syntaxe
typedef struct ALIAS {

TYPE_1 CHAMP_1;

TYPE_2 CHAMP_2;

...

} NOM;

permet de déclarer un type structuré NOM , constitué des champs CHAMP_1 , CHAMP_2 , . . .. L’alias
ALIAS est facultatif.

C’est un amalgame de types.

254 / 399
Types structurés
La syntaxe
typedef struct ALIAS {

TYPE_1 CHAMP_1;

TYPE_2 CHAMP_2;

...

} NOM;

permet de déclarer un type structuré NOM , constitué des champs CHAMP_1 , CHAMP_2 , . . .. L’alias
ALIAS est facultatif.

C’est un amalgame de types.

P.ex.,
typedef struct { } Couple;
int x;
int y; déclare un type structuré Couple qui permet de
représenter des couples d’entiers.
254 / 399
Types structurés
Si x est une variable d’un type structuré T contenant le champ ch , on accède à ce champ par
la syntaxe
x.ch

255 / 399
Types structurés
Si x est une variable d’un type structuré T contenant le champ ch , on accède à ce champ par
la syntaxe
x.ch

Si adr_x est une adresse sur une variable de type T , on accède à ce même champ par la syntaxe

adr_x->ch

255 / 399
Types structurés
Si x est une variable d’un type structuré T contenant le champ ch , on accède à ce champ par
la syntaxe
x.ch

Si adr_x est une adresse sur une variable de type T , on accède à ce même champ par la syntaxe

adr_x->ch

Cette syntaxe est un raccourci pour


(*adr_x).ch

255 / 399
Types structurés
Si x est une variable d’un type structuré T contenant le champ ch , on accède à ce champ par
la syntaxe
x.ch

Si adr_x est une adresse sur une variable de type T , on accède à ce même champ par la syntaxe

adr_x->ch

Cette syntaxe est un raccourci pour


(*adr_x).ch

P.ex., les trois suites d’instructions suivantes sont équivalentes :


Couple *c; Couple *c; Couple *c;
... ... ...
c->x = c->x + 1; (*c).x = c->x + 1; c->x = (*c).x + 1;

255 / 399
Opérations sur les types structurés
Les opérateurs relationnels ne sont pas définis sur les types structurés.
Il est donc impossible de tester si deux variables d’un même type structuré sont égales au moyen
de l’opérateur == . Il faut tester l’égalité de chacun des champs qui les constituent.

256 / 399
Opérations sur les types structurés
Les opérateurs relationnels ne sont pas définis sur les types structurés.
Il est donc impossible de tester si deux variables d’un même type structuré sont égales au moyen
de l’opérateur == . Il faut tester l’égalité de chacun des champs qui les constituent.

En revanche, l’opérateur d’affectation = est compatible avec les types structurés.

256 / 399
Opérations sur les types structurés
Les opérateurs relationnels ne sont pas définis sur les types structurés.
Il est donc impossible de tester si deux variables d’un même type structuré sont égales au moyen
de l’opérateur == . Il faut tester l’égalité de chacun des champs qui les constituent.

En revanche, l’opérateur d’affectation = est compatible avec les types structurés.

typedef struct { L’affectation en dernière ligne fait en sorte que


char nom[32]; tous les champs de p2 contiennent les mêmes
char prenom[32];
int age; valeurs que ceux de p1 .
} Personne;
...
Personne p1, p2;
scanf(" %s", p1.nom);
scanf(" %s", p1.prenom);
p1.age = 30;
p2 = p1;

256 / 399
Opérations sur les types structurés
Les opérateurs relationnels ne sont pas définis sur les types structurés.
Il est donc impossible de tester si deux variables d’un même type structuré sont égales au moyen
de l’opérateur == . Il faut tester l’égalité de chacun des champs qui les constituent.

En revanche, l’opérateur d’affectation = est compatible avec les types structurés.

typedef struct { L’affectation en dernière ligne fait en sorte que


char nom[32]; tous les champs de p2 contiennent les mêmes
char prenom[32];
int age; valeurs que ceux de p1 .
} Personne;
... Il y a recopie des tableaux statiques p1.nom et
Personne p1, p2;
scanf(" %s", p1.nom); p1.prenom dans p2.nom et p2.prenom .
scanf(" %s", p1.prenom);
p1.age = 30; Ce phénomène va être étudié en détail plus
p2 = p1; loin.

256 / 399
Types énumérés
La syntaxe
typedef enum {

ENU_1,

ENU_2,

...

} NOM;

permet de déclarer un type énuméré NOM , constitué des énumérateurs ENU_1 , ENU_2 , . . ..
(Attention, on utilise des , et non pas des ; .)

257 / 399
Types énumérés
La syntaxe
typedef enum {

ENU_1,

ENU_2,

...

} NOM;

permet de déclarer un type énuméré NOM , constitué des énumérateurs ENU_1 , ENU_2 , . . ..
(Attention, on utilise des , et non pas des ; .)

Une valeur de ce type prend pour valeur exactement un des énumérateurs qui le constituent.

257 / 399
Types énumérés
La syntaxe
typedef enum {

ENU_1,

ENU_2,

...

} NOM;

permet de déclarer un type énuméré NOM , constitué des énumérateurs ENU_1 , ENU_2 , . . ..
(Attention, on utilise des , et non pas des ; .)

Une valeur de ce type prend pour valeur exactement un des énumérateurs qui le constituent.
P.ex.,
typedef enum { est un type qui permet de représenter des
FAUX, booléens.
VRAI Une valeur de type Booleen est soit FAUX , soit
} Booleen;
VRAI .
257 / 399
Types énumérés
typedef enum { Les énumérateurs sont des expressions
LUNDI, /* = 0 */ entières. Leur valeur est déterminée par leur
MARDI, /* = 1 */
MERCREDI, /* = 2 */
ordre de déclaration dans le type.
JEUDI, /* = 3 */ Ces instructions affichent 2 . En effet, LUNDI
VENDREDI, /* = 4 */
SAMEDI, /* = 5 */ vaut 0 car il est le énumérateur déclaré et
1er
DIMANCHE /* = 6 */ les valeurs des suivants s’incrémentent selon
} Jour; leur ordre de déclaration.
...
printf("%d\n", MERCREDI);

258 / 399
Types énumérés
typedef enum { Les énumérateurs sont des expressions
LUNDI, /* = 0 */ entières. Leur valeur est déterminée par leur
MARDI, /* = 1 */
MERCREDI, /* = 2 */
ordre de déclaration dans le type.
JEUDI, /* = 3 */ Ces instructions affichent 2 . En effet, LUNDI
VENDREDI, /* = 4 */
SAMEDI, /* = 5 */ vaut 0 car il est le énumérateur déclaré et
1er
DIMANCHE /* = 6 */ les valeurs des suivants s’incrémentent selon
} Jour; leur ordre de déclaration.
...
printf("%d\n", MERCREDI);

typedef enum { Il est possible de spécifier manuellement les valeurs


LA = 0, des énumérateurs avec la syntaxe ENU = VAL où
SI = 2,
DO, /* = 3 */ ENU est un énumérateur et VAL une constante
RE = 5, entière.
MI = 7,
FA = 8,
SOL = 10 Si une valeur n’est pas spécifiée, elle est déduite de
} Note; la précédente en l’incrémentant.
258 / 399
Types énumérés
L’utilisation de branchement switch est particulièrement adaptée pour traiter une variable d’un
type énuméré.

259 / 399
Types énumérés
L’utilisation de branchement switch est particulièrement adaptée pour traiter une variable d’un
type énuméré.

Note note; Ces instructions lisent une valeur entière


(représentant une Note ) sur l’entrée standard
scanf(" %d", &note); et l’affichent (en notation internationale).
switch (note) {
case LA : printf("A"); break;
case SI : printf("B"); break;
case DO : printf("C"); break;
case RE : printf("D"); break;
case MI : printf("E"); break;
case FA : printf("F"); break;
case SOL : printf("G");break;
default : printf(
"%d non note", note);
}

259 / 399
Types énumérés
L’utilisation de branchement switch est particulièrement adaptée pour traiter une variable d’un
type énuméré.

Note note; Ces instructions lisent une valeur entière


(représentant une Note ) sur l’entrée standard
scanf(" %d", &note); et l’affichent (en notation internationale).
switch (note) { Remarque : une variable d’un type énuméré
case LA : printf("A"); break; peut prendre comme valeur n’importe quel
case SI : printf("B"); break; entier. Ceci explique la présence de la clause
case DO : printf("C"); break; default .
case RE : printf("D"); break;
case MI : printf("E"); break;
case FA : printf("F"); break;
case SOL : printf("G");break;
default : printf(
"%d non note", note);
}

259 / 399
Types énumérés
L’utilisation de branchement switch est particulièrement adaptée pour traiter une variable d’un
type énuméré.

Note note; Ces instructions lisent une valeur entière


(représentant une Note ) sur l’entrée standard
scanf(" %d", &note); et l’affichent (en notation internationale).
switch (note) { Remarque : une variable d’un type énuméré
case LA : printf("A"); break; peut prendre comme valeur n’importe quel
case SI : printf("B"); break; entier. Ceci explique la présence de la clause
case DO : printf("C"); break; default .
case RE : printf("D"); break;
L’intérêt de l’utilisation des types énumérés est
case MI : printf("E"); break;
principalement sémantique : un programme
case FA : printf("F"); break;
case SOL : printf("G");break; qui les utilise est plus facile à lire et à
default : printf( maintenir.
"%d non note", note);
}

259 / 399
Opérations sur les types énumérés

Contrairement aux types structurés, il est possible de comparer les éléments d’un type énuméré
au moyen des opérateurs relationnels.
Ceci est une conséquence du fait que les énumérateurs sont des entiers.

printf("%d\n", SOL == SOL); affiche 1


printf("%d\n", SOL == FA); affiche 0
printf("%d\n", SI <= RE);
affiche 1

260 / 399
Opérations sur les types énumérés

Contrairement aux types structurés, il est possible de comparer les éléments d’un type énuméré
au moyen des opérateurs relationnels.
Ceci est une conséquence du fait que les énumérateurs sont des entiers.

printf("%d\n", SOL == SOL); affiche 1


printf("%d\n", SOL == FA); affiche 0
printf("%d\n", SI <= RE);
affiche 1

De même, l’opérateur d’affectation = est compatible avec les types énumérés.

260 / 399
Opérations sur les types énumérés

Contrairement aux types structurés, il est possible de comparer les éléments d’un type énuméré
au moyen des opérateurs relationnels.
Ceci est une conséquence du fait que les énumérateurs sont des entiers.

printf("%d\n", SOL == SOL); affiche 1


printf("%d\n", SOL == FA); affiche 0
printf("%d\n", SI <= RE);
affiche 1

De même, l’opérateur d’affectation = est compatible avec les types énumérés.

L’opérateur de taille sizeof renvoie 4 sur les valeurs d’un type énuméré. C’est la taille
occupée par le type int .

260 / 399
Plan

Types structurés
Déclaration et initialisation
Affectation et comparaison
Dans les fonctions
Alignement en mémoire

261 / 399
Plan

Types structurés
Déclaration et initialisation
Affectation et comparaison
Dans les fonctions
Alignement en mémoire

262 / 399
Déclaration de types structurés récursifs
Il est possible de déclarer des types structurés récursifs en faisant usage de l’alias et du mot clé
struct :

1 typedef struct _Liste {


2 int e;
3 struct _Liste *s;
4 } Liste;

Ceci fonctionne car la taille d’un pointeur vers une valeur de type T est connue et indépendante
de la nature de T .

Attention, la déclaration
1 typedef struct _Liste {
2 int e;
3 struct _Liste s;
4 } Liste;

n’est pas valide car le champ récursif n’est pas un pointeur.


Le système ne peut pas connaître pas la taille de ce champ.
263 / 399
Déclaration de types structurés mutuellement récursifs
Il est possible de déclarer des types structurés mutuellement récursifs :
1 typedef struct _Flip {
2 int a;
3 int b;
4 struct _Flop *s;
5 } Flip;
6
7 typedef struct _Flop {
8 double d;
9 struct _Flip *s;
10 } Flop;

P.ex., x est une variable de type Flip représentée par

46 -3 &y 0 &z 46 -3 NULL ×

a : int b : int s : Flip * d : double s : Flip * a : int b : int s : Flip *

x : Flip y : Flop z : Flip

264 / 399
Initialisation d’une variable d’un type structuré

Il est possible d’initialiser les champs d’une variable d’un type structuré au moment de sa
déclaration.
On utilise pour cela l’opérateur d’affectation = avec comme valeur droite les valeurs des champs
à affecter dans des accolades et séparées par des virgules.
Par exemple,

1 typedef struct { Déclare, en l’initialisant, la variable tr .


2 char c;
3 int a;
4 double b;
5 } Triplet; ’h’ 55 214.35

6 ... c : char a : int b : double

7 Triplet tr = {’h’, 55, 214.35}; tr : Triplet

265 / 399
Plan

Types structurés
Déclaration et initialisation
Affectation et comparaison
Dans les fonctions
Alignement en mémoire

266 / 399
Affectation de variables d’un type structuré
Considérons le code

1 typedef struct { 6 X v1, v2;


2 int a; 7 v1.a = 10;
3 float f; 8 v1.f = 3.14;
4 } X; 9 v2 = v1;
5 ... 10 v2.a = 20;

267 / 399
Affectation de variables d’un type structuré
Considérons le code

1 typedef struct { 6 X v1, v2;


2 int a; 7 v1.a = 10;
3 float f; 8 v1.f = 3.14;
4 } X; 9 v2 = v1;
5 ... 10 v2.a = 20;

? ? ? ?

l. 6 : a : int f : float a : int f : float

v1 : X v2 : X

267 / 399
Affectation de variables d’un type structuré
Considérons le code

1 typedef struct { 6 X v1, v2;


2 int a; 7 v1.a = 10;
3 float f; 8 v1.f = 3.14;
4 } X; 9 v2 = v1;
5 ... 10 v2.a = 20;

? ? ? ?

l. 6 : a : int f : float a : int f : float

v1 : X v2 : X

10 3.14 ? ?

l. 8 : a : int f : float a : int f : float

v1 : X v2 : X

267 / 399
Affectation de variables d’un type structuré
Considérons le code

1 typedef struct { 6 X v1, v2;


2 int a; 7 v1.a = 10;
3 float f; 8 v1.f = 3.14;
4 } X; 9 v2 = v1;
5 ... 10 v2.a = 20;

? ? ? ?

l. 6 : a : int f : float a : int f : float

v1 : X v2 : X

10 3.14 ? ?

l. 8 : a : int f : float a : int f : float

v1 : X v2 : X

10 3.14 10 3.14

l. 9 : a : int f : float a : int f : float

v1 : X v2 : X

267 / 399
Affectation de variables d’un type structuré
Considérons le code

1 typedef struct { 6 X v1, v2;


2 int a; 7 v1.a = 10;
3 float f; 8 v1.f = 3.14;
4 } X; 9 v2 = v1;
5 ... 10 v2.a = 20;

? ? ? ?

l. 6 : a : int f : float a : int f : float

v1 : X v2 : X

10 3.14 ? ?

l. 8 : a : int f : float a : int f : float

v1 : X v2 : X

10 3.14 10 3.14

l. 9 : a : int f : float a : int f : float

v1 : X v2 : X

10 3.14 20 3.14

l. 10 : a : int f : float a : int f : float

v1 : X v2 : X

267 / 399
Affectation de variables d’un type structuré
Considérons le code

1 typedef struct { 6 X v1, v2;


2 int a; 7 v1.a = 10;
3 float f; 8 v1.f = 3.14;
4 } X; 9 v2 = v1;
5 ... 10 v2.a = 20;

? ? ? ?

l. 6 : a : int f : float a : int f : float

v1 : X v2 : X

10 3.14 ? ?

l. 8 : a : int f : float a : int f : float

v1 : X v2 : X

10 3.14 10 3.14

l. 9 : a : int f : float a : int f : float

v1 : X v2 : X

10 3.14 20 3.14

l. 10 : a : int f : float a : int f : float

v1 : X v2 : X

Observation : l’affectation recopie les champs d’une variable d’un type scalaire.
267 / 399
Affectation de variables d’un type structuré
Considérons le code

1 typedef struct { 7 v1.x.a = 10;


2 X x; 8 v1.x.f = 3.14;
3 char t[3]; 9 v1.t = {’a’, ’b’, ’c’};
4 } Y; 10 v2 = v1;
5 ... 11 v2.x.f = 1.8;
6 Y v1, v2; 12 v2.t[0] = ’g’;

268 / 399
Affectation de variables d’un type structuré
Considérons le code

1 typedef struct { 7 v1.x.a = 10;


2 X x; 8 v1.x.f = 3.14;
3 char t[3]; 9 v1.t = {’a’, ’b’, ’c’};
4 } Y; 10 v2 = v1;
5 ... 11 v2.x.f = 1.8;
6 Y v1, v2; 12 v2.t[0] = ’g’;

10 3.14 ? ?

a : int f : float {’a’, ’b’, ’c’} a : int f : float ?


l. 9 : x : X t : char[3] x : X t : char[3]

v1 : Y v2 : Y

268 / 399
Affectation de variables d’un type structuré
Considérons le code

1 typedef struct { 7 v1.x.a = 10;


2 X x; 8 v1.x.f = 3.14;
3 char t[3]; 9 v1.t = {’a’, ’b’, ’c’};
4 } Y; 10 v2 = v1;
5 ... 11 v2.x.f = 1.8;
6 Y v1, v2; 12 v2.t[0] = ’g’;

10 3.14 ? ?

a : int f : float {’a’, ’b’, ’c’} a : int f : float ?


l. 9 : x : X t : char[3] x : X t : char[3]

v1 : Y v2 : Y

10 3.14 10 3.14

a : int f : float {’a’, ’b’, ’c’} a : int f : float {’a’, ’b’, ’c’}
l. 10 : x : X t : char[3] x : X t : char[3]

v1 : Y v2 : Y

268 / 399
Affectation de variables d’un type structuré
Considérons le code

1 typedef struct { 7 v1.x.a = 10;


2 X x; 8 v1.x.f = 3.14;
3 char t[3]; 9 v1.t = {’a’, ’b’, ’c’};
4 } Y; 10 v2 = v1;
5 ... 11 v2.x.f = 1.8;
6 Y v1, v2; 12 v2.t[0] = ’g’;

10 3.14 ? ?

a : int f : float {’a’, ’b’, ’c’} a : int f : float ?


l. 9 : x : X t : char[3] x : X t : char[3]

v1 : Y v2 : Y

10 3.14 10 3.14

a : int f : float {’a’, ’b’, ’c’} a : int f : float {’a’, ’b’, ’c’}
l. 10 : x : X t : char[3] x : X t : char[3]

v1 : Y v2 : Y

10 3.14 10 1.8

a : int f : float {’a’, ’b’, ’c’} a : int f : float {’g’, ’b’, ’c’}
l. 12 : x : X t : char[3] x : X t : char[3]

v1 : Y v2 : Y

268 / 399
Affectation de variables d’un type structuré
Considérons le code

1 typedef struct { 7 v1.x.a = 10;


2 X x; 8 v1.x.f = 3.14;
3 char t[3]; 9 v1.t = {’a’, ’b’, ’c’};
4 } Y; 10 v2 = v1;
5 ... 11 v2.x.f = 1.8;
6 Y v1, v2; 12 v2.t[0] = ’g’;

10 3.14 ? ?

a : int f : float {’a’, ’b’, ’c’} a : int f : float ?


l. 9 : x : X t : char[3] x : X t : char[3]

v1 : Y v2 : Y

10 3.14 10 3.14

a : int f : float {’a’, ’b’, ’c’} a : int f : float {’a’, ’b’, ’c’}
l. 10 : x : X t : char[3] x : X t : char[3]

v1 : Y v2 : Y

10 3.14 10 1.8

a : int f : float {’a’, ’b’, ’c’} a : int f : float {’g’, ’b’, ’c’}
l. 12 : x : X t : char[3] x : X t : char[3]

v1 : Y v2 : Y

Observation : l’affectation recopie les champs d’une variable d’un type structuré de manière
récursive et les tableaux statiques. 268 / 399
Affectation de variables d’un type structuré
Considérons le code

1 typedef struct { 6 T v1, v2; 11 v1.t[2] = ’c’;


2 char *t; 7 v1.t = malloc(3); 12 v2 = v1;
3 int n; 8 v1.n = 3; 13 v2.n = 2;
4 } T; 9 v1.t[0] = ’a’; 14 v2.t[0] = ’g’;
5 ... 10 v1.t[1] = ’b’;

269 / 399
Affectation de variables d’un type structuré
Considérons le code

1 typedef struct { 6 T v1, v2; 11 v1.t[2] = ’c’;


2 char *t; 7 v1.t = malloc(3); 12 v2 = v1;
3 int n; 8 v1.n = 3; 13 v2.n = 2;
4 } T; 9 v1.t[0] = ’a’; 14 v2.t[0] = ’g’;
5 ... 10 v1.t[1] = ’b’;
{’a’, ’b’, ’c’}

l. 11 : v1.t 3 ? ?

t : char * n : int t : char * n : int

v1 : T v2 : T

269 / 399
Affectation de variables d’un type structuré
Considérons le code

1 typedef struct { 6 T v1, v2; 11 v1.t[2] = ’c’;


2 char *t; 7 v1.t = malloc(3); 12 v2 = v1;
3 int n; 8 v1.n = 3; 13 v2.n = 2;
4 } T; 9 v1.t[0] = ’a’; 14 v2.t[0] = ’g’;
5 ... 10 v1.t[1] = ’b’;
{’a’, ’b’, ’c’}

l. 11 : v1.t 3 ? ?

t : char * n : int t : char * n : int

v1 : T v2 : T

{’a’, ’b’, ’c’}

l. 12 : v1.t 3 v1.t 3

t : char * n : int t : char * n : int

v1 : T v2 : T

269 / 399
Affectation de variables d’un type structuré
Considérons le code

1 typedef struct { 6 T v1, v2; 11 v1.t[2] = ’c’;


2 char *t; 7 v1.t = malloc(3); 12 v2 = v1;
3 int n; 8 v1.n = 3; 13 v2.n = 2;
4 } T; 9 v1.t[0] = ’a’; 14 v2.t[0] = ’g’;
5 ... 10 v1.t[1] = ’b’;
{’a’, ’b’, ’c’}

l. 11 : v1.t 3 ? ?

t : char * n : int t : char * n : int

v1 : T v2 : T

{’a’, ’b’, ’c’}

l. 12 : v1.t 3 v1.t 3

t : char * n : int t : char * n : int

v1 : T v2 : T

{’g’, ’b’, ’c’}

l. 14 : v1.t 3 v1.t 2

t : char * n : int t : char * n : int

v1 : T v2 : T

269 / 399
Affectation de variables d’un type structuré
Considérons le code

1 typedef struct { 6 T v1, v2; 11 v1.t[2] = ’c’;


2 char *t; 7 v1.t = malloc(3); 12 v2 = v1;
3 int n; 8 v1.n = 3; 13 v2.n = 2;
4 } T; 9 v1.t[0] = ’a’; 14 v2.t[0] = ’g’;
5 ... 10 v1.t[1] = ’b’;
{’a’, ’b’, ’c’}

l. 11 : v1.t 3 ? ?

t : char * n : int t : char * n : int

v1 : T v2 : T

{’a’, ’b’, ’c’}

l. 12 : v1.t 3 v1.t 3

t : char * n : int t : char * n : int

v1 : T v2 : T

{’g’, ’b’, ’c’}

l. 14 : v1.t 3 v1.t 2

t : char * n : int t : char * n : int

v1 : T v2 : T

Observation : l’affectation ne recopie pas les tableaux dynamiques. Seule l’adresse d’un tableau
dynamique est recopiée. C’est une copie de surface. 269 / 399
Affectation de variables d’un type structuré
Règle générale : pour chaque déclaration d’un type structuré X , on définit (dans le même
module) une fonction de prototype
int copier_X(const X *v1, X *v2);

qui copie en profondeur les champs de v1 dans les champs de v2 .

Par exemple, la définition du type T précédent s’accompagne de la définition de la fonction


1 int copier_T(const T *v1, T *v2) {
2 int i;
3 assert(v1 != NULL);
4 assert(v2 != NULL);
5 v2->n = v1->n;
6 v2->t = (char *) malloc(sizeof(char) * v1->n);
7 if (v2->t == NULL) return 0;
8 for (i = 0 ; i < v1->n ; ++i)
9 v2->t[i] = v1->t[i];
10 return 1;
11 }

Cette fonction est munie du mécanisme habituel de gestion d’erreurs.


270 / 399
Comparaison de variables d’un type structuré

Considérons le code

1 typedef struct { 6 A v1, v2;


2 int a; 7 ...
3 int b; 8 if (v1 == v2) {...}
4 } A; 9 ...
5 ... 10 if (v1 != v2) {...}

Ce code est incorrect (il ne compile pas).


Le compilateur n’accepte pas la comparaison de variables d’un type structuré.
invalid operands to binary == (have ’A’ and ’A’)

invalid operands to binary != (have ’A’ and ’A’)

271 / 399
Comparaison de variables d’un type structuré
Règle générale : pour chaque déclaration d’un type structuré X , on définit (dans le même
module) deux fonctions de prototypes
int sont_ega_X(const X *v1, const X *v2);

int sont_dif_X(const X *v1, const X *v2);

qui testent l’égalité et l’inégalité entre v1 et v2 .

Par exemple, la définition du type A précédent s’accompagne de la définition des fonctions

1 int sont_ega_A(A *v1, A *v2) { 7 int sont_dif_A(A *v1, A *v2) {


2 assert(v1 != NULL); 8 assert(v1 != NULL);
3 assert(v2 != NULL); 9 assert(v2 != NULL);
4 return (v1->a == v2->a) 10 return !sont_ega_A(v1, v2);
5 && (v1->b == v2->b); 11 }
6 }

Attention : si X est composé d’un champ qui est un type structuré Y , il faut appeler dans
sont_ega_X la fonction de comparaison sont_ega_Y .
272 / 399
Destruction de variables d’un type structuré
Règle générale : pour chaque déclaration d’un type structuré X , on définit (dans le même
module) une fonction de prototype
void detruire_X(X *v);

qui libère l’espace mémoire adressé par v .

Par exemple, la déclaration du type B suivant s’accompagne de la définition de la fonction

1 typedef struct { 6 void detruire_B(B *v) {


2 int *tab; 7 assert(v != NULL);
3 int n; 8 free(v->tab);
4 } B; 9 *v = NULL;
5 10 }

Attention : si X est composé d’un champ qui est un type structuré Y , il faut appeler dans
detruire_X la fonction de destruction detruire_Y .

273 / 399
Plan

Types structurés
Déclaration et initialisation
Affectation et comparaison
Dans les fonctions
Alignement en mémoire

274 / 399
Renvoi d’une variable d’un type structuré
Le code

1 typedef struct { 7 Couple twist(Couple c) {


2 int x; 8 Couple res;
3 int y; 9 res.x = c.y;
4 } Couple; 10 res.y = c.x;
5 11 return res;
6 12 }

est correct ( twist renvoie le couple obtenu par échange des coordonnées de celui passé en
argument).
twist renvoie une variable d’un type structuré.
Cependant, il n’est pas efficace car, à chaque appel de fonction
d = twist(c);

la variable res , qui vit dans la pile, doit être recopiée.


275 / 399
Paramètre variable d’un type structuré

Le code

1 typedef struct { 6 int prem_egaux(DeuxTab x) {


2 int tab1[2048]; 7 return x.tab1[0]
3 int tab2[2048]; 8 == x.tab2[0];
4 } DeuxTab; 9 }
5

est correct ( prem_egaux teste si les premières cases des tableaux sont égales).
prem_egaux est paramétrée par une variable d’un type structuré.
Cependant, il n’est pas efficace car à chaque appel de fonction
prem_egaux(y);

les champs de l’argument y sont recopiés dans le paramètre x .

276 / 399
Passage par adresse vs passage par valeur

Soit une fonction fct paramétrée par une variable x d’un type structuré T .

277 / 399
Passage par adresse vs passage par valeur

Soit une fonction fct paramétrée par une variable x d’un type structuré T .
Il est d’usage courant d’adopter la convention suivante :
I si les champs de x doivent être modifiés par la fonction, alors on recourt à un passage par
adresse
... fct(T *x, ...) { ... }

277 / 399
Passage par adresse vs passage par valeur

Soit une fonction fct paramétrée par une variable x d’un type structuré T .
Il est d’usage courant d’adopter la convention suivante :
I si les champs de x doivent être modifiés par la fonction, alors on recourt à un passage par
adresse
... fct(T *x, ...) { ... }

I si les champs de x ne doivent pas être modifiés par la fonction, alors on recourt à un
passage par valeur
... fct(T x, ...) { ... }

277 / 399
Passage par adresse vs passage par valeur

Soit une fonction fct paramétrée par une variable x d’un type structuré T .
Il est d’usage courant d’adopter la convention suivante :
I si les champs de x doivent être modifiés par la fonction, alors on recourt à un passage par
adresse
... fct(T *x, ...) { ... }

I si les champs de x ne doivent pas être modifiés par la fonction, alors on recourt à un
passage par valeur
... fct(T x, ...) { ... }

Cette conception est erronée car il est possible de « modifier » une variable d’un type structuré
passée par valeur à une fonction.

277 / 399
Passage par adresse vs passage par valeur
Considérons en effet le code suivant :

1 tyepdef struct { 6 void init(Tab t, int k) {


2 int *tab; 7 int i;
3 int n; 8 for (i = 0 ; i < t.n ; ++i)
4 } Tab; 9 t.tab[i] = k;
5 10 }

Chaque appel de fonction


init(s, r);

provoque la recopie de trois valeurs (ce qui est encore acceptable) mais « modifie » les valeurs
pointées par le champ tab de s , malgré le passage par valeur.

Conclusion : écrire des fonctions avec passage par valeur des paramètres d’un type structuré ne
présente que des désavantages.

278 / 399
Variables d’un type structuré dans les fonctions
En résumé, on adopte les deux règles suivantes :

279 / 399
Variables d’un type structuré dans les fonctions
En résumé, on adopte les deux règles suivantes :
1. une fonction ne renvoie jamais de valeur d’un type structuré ;

279 / 399
Variables d’un type structuré dans les fonctions
En résumé, on adopte les deux règles suivantes :
1. une fonction ne renvoie jamais de valeur d’un type structuré ;
2. tous les paramètres d’un type structuré sont passés par adresse dans une fonction.

279 / 399
Variables d’un type structuré dans les fonctions
En résumé, on adopte les deux règles suivantes :
1. une fonction ne renvoie jamais de valeur d’un type structuré ;
2. tous les paramètres d’un type structuré sont passés par adresse dans une fonction.
Ainsi, un prototype de fonction habituel est
int fct(T *x, E1 e1, ..., EN en, S1 *s1, ..., SM *sm);


I le type de retour est int (renvoi d’un code d’erreur) ;

279 / 399
Variables d’un type structuré dans les fonctions
En résumé, on adopte les deux règles suivantes :
1. une fonction ne renvoie jamais de valeur d’un type structuré ;
2. tous les paramètres d’un type structuré sont passés par adresse dans une fonction.
Ainsi, un prototype de fonction habituel est
int fct(T *x, E1 e1, ..., EN en, S1 *s1, ..., SM *sm);


I le type de retour est int (renvoi d’un code d’erreur) ;
I x est l’adresse d’une variable d’un type structuré T ;

279 / 399
Variables d’un type structuré dans les fonctions
En résumé, on adopte les deux règles suivantes :
1. une fonction ne renvoie jamais de valeur d’un type structuré ;
2. tous les paramètres d’un type structuré sont passés par adresse dans une fonction.
Ainsi, un prototype de fonction habituel est
int fct(T *x, E1 e1, ..., EN en, S1 *s1, ..., SM *sm);


I le type de retour est int (renvoi d’un code d’erreur) ;
I x est l’adresse d’une variable d’un type structuré T ;
I e1, ..., en sont les entrées de la fonction (adresses ou non) ;

279 / 399
Variables d’un type structuré dans les fonctions
En résumé, on adopte les deux règles suivantes :
1. une fonction ne renvoie jamais de valeur d’un type structuré ;
2. tous les paramètres d’un type structuré sont passés par adresse dans une fonction.
Ainsi, un prototype de fonction habituel est
int fct(T *x, E1 e1, ..., EN en, S1 *s1, ..., SM *sm);


I le type de retour est int (renvoi d’un code d’erreur) ;
I x est l’adresse d’une variable d’un type structuré T ;
I e1, ..., en sont les entrées de la fonction (adresses ou non) ;
I s1, ..., sm sont les sorties de la fonction (qui sont des adresses).

279 / 399
Variables d’un type structuré dans les fonctions
P.ex., voici le nécessaire pour calculer la somme pondérée de deux points selon les conventions
établies :
1 typdef struct {
2 float x;
3 float y;
4 } Point;
5
6 void somme_points(const Point *p1, const Point *p2,
7 float coeff1, float coeff2,
8 Point *res) {
9
10 assert(p1 != NULL);
11 assert(p2 != NULL);
12 assert(res != NULL);
13
14 res->x = coeff1 * p1->x + coeff2 * p2->x;
15 res->y = coeff1 * p1->y + coeff2 * p2->y;
16 }

280 / 399
Résumé
Voici en résumé la bonne marche à suivre lors de la manipulation de types structurés :
1. on utilise l’alias lors de la déclaration de types structurés récursifs et/ou mutuellement
récursifs ;

2. toute déclaration d’un type structuré s’accompagne de la définition des quatre fonctions
suivantes :
I une fonction de copie ;
I une fonction de test d’égalité ;
I une fonction de test d’inégalité ;
I une fonction de destruction ;

3. on ne renvoie jamais de valeur d’un type structuré ;

4. on passe les paramètres d’un type structuré par adresse (ne pas oublier d’ajouter les
qualificateurs const nécessaires).

281 / 399
Plan

Types structurés
Déclaration et initialisation
Affectation et comparaison
Dans les fonctions
Alignement en mémoire

282 / 399
Alignement en mémoire

L’alignement en mémoire d’une donnée est la façon dont celle-ci est organisée dans la mémoire.

283 / 399
Alignement en mémoire

L’alignement en mémoire d’une donnée est la façon dont celle-ci est organisée dans la mémoire.

Par exemple, nous avons déjà vu que les tableaux de taille n d’éléments d’un type T sont
organisés en un segment contigu de sizeof(T) * n octets.

283 / 399
Alignement en mémoire

L’alignement en mémoire d’une donnée est la façon dont celle-ci est organisée dans la mémoire.

Par exemple, nous avons déjà vu que les tableaux de taille n d’éléments d’un type T sont
organisés en un segment contigu de sizeof(T) * n octets.

Ainsi, un tableau t de 3 éléments de type short est organisé en

1 octet

... ...

t[0] t[1] t[2]

283 / 399
Alignement en mémoire

L’alignement en mémoire d’une donnée est la façon dont celle-ci est organisée dans la mémoire.

Par exemple, nous avons déjà vu que les tableaux de taille n d’éléments d’un type T sont
organisés en un segment contigu de sizeof(T) * n octets.

Ainsi, un tableau t de 3 éléments de type short est organisé en

1 octet

... ...

t[0] t[1] t[2]

On peut se poser de la même manière la question de l’alignement mémoire des variables d’un
type structuré.

283 / 399
Alignement en mémoire des variables d’un type structuré
Considérons les déclarations de types

1 typedef struct { 6 typedef struct {


2 short x; 7 short x;
3 short y; 8 int z;
4 int z; 9 short y;
5 } A; 10 } B;

A et B sont des types structurés composés des mêmes champs. Il n’y a que l’ordre de leur
déclaration qui diffère.
Cependant,
1 printf("%lu␣%lu\n", sizeof(A), sizeof(B));

affiche 8 12 .

Le fait que les tailles des variables de type A et B diffèrent est dû à leur alignements en
mémoire respectifs qui ne sont pas les mêmes.
284 / 399
Alignement en mémoire des variables d’un type structuré
Soit a une variable de type A . Cette variable est organisée en mémoire en
1 octet

... ...

a.x a.y a.z

285 / 399
Alignement en mémoire des variables d’un type structuré
Soit a une variable de type A . Cette variable est organisée en mémoire en
1 octet

... ...

a.x a.y a.z

Soit b une variable de type B . Cette variable est organisée en mémoire en


1 octet

... ...

a.x a.z a.y

285 / 399
Alignement en mémoire des variables d’un type structuré
Soit a une variable de type A . Cette variable est organisée en mémoire en
1 octet

... ...

a.x a.y a.z

Soit b une variable de type B . Cette variable est organisée en mémoire en


1 octet

... ...

a.x a.z a.y

Les octets en gris intervenant dans l’alignement mémoire de b sont des octets de complétion.

285 / 399
Octets de complétion
Des octets de complétion sont introduits pour que chaque champ c d’une variable d’un type
structuré commence à une adresse multiple d’un entier dépendant du type de c .

286 / 399
Octets de complétion
Des octets de complétion sont introduits pour que chaque champ c d’une variable d’un type
structuré commence à une adresse multiple d’un entier dépendant du type de c .

Dans notre exemple, en sachant que tout champ de type short (resp. int ) doit commencer à une
adresse multiple de 2 (resp. 4 ), on explique l’alignement en mémoire de b précédent :
1 octet

... ...

a.x a.z a.y

adr. mult. de 4

adr. non mult. de 4

286 / 399
Octets de complétion
Des octets de complétion sont introduits pour que chaque champ c d’une variable d’un type
structuré commence à une adresse multiple d’un entier dépendant du type de c .

Dans notre exemple, en sachant que tout champ de type short (resp. int ) doit commencer à une
adresse multiple de 2 (resp. 4 ), on explique l’alignement en mémoire de b précédent :
1 octet

... ...

a.x a.z a.y

adr. mult. de 4

adr. non mult. de 4

Les derniers octets de complétion sont introduits pour que les tableaux de variables de type B
puissent être représentés en vérifiant cet alignement en mémoire.

286 / 399
Accès manuel aux champs
Soit a une variable de type A initialisée par
1 A a = {1000, 2000, 3000};

Cette variable est organisée en mémoire en


1 octet

... ...

a.x a.y a.z

On peut accéder aux champs de a de la manière suivante :


1 short x, y;
2 int z;
3 void *p;
4 p = &a;
5 x = *((short *) p); /∗ Equivalent a x = a . x ; ∗/
6 p += 2; /∗ p est de type void ∗ : l ’ adresse p est incrementee de 2 ∗/
7 y = *((short *) p); /∗ Equivalent a y = a . y ; ∗/
8 p += 2;
9 z = *((int *) p); /∗ Equivalent a z = a . z ; ∗/
287 / 399
Accès manuel aux champs
Soit b une variable de type B initialisée par
1 B b = {1000, 2000, 3000};

Cette variable est organisée en mémoire en


1 octet

... ...

a.x a.z a.y

On peut accéder aux champs de b de la manière suivante :


1 short x, y;
2 int z;
3 void *p;
4 p = &b;
5 x = *((short *) p); /∗ Equivalent a x = b . x ; ∗/
6 p += 4;
7 z = *((int *) p); /∗ Equivalent a z = b . z ; ∗/
8 p += 4;
9 y = *((short *) p); /∗ Equivalent a y = b . y ; ∗/
288 / 399
L’option Wpadded
L’option du compilateur -Wpadded permet d’obtenir un avertissement sanctionnant la déclaration
d’un type structuré nécessitant des octets de complétion.
Par exemple, avec le type structuré B défini par
1 typedef struct {
2 short x;
3 int z;
4 short y;
5 } B;

on obtient l’avertissement
Prog.c:3:9: warning: padding struct to align ‘z’ [-Wpadded]
int z;
^
Prog.c:5:1: warning: padding struct size to alignment boundary [-Wpadded]
} B;
^

289 / 399
Alignement en mémoire — ce qu’il faut retenir

L’alignement mémoire d’une variable d’un type structuré dépend de beaucoup de paramètres,
notamment :
I de l’architecture de la machine exécutant ou ayant compilé le programme ;

290 / 399
Alignement en mémoire — ce qu’il faut retenir

L’alignement mémoire d’une variable d’un type structuré dépend de beaucoup de paramètres,
notamment :
I de l’architecture de la machine exécutant ou ayant compilé le programme ;
I du compilateur ayant compilé le programme.

290 / 399
Alignement en mémoire — ce qu’il faut retenir

L’alignement mémoire d’une variable d’un type structuré dépend de beaucoup de paramètres,
notamment :
I de l’architecture de la machine exécutant ou ayant compilé le programme ;
I du compilateur ayant compilé le programme.

Il est donc important de savoir que le calcul de la taille d’une variable d’un type structuré n’est
pas immédiat et dépend du contexte.

290 / 399
Alignement en mémoire — ce qu’il faut retenir

L’alignement mémoire d’une variable d’un type structuré dépend de beaucoup de paramètres,
notamment :
I de l’architecture de la machine exécutant ou ayant compilé le programme ;
I du compilateur ayant compilé le programme.

Il est donc important de savoir que le calcul de la taille d’une variable d’un type structuré n’est
pas immédiat et dépend du contexte.

Dans la pratique, on ne cherchera pas à déclarer des types structurés qui ne nécessitent pas
d’octet de complétion. Au contraire : il est de loin préférable de déclarer les champs dans un
ordre logique favorisant la relecture et la maintenance.

290 / 399
Axe 3 : utiliser quelques techniques avancées
Opérateurs

Pointeurs de fonction

Génération aléatoire

Mémoïsation

291 / 399
Plan

Opérateurs
Généralités
Opérateurs d’accès
Opérateurs de calcul
Opérateurs d’affectation
Autres opérateurs

292 / 399
Plan

Opérateurs
Généralités
Opérateurs d’accès
Opérateurs de calcul
Opérateurs d’affectation
Autres opérateurs

293 / 399
Caractéristiques d’un opérateur

Un opérateur dispose des caractéristiques structurelles suivantes :

294 / 399
Caractéristiques d’un opérateur

Un opérateur dispose des caractéristiques structurelles suivantes :


1. son arité, qui désigne le nombre d’opérandes sur lesquelles il agit ;

294 / 399
Caractéristiques d’un opérateur

Un opérateur dispose des caractéristiques structurelles suivantes :


1. son arité, qui désigne le nombre d’opérandes sur lesquelles il agit ;

2. sa précédence, qui permet de savoir, dans une expression, dans quel ordre appliquer les
différents opérateurs qui la composent ;

294 / 399
Caractéristiques d’un opérateur

Un opérateur dispose des caractéristiques structurelles suivantes :


1. son arité, qui désigne le nombre d’opérandes sur lesquelles il agit ;

2. sa précédence, qui permet de savoir, dans une expression, dans quel ordre appliquer les
différents opérateurs qui la composent ;

3. pour les opérateurs binaires (d’arité 2), son sens d’associativité, qui permet de savoir, dans
une expression, dans quel sens appliquer des mêmes opérateurs qui la composent.

294 / 399
Précédence et associativité des opérateurs
Considérons l’expression 3 * 2 + 1 .

295 / 399
Précédence et associativité des opérateurs
Considérons l’expression 3 * 2 + 1 .
Suivant les priorités relatives des opérateurs * et + , il y deux manières de l’évaluer :

295 / 399
Précédence et associativité des opérateurs
Considérons l’expression 3 * 2 + 1 .
Suivant les priorités relatives des opérateurs * et + , il y deux manières de l’évaluer :

1. (3 * 2) + 1 , si * est plus prioritaire que + ;

295 / 399
Précédence et associativité des opérateurs
Considérons l’expression 3 * 2 + 1 .
Suivant les priorités relatives des opérateurs * et + , il y deux manières de l’évaluer :

1. (3 * 2) + 1 , si * est plus prioritaire que + ;

2. 3 * (2 + 1) , si + est plus prioritaire que * .

295 / 399
Précédence et associativité des opérateurs
Considérons l’expression 3 * 2 + 1 .
Suivant les priorités relatives des opérateurs * et + , il y deux manières de l’évaluer :

1. (3 * 2) + 1 , si * est plus prioritaire que + ;

2. 3 * (2 + 1) , si + est plus prioritaire que * .

Considérons l’expression 4 - 3 - 2 - 1 .

295 / 399
Précédence et associativité des opérateurs
Considérons l’expression 3 * 2 + 1 .
Suivant les priorités relatives des opérateurs * et + , il y deux manières de l’évaluer :

1. (3 * 2) + 1 , si * est plus prioritaire que + ;

2. 3 * (2 + 1) , si + est plus prioritaire que * .

Considérons l’expression 4 - 3 - 2 - 1 .
Suivant le sens d’associativité de - , il y a deux manières de l’évaluer :

295 / 399
Précédence et associativité des opérateurs
Considérons l’expression 3 * 2 + 1 .
Suivant les priorités relatives des opérateurs * et + , il y deux manières de l’évaluer :

1. (3 * 2) + 1 , si * est plus prioritaire que + ;

2. 3 * (2 + 1) , si + est plus prioritaire que * .

Considérons l’expression 4 - 3 - 2 - 1 .
Suivant le sens d’associativité de - , il y a deux manières de l’évaluer :

1. ((4 - 3) - 2) - 1 , si - est associatif de gauche à droite ;

295 / 399
Précédence et associativité des opérateurs
Considérons l’expression 3 * 2 + 1 .
Suivant les priorités relatives des opérateurs * et + , il y deux manières de l’évaluer :

1. (3 * 2) + 1 , si * est plus prioritaire que + ;

2. 3 * (2 + 1) , si + est plus prioritaire que * .

Considérons l’expression 4 - 3 - 2 - 1 .
Suivant le sens d’associativité de - , il y a deux manières de l’évaluer :

1. ((4 - 3) - 2) - 1 , si - est associatif de gauche à droite ;

2. 4 - (3 - (2 - 1)) , si - est associatif de droite à gauche.

295 / 399
Précédence et associativité des opérateurs
Considérons l’expression 3 * 2 + 1 .
Suivant les priorités relatives des opérateurs * et + , il y deux manières de l’évaluer :

1. (3 * 2) + 1 , si * est plus prioritaire que + ;

2. 3 * (2 + 1) , si + est plus prioritaire que * .

Considérons l’expression 4 - 3 - 2 - 1 .
Suivant le sens d’associativité de - , il y a deux manières de l’évaluer :

1. ((4 - 3) - 2) - 1 , si - est associatif de gauche à droite ;

2. 4 - (3 - (2 - 1)) , si - est associatif de droite à gauche.

Tout ceci peut être rendu explicite par l’utilisation de parenthèses.


295 / 399
Plan

Opérateurs
Généralités
Opérateurs d’accès
Opérateurs de calcul
Opérateurs d’affectation
Autres opérateurs

296 / 399
Opérateurs de gestion de la mémoire

Op. Rôle Ari. Assoc. Opérandes

297 / 399
Opérateurs de gestion de la mémoire

Op. Rôle Ari. Assoc. Opérandes


& référencement 1 – une variable

297 / 399
Opérateurs de gestion de la mémoire

Op. Rôle Ari. Assoc. Opérandes


& référencement 1 – une variable
* déréférencement 1 – un pointeur

297 / 399
Opérateurs de gestion de la mémoire

Op. Rôle Ari. Assoc. Opérandes


& référencement 1 – une variable
* déréférencement 1 – un pointeur
[] élément d’un tableau 2 −→ un pointeur et un entier

297 / 399
Opérateurs de gestion de la mémoire

Op. Rôle Ari. Assoc. Opérandes


& référencement 1 – une variable
* déréférencement 1 – un pointeur
[] élément d’un tableau 2 −→ un pointeur et un entier
une var. d’un type struct.
. valeur d’un champ 2 −→
et un id. de champ

297 / 399
Opérateurs de gestion de la mémoire

Op. Rôle Ari. Assoc. Opérandes


& référencement 1 – une variable
* déréférencement 1 – un pointeur
[] élément d’un tableau 2 −→ un pointeur et un entier
une var. d’un type struct.
. valeur d’un champ 2 −→
et un id. de champ
une pointeur sur
-> valeur d’un champ 2 −→ une var. d’un type struct.
et un id. de champ

297 / 399
Plan

Opérateurs
Généralités
Opérateurs d’accès
Opérateurs de calcul
Opérateurs d’affectation
Autres opérateurs

298 / 399
Opérateurs arithmétiques

Op. Rôle Ari. Assoc. Opérandes

299 / 399
Opérateurs arithmétiques

Op. Rôle Ari. Assoc. Opérandes


+ , - , * , / opérations arith. 2 −→ deux val. numériques

299 / 399
Opérateurs arithmétiques

Op. Rôle Ari. Assoc. Opérandes


+ , - , * , / opérations arith. 2 −→ deux val. numériques
% modulo 2 −→ deux entiers

299 / 399
Opérateurs arithmétiques

Op. Rôle Ari. Assoc. Opérandes


+ , - , * , / opérations arith. 2 −→ deux val. numériques
% modulo 2 −→ deux entiers
+ , - signe 1 – une val. numérique

299 / 399
Opérateurs arithmétiques

Op. Rôle Ari. Assoc. Opérandes


+ , - , * , / opérations arith. 2 −→ deux val. numériques
% modulo 2 −→ deux entiers
+ , - signe 1 – une val. numérique
une var. d’un
++ , -- incr./décr. 1 –
type numérique
ou un pointeur

299 / 399
L’opérateur modulo
L’opérateur modulo % calcule le reste de la division euclidienne de son premier opérande par son
second.

300 / 399
L’opérateur modulo
L’opérateur modulo % calcule le reste de la division euclidienne de son premier opérande par son
second.
D’un point de vue mathématique, si a et b sont deux entiers, on a a = b × q + r, où
0 6 r 6 b − 1 et q est un entier. q est le quotient et r est le reste, toujours positif.

300 / 399
L’opérateur modulo
L’opérateur modulo % calcule le reste de la division euclidienne de son premier opérande par son
second.
D’un point de vue mathématique, si a et b sont deux entiers, on a a = b × q + r, où
0 6 r 6 b − 1 et q est un entier. q est le quotient et r est le reste, toujours positif.

Cependant, % peut produire des valeurs négatives, dans le cas où l’un des deux opérandes est
négatif.

300 / 399
L’opérateur modulo
L’opérateur modulo % calcule le reste de la division euclidienne de son premier opérande par son
second.
D’un point de vue mathématique, si a et b sont deux entiers, on a a = b × q + r, où
0 6 r 6 b − 1 et q est un entier. q est le quotient et r est le reste, toujours positif.

Cependant, % peut produire des valeurs négatives, dans le cas où l’un des deux opérandes est
négatif.

Solution pour un modulo qui respecte la définition mathématique :


int vrai_modulo(int a, int b) {
int r;
r = a % b;
if (r < 0)
return r + b;
return r;
}

300 / 399
Les opérateurs d’incrémentation et de décrémentation

Les opérateurs ++ et -- existent en deux versions, suivant qu’ils soient préfixes ou suffixes :

301 / 399
Les opérateurs d’incrémentation et de décrémentation

Les opérateurs ++ et -- existent en deux versions, suivant qu’ils soient préfixes ou suffixes :

1. a++ , incrémente (de un) la valeur de la variable a et est une expression dont la valeur est
l’ancienne valeur de a ;

301 / 399
Les opérateurs d’incrémentation et de décrémentation

Les opérateurs ++ et -- existent en deux versions, suivant qu’ils soient préfixes ou suffixes :

1. a++ , incrémente (de un) la valeur de la variable a et est une expression dont la valeur est
l’ancienne valeur de a ;

2. ++a , incrémente (de un) la valeur de la variable a et est une expression dont la valeur est la
nouvelle valeur de a .

301 / 399
Les opérateurs d’incrémentation et de décrémentation

Les opérateurs ++ et -- existent en deux versions, suivant qu’ils soient préfixes ou suffixes :

1. a++ , incrémente (de un) la valeur de la variable a et est une expression dont la valeur est
l’ancienne valeur de a ;

2. ++a , incrémente (de un) la valeur de la variable a et est une expression dont la valeur est la
nouvelle valeur de a .

int a = 5, b;
b = 3 + a++;
b vaut 8 et a vaut 6 .

301 / 399
Les opérateurs d’incrémentation et de décrémentation

Les opérateurs ++ et -- existent en deux versions, suivant qu’ils soient préfixes ou suffixes :

1. a++ , incrémente (de un) la valeur de la variable a et est une expression dont la valeur est
l’ancienne valeur de a ;

2. ++a , incrémente (de un) la valeur de la variable a et est une expression dont la valeur est la
nouvelle valeur de a .

int a = 5, b; int a = 5, b;
b = 3 + a++; b = 3 + ++a;
b vaut 8 et a vaut 6 . b vaut 9 et a vaut 6 .

301 / 399
Les opérateurs d’incrémentation et de décrémentation

Attention au pièges d’utilisation de ces opérateurs.

302 / 399
Les opérateurs d’incrémentation et de décrémentation

Attention au pièges d’utilisation de ces opérateurs.

P.ex., les instructions

int a = 5, b; int a = 5; int a = 5;


b = a++ + ++a; a = a++; a = ++a;

ne sont pas évaluables (l’effet produit par les lignes 2 dépend du compilateur et de ses options).

302 / 399
Les opérateurs d’incrémentation et de décrémentation

Attention au pièges d’utilisation de ces opérateurs.

P.ex., les instructions

int a = 5, b; int a = 5; int a = 5;


b = a++ + ++a; a = a++; a = ++a;

ne sont pas évaluables (l’effet produit par les lignes 2 dépend du compilateur et de ses options).

Règle : pour éviter ce type de piège, on s’interdit de réaliser plus d’une modification d’une même
variable dans une même expression.

302 / 399
Opérateurs relationnels
Op. Rôle Ari. Assoc. Opérandes

303 / 399
Opérateurs relationnels
Op. Rôle Ari. Assoc. Opérandes
< , > comparaison stricte 2 −→ deux val. numériques

303 / 399
Opérateurs relationnels
Op. Rôle Ari. Assoc. Opérandes
< , > comparaison stricte 2 −→ deux val. numériques
<= , >= comparaison large 2 −→ deux val. numériques

303 / 399
Opérateurs relationnels
Op. Rôle Ari. Assoc. Opérandes
< , > comparaison stricte 2 −→ deux val. numériques
<= , >= comparaison large 2 −→ deux val. numériques
== égalité 2 −→ deux val. numériques

303 / 399
Opérateurs relationnels
Op. Rôle Ari. Assoc. Opérandes
< , > comparaison stricte 2 −→ deux val. numériques
<= , >= comparaison large 2 −→ deux val. numériques
== égalité 2 −→ deux val. numériques
!= différence 2 −→ deux val. numériques

303 / 399
Opérateurs relationnels
Op. Rôle Ari. Assoc. Opérandes
< , > comparaison stricte 2 −→ deux val. numériques
<= , >= comparaison large 2 −→ deux val. numériques
== égalité 2 −→ deux val. numériques
!= différence 2 −→ deux val. numériques

Toutes les expressions de la forme


v1 CMP v2

où v1 et v2 sont des valeurs numériques et CMP est un opérateur de comparaison produisent


une valeur :
I 1 si la comparaison est vraie ;
I 0 sinon.

303 / 399
Opérateurs relationnels
Un pointeur étant une adresse, et donc une valeur numérique, il est possible de comparer deux
pointeurs.

304 / 399
Opérateurs relationnels
Un pointeur étant une adresse, et donc une valeur numérique, il est possible de comparer deux
pointeurs.

char *ptr1, *ptr2; Ceci affiche seulement ok1 .


char c;
c = ’a’; En effet, les deux pointeurs ptr1 et ptr2
ptr1 = &c;
ptr2 = &c;
pointent vers le même emplacement en
if (ptr1 == ptr2) mémoire.
printf("ok1\n");
if (&ptr1 == &ptr2) Le second test est faux car les adresses des
printf("ok2\n"); variables ptr1 et ptr2 sont différentes.

304 / 399
Opérateurs relationnels
Un pointeur étant une adresse, et donc une valeur numérique, il est possible de comparer deux
pointeurs.

char *ptr1, *ptr2; Ceci affiche seulement ok1 .


char c;
c = ’a’; En effet, les deux pointeurs ptr1 et ptr2
ptr1 = &c;
ptr2 = &c;
pointent vers le même emplacement en
if (ptr1 == ptr2) mémoire.
printf("ok1\n");
if (&ptr1 == &ptr2) Le second test est faux car les adresses des
printf("ok2\n"); variables ptr1 et ptr2 sont différentes.

int t1[2], t2[2]; Ceci compare les adresses de t1 et t2 et non pas


t1[0] = 1; les valeurs de leurs cases.
t1[1] = 2;
t2[0] = 1; Rien n’est donc affiché car les tableaux t1 et t2
t2[1] = 2; sont à des adresses différentes.
if (t1 == t2)
printf("ok\n");

304 / 399
Opérateurs logiques

Op. Rôle Ari. Assoc. Opérandes

305 / 399
Opérateurs logiques

Op. Rôle Ari. Assoc. Opérandes


&& et logique 2 −→ deux val. numériques

305 / 399
Opérateurs logiques

Op. Rôle Ari. Assoc. Opérandes


&& et logique 2 −→ deux val. numériques
|| ou logique 2 −→ deux val. numériques

305 / 399
Opérateurs logiques

Op. Rôle Ari. Assoc. Opérandes


&& et logique 2 −→ deux val. numériques
|| ou logique 2 −→ deux val. numériques
! non logique 1 – une val. numérique

305 / 399
Opérateurs logiques

Op. Rôle Ari. Assoc. Opérandes


&& et logique 2 −→ deux val. numériques
|| ou logique 2 −→ deux val. numériques
! non logique 1 – une val. numérique

Toutes les expressions formées d’opérateurs logiques produisent une valeur, 0 ou bien 1 .
Cette valeur est
I 1 si l’expression logique est vraie ;
I 0 sinon.

305 / 399
Opérateurs bit à bit

Op. Rôle Ari. Assoc. Opérandes

306 / 399
Opérateurs bit à bit

Op. Rôle Ari. Assoc. Opérandes


& et bit à bit 2 −→ deux val. entières

306 / 399
Opérateurs bit à bit

Op. Rôle Ari. Assoc. Opérandes


& et bit à bit 2 −→ deux val. entières
| ou bit à bit 2 −→ deux val. entières

306 / 399
Opérateurs bit à bit

Op. Rôle Ari. Assoc. Opérandes


& et bit à bit 2 −→ deux val. entières
| ou bit à bit 2 −→ deux val. entières
^ xor bit à bit 2 −→ deux val. entières

306 / 399
Opérateurs bit à bit

Op. Rôle Ari. Assoc. Opérandes


& et bit à bit 2 −→ deux val. entières
| ou bit à bit 2 −→ deux val. entières
^ xor bit à bit 2 −→ deux val. entières
∼ non bit à bit 1 – une val. entière

306 / 399
Opérateurs bit à bit

Op. Rôle Ari. Assoc. Opérandes


& et bit à bit 2 −→ deux val. entières
| ou bit à bit 2 −→ deux val. entières
^ xor bit à bit 2 −→ deux val. entières
∼ non bit à bit 1 – une val. entière
<< , >> déc. g./d. bit à bit 2 −→ deux val. entières

306 / 399
Et/ou/xor/non bit à bit

Quelques exemples d’opérations bit à bit :

x 0 1 1 1 0 1 0 1

y 1 0 1 0 1 1 0 0

x & y 0 0 1 0 0 1 0 0

307 / 399
Et/ou/xor/non bit à bit

Quelques exemples d’opérations bit à bit :

x 0 1 1 1 0 1 0 1

y 1 0 1 0 1 1 0 0

x & y 0 0 1 0 0 1 0 0

x 0 1 1 1 0 1 0 1

y 1 0 1 0 1 1 0 0

x | y 1 1 1 1 1 1 0 1

307 / 399
Et/ou/xor/non bit à bit

Quelques exemples d’opérations bit à bit :

x 0 1 1 1 0 1 0 1 x 0 1 1 1 0 1 0 1

y 1 0 1 0 1 1 0 0 y 1 0 1 0 1 1 0 0

x & y 0 0 1 0 0 1 0 0 x ^ y 1 1 0 1 1 0 0 1

x 0 1 1 1 0 1 0 1

y 1 0 1 0 1 1 0 0

x | y 1 1 1 1 1 1 0 1

307 / 399
Et/ou/xor/non bit à bit

Quelques exemples d’opérations bit à bit :

x 0 1 1 1 0 1 0 1 x 0 1 1 1 0 1 0 1

y 1 0 1 0 1 1 0 0 y 1 0 1 0 1 1 0 0

x & y 0 0 1 0 0 1 0 0 x ^ y 1 1 0 1 1 0 0 1

x 0 1 1 1 0 1 0 1

y 1 0 1 0 1 1 0 0 x 0 1 1 1 0 1 0 1

x | y 1 1 1 1 1 1 0 1 ∼x 1 0 0 0 1 0 1 0

307 / 399
Et/ou/xor/non bit à bit
Si les deux opérandes n’ont pas la même taille (en nombre de bits), le plus petit est complété à
gauche par des
I zéros s’il est non signé ou bien positif ;
I uns s’il est négatif et signé.

308 / 399
Et/ou/xor/non bit à bit
Si les deux opérandes n’ont pas la même taille (en nombre de bits), le plus petit est complété à
gauche par des
I zéros s’il est non signé ou bien positif ;
I uns s’il est négatif et signé.
Le signe d’un entier signé est lu sur son bit de poids fort :
I 0 s’il est positif ;
I 1 s’il est négatif.

308 / 399
Et/ou/xor/non bit à bit
Si les deux opérandes n’ont pas la même taille (en nombre de bits), le plus petit est complété à
gauche par des
I zéros s’il est non signé ou bien positif ;
I uns s’il est négatif et signé.
Le signe d’un entier signé est lu sur son bit de poids fort :
I 0 s’il est positif ;
I 1 s’il est négatif.

short x = 5; x 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1

char y = 10; y 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0

x = x | y; x | y 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1

308 / 399
Et/ou/xor/non bit à bit
Si les deux opérandes n’ont pas la même taille (en nombre de bits), le plus petit est complété à
gauche par des
I zéros s’il est non signé ou bien positif ;
I uns s’il est négatif et signé.
Le signe d’un entier signé est lu sur son bit de poids fort :
I 0 s’il est positif ;
I 1 s’il est négatif.

short x = 5; x 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1

char y = 10; y 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0

x = x | y; x | y 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1

short x = 5; x 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1

char y = -10; y 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 0

x = x | y; x | y 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1

308 / 399
Décalage bit à bit

Si x est non signé (déclaré avec unsigned ),

309 / 399
Décalage bit à bit

Si x est non signé (déclaré avec unsigned ),

x 0 1 1 1 0 1 0 1

x << 3 1 0 1 0 1 0 0 0

309 / 399
Décalage bit à bit

Si x est non signé (déclaré avec unsigned ),

x 0 1 1 1 0 1 0 1 x 0 1 1 1 0 1 0 1

x << 3 1 0 1 0 1 0 0 0 x >> 3 0 0 0 0 1 1 1 0

309 / 399
Décalage bit à bit

Si x est non signé (déclaré avec unsigned ),

x 0 1 1 1 0 1 0 1 x 0 1 1 1 0 1 0 1

x << 3 1 0 1 0 1 0 0 0 x >> 3 0 0 0 0 1 1 1 0

Si x est signé (déclaré sans unsigned ),

309 / 399
Décalage bit à bit

Si x est non signé (déclaré avec unsigned ),

x 0 1 1 1 0 1 0 1 x 0 1 1 1 0 1 0 1

x << 3 1 0 1 0 1 0 0 0 x >> 3 0 0 0 0 1 1 1 0

Si x est signé (déclaré sans unsigned ),

x 0 1 1 1 0 1 0 1

x << 3 1 0 1 0 1 0 0 0

309 / 399
Décalage bit à bit

Si x est non signé (déclaré avec unsigned ),

x 0 1 1 1 0 1 0 1 x 0 1 1 1 0 1 0 1

x << 3 1 0 1 0 1 0 0 0 x >> 3 0 0 0 0 1 1 1 0

Si x est signé (déclaré sans unsigned ),

x 0 1 1 1 0 1 0 1 x 0 1 1 1 0 1 0 1

x << 3 1 0 1 0 1 0 0 0 x >> 3 0 0 0 0 1 1 1 0

309 / 399
Décalage bit à bit

Si x est non signé (déclaré avec unsigned ),

x 0 1 1 1 0 1 0 1 x 0 1 1 1 0 1 0 1

x << 3 1 0 1 0 1 0 0 0 x >> 3 0 0 0 0 1 1 1 0

Si x est signé (déclaré sans unsigned ),

x 0 1 1 1 0 1 0 1 x 0 1 1 1 0 1 0 1

x << 3 1 0 1 0 1 0 0 0 x >> 3 0 0 0 0 1 1 1 0

x 1 1 1 1 0 1 0 1

x << 3 1 0 1 0 1 0 0 0

309 / 399
Décalage bit à bit

Si x est non signé (déclaré avec unsigned ),

x 0 1 1 1 0 1 0 1 x 0 1 1 1 0 1 0 1

x << 3 1 0 1 0 1 0 0 0 x >> 3 0 0 0 0 1 1 1 0

Si x est signé (déclaré sans unsigned ),

x 0 1 1 1 0 1 0 1 x 0 1 1 1 0 1 0 1

x << 3 1 0 1 0 1 0 0 0 x >> 3 0 0 0 0 1 1 1 0

x 1 1 1 1 0 1 0 1 x 1 1 1 1 0 1 0 1

x << 3 1 0 1 0 1 0 0 0 x >> 3 1 1 1 1 1 1 1 0

309 / 399
Exemple — ensembles finis
Les opérateurs bit à bit sont adaptés pour représenter des ensembles finis et réaliser des
opérations ensemblistes de manière simple et efficace.

310 / 399
Exemple — ensembles finis
Les opérateurs bit à bit sont adaptés pour représenter des ensembles finis et réaliser des
opérations ensemblistes de manière simple et efficace.

Soit E un ensemble à 32 éléments.


On considère que E est muni d’une relation d’ordre totale de sorte que ses éléments puissent
être indexés de 0 à 31. Ainsi,
E = {e0 , e1 , e2 , . . . , e31 }.

310 / 399
Exemple — ensembles finis
Les opérateurs bit à bit sont adaptés pour représenter des ensembles finis et réaliser des
opérations ensemblistes de manière simple et efficace.

Soit E un ensemble à 32 éléments.


On considère que E est muni d’une relation d’ordre totale de sorte que ses éléments puissent
être indexés de 0 à 31. Ainsi,
E = {e0 , e1 , e2 , . . . , e31 }.

Cette indexation permet de représenter tout sous-ensemble S de E par un mot de 32 bit dont le
ie bit code la présence ( 1 ) ou l’absence ( 0 ) de ei dans S.

310 / 399
Exemple — ensembles finis
Les opérateurs bit à bit sont adaptés pour représenter des ensembles finis et réaliser des
opérations ensemblistes de manière simple et efficace.

Soit E un ensemble à 32 éléments.


On considère que E est muni d’une relation d’ordre totale de sorte que ses éléments puissent
être indexés de 0 à 31. Ainsi,
E = {e0 , e1 , e2 , . . . , e31 }.

Cette indexation permet de représenter tout sous-ensemble S de E par un mot de 32 bit dont le
ie bit code la présence ( 1 ) ou l’absence ( 0 ) de ei dans S.
P.ex., l’entier dont l’écriture binaire est

00010000100000000000001100000101

code le sous ensemble {e0 , e2 , e8 , e9 , e23 , e28 } de E.

310 / 399
Exemple — ensembles finis

Ceci mène à la déclaration du type alias


1 typedef unsigned int SousEnsemble;

Pour tester si ei appartient à S, il suffit de réaliser un et bit à bit entre l’entier représentant S et
le mot binaire constitué d’un unique 1 en ie position.
Cette expression vaut 0 si ei ∈
/ S et une valeur non nulle sinon.
Ainsi,
1 int appartient_e_i(SousEnsemble s, int i) {
2 assert(0 <= i);
3 assert(i <= 31);
4
5 return (1 << i) & s;
6 }

311 / 399
Exemple — ensembles finis

Pour réaliser l’union de deux sous-ensembles S1 et S2 de E, il suffit de réaliser un ou bit à bit


des deux entiers représentant S1 et S2 . En effet, pour tout i, ei ∈ S1 ∪ S2 si ei ∈ S1 ou ei ∈ S2 .
Ainsi,
1 SousEnsemble union(SousEnsemble s_1, SousEnsemble s_2) {
2 return s_1 | s_2;
3 }

Pour réaliser l’intersection de deux sous-ensembles S1 et S2 de E, il suffit de réaliser un et bit à


bit des deux entiers représentant S1 et S2 . En effet, pour tout i, ei ∈ S1 ∩ S2 si ei ∈ S1 et ei ∈ S2 .
Ainsi,
1 SousEnsemble intersection(SousEnsemble s_1, SousEnsemble s_2) {
2 return s_1 & s_2;
3 }

312 / 399
Exemple — Compter le nombre de bits à un
But : écrire une fonction qui renvoie le nombre de bits à un de son paramètre.

313 / 399
Exemple — Compter le nombre de bits à un
But : écrire une fonction qui renvoie le nombre de bits à un de son paramètre.

On travaille sur des variables de 64 bits. On considère pour cela le type alias Mot64 défini par
typedef unsigned long long Mot64;

313 / 399
Exemple — Compter le nombre de bits à un
But : écrire une fonction qui renvoie le nombre de bits à un de son paramètre.

On travaille sur des variables de 64 bits. On considère pour cela le type alias Mot64 défini par
typedef unsigned long long Mot64;

1re méthode : attraper le bit de poids faible et le pousser à droite.

313 / 399
Exemple — Compter le nombre de bits à un
But : écrire une fonction qui renvoie le nombre de bits à un de son paramètre.

On travaille sur des variables de 64 bits. On considère pour cela le type alias Mot64 défini par
typedef unsigned long long Mot64;

1re méthode : attraper le bit de poids faible et le pousser à droite.


int compter_un_1(Mot64 x) {

313 / 399
Exemple — Compter le nombre de bits à un
But : écrire une fonction qui renvoie le nombre de bits à un de son paramètre.

On travaille sur des variables de 64 bits. On considère pour cela le type alias Mot64 défini par
typedef unsigned long long Mot64;

1re méthode : attraper le bit de poids faible et le pousser à droite.


int compter_un_1(Mot64 x) {
int res, i;
res = 0;

return res;
}

313 / 399
Exemple — Compter le nombre de bits à un
But : écrire une fonction qui renvoie le nombre de bits à un de son paramètre.

On travaille sur des variables de 64 bits. On considère pour cela le type alias Mot64 défini par
typedef unsigned long long Mot64;

1re méthode : attraper le bit de poids faible et le pousser à droite.


int compter_un_1(Mot64 x) {
int res, i;
res = 0;
for (i = 0 ; i < 64 ; ++i) {

x = x > > 1;
}
return res;
}

313 / 399
Exemple — Compter le nombre de bits à un
But : écrire une fonction qui renvoie le nombre de bits à un de son paramètre.

On travaille sur des variables de 64 bits. On considère pour cela le type alias Mot64 défini par
typedef unsigned long long Mot64;

1re méthode : attraper le bit de poids faible et le pousser à droite.


int compter_un_1(Mot64 x) {
int res, i;
res = 0;
for (i = 0 ; i < 64 ; ++i) {
if ((x & 1) == 1)
res += 1;
x = x > > 1;
}
return res;
}

313 / 399
Exemple — Compter le nombre de bits à un
2e méthode : on constate que pour tout entier x non nul (avec au moins un bit à un),
l’expression x & -x est l’entier qui contient un unique bit à un, le plus à droite de x .

314 / 399
Exemple — Compter le nombre de bits à un
2e méthode : on constate que pour tout entier x non nul (avec au moins un bit à un),
l’expression x & -x est l’entier qui contient un unique bit à un, le plus à droite de x .
Ainsi, l’instruction
x = x ^ (x & -x);

transforme le bit à un le plus à droite de x en un zéro.

314 / 399
Exemple — Compter le nombre de bits à un
2e méthode : on constate que pour tout entier x non nul (avec au moins un bit à un),
l’expression x & -x est l’entier qui contient un unique bit à un, le plus à droite de x .
Ainsi, l’instruction
x = x ^ (x & -x);

transforme le bit à un le plus à droite de x en un zéro.

L’exploitation de cette idée donne

314 / 399
Exemple — Compter le nombre de bits à un
2e méthode : on constate que pour tout entier x non nul (avec au moins un bit à un),
l’expression x & -x est l’entier qui contient un unique bit à un, le plus à droite de x .
Ainsi, l’instruction
x = x ^ (x & -x);

transforme le bit à un le plus à droite de x en un zéro.

L’exploitation de cette idée donne


int compter_un_2(Mot64 x) {

}
314 / 399
Exemple — Compter le nombre de bits à un
2e méthode : on constate que pour tout entier x non nul (avec au moins un bit à un),
l’expression x & -x est l’entier qui contient un unique bit à un, le plus à droite de x .
Ainsi, l’instruction
x = x ^ (x & -x);

transforme le bit à un le plus à droite de x en un zéro.

L’exploitation de cette idée donne


int compter_un_2(Mot64 x) {
int res;
res = 0;

return res;
}
314 / 399
Exemple — Compter le nombre de bits à un
2e méthode : on constate que pour tout entier x non nul (avec au moins un bit à un),
l’expression x & -x est l’entier qui contient un unique bit à un, le plus à droite de x .
Ainsi, l’instruction
x = x ^ (x & -x);

transforme le bit à un le plus à droite de x en un zéro.

L’exploitation de cette idée donne


int compter_un_2(Mot64 x) {
int res;
res = 0;
while (x != 0) {
x = x ^ (x & -x);
res += 1;
}
return res;
}
314 / 399
Exemple — Compter le nombre de bits à un

3e méthode : cette troisième méthode n’utilise pas de boucle.

315 / 399
Exemple — Compter le nombre de bits à un

3e méthode : cette troisième méthode n’utilise pas de boucle.


On commence par construire un tableau qui associe à tout entier de huit bits (un char ) le
nombre de bits à un qu’il contient, en utilisant au choix l’une des deux méthodes précédentes.

315 / 399
Exemple — Compter le nombre de bits à un

3e méthode : cette troisième méthode n’utilise pas de boucle.


On commence par construire un tableau qui associe à tout entier de huit bits (un char ) le
nombre de bits à un qu’il contient, en utilisant au choix l’une des deux méthodes précédentes.
int nombre_un[256];

void initialiser_nombre_un() {
int i;
Mot64 x;
for (i = 0 ; i < 256 ; ++i) {
x = (Mot64) i;
nombre_un[i] = compter_un_2(x);
}
}

315 / 399
Exemple — Compter le nombre de bits à un
On isole l’octet d’indice i d’une variable x de type Mot64 par l’expression

0xFF & (x > > (8 * i))

316 / 399
Exemple — Compter le nombre de bits à un
On isole l’octet d’indice i d’une variable x de type Mot64 par l’expression

0xFF & (x > > (8 * i))

Comme le nombre de bits à un de x est la somme du nombre de bits à un de chacun des huit
octets qui le constituent, on obtient

316 / 399
Exemple — Compter le nombre de bits à un
On isole l’octet d’indice i d’une variable x de type Mot64 par l’expression

0xFF & (x > > (8 * i))

Comme le nombre de bits à un de x est la somme du nombre de bits à un de chacun des huit
octets qui le constituent, on obtient
int compter_un_3(Mot64 x) {

}
316 / 399
Exemple — Compter le nombre de bits à un
On isole l’octet d’indice i d’une variable x de type Mot64 par l’expression

0xFF & (x > > (8 * i))

Comme le nombre de bits à un de x est la somme du nombre de bits à un de chacun des huit
octets qui le constituent, on obtient
int compter_un_3(Mot64 x) {
int res;
res = 0;

return res;
}
316 / 399
Exemple — Compter le nombre de bits à un
On isole l’octet d’indice i d’une variable x de type Mot64 par l’expression

0xFF & (x > > (8 * i))

Comme le nombre de bits à un de x est la somme du nombre de bits à un de chacun des huit
octets qui le constituent, on obtient
int compter_un_3(Mot64 x) {
int res;
res = 0;
res += nombre_un[0xFF & x];

return res;
}
316 / 399
Exemple — Compter le nombre de bits à un
On isole l’octet d’indice i d’une variable x de type Mot64 par l’expression

0xFF & (x > > (8 * i))

Comme le nombre de bits à un de x est la somme du nombre de bits à un de chacun des huit
octets qui le constituent, on obtient
int compter_un_3(Mot64 x) {
int res;
res = 0;
res += nombre_un[0xFF & x];
res += nombre_un[0xFF & (x > > 8)];

return res;
}
316 / 399
Exemple — Compter le nombre de bits à un
On isole l’octet d’indice i d’une variable x de type Mot64 par l’expression

0xFF & (x > > (8 * i))

Comme le nombre de bits à un de x est la somme du nombre de bits à un de chacun des huit
octets qui le constituent, on obtient
int compter_un_3(Mot64 x) {
int res;
res = 0;
res += nombre_un[0xFF & x];
res += nombre_un[0xFF & (x > > 8)];
res += nombre_un[0xFF & (x > > 16)];

return res;
}
316 / 399
Exemple — Compter le nombre de bits à un
On isole l’octet d’indice i d’une variable x de type Mot64 par l’expression

0xFF & (x > > (8 * i))

Comme le nombre de bits à un de x est la somme du nombre de bits à un de chacun des huit
octets qui le constituent, on obtient
int compter_un_3(Mot64 x) {
int res;
res = 0;
res += nombre_un[0xFF & x];
res += nombre_un[0xFF & (x > > 8)];
res += nombre_un[0xFF & (x > > 16)];
res += nombre_un[0xFF & (x > > 24)];

return res;
}
316 / 399
Exemple — Compter le nombre de bits à un
On isole l’octet d’indice i d’une variable x de type Mot64 par l’expression

0xFF & (x > > (8 * i))

Comme le nombre de bits à un de x est la somme du nombre de bits à un de chacun des huit
octets qui le constituent, on obtient
int compter_un_3(Mot64 x) {
int res;
res = 0;
res += nombre_un[0xFF & x];
res += nombre_un[0xFF & (x > > 8)];
res += nombre_un[0xFF & (x > > 16)];
res += nombre_un[0xFF & (x > > 24)];
res += nombre_un[0xFF & (x > > 32)];

return res;
}
316 / 399
Exemple — Compter le nombre de bits à un
On isole l’octet d’indice i d’une variable x de type Mot64 par l’expression

0xFF & (x > > (8 * i))

Comme le nombre de bits à un de x est la somme du nombre de bits à un de chacun des huit
octets qui le constituent, on obtient
int compter_un_3(Mot64 x) {
int res;
res = 0;
res += nombre_un[0xFF & x];
res += nombre_un[0xFF & (x > > 8)];
res += nombre_un[0xFF & (x > > 16)];
res += nombre_un[0xFF & (x > > 24)];
res += nombre_un[0xFF & (x > > 32)];
res += nombre_un[0xFF & (x > > 40)];

return res;
}
316 / 399
Exemple — Compter le nombre de bits à un
On isole l’octet d’indice i d’une variable x de type Mot64 par l’expression

0xFF & (x > > (8 * i))

Comme le nombre de bits à un de x est la somme du nombre de bits à un de chacun des huit
octets qui le constituent, on obtient
int compter_un_3(Mot64 x) {
int res;
res = 0;
res += nombre_un[0xFF & x];
res += nombre_un[0xFF & (x > > 8)];
res += nombre_un[0xFF & (x > > 16)];
res += nombre_un[0xFF & (x > > 24)];
res += nombre_un[0xFF & (x > > 32)];
res += nombre_un[0xFF & (x > > 40)];
res += nombre_un[0xFF & (x > > 48)];

return res;
}
316 / 399
Exemple — Compter le nombre de bits à un
On isole l’octet d’indice i d’une variable x de type Mot64 par l’expression

0xFF & (x > > (8 * i))

Comme le nombre de bits à un de x est la somme du nombre de bits à un de chacun des huit
octets qui le constituent, on obtient
int compter_un_3(Mot64 x) {
int res;
res = 0;
res += nombre_un[0xFF & x];
res += nombre_un[0xFF & (x > > 8)];
res += nombre_un[0xFF & (x > > 16)];
res += nombre_un[0xFF & (x > > 24)];
res += nombre_un[0xFF & (x > > 32)];
res += nombre_un[0xFF & (x > > 40)];
res += nombre_un[0xFF & (x > > 48)];
res += nombre_un[0xFF & (x > > 56)];
return res;
}
316 / 399
Exemple — Compter le nombre de bits à un
4e méthode : on peut pousser plus loin l’idée précédente en considérant des blocs de deux octets
(au lieu d’un seul).

317 / 399
Exemple — Compter le nombre de bits à un
4e méthode : on peut pousser plus loin l’idée précédente en considérant des blocs de deux octets
(au lieu d’un seul).
int nombre_un[65536]; /* 2^16 */

void initialiser_nombre_un() {
int i;
Mot64 x;
for (i = 0 ; i < 65536 ; ++i) {
x = (Mot64) i;
nombre_un[i] = compter_un_2(x);
}
}

317 / 399
Exemple — Compter le nombre de bits à un
4e méthode : on peut pousser plus loin l’idée précédente en considérant des blocs de deux octets
(au lieu d’un seul).
int nombre_un[65536]; /* 2^16 */

void initialiser_nombre_un() {
int i;
Mot64 x;
for (i = 0 ; i < 65536 ; ++i) {
x = (Mot64) i;
nombre_un[i] = compter_un_2(x);
}
}

Taille mémoire occupée par le tableau nombre_un :

216 × sizeof(int) o

317 / 399
Exemple — Compter le nombre de bits à un
4e méthode : on peut pousser plus loin l’idée précédente en considérant des blocs de deux octets
(au lieu d’un seul).
int nombre_un[65536]; /* 2^16 */

void initialiser_nombre_un() {
int i;
Mot64 x;
for (i = 0 ; i < 65536 ; ++i) {
x = (Mot64) i;
nombre_un[i] = compter_un_2(x);
}
}

Taille mémoire occupée par le tableau nombre_un :

216 × sizeof(int) o = 218 o

317 / 399
Exemple — Compter le nombre de bits à un
4e méthode : on peut pousser plus loin l’idée précédente en considérant des blocs de deux octets
(au lieu d’un seul).
int nombre_un[65536]; /* 2^16 */

void initialiser_nombre_un() {
int i;
Mot64 x;
for (i = 0 ; i < 65536 ; ++i) {
x = (Mot64) i;
nombre_un[i] = compter_un_2(x);
}
}

Taille mémoire occupée par le tableau nombre_un :

218
216 × sizeof(int) o = 218 o = Kio
210

317 / 399
Exemple — Compter le nombre de bits à un
4e méthode : on peut pousser plus loin l’idée précédente en considérant des blocs de deux octets
(au lieu d’un seul).
int nombre_un[65536]; /* 2^16 */

void initialiser_nombre_un() {
int i;
Mot64 x;
for (i = 0 ; i < 65536 ; ++i) {
x = (Mot64) i;
nombre_un[i] = compter_un_2(x);
}
}

Taille mémoire occupée par le tableau nombre_un :

218
216 × sizeof(int) o = 218 o = Kio = 256 Kio.
210

317 / 399
Exemple — Compter le nombre de bits à un
Ceci fournit la solution suivante.
int compter_un_4(Mot64 x) {
int res;
res = 0;
res += nombre_un[0xFFFF & x];
res += nombre_un[0xFFFF & (x > > 16)];
res += nombre_un[0xFFFF & (x > > 32)];
res += nombre_un[0xFFFF & (x > > 48)];
return res;
}

318 / 399
Exemple — Compter le nombre de bits à un
Ceci fournit la solution suivante.
int compter_un_4(Mot64 x) {
int res;
res = 0;
res += nombre_un[0xFFFF & x];
res += nombre_un[0xFFFF & (x > > 16)];
res += nombre_un[0xFFFF & (x > > 32)];
res += nombre_un[0xFFFF & (x > > 48)];
return res;
}

Elle ne demande que quatre lectures dans le tableau nombre_un , au lieu des huit de la méthode
précédente.

318 / 399
Exemple — Compter le nombre de bits à un
Ceci fournit la solution suivante.
int compter_un_4(Mot64 x) {
int res;
res = 0;
res += nombre_un[0xFFFF & x];
res += nombre_un[0xFFFF & (x > > 16)];
res += nombre_un[0xFFFF & (x > > 32)];
res += nombre_un[0xFFFF & (x > > 48)];
return res;
}

Elle ne demande que quatre lectures dans le tableau nombre_un , au lieu des huit de la méthode
précédente.

À l’extrême, il est impossible de maintenir un tableau nombre_un qui ne demanderait qu’une seule
lecture. En effet, sa taille mémoire serait de
266
264 × sizeof(int) o = 266 o = Gio = 236 Gio.
230
318 / 399
Exemple — Compter le nombre de bits à un

5e méthode : cette méthode est la plus compliquée. Elle se base sur une compréhension très fine
du fonctionnement des opérateurs bit à bit.

319 / 399
Exemple — Compter le nombre de bits à un

5e méthode : cette méthode est la plus compliquée. Elle se base sur une compréhension très fine
du fonctionnement des opérateurs bit à bit.

int compter_un_5(Mot64 x) {
x = x - ((x > > 1) & 0x5555555555555555LLU);
x = (x & 0x3333333333333333LLU) +
((x > > 2) & 0x3333333333333333LLU);
x = (x + (x > > 4)) & 0x0F0F0F0F0F0F0F0FLLU;
x = (x * 0x0101010101010101LLU) > > 56;
return (int) x;
}

319 / 399
Mesure du temps d’exécution
Mesurons maintenant les efficacités des cinq méthodes.

320 / 399
Mesure du temps d’exécution
Mesurons maintenant les efficacités des cinq méthodes.
On utilise pour cela la fonction
clock_t clock(void);

de time.h .

320 / 399
Mesure du temps d’exécution
Mesurons maintenant les efficacités des cinq méthodes.
On utilise pour cela la fonction
clock_t clock(void);

de time.h .

Schéma général pour mesurer le temps d’exécution d’une suite d’instructions :

320 / 399
Mesure du temps d’exécution
Mesurons maintenant les efficacités des cinq méthodes.
On utilise pour cela la fonction
clock_t clock(void);

de time.h .

Schéma général pour mesurer le temps d’exécution d’une suite d’instructions :

...
/* Instructions */
...

320 / 399
Mesure du temps d’exécution
Mesurons maintenant les efficacités des cinq méthodes.
On utilise pour cela la fonction
clock_t clock(void);

de time.h .

Schéma général pour mesurer le temps d’exécution d’une suite d’instructions :


clock_t debut, fin;
double temps;

...
/* Instructions */
...

320 / 399
Mesure du temps d’exécution
Mesurons maintenant les efficacités des cinq méthodes.
On utilise pour cela la fonction
clock_t clock(void);

de time.h .

Schéma général pour mesurer le temps d’exécution d’une suite d’instructions :


clock_t debut, fin;
double temps;
debut = clock();
...
/* Instructions */
...

320 / 399
Mesure du temps d’exécution
Mesurons maintenant les efficacités des cinq méthodes.
On utilise pour cela la fonction
clock_t clock(void);

de time.h .

Schéma général pour mesurer le temps d’exécution d’une suite d’instructions :


clock_t debut, fin;
double temps;
debut = clock();
...
/* Instructions */
...
fin = clock();

320 / 399
Mesure du temps d’exécution
Mesurons maintenant les efficacités des cinq méthodes.
On utilise pour cela la fonction
clock_t clock(void);

de time.h .

Schéma général pour mesurer le temps d’exécution d’une suite d’instructions :


clock_t debut, fin;
double temps;
debut = clock();
...
/* Instructions */
...
fin = clock();
temps = (double) (fin - debut) / CLOCKS_PER_SEC;

320 / 399
Mesure du temps d’exécution
Mesurons maintenant les efficacités des cinq méthodes.
On utilise pour cela la fonction
clock_t clock(void);

de time.h .

Schéma général pour mesurer le temps d’exécution d’une suite d’instructions :


clock_t debut, fin;
double temps;
debut = clock();
...
/* Instructions */
...
fin = clock();
temps = (double) (fin - debut) / CLOCKS_PER_SEC;
printf("%g s\n", temps);

320 / 399
Mesure du temps d’exécution
On mesure le temps d’exécution des cinq méthodes en leur donnant en entrée des nombres de 64
bits générés de manière aléatoire.

321 / 399
Mesure du temps d’exécution
On mesure le temps d’exécution des cinq méthodes en leur donnant en entrée des nombres de 64
bits générés de manière aléatoire.
Pour générer un nombre de 32 bits de manière aléatoire, on utilise la fonction
int rand(void);

de stdlib.h .

321 / 399
Mesure du temps d’exécution
On mesure le temps d’exécution des cinq méthodes en leur donnant en entrée des nombres de 64
bits générés de manière aléatoire.
Pour générer un nombre de 32 bits de manière aléatoire, on utilise la fonction
int rand(void);

de stdlib.h .
Le nombre de 64 bits est construit en générant aléatoirement ses quatre octets de droite, puis ses
quatre octets de gauche et en les associant avec un ou bit à bit :

321 / 399
Mesure du temps d’exécution
On mesure le temps d’exécution des cinq méthodes en leur donnant en entrée des nombres de 64
bits générés de manière aléatoire.
Pour générer un nombre de 32 bits de manière aléatoire, on utilise la fonction
int rand(void);

de stdlib.h .
Le nombre de 64 bits est construit en générant aléatoirement ses quatre octets de droite, puis ses
quatre octets de gauche et en les associant avec un ou bit à bit :
Mot64 mot64_alea() {

321 / 399
Mesure du temps d’exécution
On mesure le temps d’exécution des cinq méthodes en leur donnant en entrée des nombres de 64
bits générés de manière aléatoire.
Pour générer un nombre de 32 bits de manière aléatoire, on utilise la fonction
int rand(void);

de stdlib.h .
Le nombre de 64 bits est construit en générant aléatoirement ses quatre octets de droite, puis ses
quatre octets de gauche et en les associant avec un ou bit à bit :
Mot64 mot64_alea() {
Mot64 gauche, droite;

321 / 399
Mesure du temps d’exécution
On mesure le temps d’exécution des cinq méthodes en leur donnant en entrée des nombres de 64
bits générés de manière aléatoire.
Pour générer un nombre de 32 bits de manière aléatoire, on utilise la fonction
int rand(void);

de stdlib.h .
Le nombre de 64 bits est construit en générant aléatoirement ses quatre octets de droite, puis ses
quatre octets de gauche et en les associant avec un ou bit à bit :
Mot64 mot64_alea() {
Mot64 gauche, droite;
droite = (Mot64) rand();

321 / 399
Mesure du temps d’exécution
On mesure le temps d’exécution des cinq méthodes en leur donnant en entrée des nombres de 64
bits générés de manière aléatoire.
Pour générer un nombre de 32 bits de manière aléatoire, on utilise la fonction
int rand(void);

de stdlib.h .
Le nombre de 64 bits est construit en générant aléatoirement ses quatre octets de droite, puis ses
quatre octets de gauche et en les associant avec un ou bit à bit :
Mot64 mot64_alea() {
Mot64 gauche, droite;
droite = (Mot64) rand();
gauche = ((Mot64) rand()) < < 32;

321 / 399
Mesure du temps d’exécution
On mesure le temps d’exécution des cinq méthodes en leur donnant en entrée des nombres de 64
bits générés de manière aléatoire.
Pour générer un nombre de 32 bits de manière aléatoire, on utilise la fonction
int rand(void);

de stdlib.h .
Le nombre de 64 bits est construit en générant aléatoirement ses quatre octets de droite, puis ses
quatre octets de gauche et en les associant avec un ou bit à bit :
Mot64 mot64_alea() {
Mot64 gauche, droite;
droite = (Mot64) rand();
gauche = ((Mot64) rand()) < < 32;
return gauche | droite;
}

321 / 399
Mesure du temps d’exécution
Voici les temps réalisés par chacune des cinq méthodes sur 84000000 nombres de 64 bits
aléatoires :
Méthode Caractéristique Temps (s)
1 Décalage droite 32.9
2 Suppression 1 droite 9.38
3 Tableau 256 2.23
4 Tableau 65536 1.93
5 Compliquée 1.82

322 / 399
Mesure du temps d’exécution
Voici les temps réalisés par chacune des cinq méthodes sur 84000000 nombres de 64 bits
aléatoires :
Méthode Caractéristique Temps (s)
1 Décalage droite 32.9
2 Suppression 1 droite 9.38
3 Tableau 256 2.23
4 Tableau 65536 1.93
5 Compliquée 1.82

La 4e méthode est plus de 16 fois plus rapide que la 1re. Elle demande en revanche (tout comme la
3e) un pré-calcul et une occupation mémoire (par le tableau nombre_un ).

322 / 399
Mesure du temps d’exécution
Voici les temps réalisés par chacune des cinq méthodes sur 84000000 nombres de 64 bits
aléatoires :
Méthode Caractéristique Temps (s)
1 Décalage droite 32.9
2 Suppression 1 droite 9.38
3 Tableau 256 2.23
4 Tableau 65536 1.93
5 Compliquée 1.82

La 4e méthode est plus de 16 fois plus rapide que la 1re. Elle demande en revanche (tout comme la
3e) un pré-calcul et une occupation mémoire (par le tableau nombre_un ).
La 5e méthode est la plus rapide et ne demande aucun pré-calcul. Elle est en revanche difficile à
comprendre et très difficile à imaginer.

322 / 399
Plan

Opérateurs
Généralités
Opérateurs d’accès
Opérateurs de calcul
Opérateurs d’affectation
Autres opérateurs

323 / 399
Opérateurs d’affectation

Op. Rôle Ari. Assoc. Opérandes

324 / 399
Opérateurs d’affectation

Op. Rôle Ari. Assoc. Opérandes


une var.
= affect. 2 ←−
et une val.

324 / 399
Opérateurs d’affectation

Op. Rôle Ari. Assoc. Opérandes


une var.
= affect. 2 ←−
et une val.
affect. compo. ←− une var. num.
+= , -= , *= , /= , %= 2
arith. et une val. num

324 / 399
Opérateurs d’affectation

Op. Rôle Ari. Assoc. Opérandes


une var.
= affect. 2 ←−
et une val.
affect. compo. ←− une var. num.
+= , -= , *= , /= , %= 2
arith. et une val. num
affect. compo. ←− une var. ent.
&= , |= , ^= , < <= , > >= 2
bit à bit et une val. ent.

324 / 399
Opérateurs d’affectation

Op. Rôle Ari. Assoc. Opérandes


une var.
= affect. 2 ←−
et une val.
affect. compo. ←− une var. num.
+= , -= , *= , /= , %= 2
arith. et une val. num
affect. compo. ←− une var. ent.
&= , |= , ^= , < <= , > >= 2
bit à bit et une val. ent.

Toute expression de la forme a X= b est équivalente à a = a X b .

324 / 399
Opérateurs d’affectation

Toutes les expressions d’affectation produisent une valeur qui est la valeur qui vient d’être
affectée.

325 / 399
Opérateurs d’affectation

Toutes les expressions d’affectation produisent une valeur qui est la valeur qui vient d’être
affectée.
Par exemple, dans
int a, b;
a = 2;
b = 5;
a *= b += 3;

à cause de l’associativité des opérateurs d’affectation, la l. 4 s’interprète comme a *= (b += 3); .


Ainsi, comme b += 3 produit la valeur 8 , a vaut finalement 16 .

325 / 399
Plan

Opérateurs
Généralités
Opérateurs d’accès
Opérateurs de calcul
Opérateurs d’affectation
Autres opérateurs

326 / 399
Autres opérateurs

Op. Rôle Ari. Assoc. Opérandes

327 / 399
Autres opérateurs

Op. Rôle Ari. Assoc. Opérandes


sizeof taille 1 – une var. ou un type

327 / 399
Autres opérateurs

Op. Rôle Ari. Assoc. Opérandes


sizeof taille 1 – une var. ou un type
coercition
(T) 1 – une val.
T est un type

327 / 399
Autres opérateurs

Op. Rôle Ari. Assoc. Opérandes


sizeof taille 1 – une var. ou un type
coercition
(T) 1 – une val.
T est un type
? : condition 3 – une val. num. et deux val.

327 / 399
Autres opérateurs

Op. Rôle Ari. Assoc. Opérandes


sizeof taille 1 – une var. ou un type
coercition
(T) 1 – une val.
T est un type
? : condition 3 – une val. num. et deux val.
, séquence 2 −→ deux val

327 / 399
L’opérateur de séquence
Dans l’expression V1, V2 , où V1 et V2 sont des valeurs, on commence par évaluer V1 puis
ensuite V2 . Cette expression produit la valeur V1 .

Variante : avec des parenthèses, dans (V1, V2) , l’évaluation se fait aussi de la gauche vers la
droite mais la valeur de l’expression est V2 .

328 / 399
L’opérateur de séquence
Dans l’expression V1, V2 , où V1 et V2 sont des valeurs, on commence par évaluer V1 puis
ensuite V2 . Cette expression produit la valeur V1 .

Variante : avec des parenthèses, dans (V1, V2) , l’évaluation se fait aussi de la gauche vers la
droite mais la valeur de l’expression est V2 .

L’opérateur , est le plus souvent utilisé dans les champs des boucles for .

328 / 399
L’opérateur de séquence
Dans l’expression V1, V2 , où V1 et V2 sont des valeurs, on commence par évaluer V1 puis
ensuite V2 . Cette expression produit la valeur V1 .

Variante : avec des parenthèses, dans (V1, V2) , l’évaluation se fait aussi de la gauche vers la
droite mais la valeur de l’expression est V2 .

L’opérateur , est le plus souvent utilisé dans les champs des boucles for .
P.ex.,
int i, j, l;
...
for (i = 0, j = l - 1 ; i < j ; ++i, - -j) {
...
}

permet d’obtenir une boucle for avec deux compteurs : i croît et j décroît dans l’intervalle
allant de 0 à l - 1 .
328 / 399
Plan

Pointeurs de fonction
Principe
En paramètre et en retour
Généricité
Implantation de monoïdes

329 / 399
Plan

Pointeurs de fonction
Principe
En paramètre et en retour
Généricité
Implantation de monoïdes

330 / 399
Pointeurs de fonction

En C, on peut manipuler divers types d’objets : des variables d’un type scalaire, des tableaux, des
variables d’un type structuré, des adresses, etc.

331 / 399
Pointeurs de fonction

En C, on peut manipuler divers types d’objets : des variables d’un type scalaire, des tableaux, des
variables d’un type structuré, des adresses, etc.

À l’inverse, les fonctions n’entrent pas dans cette catégorie d’objets directement manipulables.

331 / 399
Pointeurs de fonction

En C, on peut manipuler divers types d’objets : des variables d’un type scalaire, des tableaux, des
variables d’un type structuré, des adresses, etc.

À l’inverse, les fonctions n’entrent pas dans cette catégorie d’objets directement manipulables.

Cependant, au même titre qu’une variable, toute fonction possède une adresse en mémoire. Il
devient alors possible de réaliser des opérations sur les fonctions au moyen de leur adresse.

331 / 399
Pointeurs de fonction

En C, on peut manipuler divers types d’objets : des variables d’un type scalaire, des tableaux, des
variables d’un type structuré, des adresses, etc.

À l’inverse, les fonctions n’entrent pas dans cette catégorie d’objets directement manipulables.

Cependant, au même titre qu’une variable, toute fonction possède une adresse en mémoire. Il
devient alors possible de réaliser des opérations sur les fonctions au moyen de leur adresse.

On parle alors de pointeur de fonction.

331 / 399
Adresse d’une fonction
Si fct est une fonction, la syntaxe

&fct

permet d’accéder à l’adresse de fct .

332 / 399
Adresse d’une fonction
Si fct est une fonction, la syntaxe

&fct

permet d’accéder à l’adresse de fct .

#include <stdio.h> }
int somme(int a, int b) {
return a + b; Ce programme affiche 0x40052d et 0x400541 ,
} respectivement les adresses des fonctions
int produit(int a, int b) { somme et produit .
return a * b;
}
int main() {
printf("%p\n", &somme);
printf("%p\n", &produit);
return 0;

332 / 399
Adresse d’une fonction
Si fct est une fonction, la syntaxe

&fct

permet d’accéder à l’adresse de fct .

#include <stdio.h> }
int somme(int a, int b) {
return a + b; Ce programme affiche 0x40052d et 0x400541 ,
} respectivement les adresses des fonctions
int produit(int a, int b) { somme et produit .
return a * b;
}
int main() { Note : aux lignes 9 et 10, il est possible de ne
printf("%p\n", &somme); pas mentionner les & . Le compilateur
printf("%p\n", &produit); comprend implicitement qu’il s’agit de
return 0; pointeurs de fonction.

332 / 399
Le type pointeur de fonction
La syntaxe
T (*FCT)(T1, ..., TN);

I T , T1 , . . ., TN sont des types ;
I FCT est un identificateur ;
permet de déclarer un pointeur de fonction.

333 / 399
Le type pointeur de fonction
La syntaxe
T (*FCT)(T1, ..., TN);

I T , T1 , . . ., TN sont des types ;
I FCT est un identificateur ;
permet de déclarer un pointeur de fonction.
Celui-ci a FCT pour identificateur et peut être l’adresse d’une fonction de type de retour T et de
signature (T1, ..., TN) .

333 / 399
Le type pointeur de fonction
La syntaxe
T (*FCT)(T1, ..., TN);

I T , T1 , . . ., TN sont des types ;
I FCT est un identificateur ;
permet de déclarer un pointeur de fonction.
Celui-ci a FCT pour identificateur et peut être l’adresse d’une fonction de type de retour T et de
signature (T1, ..., TN) .

float moyenne(int a, int b) { Pour la même raison que dans l’exemple


return (0.0 + a + b) / 2; précédent, il est possible à la ligne 9 de ne pas
}
...
mentionner le & .
/* Decl. d’un ptr de fonction */
float (*moy)(int, int);
Cependant, pour la clarté du code, nous
/* Utilisation */
moy = &moyenne; prenons la convention de mentionner tous
printf("%f\n", moy(2, 3)); les & .
333 / 399
Champs pointeurs de fonction
Un champ d’un type structuré peut être un pointeur sur une fonction.

334 / 399
Champs pointeurs de fonction
Un champ d’un type structuré peut être un pointeur sur une fonction.
Ceci déclare un type structuré sensé modéliser des suites d’entiers :
typedef struct {
int t;
int (*t_suiv)(int);
} Suite;
t contient le terme courant de la suite et t_suiv est la fonction qui, étant donné un terme en
entrée, calcule le terme suivant.

334 / 399
Champs pointeurs de fonction
Un champ d’un type structuré peut être un pointeur sur une fonction.
Ceci déclare un type structuré sensé modéliser des suites d’entiers :
typedef struct {
int t;
int (*t_suiv)(int);
} Suite;
t contient le terme courant de la suite et t_suiv est la fonction qui, étant donné un terme en
entrée, calcule le terme suivant.

int suivant(Suite *s) { ...


s->t = s->t_suiv(s->t); int i;
return s->t; Suite s;
} s.t = 1;
s.t_suiv = &mul_2;
int mul_2(int x) { for (i = 0 ; i < 6 ; ++i) {
return 2 * x; printf("%d ", suivant(&s));
} }

Ceci affiche 1 2 4 8 16 32 .
334 / 399
Tableaux de pointeurs de fonction

Il est possible de manipuler des tableaux de pointeurs de fonction.

335 / 399
Tableaux de pointeurs de fonction

Il est possible de manipuler des tableaux de pointeurs de fonction.


Pour cela, on procède en deux étapes :
1. on déclare un type alias pour le pointeur de fonction. Syntaxe :
typedef T (*FCT)(T1, ..., TN);

335 / 399
Tableaux de pointeurs de fonction

Il est possible de manipuler des tableaux de pointeurs de fonction.


Pour cela, on procède en deux étapes :
1. on déclare un type alias pour le pointeur de fonction. Syntaxe :
typedef T (*FCT)(T1, ..., TN);

2. on déclare ensuite le tableau de manière usuelle. Syntaxe :


FCT tab[M];

335 / 399
Tableaux de pointeurs de fonction

Il est possible de manipuler des tableaux de pointeurs de fonction.


Pour cela, on procède en deux étapes :
1. on déclare un type alias pour le pointeur de fonction. Syntaxe :
typedef T (*FCT)(T1, ..., TN);

2. on déclare ensuite le tableau de manière usuelle. Syntaxe :


FCT tab[M];

La syntaxe plus directe


T (*tab[M])(T1, ..., TN);

existe mais rend le code plus difficile à lire. Elle déclare un tableau tab de pointeurs de fonction
sans la déclaration de type préalable de la 1re méthode.

335 / 399
Tableaux de pointeurs de fonction

/* Alias pour ptr. de fct. */ ...


typedef int (*opb)(int, int); opb tab[2];

int add(int a, int b) { tab[0] = &add;


return a + b; tab[1] = &mul;
} printf("%d %d\n",
int mul(int a, int b) { tab[0](10, 20),
return a * b; tab[1](10, 20));
}

Ceci affiche 30 200 .

336 / 399
Tableaux de pointeurs de fonction

/* Alias pour ptr. de fct. */ ...


typedef int (*opb)(int, int); opb tab[2];

int add(int a, int b) { tab[0] = &add;


return a + b; tab[1] = &mul;
} printf("%d %d\n",
int mul(int a, int b) { tab[0](10, 20),
return a * b; tab[1](10, 20));
}

Ceci affiche 30 200 .

Les tableaux de pointeurs de fonction peuvent être dynamiques. La ligne 10 peut être remplacée
par
opb *tab;
tab = (opb *) malloc(sizeof(opb) * 2);

336 / 399
Plan

Pointeurs de fonction
Principe
En paramètre et en retour
Généricité
Implantation de monoïdes

337 / 399
Pointeur de fonction en paramètre

Une fonction peut être paramétrée par un pointeur de fonction. Un paramètre pointeur de
fonction est spécifié avec la même syntaxe que celle qui sert à le déclarer.

338 / 399
Pointeur de fonction en paramètre

Une fonction peut être paramétrée par un pointeur de fonction. Un paramètre pointeur de
fonction est spécifié avec la même syntaxe que celle qui sert à le déclarer.

P.ex.,
int appliquer(int n, int k, int (*f)(int)) {
int i;
for (i = 0 ; i < k ; ++i)
n = f(n);
return n;
}

est une fonction est paramétrée par un pointeur de fonction acceptant un entier et renvoyant un
entier.

338 / 399
Pointeur de fonction en paramètre
int add_1(int n) {
return n + 1; ...
} printf("%d\n",
appliquer(3, 4, &add_1));
int mul_2(int n) {
return 2 * n; printf("%d\n",
} appliquer(3, 4, &mul_2));

339 / 399
Pointeur de fonction en paramètre
int add_1(int n) {
return n + 1; ...
} printf("%d\n",
appliquer(3, 4, &add_1));
int mul_2(int n) {
return 2 * n; printf("%d\n",
} appliquer(3, 4, &mul_2));

Le 1er appel à appliquer calcule

(((3 + 1) + 1) + 1) + 1

et affiche donc 7 .

339 / 399
Pointeur de fonction en paramètre
int add_1(int n) {
return n + 1; ...
} printf("%d\n",
appliquer(3, 4, &add_1));
int mul_2(int n) {
return 2 * n; printf("%d\n",
} appliquer(3, 4, &mul_2));

Le 1er appel à appliquer calcule

(((3 + 1) + 1) + 1) + 1

et affiche donc 7 .

Le 2e appel à appliquer calcule

(((3 * 2) * 2) * 2) * 2

et affiche donc 48 .
339 / 399
Renvoi d’un pointeur de fonction

Il est possible de définir des fonctions dont le type de retour est un pointeur de fonction.

340 / 399
Renvoi d’un pointeur de fonction

Il est possible de définir des fonctions dont le type de retour est un pointeur de fonction.
Pour cela, on procède en deux étapes :
1. on déclare un type alias R pour le pointeur de fonction que l’on souhaite renvoyer ;

340 / 399
Renvoi d’un pointeur de fonction

Il est possible de définir des fonctions dont le type de retour est un pointeur de fonction.
Pour cela, on procède en deux étapes :
1. on déclare un type alias R pour le pointeur de fonction que l’on souhaite renvoyer ;

2. on définit la fonction souhaitée, dont le type de retour est R .

340 / 399
Renvoi d’un pointeur de fonction

Il est possible de définir des fonctions dont le type de retour est un pointeur de fonction.
Pour cela, on procède en deux étapes :
1. on déclare un type alias R pour le pointeur de fonction que l’on souhaite renvoyer ;

2. on définit la fonction souhaitée, dont le type de retour est R .

La syntaxe plus directe


R (*FCT(T1 ARG1, ..., TN ARGN))(R1, ..., RM) {
...
}

permet de définir directement une fonction FCT de signature


(T1, ..., TN) renvoyant l’adresse d’une fonction de type de retour R et de signature
(R1, ..., RM) . Cependant, le code devient illisible.

340 / 399
Renvoi d’un pointeur de fonction
Exemple : opération aléatoire sur des entiers.

/* Definition des operations */ /* Type de retour */


int add(int a, int b) { typedef int (*opb)(int, int);
return a + b;
} opb op_alea() {
int sub(int a, int b) { opb tab[3];
return a - b; tab[0] = &add;
} tab[1] = &sub;
int mod(int a, int b) { tab[2] = &mod;
return a % b; return tab[rand() % 3];
} }

341 / 399
Renvoi d’un pointeur de fonction
Exemple : opération aléatoire sur des entiers.

/* Definition des operations */ /* Type de retour */


int add(int a, int b) { typedef int (*opb)(int, int);
return a + b;
} opb op_alea() {
int sub(int a, int b) { opb tab[3];
return a - b; tab[0] = &add;
} tab[1] = &sub;
int mod(int a, int b) { tab[2] = &mod;
return a % b; return tab[rand() % 3];
} }

On peut utiliser op_alea de la manière suivante :


int n;
n = op_alea()(3, 4);

Ceci affecte, de manière aléatoire, 7 , -1 ou 3 à n .


341 / 399
Plan

Pointeurs de fonction
Principe
En paramètre et en retour
Généricité
Implantation de monoïdes

342 / 399
Principe de généricité
Une fonction est dite générique si elle peut accepter des arguments qui ne sont pas seulement
ceux d’un type bien précis.

343 / 399
Principe de généricité
Une fonction est dite générique si elle peut accepter des arguments qui ne sont pas seulement
ceux d’un type bien précis.
Exemples :
I une fonction qui teste si deux valeurs sont égales ;

343 / 399
Principe de généricité
Une fonction est dite générique si elle peut accepter des arguments qui ne sont pas seulement
ceux d’un type bien précis.
Exemples :
I une fonction qui teste si deux valeurs sont égales ;
I une fonction qui affiche plusieurs fois une même valeur.

343 / 399
Principe de généricité
Une fonction est dite générique si elle peut accepter des arguments qui ne sont pas seulement
ceux d’un type bien précis.
Exemples :
I une fonction qui teste si deux valeurs sont égales ;
I une fonction qui affiche plusieurs fois une même valeur.

Une structure de donnée est dite générique si elle peut représenter des données dont le type
n’est pas fixé.

343 / 399
Principe de généricité
Une fonction est dite générique si elle peut accepter des arguments qui ne sont pas seulement
ceux d’un type bien précis.
Exemples :
I une fonction qui teste si deux valeurs sont égales ;
I une fonction qui affiche plusieurs fois une même valeur.

Une structure de donnée est dite générique si elle peut représenter des données dont le type
n’est pas fixé.
Exemples :
I une liste dont les éléments sont d’un type non fixé ;

343 / 399
Principe de généricité
Une fonction est dite générique si elle peut accepter des arguments qui ne sont pas seulement
ceux d’un type bien précis.
Exemples :
I une fonction qui teste si deux valeurs sont égales ;
I une fonction qui affiche plusieurs fois une même valeur.

Une structure de donnée est dite générique si elle peut représenter des données dont le type
n’est pas fixé.
Exemples :
I une liste dont les éléments sont d’un type non fixé ;
I un arbre binaire dont les éléments sont d’un type non fixé ;

343 / 399
Principe de généricité
Une fonction est dite générique si elle peut accepter des arguments qui ne sont pas seulement
ceux d’un type bien précis.
Exemples :
I une fonction qui teste si deux valeurs sont égales ;
I une fonction qui affiche plusieurs fois une même valeur.

Une structure de donnée est dite générique si elle peut représenter des données dont le type
n’est pas fixé.
Exemples :
I une liste dont les éléments sont d’un type non fixé ;
I un arbre binaire dont les éléments sont d’un type non fixé ;
I un tableau dont les éléments sont d’un type non fixé.

343 / 399
Le type void *

Pour manipuler une donnée dont le type n’est pas spécifié à l’avance, on utilise son adresse.

344 / 399
Le type void *

Pour manipuler une donnée dont le type n’est pas spécifié à l’avance, on utilise son adresse.
Il s’agit donc d’une adresse dont on ne connaît pas le type : c’est une adresse de type void * .

344 / 399
Le type void *

Pour manipuler une donnée dont le type n’est pas spécifié à l’avance, on utilise son adresse.
Il s’agit donc d’une adresse dont on ne connaît pas le type : c’est une adresse de type void * .

Le type void * est appelé type pointeur générique.

344 / 399
Le type void *

Pour manipuler une donnée dont le type n’est pas spécifié à l’avance, on utilise son adresse.
Il s’agit donc d’une adresse dont on ne connaît pas le type : c’est une adresse de type void * .

Le type void * est appelé type pointeur générique.

Pour convertir un pointeur générique ptr_g vers un pointeur d’un type connu T , on utilise
l’opérateur de coercition
(T *) ptr_g .

344 / 399
Le type void *

Pour manipuler une donnée dont le type n’est pas spécifié à l’avance, on utilise son adresse.
Il s’agit donc d’une adresse dont on ne connaît pas le type : c’est une adresse de type void * .

Le type void * est appelé type pointeur générique.

Pour convertir un pointeur générique ptr_g vers un pointeur d’un type connu T , on utilise
l’opérateur de coercition
(T *) ptr_g .

Avant de pouvoir interpréter (c.-à-d. déréférencer) la valeur située à une adresse spécifiée par un
pointeur générique, le convertir est primordial.

344 / 399
Fonction générique de comparaison
int ega(int nbo, void *x, void *y) {

La fonction ega est générique : elle permet de tester l’égalité entre deux variables dont le type
n’est pas connu lors de l’écriture de la fonction.

345 / 399
Fonction générique de comparaison
int ega(int nbo, void *x, void *y) {
char *xc, *yc;
int i;

La fonction ega est générique : elle permet de tester l’égalité entre deux variables dont le type
n’est pas connu lors de l’écriture de la fonction.

345 / 399
Fonction générique de comparaison
int ega(int nbo, void *x, void *y) {
char *xc, *yc;
int i;

xc = (char *) x;
yc = (char *) y;

La fonction ega est générique : elle permet de tester l’égalité entre deux variables dont le type
n’est pas connu lors de l’écriture de la fonction.

345 / 399
Fonction générique de comparaison
int ega(int nbo, void *x, void *y) {
char *xc, *yc;
int i;

xc = (char *) x;
yc = (char *) y;
for (i = 0 ; i < nbo ; ++i)

La fonction ega est générique : elle permet de tester l’égalité entre deux variables dont le type
n’est pas connu lors de l’écriture de la fonction.

345 / 399
Fonction générique de comparaison
int ega(int nbo, void *x, void *y) {
char *xc, *yc;
int i;

xc = (char *) x;
yc = (char *) y;
for (i = 0 ; i < nbo ; ++i)
if (xc[i] != yc[i])
return 0;

La fonction ega est générique : elle permet de tester l’égalité entre deux variables dont le type
n’est pas connu lors de l’écriture de la fonction.

345 / 399
Fonction générique de comparaison
int ega(int nbo, void *x, void *y) {
char *xc, *yc;
int i;

xc = (char *) x;
yc = (char *) y;
for (i = 0 ; i < nbo ; ++i)
if (xc[i] != yc[i])
return 0;
return 1;
}

La fonction ega est générique : elle permet de tester l’égalité entre deux variables dont le type
n’est pas connu lors de l’écriture de la fonction.

345 / 399
Fonction générique de comparaison
int ega(int nbo, void *x, void *y) {
char *xc, *yc;
int i;

xc = (char *) x;
yc = (char *) y;
for (i = 0 ; i < nbo ; ++i)
if (xc[i] != yc[i])
return 0;
return 1;
}

La fonction ega est générique : elle permet de tester l’égalité entre deux variables dont le type
n’est pas connu lors de l’écriture de la fonction.
On l’utilise de la manière suivante :
ega(sizeof(T), &t1, &t2)

pour comparer deux variables t1 et t2 de type T .


345 / 399
Fonction générique d’affichage de tableau

void aff_tab(void **tab, int n, void (*aff_elt)(void *)) {

La fonction aff_tab est générique : elle permet d’afficher les éléments d’un tableau dont le type
n’est pas connu lors de l’écriture de la fonction.

346 / 399
Fonction générique d’affichage de tableau

void aff_tab(void **tab, int n, void (*aff_elt)(void *)) {


int i;

La fonction aff_tab est générique : elle permet d’afficher les éléments d’un tableau dont le type
n’est pas connu lors de l’écriture de la fonction.

346 / 399
Fonction générique d’affichage de tableau

void aff_tab(void **tab, int n, void (*aff_elt)(void *)) {


int i;

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

}
}

La fonction aff_tab est générique : elle permet d’afficher les éléments d’un tableau dont le type
n’est pas connu lors de l’écriture de la fonction.

346 / 399
Fonction générique d’affichage de tableau

void aff_tab(void **tab, int n, void (*aff_elt)(void *)) {


int i;

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


aff_elt(tab[i]);

}
}

La fonction aff_tab est générique : elle permet d’afficher les éléments d’un tableau dont le type
n’est pas connu lors de l’écriture de la fonction.

346 / 399
Fonction générique d’affichage de tableau

void aff_tab(void **tab, int n, void (*aff_elt)(void *)) {


int i;

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


aff_elt(tab[i]);
printf(" ");
}
}

La fonction aff_tab est générique : elle permet d’afficher les éléments d’un tableau dont le type
n’est pas connu lors de l’écriture de la fonction.

346 / 399
Fonction générique d’affichage de tableau
On l’utilise la fonction aff_tab de la manière suivante.

Pour afficher un tableau tab de taille 13 de pointeurs sur des entiers :

347 / 399
Fonction générique d’affichage de tableau
On l’utilise la fonction aff_tab de la manière suivante.

Pour afficher un tableau tab de taille 13 de pointeurs sur des entiers :

void aff_int(void *x) {

347 / 399
Fonction générique d’affichage de tableau
On l’utilise la fonction aff_tab de la manière suivante.

Pour afficher un tableau tab de taille 13 de pointeurs sur des entiers :

void aff_int(void *x) {


int e;

347 / 399
Fonction générique d’affichage de tableau
On l’utilise la fonction aff_tab de la manière suivante.

Pour afficher un tableau tab de taille 13 de pointeurs sur des entiers :

void aff_int(void *x) {


int e;
e = *((int *) x);

347 / 399
Fonction générique d’affichage de tableau
On l’utilise la fonction aff_tab de la manière suivante.

Pour afficher un tableau tab de taille 13 de pointeurs sur des entiers :

void aff_int(void *x) {


int e;
e = *((int *) x);
printf("%d", e);
}

347 / 399
Fonction générique d’affichage de tableau
On l’utilise la fonction aff_tab de la manière suivante.

Pour afficher un tableau tab de taille 13 de pointeurs sur des entiers :

void aff_int(void *x) {


int e;
e = *((int *) x);
printf("%d", e);
}

/* Version raccourcie. */
void aff_int(void *x) {
printf("%d", *((int *) x));
}

347 / 399
Fonction générique d’affichage de tableau
On l’utilise la fonction aff_tab de la manière suivante.

Pour afficher un tableau tab de taille 13 de pointeurs sur des entiers :

void aff_int(void *x) {


int e;
e = *((int *) x);
printf("%d", e);
}

/* Version raccourcie. */
void aff_int(void *x) {
printf("%d", *((int *) x));
}
...
aff_tab((void **) tab, 13, &aff_int);

347 / 399
Fonction générique d’affichage de tableau

Pour afficher un tableau tab de taille 23 de pointeurs sur des variables de type structuré Date :

typedef struct {
int jour;
int mois;
int annee;
} Date;
...
void aff_date(void *d) {
Date dd;
dd = *((Date *) d);
printf("%d-%d-%d", dd.jour, dd.mois, dd.annee);
}
...
aff_tab((void **) tab, 23, &aff_date);

348 / 399
Listes génériques

On souhaite définir une structure de donnée liste dont les types des éléments ne sont pas fixés.

349 / 399
Listes génériques

On souhaite définir une structure de donnée liste dont les types des éléments ne sont pas fixés.
Pour cela, on utilise un pointeur générique pour le champ qui contient l’élément de chaque
cellule :
typedef struct _Cellule {
struct _Cellule *suiv;
void *e;
} Cellule;

typedef Cellule *Liste;

349 / 399
Listes génériques

On souhaite définir une structure de donnée liste dont les types des éléments ne sont pas fixés.
Pour cela, on utilise un pointeur générique pour le champ qui contient l’élément de chaque
cellule :
typedef struct _Cellule {
struct _Cellule *suiv;
void *e;
} Cellule;

typedef Cellule *Liste;

Le type Liste permet ainsi de représenter des listes génériques.

349 / 399
Listes génériques

On souhaite définir une structure de donnée liste dont les types des éléments ne sont pas fixés.
Pour cela, on utilise un pointeur générique pour le champ qui contient l’élément de chaque
cellule :
typedef struct _Cellule {
struct _Cellule *suiv;
void *e;
} Cellule;

typedef Cellule *Liste;

Le type Liste permet ainsi de représenter des listes génériques.


C’est une structure de donnée générique car le type des éléments que les futures listes
pourront contenir n’est pas connu lors de l’écriture de la fonction.

349 / 399
Listes génériques

La fonction
void aff_lst(Liste lst, void (*aff_elt)(void *)) {
Cellule *x;

assert(lst != NULL);
assert(aff_elt != NULL);

for (x = lst ; x != NULL ; x = x->suiv) {


aff_elt(x->e);
printf(" ");
}
}

est une fonction générique pour l’affichage des éléments d’une liste générique.

350 / 399
Listes génériques
On l’utilise de la manière suivante (dans le cas ici d’une liste d’entiers).

void aff_int(void *e) {


printf("%d", *((int *) e));
}
...
Liste lst;
int a, b, c;
a = 3; b = 14; c = 414;

lst = (Cellule *) malloc(sizeof(Cellule));


lst->e = &a;
lst->suiv = (Cellule *) malloc(sizeof(Cellule));
lst->suiv->e = &b;
lst->suiv->suiv = (Cellule *) malloc(sizeof(Cellule));
lst->suiv->suiv->e = &c;
lst->suiv->suiv->suiv = NULL;
aff_lst(lst, &aff_int);

351 / 399
Listes génériques

Beaucoup de fonctions sur les listes peuvent ainsi être rendues génériques. Entre autres :

352 / 399
Listes génériques

Beaucoup de fonctions sur les listes peuvent ainsi être rendues génériques. Entre autres :
1. void aff_lst(Liste lst, void (*aff_elt)(void *));

352 / 399
Listes génériques

Beaucoup de fonctions sur les listes peuvent ainsi être rendues génériques. Entre autres :
1. void aff_lst(Liste lst, void (*aff_elt)(void *));

2. void *elt_indice(Liste lst, int i);

352 / 399
Listes génériques

Beaucoup de fonctions sur les listes peuvent ainsi être rendues génériques. Entre autres :
1. void aff_lst(Liste lst, void (*aff_elt)(void *));

2. void *elt_indice(Liste lst, int i);

3. int est_triee(Liste lst, int (*est_inf)(void *, void *));

352 / 399
Listes génériques

Beaucoup de fonctions sur les listes peuvent ainsi être rendues génériques. Entre autres :
1. void aff_lst(Liste lst, void (*aff_elt)(void *));

2. void *elt_indice(Liste lst, int i);

3. int est_triee(Liste lst, int (*est_inf)(void *, void *));

4. void *max(Liste lst, int (*est_inf)(void *, void *));

352 / 399
Listes génériques

Beaucoup de fonctions sur les listes peuvent ainsi être rendues génériques. Entre autres :
1. void aff_lst(Liste lst, void (*aff_elt)(void *));

2. void *elt_indice(Liste lst, int i);

3. int est_triee(Liste lst, int (*est_inf)(void *, void *));

4. void *max(Liste lst, int (*est_inf)(void *, void *));

Les cas 3 et 4 supposent que les éléments représentés par les listes sont comparables au moyen
d’une fonction est_inf à fournir.

352 / 399
Plan

Pointeurs de fonction
Principe
En paramètre et en retour
Généricité
Implantation de monoïdes

353 / 399
Monoïdes

En maths, un monoïde est un triplet (M, •, 1) où

354 / 399
Monoïdes

En maths, un monoïde est un triplet (M, •, 1) où


1. M est un ensemble ;

354 / 399
Monoïdes

En maths, un monoïde est un triplet (M, •, 1) où


1. M est un ensemble ;
2. • est une application • : M × M → M associative, c.-à-d., pour tous x, y, z ∈ M, on a
(x • y) • z = x • (y • z) ;

354 / 399
Monoïdes

En maths, un monoïde est un triplet (M, •, 1) où


1. M est un ensemble ;
2. • est une application • : M × M → M associative, c.-à-d., pour tous x, y, z ∈ M, on a
(x • y) • z = x • (y • z) ;
3. 1 ∈ M est un élément unitaire, c.-à-d., pour tout x ∈ M, on a x • 1 = x = 1 • x.

354 / 399
Monoïdes

En maths, un monoïde est un triplet (M, •, 1) où


1. M est un ensemble ;
2. • est une application • : M × M → M associative, c.-à-d., pour tous x, y, z ∈ M, on a
(x • y) • z = x • (y • z) ;
3. 1 ∈ M est un élément unitaire, c.-à-d., pour tout x ∈ M, on a x • 1 = x = 1 • x.

Exemples :
I (N, +, 0) est le monoïde additif des entiers naturels ;

354 / 399
Monoïdes

En maths, un monoïde est un triplet (M, •, 1) où


1. M est un ensemble ;
2. • est une application • : M × M → M associative, c.-à-d., pour tous x, y, z ∈ M, on a
(x • y) • z = x • (y • z) ;
3. 1 ∈ M est un élément unitaire, c.-à-d., pour tout x ∈ M, on a x • 1 = x = 1 • x.

Exemples :
I (N, +, 0) est le monoïde additif des entiers naturels ;
I (N, ×, 1) est le monoïde multiplicatif des entiers naturels ;

354 / 399
Monoïdes

En maths, un monoïde est un triplet (M, •, 1) où


1. M est un ensemble ;
2. • est une application • : M × M → M associative, c.-à-d., pour tous x, y, z ∈ M, on a
(x • y) • z = x • (y • z) ;
3. 1 ∈ M est un élément unitaire, c.-à-d., pour tout x ∈ M, on a x • 1 = x = 1 • x.

Exemples :
I (N, +, 0) est le monoïde additif des entiers naturels ;
I (N, ×, 1) est le monoïde multiplicatif des entiers naturels ;
I ({a, b}∗ , ·, ) est le monoïde libre sur les lettres a et b. Ses éléments sont les chaînes de
caractères composées de a et de b. Son produit · est la concaténation et son unité  est la
chaîne vide.

354 / 399
Implantation de monoïdes

On se donne les deux objectifs suivants :

355 / 399
Implantation de monoïdes

On se donne les deux objectifs suivants :

1. implanter une structure de donnée générique pour représenter des monoïdes dont la
nature des éléments n’est pas connue à l’avance ;

355 / 399
Implantation de monoïdes

On se donne les deux objectifs suivants :

1. implanter une structure de donnée générique pour représenter des monoïdes dont la
nature des éléments n’est pas connue à l’avance ;

2. implanter une fonction générique qui élève à la puissance n > 0 un élément x d’un
monoïde.

355 / 399
Structure de donnée générique de monoïde

D’après la définition mathématique d’un monoïde, on aboutit à la définition suivante :


typedef struct {
void *unite;
void *(*produit)(void *, void *);
} Monoide;

356 / 399
Structure de donnée générique de monoïde

D’après la définition mathématique d’un monoïde, on aboutit à la définition suivante :


typedef struct {
void *unite;
void *(*produit)(void *, void *);
} Monoide;

On utilise des pointeurs génériques void * pour les éléments du monoïde.

Le champ unite est un pointeur générique vers l’unité du monoïde.

Le champ produit est un pointeur de fonction vers une fonction qui réalise le produit du
monoïde.

356 / 399
Définition du monoïde additif des entiers naturels

On commence par définir une fonction qui réalise le produit de (N, +, 0) et dont le type de
retour et la signature respectent ceux du champ produit :

void *add(void *a, void *b) {

357 / 399
Définition du monoïde additif des entiers naturels

On commence par définir une fonction qui réalise le produit de (N, +, 0) et dont le type de
retour et la signature respectent ceux du champ produit :

void *add(void *a, void *b) {


int *res;

return (void *) res;


}

357 / 399
Définition du monoïde additif des entiers naturels

On commence par définir une fonction qui réalise le produit de (N, +, 0) et dont le type de
retour et la signature respectent ceux du champ produit :

void *add(void *a, void *b) {


int *res;
res = (int *) malloc(sizeof(int));

return (void *) res;


}

357 / 399
Définition du monoïde additif des entiers naturels

On commence par définir une fonction qui réalise le produit de (N, +, 0) et dont le type de
retour et la signature respectent ceux du champ produit :

void *add(void *a, void *b) {


int *res;
res = (int *) malloc(sizeof(int));
*res = *((int *) a) + *((int *) b);
return (void *) res;
}

357 / 399
Définition du monoïde additif des entiers naturels

On commence par définir une fonction qui réalise le produit de (N, +, 0) et dont le type de
retour et la signature respectent ceux du champ produit :

void *add(void *a, void *b) {


int *res;
res = (int *) malloc(sizeof(int));
*res = *((int *) a) + *((int *) b);
return (void *) res;
}

Ensuite, on créé le monoïde de la manière suivante :


Monoide m_add;
int zero = 0;
m_add.unite = &zero;
m_add.produit = &add;

357 / 399
Définition du monoïde multiplicatif des entiers naturels

On commence par définir une fonction qui réalise le produit de (N, ×, 1) et dont le type de
retour et la signature respectent ceux du champ produit :

void *mul(void *a, void *b) {

358 / 399
Définition du monoïde multiplicatif des entiers naturels

On commence par définir une fonction qui réalise le produit de (N, ×, 1) et dont le type de
retour et la signature respectent ceux du champ produit :

void *mul(void *a, void *b) {


int *res;

return (void *) res;


}

Ensuite, on créé le monoïde de la manière suivante :


Monoide m_mul;
int un = 1;
m_mul.unite = &un;
m_mul.produit = &mul;

358 / 399
Définition du monoïde multiplicatif des entiers naturels

On commence par définir une fonction qui réalise le produit de (N, ×, 1) et dont le type de
retour et la signature respectent ceux du champ produit :

void *mul(void *a, void *b) {


int *res;
res = (int *) malloc(sizeof(int));

return (void *) res;


}

Ensuite, on créé le monoïde de la manière suivante :


Monoide m_mul;
int un = 1;
m_mul.unite = &un;
m_mul.produit = &mul;

358 / 399
Définition du monoïde multiplicatif des entiers naturels

On commence par définir une fonction qui réalise le produit de (N, ×, 1) et dont le type de
retour et la signature respectent ceux du champ produit :

void *mul(void *a, void *b) {


int *res;
res = (int *) malloc(sizeof(int));
*res = *((int *) a) * *((int *) b);
return (void *) res;
}

Ensuite, on créé le monoïde de la manière suivante :


Monoide m_mul;
int un = 1;
m_mul.unite = &un;
m_mul.produit = &mul;

358 / 399
Définition du monoïde libre sur a et b
On commence par définir une fonction qui réalise le produit de ({a, b}∗ , ·, ) et dont le type de
retour et la signature respectent ceux du champ produit :

void *conc(void *u, void *v) {

359 / 399
Définition du monoïde libre sur a et b
On commence par définir une fonction qui réalise le produit de ({a, b}∗ , ·, ) et dont le type de
retour et la signature respectent ceux du champ produit :

void *conc(void *u, void *v) {


char *res;

return (void *) res;


}

359 / 399
Définition du monoïde libre sur a et b
On commence par définir une fonction qui réalise le produit de ({a, b}∗ , ·, ) et dont le type de
retour et la signature respectent ceux du champ produit :

void *conc(void *u, void *v) {


char *res;
int i, lu, lv;

return (void *) res;


}

359 / 399
Définition du monoïde libre sur a et b
On commence par définir une fonction qui réalise le produit de ({a, b}∗ , ·, ) et dont le type de
retour et la signature respectent ceux du champ produit :

void *conc(void *u, void *v) {


char *res;
int i, lu, lv;
lu = strlen((char *) u);
lv = strlen((char *) v);

return (void *) res;


}

359 / 399
Définition du monoïde libre sur a et b
On commence par définir une fonction qui réalise le produit de ({a, b}∗ , ·, ) et dont le type de
retour et la signature respectent ceux du champ produit :

void *conc(void *u, void *v) {


char *res;
int i, lu, lv;
lu = strlen((char *) u);
lv = strlen((char *) v);
res = (char *) malloc(sizeof(char) * (1 + lu + lv));

return (void *) res;


}

359 / 399
Définition du monoïde libre sur a et b
On commence par définir une fonction qui réalise le produit de ({a, b}∗ , ·, ) et dont le type de
retour et la signature respectent ceux du champ produit :

void *conc(void *u, void *v) {


char *res;
int i, lu, lv;
lu = strlen((char *) u);
lv = strlen((char *) v);
res = (char *) malloc(sizeof(char) * (1 + lu + lv));
for (i = 0 ; i < lu ; ++i) res[i] = ((char *) u)[i];
for (i = 0 ; i < lv ; ++i) res[lu + i] = ((char *) v)[i];

return (void *) res;


}

359 / 399
Définition du monoïde libre sur a et b
On commence par définir une fonction qui réalise le produit de ({a, b}∗ , ·, ) et dont le type de
retour et la signature respectent ceux du champ produit :

void *conc(void *u, void *v) {


char *res;
int i, lu, lv;
lu = strlen((char *) u);
lv = strlen((char *) v);
res = (char *) malloc(sizeof(char) * (1 + lu + lv));
for (i = 0 ; i < lu ; ++i) res[i] = ((char *) u)[i];
for (i = 0 ; i < lv ; ++i) res[lu + i] = ((char *) v)[i];
res[lu + lv] = ’\0’;
return (void *) res;
}

359 / 399
Définition du monoïde libre sur a et b
On commence par définir une fonction qui réalise le produit de ({a, b}∗ , ·, ) et dont le type de
retour et la signature respectent ceux du champ produit :

void *conc(void *u, void *v) {


char *res;
int i, lu, lv;
lu = strlen((char *) u);
lv = strlen((char *) v);
res = (char *) malloc(sizeof(char) * (1 + lu + lv));
for (i = 0 ; i < lu ; ++i) res[i] = ((char *) u)[i];
for (i = 0 ; i < lv ; ++i) res[lu + i] = ((char *) v)[i];
res[lu + lv] = ’\0’;
return (void *) res;
}

Ensuite, on créé le monoïde de la manière suivante :

Monoide m_lib; m_lib.unite = &epsilon;


char epsilon[1] = ""; m_lib.produit = &conc;

359 / 399
Fonction générique d’exponentiation

void *puissance(Monoide m, void *x, int n) {

360 / 399
Fonction générique d’exponentiation

void *puissance(Monoide m, void *x, int n) {


void *tmp;

360 / 399
Fonction générique d’exponentiation

void *puissance(Monoide m, void *x, int n) {


void *tmp;

if (n == 0)
return m.unite;

360 / 399
Fonction générique d’exponentiation

void *puissance(Monoide m, void *x, int n) {


void *tmp;

if (n == 0)
return m.unite;
tmp = puissance(m, x, n / 2);

360 / 399
Fonction générique d’exponentiation

void *puissance(Monoide m, void *x, int n) {


void *tmp;

if (n == 0)
return m.unite;
tmp = puissance(m, x, n / 2);
if (n % 2 == 0)
return m.produit(tmp, tmp);

360 / 399
Fonction générique d’exponentiation

void *puissance(Monoide m, void *x, int n) {


void *tmp;

if (n == 0)
return m.unite;
tmp = puissance(m, x, n / 2);
if (n % 2 == 0)
return m.produit(tmp, tmp);
return m.produit(x, m.produit(tmp, tmp));
}

360 / 399
Fonction générique d’exponentiation

void *puissance(Monoide m, void *x, int n) {


void *tmp;

if (n == 0)
return m.unite;
tmp = puissance(m, x, n / 2);
if (n % 2 == 0)
return m.produit(tmp, tmp);
return m.produit(x, m.produit(tmp, tmp));
}

Cette fonction renvoie un pointeur générique sur une variable contenant le résultat de la valeur à
l’adresse x élevée à la puissance n , pour le produit spécifié par le monoïde m .

360 / 399
Fonction générique d’exponentiation

void *puissance(Monoide m, void *x, int n) {


void *tmp;

if (n == 0)
return m.unite;
tmp = puissance(m, x, n / 2);
if (n % 2 == 0)
return m.produit(tmp, tmp);
return m.produit(x, m.produit(tmp, tmp));
}

Cette fonction renvoie un pointeur générique sur une variable contenant le résultat de la valeur à
l’adresse x élevée à la puissance n , pour le produit spécifié par le monoïde m .
Celle fonction utilise l’algorithme dit d’exponentiation rapide.

360 / 399
Application de la fonction d’exponentiation
int res, val = 5;
res = *((int *) puissance(m_add, &val, 3));
printf("%d\n", res);

Ceci affiche 53 dans le monoïde additif, c’est à dire 15 .

361 / 399
Application de la fonction d’exponentiation
int res, val = 5;
res = *((int *) puissance(m_add, &val, 3));
printf("%d\n", res);

Ceci affiche 53 dans le monoïde additif, c’est à dire 15 .

int res, val = 5;


res = *((int *) puissance(m_mul, &val, 3));
printf("%d\n", res);

Ceci affiche 53 dans le monoïde multiplicatif, c’est à dire 125 .

361 / 399
Application de la fonction d’exponentiation
int res, val = 5;
res = *((int *) puissance(m_add, &val, 3));
printf("%d\n", res);

Ceci affiche 53 dans le monoïde additif, c’est à dire 15 .

int res, val = 5;


res = *((int *) puissance(m_mul, &val, 3));
printf("%d\n", res);

Ceci affiche 53 dans le monoïde multiplicatif, c’est à dire 125 .

char *res, ch[3] = "ab";


res = (char *) puissance(m_lib, &ch, 4);
printf("%s\n", res);

Ceci affiche (ab)4 dans le monoïde libre, c’est à dire abababab .


361 / 399
Plan

Génération aléatoire
Générateurs aléatoires
Nombres pseudo-aléatoires
Utilisation et implantation

362 / 399
Plan

Génération aléatoire
Générateurs aléatoires
Nombres pseudo-aléatoires
Utilisation et implantation

363 / 399
Générateurs aléatoires

Un générateur aléatoire sur un ensemble E d’objets est une fonction g à valeur dans E non
déterministe. La valeur renvoyée par g n’est pas nécessairement la même d’un appel à un autre.

On cherche dans la mesure du possible à faire en sorte que, étant donné un ensemble E d’objets,
tout objet de E ait la même probabilité d’être renvoyé par le générateur aléatoire g. On parle
alors de générateur aléatoire uniforme.

Par exemple, si E est l’ensemble des faces d’un dé à six faces, le procédé g qui consiste à lancer le
dé et renvoyer la face visible constitue un générateur aléatoire uniforme (et non uniforme si le dé
est pipé).

364 / 399
Intérêt des générateurs aléatoires

Les générateurs aléatoires offrent de nombreuses applications. Ils sont utilisés pour :
1. les méthodes de Monte-Carlo pour obtenir des solutions approchées à des problèmes ;
2. tester des algorithmes en générant des entrées de manière aléatoire (permutations, listes,
arbres, graphes, etc.) ;
3. rompre le caractère déterministe d’un programme en y incluant des éléments
imprévisibles (dans les jeux par exemple) ;
4. générer des mots de passe ou des clés de chiffrement.

365 / 399
Universalité des générateurs aléatoires d’entiers

Un générateur aléatoire d’entiers d’ordre n est un générateur aléatoire sur l’ensemble


{0, 1, 2, . . . , n − 1}.

Pour construire un générateur aléatoire sur un ensemble E d’objets, il suffit en pratique d’avoir
un générateur aléatoire d’entiers d’ordre #E.
En effet, il suffit de placer les objets (ou mieux : les adresses des objets) de E dans un tableau

e0 e1 ... en−1 ,

d’appeler g et de renvoyer l’objet du tableau figurant à l’indice spécifié par l’entier généré par g.

Ainsi, pour faire de la génération aléatoire d’objets, il est suffisant (en 1re approximation) de
savoir construire des générateurs aléatoires d’entiers d’un ordre suffisamment grand.

366 / 399
Réduire l’ordre d’un générateur aléatoire d’entiers

L’opérateur « modulo » offre un moyen très simple pour réduire l’ordre d’un générateur
aléatoire d’entiers.
En effet, supposons que l’on dispose d’un générateur aléatoire d’entiers g d’ordre n. On souhaite
obtenir un générateur aléatoire d’entiers h d’ordre k avec 1 6 k 6 n. On pose pour cela

h := g mod k.

Il est clair que les entiers générés par h sont dans l’ensemble {0, . . . , k − 1}. On a ainsi réduit
l’ordre du générateur g.

On dit que h est la réduction à l’ordre k de g.

367 / 399
Réduction de l’ordre et uniformité

Supposons que g soit un générateur aléatoire uniforme d’ordre n := 4 et considérons la


réduction h à l’ordre k := 3 de g.
On a la situation suivante :
g: 0 1 2 3

h: 0 1 2

Il y a ainsi deux manières de générer 0 par h (probabilité de 12 ) alors qu’il n’y en a qu’une seule
pour 1 et 2 (probabilités resp. de 14 ).

Ceci montre que le générateur aléatoire h perd la propriété d’uniformité.

368 / 399
Mesure de la non uniformité de la réduction
Soient g un générateur aléatoire uniforme d’ordre n et h la réduction de g à l’ordre k, où k 6 n.
Notons genh (i) le nombre de manières de générer l’entier i 6 k − 1 par h. Alors, on voit
facilement que (
b nk c + 1 si i ∈ {0, . . . , (n mod k) − 1}
genh (i) =
b nk c sinon.
La probabilité Ph (i) de générer l’entier i 6 k − 1 par h vérifie

genh (i)
Ph (i) = .
n
Les entiers i de l’ensemble {0, . . . , k − 1} ont ainsi des probabilités différentes d’être générés. Ils
se divisent en deux classes :
1. les petits, de 0 à (n mod k) − 1, avec une probabilité plus grande ;
2. les grands, de (n mod k) à k − 1, avec une probabilité plus petite.

369 / 399
Mesure de la non uniformité de la réduction
Comparons numériquement les probabilités de génération des petits et des grands nombres pour
différentes valeurs de n et de k :
n k Prob. des petits Prob. des grands Rapport
4 3 0.5 0.25 2
400 300 0.005 0.0025 2
400 200 0.005 0.005 1
400 101 0.01 0.0075 ' 1.3333
400 99 0.0125 0.01 1.25
500 99 0.012 0.01 1.2
5000 99 0.0102 0.01 1.02
50000 99 0.01012 0.0101 ' 1.00198

On observe que plus k est petit par rapport à n, plus h se rapproche d’un générateur uniforme.
Lorsque k est un diviseur de n, l’uniformité est immédiate.
En pratique, la réduction est considérée comme préservant l’uniformité, à condition de partir
d’un générateur aléatoire d’entiers uniforme g d’ordre le plus grand possible.
370 / 399
Plan

Génération aléatoire
Générateurs aléatoires
Nombres pseudo-aléatoires
Utilisation et implantation

371 / 399
Machines et aléatoire

Un ordinateur est par essence une machine déterministe : le résultat d’un programme est
toujours le même s’il est exécuté deux fois dans les mêmes conditions.

Il est ainsi impossible d’écrire un programme qui implante un générateur aléatoire d’entiers.

La seule chose qu’il est possible de faire est de programmer une fonction qui semble le plus
possible se comporter comme un générateur aléatoire. On parle alors de générateur
pseudo-aléatoire.

Les entiers qu’un générateur pseudo-aléatoire génère lorsqu’on l’appelle plusieurs fois de suite
forme une suite. On appelle cette suite une suite d’entiers pseudo-aléatoires.

372 / 399
Principe de fonctionnement

Un générateur pseudo-aléatoire g d’ordre n fonctionne de la manière suivante :


I il dispose d’une graine g1 , g2 , . . . , g` , qui est une suite d’entiers ;
I lorsqu’on l’appelle pour la re fois, il renvoie un entier g`+r calculé à partir des entiers
g1 , . . . , g` , . . . , g`+r−1 précédemment calculés, selon une règle spécifiée.
Ainsi, g produit une suite d’entiers

g`+1 , g`+2 , g`+3 , . . .

à partir de la graine dans laquelle figurent les données initiales et de la règle qui explique
comment générer le prochain entier de la suite en se basant sur les entiers précédemment
générés.

373 / 399
Qualités d’un générateur pseudo-aléatoire

Un bon générateur pseudo-aléatoire produit une suite d’entiers qui semble aléatoire.
Il est possible de mesurer la qualité d’un générateur aléatoire conformément à plusieurs critères :
1. uniformité de la génération ;
2. ses périodes (longueurs des cycles de génération) ;
3. sensibilité à la graine (existence de mauvaises graines) ;
4. corrélation entre un entier engendré et les précédents ;
5. analyse de la distribution en plusieurs dimensions.

374 / 399
Méthode du carré médian
Le générateur pseudo-aléatoire g utilisant la méthode du carré médian fonctionne de la manière
suivante.
La graine de ce générateur est un entier compris entre 0 et 9999. Si gi−1 est l’entier
précédemment généré (ou bien la graine), le prochain entier généré est
j g k 2
i−1
gi := mod 100 .
10
On obtient p.ex. les suites
Graine Suite
1234 529, 2704, 4900, 8100, 100, 100, ...
3733 5329, 1024, 4, 0, 0, ...
9999 9801, 6400, 1600, 3600, 3600, ...

C’est un mauvais générateur pseudo-aléatoire. Les périodes sont bien trop courtes et il y a
des mauvaises graines (comme 0).

375 / 399
Méthode additive

Le générateur pseudo-aléatoire g utilisant la méthode additive fonctionne de la manière suivante.


On se fixe un ordre n > 1. La graine de ce générateur est un triplet (g1 , g2 , g3 ) d’entiers compris
entre 0 et n − 1. La génération de gi dépend des trois entiers précédemment générés gi−1 , gi−2
et gi−3 . Il est défini par
gi := (gi−1 + gi−2 + gi−3 ) mod n.
On obtient p.ex. les suites
n Graine Suite
211 (1, 1, 1) 3, 5, 9, 17, 31, 57, 105, 193, 144, 20, 146, 99, 54, 88, 30, 172, 79
3099 (1, 1, 1) 3, 5, 9, 17, 31, 57, 105, 193, 355, 653, 1201, 2209, 964, 1275, 1349
1347 (600, 31, 1) 632, 1263, 1148, 349, 66, 216, 631, 913, 413, 610, 589, 265, 117

Ce générateur pseudo-aléatoire est bien meilleur que le précédent mais échoue à de nombreux
tests statistiques.

376 / 399
Générateurs congruentiels linéaires

Un générateur pseudo-aléatoire g congruentiel linéaire fonctionne de la manière suivante.


On se fixe un ordre n > 1 ainsi que deux entiers positifs a et b. La graine de ce générateur est un
entier compris entre 0 et n − 1. La génération de gi dépend de l’entier précédemment généré
gi−1 et est défini par
gi := (a × gi−1 + b) mod n.
On obtient p.ex. les suites
n a b Graine Suite
128 3 1 1 4, 13, 40, 121, 108, 69, 80, 113, 84, 125, 120, 105, 60
128 7 1 1 8, 57, 16, 113, 24, 41, 32, 97, 40, 25, 48, 81, 56, 9, 64
232 1664525 1013904223 1 1015568748, 1586005467, 2165703038, 3027450565

Sous réserve de choisir des bons paramètres n, a et b, ce générateur pseudo-aléatoire est plutôt
bon. Il est très utilisé en pratique.

377 / 399
Plan

Génération aléatoire
Générateurs aléatoires
Nombres pseudo-aléatoires
Utilisation et implantation

378 / 399
Les fonctions rand et srand

La fonction
int rand();

du module stdlib implante un générateur pseudo-aléatoire d’ordre RAND_MAX + 1 . C’est un


générateur congruentiel linéaire. À chaque appel, il renvoie le prochain entier de la suite.

La graine de ce générateur peut-être affectée par la fonction


void srand(unsigned int g1);

Par défaut, la graine du générateur est 1 .

379 / 399
Modèle d’utilisation de rand et de srand

Tout projet qui utilise la fonction rand doit /∗ Main.c ∗/


...
avoir une fonction principale main de la forme #include <stdlib.h>
ci-contre. #include <time.h>
...
int main() {
...
srand(time(NULL));
L’appel à srand doit être fait le plus tôt ...
}
possible.

La fonction time_t time (time_t* timer); du module time , lorsque appelée avec l’argument NULL
renvoie le temps écoulé en secondes depuis le 1er janvier 1970 à minuit, UTC.
Considérer cette valeur est donc un bon moyen d’initialiser la graine du générateur.
Attention (point très important) : il est totalement erroné d’initialiser dans un même projet deux
fois la graine. Le seul appel à srand doit figurer dans main et nulle part ailleurs.

380 / 399
Implantation avec variable globale
Une implantation possible des fonctions rand et srand s’appuie sur une variable globale pour
mémoriser la dernière valeur générée.

/∗ RandPerso . c ∗/
#include "RandPerso.h"
/∗ RandPerso . h ∗/
#ifndef __RAND_PERSO__ unsigned int valeur_rand =
#define __RAND_PERSO__ RAND_PERSO_GRAINE;

#define RAND_PERSO_ORDRE 32767 int rand_perso() {


#define RAND_PERSO_A 1024 valeur_rand =
#define RAND_PERSO_B 1 (valeur_rand * RAND_PERSO_A
#define RAND_PERSO_GRAINE 1 + RAND_PERSO_B)
% RAND_PERSO_ORDRE;
int rand_perso(); return valeur_rand;
void srand_perso(int graine); }

#endif void srand_perso(int graine) {


valeur_rand = graine;
}

381 / 399
Implantation avec variable statique
Dans cette implantation, on utilise une variable statique pour mémoriser la dernière valeur
générée.

/∗ RandPerso . c ∗/
#include "RandPerso.h"

/∗ RandPerso . h ∗/ #include <time.h>


#ifndef __RAND_PERSO__
#define __RAND_PERSO__ int rand_perso() {
static int premier_appel = 1;
#define RAND_PERSO_ORDRE 32767 static unsigned int valeur_rand;
#define RAND_PERSO_A 1024 if (premier_appel) {
#define RAND_PERSO_B 1 valeur_rand = time(NULL);
#define RAND_PERSO_GRAINE 1 premier_appel = 0;
}
int rand_perso(); valeur_rand =
(valeur_rand * RAND_PERSO_A
#endif + RAND_PERSO_B)
% RAND_PERSO_ORDRE;
return valeur_rand;
}

L’initialisation ne se fait qu’au 1er appel et l’utilisateur n’a pas à la faire explicitement. Avec cette méthode,
il n’est pas possible de fixer la graine.
382 / 399
Retrouver le déterminisme
Pour pouvoir reproduire les résultats fournis par un programme utilisant la fonction rand , il est
nécessaire de proposer un mode de fonctionnement déterministe dans lequel la graine a été fixée.
Pour cela, on met en place dans le module principal d’un projet et dans sa fonction main le
mécanisme suivant :

/∗ Main.c ∗/ De cette manière, on peut imposer un


... comportement déterministe (reproductible) à
#define DETERMINISTE
... l’exécution du projet en gardant la définition
int main() { de la macro DETERMINISTE .
...
#ifdef DETERMINISTE Pour éviter ce comportement, il suffit de
srand(0);
#else commenter cette macro-définition.
srand(time(NULL));
#endif
...
}

Meilleure solution : utiliser l’option -D de gcc . Il suffit de supprimer la ligne #define DETERMINISTE et de compiler
avec -DDETERMINISTE pour définir la macro en question.
383 / 399
Plan

Mémoïsation
Variables statiques
Mémoïsation

384 / 399
Plan

Mémoïsation
Variables statiques
Mémoïsation

385 / 399
Notion de variable statique

Une variable statique est une variable dont la durée de vie est égale à celle de l’exécution du
programme. Une telle variable est créée et initialisée à la compilation.
L’instruction
static T ID;

déclare une variable statique ID de type T .

Elles sont les contreparties des variables automatiques dont la durée de vie s’étend à
l’exécution du bloc dans lesquels elles se trouvent et dont la création se fait à l’exécution le
moment voulu.
Les variables automatiques sont les variables habituelles (se déclarent sans le mot clé static ).

386 / 399
Persistance des variables statiques
Une variable statique est rémanente : elle conserve sa dernière valeur jusqu’à une éventuelle
modification.

1 void fct(int a) { Produit l’affichage


2 static int s = 3; 4
3 s = s + a;
4 printf("%d\n", s); 9
5 } 11 .
6 ...
7 fct(1);
8 fct(5);
9 fct(2);

Ici, s est une variable statique. La valeur 3 (initialisation) ne lui est attribuée qu’une fois, lors
de la compilation.
De plus, s conserve sa valeur (qui peut être modifiée) au fil de l’exécution, d’appel en appel à
fct .
387 / 399
Géographie de la mémoire lors d’une exécution
Lors de l’exécution d’un programme, la mémoire qui lui est consacrée est divisée de la manière
suivante.

Les zones de la mémoire réservées à la pile et


Zone statique au tas ont des tailles variables durant
l’exécution.

Tas Le tas grandit vers le bas (vers les adresses


hautes) et la pile grandit vers le haut (vers les
adresses basses).
...
La zone statique, qui contient les variables
statiques et le code du programme, est de taille
constante durant l’exécution.
Pile
Cette taille est connue et calculée lors de la
Autre
compilation.

388 / 399
Compter le nombre d’appels à une fonction
Étant donnée une fonction, on souhaite compter le nombre de fois qu’elle a été appelée depuis le
lancement du programme.

1 int fact(int n) { 1 int fact(int n) {


2 if (n <= 1) return 1; 2
3 return n * fact(n - 1); 3 /∗ Mecanisme de comptage ∗/
4 } 4 static int nb_app = 0;
5 nb_app += 1;
6
7 if (n <= 1) return 1;
8 return n * fact(n - 1);
9 }

À chaque instant, la variable statique nb_app a pour valeur le nombre de fois que fact a été
appelée.
Problème : cette variable a pour portée lexicale le corps de la fonction fact et est invisible
ailleurs. On ne peut donc pas la lire depuis l’extérieur.
389 / 399
Compter le nombre d’appels à une fonction
Pour avoir accès à la valeur de nb_app hors du corps de fact , on la transmet à l’aide d’un
passage par adresse.
1 int fact(int n, int *nb) {
2
3 /∗ Mecanisme de comptage ∗/
4 static int nb_app = 0;
5 nb_app += 1;
6 *nb = nb_app;
7
8 if (n <= 1) return 1;
9 return n * fact(n - 1, nb);
10 }

On l’utilise se la manière suivante :

1 int nb; 3 printf("%d\n", nb);


2 fact(6, &nb);
Ceci affiche 6 .

390 / 399
Plan

Mémoïsation
Variables statiques
Mémoïsation

391 / 399
Principe de la mémoïsation

Soit f une fonction sans effet de bord et déterministe (sa valeur de retour ne dépend que de
ses arguments).

Observation : si l’on appelle f deux fois avec des mêmes arguments, elle renverra deux fois la
même valeur.

Conséquence : comme il est inutile (et inefficace) de refaire deux fois un même calcul, il suffit de
mémoriser le résultat renvoyé par f lors du 1er appel et de le renvoyer sans calcul lors du 2e.

Marche à suivre : on enregistre toutes les valeurs de retour de f au fur et à mesure de ses
appels.

392 / 399
Détails sur la mémoïsation

Les résultats renvoyés par f sont mémorisés dans une table de hachage mem associant à
chaque jeu d’arguments avec lequel elle est appelée la valeur de retour de f .

Il est nécessaire de disposer d’une fonction de hachage h associant à chaque jeu d’arguments
un entier.

Il ne doit y avoir qu’une seule table mem pour f , dont la durée de vie est égale à l’exécution du
programme et rémanente : mem est ainsi une variable statique, locale à f .

393 / 399
Procédé général de mémoïsation d’une fonction
Soit f une fonction à n paramètres.

On appelle f’ la version mémoïsée de f , définie de la manière suivante.

Lors de l’appel à f’(a_1, ..., a_n) :

(1) si mem contient une donnée associée à (a_1, ..., a_n) ,


(a) renvoyer cette valeur ;

(2) sinon,
(a) calculer la valeur de retour de f(a_1, ..., a_n) ;
(b) mémoriser cette valeur dans mem ;
(c) renvoyer cette valeur.

(1) est l’étape de lecture et (2) est l’étape de mémorisation.

394 / 399
Exemple de mémoïsation d’une fonction à un paramètre
On souhaite mémoïser la fonction
1 int fibo(int n) {
2 if (n <= 1) return n;
3 return fibo(n - 1) + fibo(n - 2);
4 }

Cette fonction est paramétrée par un entier et renvoie un entier. La table mem est ainsi un tableau
d’entiers. La valeur de hachage d’un argument n est n .
On l’utilise de la manière suivante :
1. elle est de taille T_MEM où T_MEM est une constante suffisamment grande ;

2. pour tout 0 6 n < T_MEM , la valeur de mem[n] est


I -1 si fibo(n) n’a pas encore été appelée ;
I la valeur renvoyée par fibo(n) si fibo a été appelée au moins une fois avec l’argument
n .

395 / 399
Exemple de mémoïsation d’une fonction à un paramètre

1 int fibo(int n) { 17 /∗ Lecture ∗/


2 /∗ Table ∗/ 18 if (mem[n] != -1)
3 static int mem[T_MEM]; 19 return mem[n];
4 20
5 /∗ Booleen 1 er appel ∗/ 21 /∗ Calcul ∗/
6 static int init = 1; 22 if (n <= 1)
7 23 res = n;
8 int i, res; 24 else
9 25 res = fibo(n - 1)
10 /∗ Init . 1 er appel ∗/ 26 + fibo(n - 2);
11 if (init) { 27
12 for (i=0;i<T_MEM;++i) 28 /∗ Memorisation ∗/
13 mem[i] = -1; 29 mem[n] = res;
14 init = 0; 30
15 } 31 return res;
16 32 }

396 / 399
Mémoïsation de fonctions à plusieurs paramètres

Soit f une fonction à n paramètres.

La table mem devient un tableau de variables d’un type structuré E_f composé
1. d’un champ pour chaque paramètre de f ;
2. d’un champ pour le résultat renvoyé par f appelé avec les arguments spécifiés par les
précédents champs ;
3. d’un champ booléen indiquant si le résultat de f appelée avec les arguments spécifiés par
les précédents champs a bien été calculé.

Il faut de plus disposer d’une fonction de hachage h_f de même signature que f et qui renvoie
un entier positif.
Elle permet d’attribuer à chaque jeu d’arguments une position dans la table mem .

397 / 399
Exemple de mémoïsation d’une fonction à deux paramètres
On souhaite mémoïser la fonction
1 int puiss(int n, int p) {
2 if (p == 0) return 1;
3 return n * puiss(n, p - 1);
4 }

Celle-ci est paramétrée par deux entiers. Le type structuré E_puiss est donc défini par
1 typedef struct {
2 int n; /∗ 1 er parametre ∗/
3 int p; /∗ 2e parametre ∗/
4 int res; /∗ Resultat ∗/
5 int ok; /∗ Indique si le calcul a deja ete fait ∗/
6 } E_puiss;

On utilise la fonction de hachage suivante :


1 int h_puiss(int n, int p) {
2 return n ^ (p << 2);
3 }

398 / 399
Exemple de mémoïsation d’une fonction à deux paramètres

1 int puiss(int n, int p) { 20 if (mem[h].n == n


2 /∗ Table ∗/ 21 && mem[h].p == p)
3 static E_puiss mem[T_MEM]; 22 return mem[h].res;
4 23 }
5 /∗ Booleen 1 er appel ∗/ 24
6 static int init = 1; 25 /∗ Calcul ∗/
7 26 if (p == 0) res = 1;
8 int i, res, h; 27 else
9 28 res = n * puiss(n, p - 1);
10 /∗ Init . 1 er appel ∗/ 29
11 if (init) { 30 /∗ Memorisation ∗/
12 for (i=0;i<T_MEM;++i) 31 mem[h].n = n;
13 mem[i].ok = 0; 32 mem[h].p = p;
14 init = 0; 33 mem[h].res = res;
15 } 34 mem[h].ok = 1;
16 35
17 /∗ Lecture ∗/ 36 return res;
18 h = h_puiss(n, p) % T_MEM; 37 }
19 if (mem[h].ok) {

399 / 399

Vous aimerez peut-être aussi