SDD Et Algo

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

Université Polytechnique de Mongo

Faculté des Sciences Fondamentales

Support de cours / MI-L2

STRUCTURES DE DONNÉES ET
ALGORITHME

Chargé du cours M. BAÏMI BADJOUA


Grade Assistant
<baimibadjoua@yahoo.com>

Année académique 2020-2021


TABLE DES MATIÈRES

Introduction Générale 7

1 Rappels et compléments de C 8
1.1 Les pointeurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.1.1 Créer un pointeur . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.1.2 Envoyer un pointeur à une fonction . . . . . . . . . . . . . . . . . . 11
1.1.3 Une autre façon d’envoyer un pointeur à une fonction . . . . . . . . 12
1.2 Qui a dit : ”Un problème bien ennuyeux” ? . . . . . . . . . . . . . . . . . . 13
1.3 Les tableaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.3.1 Les tableaux dans la mémoire . . . . . . . . . . . . . . . . . . . . . 14
1.3.2 Définir un tableau . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.3.3 Les tableaux à taille dynamique . . . . . . . . . . . . . . . . . . . . 16
1.3.4 Parcourir un tableau . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.3.5 Initialiser un tableau . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.3.6 Une autre façon d’initialiser . . . . . . . . . . . . . . . . . . . . . . 17
1.3.7 Passage de tableau à une fonction . . . . . . . . . . . . . . . . . . . 18
1.4 Les chaînes de caractères . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.4.1 Le type char . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.4.2 Les chaînes de caractères sont les tableaux de char . . . . . . . . . 20
1.4.3 Création et initialisation de la chaîne . . . . . . . . . . . . . . . . . 21
1.4.4 Récupération d’une chaîne via un scanf . . . . . . . . . . . . . . . . 22
1.4.5 Fonctions de manipulation des chaînes . . . . . . . . . . . . . . . . 23
1.5 Les structures et les énumérations . . . . . . . . . . . . . . . . . . . . . . . 24
1.5.1 Définir une structure . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.5.2 Utilisation d’une structure . . . . . . . . . . . . . . . . . . . . . . . 26
1.5.3 Pointeur de structure . . . . . . . . . . . . . . . . . . . . . . . . . . 28
1.5.4 Les énumérations . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
1.6 L’allocation dynamique de mémoire . . . . . . . . . . . . . . . . . . . . . . 33
1.6.1 La taille des variables . . . . . . . . . . . . . . . . . . . . . . . . . 33
1.6.2 Une nouvelle façon de voir la mémoire . . . . . . . . . . . . . . . . 34
1.6.3 Allocation de mémoire dynamique . . . . . . . . . . . . . . . . . . 36

1
Chargé du cours : M. Baïmi Badjoua ©UPM

1.6.4 Tester le pointeur . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37


1.6.5 free : libérer de la mémoire . . . . . . . . . . . . . . . . . . . . . . 37
1.6.6 Exemple concret d’utilisation . . . . . . . . . . . . . . . . . . . . . 38
1.6.7 Allocation dynamique d’un tableau . . . . . . . . . . . . . . . . . . 39

2 Notions d’algorithme 42
2.1 Qu’est-ce qu’un algorithme ? . . . . . . . . . . . . . . . . . . . . . . . . . . 42
2.1.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
2.1.2 Omniprésence des algorithmes . . . . . . . . . . . . . . . . . . . . . 43
2.1.3 Rôle privilégié des algorithmes . . . . . . . . . . . . . . . . . . . . 43
2.1.4 Notion de structure de données . . . . . . . . . . . . . . . . . . . . 44
2.2 Découvrez l’intérêt des algorithmes . . . . . . . . . . . . . . . . . . . . . . 44
2.2.1 C’est logique ! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
2.2.2 Les algorithmes que nous connaissons . . . . . . . . . . . . . . . . . 45
2.2.3 Qu’est-ce qu’un programme ? . . . . . . . . . . . . . . . . . . . . . 46
2.3 Conventions pour écrire un algorithme . . . . . . . . . . . . . . . . . . . . 47
2.4 Types d’instructions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47

3 Complexités d’un algorithme 49


3.1 Algorithme et programme . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
3.2 L’efficacité d’un algorithme . . . . . . . . . . . . . . . . . . . . . . . . . . 51
3.3 Théorie de la complexité . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
3.3.1 Définition et Objectifs . . . . . . . . . . . . . . . . . . . . . . . . . 51
3.3.2 Evaluation de la rapidité d’un algorithme . . . . . . . . . . . . . . 51
3.3.3 Calcul du temps d’exécution . . . . . . . . . . . . . . . . . . . . . . 52

4 Les listes 54
4.1 Représentation d’une liste chaînée . . . . . . . . . . . . . . . . . . . . . . . 54
4.2 Construction d’une liste chaînée . . . . . . . . . . . . . . . . . . . . . . . . 55
4.2.1 Un élément de la liste . . . . . . . . . . . . . . . . . . . . . . . . . 56
4.2.2 La structure de contrôle . . . . . . . . . . . . . . . . . . . . . . . . 56
4.2.3 Le dernier élément de la liste . . . . . . . . . . . . . . . . . . . . . 57
4.3 Les fonctions de gestion de la liste . . . . . . . . . . . . . . . . . . . . . . . 57
4.3.1 Initialiser la liste . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
4.3.2 Ajouter un élément . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
4.3.3 Supprimer un élément . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.3.4 Afficher la liste chaînée . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.4 Travaux Pratiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62

5 Les piles et les files 64


5.1 Les piles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
5.1.1 Fonctionnement des piles . . . . . . . . . . . . . . . . . . . . . . . 65
5.1.2 Création d’un système de pile . . . . . . . . . . . . . . . . . . . . . 67
5.1.3 Empilage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
5.1.4 Dépilage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68

TABLE DES MATIÈRES 2


Chargé du cours : M. Baïmi Badjoua ©UPM

5.1.5 Affichage de la pile . . . . . . . . . . . . . . . . . . . . . . . . . . . 69


5.2 Les files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
5.2.1 Fonctionnement des files . . . . . . . . . . . . . . . . . . . . . . . . 71
5.2.2 Création d’un système de file . . . . . . . . . . . . . . . . . . . . . 72
5.2.3 Enfilage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
5.2.4 Défilage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
5.2.5 Travaux Pratiques . . . . . . . . . . . . . . . . . . . . . . . . . . . 74

6 Les arbres binaires 76

7 Étude de quelques algorithmes 77

Conclusion Générale 78

Références Bibliographiques 79

TABLE DES MATIÈRES 3


TABLE DES FIGURES

1.1 Détails variable pointeur dans la mémoire . . . . . . . . . . . . . . . . . . 10


1.2 Représentation d’un tableau en mémoire . . . . . . . . . . . . . . . . . . . 14
1.3 Représentation de chaîne de caractères en mémoire . . . . . . . . . . . . . 20
1.4 Représentation correcte de chaîne de caractères en mémoire . . . . . . . . 21
1.5 Nouvelle représentation d’une variable en mémoire . . . . . . . . . . . . . 35
1.6 Nouvelle représentation d’une variable de type char en mémoire . . . . . . 35

2.1 Exemples d’algorithmes . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47


2.2 Quelques langages de programmation . . . . . . . . . . . . . . . . . . . . . 48

3.1 Mise en oeuvre du principe d’un programme . . . . . . . . . . . . . . . . . 50

4.1 Représentation d’une liste chaînée . . . . . . . . . . . . . . . . . . . . . . . 54


4.2 Limites d’un tableau classique en C . . . . . . . . . . . . . . . . . . . . . . 55
4.3 Représentation simple d’une liste chaînée . . . . . . . . . . . . . . . . . . . 55
4.4 Représentation complete d’une liste chaînée . . . . . . . . . . . . . . . . . 55
4.5 Représentation exacte de notre liste chaînée . . . . . . . . . . . . . . . . . 57
4.6 Schéma complet de notre liste chaînée . . . . . . . . . . . . . . . . . . . . 57
4.7 Liste chaînée avec un seul élément . . . . . . . . . . . . . . . . . . . . . . . 59
4.8 Ajout d’un nouvel élément dans la liste chaînée . . . . . . . . . . . . . . . 59
4.9 Notre liste chaînée, après ajout d’un nouvel élément au debut . . . . . . . 60
4.10 Notre liste chaînée, après suppression du premier élément . . . . . . . . . . 61
4.11 Résultat après exécution de notre liste chaînée . . . . . . . . . . . . . . . . 62

5.1 Une illustration des piles . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65


5.2 Empilage d’élément dans une pile . . . . . . . . . . . . . . . . . . . . . . . 65
5.3 Dépilage d’élément dans une pile . . . . . . . . . . . . . . . . . . . . . . . 65
5.4 Représentation des éléménts d’une pile . . . . . . . . . . . . . . . . . . . . 66
5.5 Empilage et dépilage d’une pile . . . . . . . . . . . . . . . . . . . . . . . . 67
5.6 Résultat exécution exemple d’une pile . . . . . . . . . . . . . . . . . . . . 70
5.7 Représentation simple d’une file . . . . . . . . . . . . . . . . . . . . . . . . 71
5.8 Représentation complete d’une file . . . . . . . . . . . . . . . . . . . . . . 71

4
Chargé du cours : M. Baïmi Badjoua ©UPM

5.9 Résultats d’exécution d’une file . . . . . . . . . . . . . . . . . . . . . . . . 74

TABLE DES FIGURES 5


LISTE DES TABLEAUX

6
INTRODUCTION GÉNÉRALE

Avec les éléments de programmation que nous connaissons, nous sommes capables de
programmer tout ce qui est programmable. Cependant l’expérience des programmeurs a
conduit à introduire d’autres notions, théoriquement non indispensables mais facilitant
nettement la pratique de la programmation. Ceci est le cas des sous-programmes ; c’est
le cas également des structures de données qui sont avec les algorithmes, l’objet principal
de ce cours.

Jusqu’à maintenant on attribuait une variable simple par donnée à un instant précis. Il
arrive que les données soient nombreuses et aient un certain lien entre elles, ce concept
de lien étant considéré de façon intuitive et suffisamment vague pour l’instant. Ce phé-
nomène conduit à considérer ce que l’on appelle une structure de données en informatique.

Les structures de données dont le besoin s’est fait le plus ressentir au cours de l’histoire
(assez courte, certes) de la programmation apparaissent explicitement dans les langages
informatiques. Nous allons les étudier l’une après l’autre en montrant leur intérêt. Nous
réfléchirons plus tard à la notion générale de structure de données, en considérant en
particulier celles qui ne sont pas implémentées dans les langages de programmation.

Ce support de cours est destiné aux étudiants de deuxième année de Mathèmatiques-


Informatique de la Faculté des Sciences Fondamentales de l’Université Polytechnique de
Mongo. Afin de bien comprendre les concepts qui seront exposés dans ce cours, il est
intéressant d’avoir assimilé les prérequis tels que :

— Algorithmique et Programmation en Turba Pascal ;


— Programmation en langage C.

7
CHAPITRE 1
RAPPELS ET COMPLÉMENTS DE C

Introduction
Dans ce chapitre nous allons exposés les concepts clés, necéssaires à la comprehension des
structures des données. Attention, il ne s’agit pas ici de faire un cours sur le langage C,
c’est juste une mise à jour. Soyez donc très attentifs.

1.1 Les pointeurs


Jusqu’ici, nous avons uniquement créé des variables faites pour contenir des nombres.
Maintenant, nous allons apprendre à créer des variables faites pour contenir des adresses :
ce sont justement ce qu’on appelle des pointeurs.

1.1.1 Créer un pointeur


Pour créer une variable de type pointeur, on doit rajouter le symbole * devant le nom de
la variable.

1 i n t ∗ monPointeur ;

Comme je vous l’ai appris, il est important d’initialiser dès le début ses variables, en leur
donnant la valeur 0 par exemple. C’est encore plus important de le faire avec les pointeurs !
Pour initialiser un pointeur, c’est-à-dire lui donner une valeur par défaut, on n’utilise
généralement pas le nombre 0 mais le mot-clé NULL (veillez à l’écrire en majuscules) :

1 i n t ∗ monPointeur = NULL ;

Là, vous avez un pointeur initialisé à NULL. Comme ça, vous saurez dans la suite de
votre programme que votre pointeur ne contient aucune adresse.

8
Chargé du cours : M. Baïmi Badjoua ©UPM

Que se passe-t-il ? Ce code va réserver une case en mémoire comme si vous aviez créé une
variable normale. Cependant, et c’est ce qui change, la valeur du pointeur est faite pour
contenir une adresse. L’adresse… d’une autre variable.

Pourquoi pas l’adresse de la variable age ? Vous savez maintenant comment indiquer
l’adresse d’une variable au lieu de sa valeur (en utilisant le symbole &), alors allons-y !
Ça nous donne :

1 i n t age = 10 ;
2 i n t ∗ p o i n t e u r S u r A g e = &age ;

La première ligne signifie : « Créer une variable de type int dont la valeur vaut 10 ». La
seconde ligne signifie : « Créer une variable de type pointeur ici int* dont la valeur vaut
l’adresse de la variable age ».

La seconde ligne fait donc deux choses à la fois. Si vous le souhaitez, pour ne pas tout
mélanger, sachez qu’on peut la découper en deux temps :

1 i n t age = 10 ;
2 i n t ∗ p o i n t e u r S u r A g e ; // 1 ) s i g n i f i e ” Je c r é e un p o i n t e u r ”
3 p o i n t e u r S u r A g e = &age ; // 2 ) s i g n i f i e ” p o i n t e u r S u r A g e c o n t i e n t l ’ a d r e s s e
de l a v a r i a b l e age ”

Vous avez remarqué qu’il n’y a pas de type « pointeur » comme il y a un type int et un
type double. On n’écrit donc pas :

1 pointeur pointeurSurAge ;

Au lieu de ça, on utilise le symbole *, mais on continue à écrire int. Qu’est-ce que ça signi-
fie ? En fait, on doit indiquer quel est le type de la variable dont le pointeur va contenir
l’adresse. Comme notre pointeur pointeurSurAge va contenir l’adresse de la variable age
(qui est de type int), alors mon pointeur doit être de type int* ! Si ma variable age avait
été de type double, alors j’aurais dû écrire double *monPointeur.

Vocabulaire : on dit que le pointeur pointeurSurAge pointe sur la variable age.

La figure suivante résume ce qu’il s’est passé dans la mémoire.

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 9


Chargé du cours : M. Baïmi Badjoua ©UPM

Figure 1.1 – Détails variable pointeur dans la mémoire

Dans ce schéma, la variable age a été placée à l’adresse 177450 (vous voyez d’ailleurs que
sa valeur est 10), et le pointeur pointeurSurAge a été placé à l’adresse 3 (c’est tout à fait
le fruit du hasard).

Lorsque mon pointeur est créé, le système d’exploitation réserve une case en mémoire
comme il l’a fait pour age. La différence ici, c’est que la valeur de pointeurSurAge est un
peu particulière. Regardez bien le schéma : c’est l’adresse de la variable age !

Ceci, chers lecteurs, est le secret absolu de tout programme écrit en langage C. On y est,
nous venons de rentrer dans le monde merveilleux des pointeurs !

Ça ne transforme pas encore votre ordinateur en machine à café, certes. Seulement main-
tenant, on a un pointeurSurAge qui contient l’adresse de la variable age. Essayons de voir
ce que contient le pointeur à l’aide d’un printf :

1 i n t age = 10 ;
2 i n t ∗ p o i n t e u r S u r A g e = &age ;
3 p r i n t f ( ”%d” , p o i n t e u r S u r A g e ) ;

En fait, cela n’est pas très étonnant. On demande la valeur de pointeurSurAge, et sa


valeur c’est l’adresse de la variable age (177450).

Comment faire pour demander à avoir la valeur de la variable se trouvant à l’adresse


indiquée dans pointeurSurAge ? Il faut placer le symbole * devant le nom du pointeur.

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 10


Chargé du cours : M. Baïmi Badjoua ©UPM

1 i n t age = 10 ;
2 i n t ∗ p o i n t e u r S u r A g e = &age ;
3 p r i n t f ( ”%d” , ∗ p o i n t e u r S u r A g e ) ;

À retenir absolument
Voici ce qu’il faut avoir compris et ce qu’il faut retenir pour la suite de ce chapitre :
— sur une variable, comme la variable age :
— age signifie : « Je veux la valeur de la variable age »,
— &age signifie : « Je veux l’adresse à laquelle se trouve la variable age » ;
— sur un pointeur, comme pointeurSurAge :
— pointeurSurAge signifie : « Je veux la valeur de pointeurSurAge » (cette valeur
étant une adresse),
— *pointeurSurAge signifie : « Je veux la valeur de la variable qui se trouve à
l’adresse contenue dans pointeurSurAge ».
Contentez-vous de bien retenir ces quatre points. Faites des tests et vérifiez que ça marche.

1.1.2 Envoyer un pointeur à une fonction


Le gros intérêt des pointeurs (mais ce n’est pas le seul) est qu’on peut les envoyer à des
fonctions pour qu’ils modifient directement une variable en mémoire, et non une copie
comme on l’a vu.

Comment ça marche ? Il y a en fait plusieurs façons de faire. Voici un premier exemple :

1 v o i d t r i p l e P o i n t e u r ( i n t ∗ pointeurSurNombre ) ;
2
3 i n t main ( i n t argc , c h a r ∗ argv [ ] )
4 {
5 i n t nombre = 5 ;
6 t r i p l e P o i n t e u r (&nombre ) ; // On e n v o i e l ’ a d r e s s e de nombre à l a
fonction
7 p r i n t f (”%d ” , nombre ) ; // On a f f i c h e l a v a r i a b l e nombre . La f o n c t i o n a
d i r e c t e m e n t m o d i f i é l a v a l e u r de l a v a r i a b l e c a r e l l e c o n n a i s s a i t
son a d r e s s e
8 return 0 ;
9 }
10
11 v o i d t r i p l e P o i n t e u r ( i n t ∗ pointeurSurNombre )
12 {
13 ∗ pointeurSurNombre ∗= 3 ; // On m u l t i p l i e par 3 l a v a l e u r de nombre
14 }

La fonction triplePointeur prend un paramètre de type int* (c’est-à-dire un pointeur sur


int). Voici ce qu’il se passe dans l’ordre, en partant du début du main :

1. une variable nombre est créée dans le main. On lui affecte la valeur 5. Ça, vous
connaissez ;
2. on appelle la fonction triplePointeur. On lui envoie en paramètre l’adresse de notre
variable nombre ;

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 11


Chargé du cours : M. Baïmi Badjoua ©UPM

3. la fonction triplePointeur reçoit cette adresse dans pointeurSurNombre. À l’inté-


rieur de la fonction triplePointeur, on a donc un pointeur pointeurSurNombre qui
contient l’adresse de la variable nombre ;
4. maintenant qu’on a un pointeur sur nombre, on peut modifier directement la va-
riable nombre en mémoire ! Il suffit d’utiliser *pointeurSurNombre pour désigner
la variable nombre ! Pour l’exemple, on fait un simple test : on multiplie la variable
nombre par 3 ;
5. de retour dans la fonction main, notre nombre vaut maintenant 15 car la fonction
triplePointeur a modifié directement la valeur de nombre.
Bien sûr, j’aurais pu faire un simple return comme on a appris à le faire dans le chapitre
sur les fonctions. Mais l’intérêt, là, c’est que de cette manière, en utilisant des pointeurs,
on peut modifier la valeur de plusieurs variables en mémoire (on peut donc « renvoyer
plusieurs valeurs »). Nous ne sommes plus limités à une seule valeur !

1.1.3 Une autre façon d’envoyer un pointeur à une fonction


Dans le code source qu’on vient de voir, il n’y avait pas de pointeur dans la fonction main.
Juste une variable nombre. Le seul pointeur qu’il y avait vraiment était dans la fonction
triplePointeur (de type int*).

Il faut absolument que vous sachiez qu’il y a une autre façon d’écrire le code précédent,
en ajoutant un pointeur dans la fonction main :

1 v o i d t r i p l e P o i n t e u r ( i n t ∗ pointeurSurNombre ) ;
2
3 i n t main ( i n t argc , c h a r ∗ argv [ ] )
4 {
5 i n t nombre = 5 ;
6 i n t ∗ p o i n t e u r = &nombre ; // p o i n t e u r prend l ’ a d r e s s e de nombre
7 t r i p l e P o i n t e u r ( p o i n t e u r ) ; // On e n v o i e p o i n t e u r ( l ’ a d r e s s e de nombre )
à la fonction
8 p r i n t f ( ”%d” , ∗ p o i n t e u r ) ; // On a f f i c h e l a v a l e u r de nombre avec ∗
pointeur
9 return 0 ;
10 }
11
12 v o i d t r i p l e P o i n t e u r ( i n t ∗ pointeurSurNombre )
13 {
14 ∗ pointeurSurNombre ∗= 3 ; // On m u l t i p l i e par 3 l a v a l e u r de nombre
15 }

Ce qui compte, c’est d’envoyer l’adresse de la variable nombre à la fonction. Or, pointeur
vaut l’adresse de la variable nombre, donc c’est bon de ce côté ! On le fait seulement d’une
manière différente en créant un pointeur dans la fonction main.

Dans le printf(et c’est juste pour l’exercice), j’affiche le contenu de la variable nombre en
tapant *pointeur. Notez qu’à la place, j’aurais pu écrire nombre : le résultat aurait été
identique car *pointeur et nombre désignent la même chose dans la mémoire.

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 12


Chargé du cours : M. Baïmi Badjoua ©UPM

1.2 Qui a dit : ”Un problème bien ennuyeux” ?


Voici une solution pour resoudre notre problème :

1 v o i d decoupeMinutes ( i n t ∗ p o i n t e u r H e u r e s , i n t ∗ p o i n t e u r M i n u t e s ) ;
2

3 i n t main ( i n t argc , c h a r ∗ argv [ ] )


4 {
5 i n t h e u r e s = 0 , minutes = 90 ;
6 // On e n v o i e l ’ a d r e s s e de h e u r e s e t minutes
7 decoupeMinutes (& h e u r e s , &minutes ) ;
8 // C e t t e f o i s , l e s v a l e u r s ont é t é m o d i f i é e s !
9 p r i n t f (”%d h e u r e s e t %d minutes ” , h e u r e s , minutes ) ;
10 return 0 ;
11 }
12
13 v o i d decoupeMinutes ( i n t ∗ p o i n t e u r H e u r e s , i n t ∗ p o i n t e u r M i n u t e s )
14 {
15 /∗ A t t e n t i o n à ne pas o u b l i e r de m e t t r e une é t o i l e devant l e nom
16 d e s p o i n t e u r s ! Comme ç a , vous pouvez m o d i f i e r l a v a l e u r d e s
variables ,
17 e t non l e u r a d r e s s e ! Vous ne v o u d r i e z pas d i v i s e r d e s a d r e s s e s ,
18 n ’ e s t −c e pas ? ; o ) ∗/
19 ∗ p o i n t e u r H e u r e s = ∗ p o i n t e u r M i n u t e s / 60 ;
20 ∗ p o i n t e u r M i n u t e s = ∗ p o i n t e u r M i n u t e s % 60 ;
21 }

Rien ne devrait vous surprendre dans ce code source. Toutefois, comme on n’est jamais
trop prudent, je vais râbacher une fois de plus ce qui se passe dans ce code afin d’être
certain que tout le monde me suive bien. C’est un chapitre important, vous devez faire
beaucoup d’efforts pour comprendre : je peux donc bien en faire moi aussi pour vous !

1. Les variables heures et minutes sont créées dans le main.


2. On envoie à la fonction decoupeMinutes l’adresse de heures et minutes.
3. La fonction decoupeMinutes récupère ces adresses dans des pointeurs appelés poin-
teurHeures et pointeurMinutes. Notez que là encore, le nom importe peu. J’aurais
pu les appeler h et m, ou même encore heures et minutes. Je ne l’ai pas fait car je
ne veux pas que vous risquiez de confondre avec les variables heures et minutes du
main, qui ne sont pas les mêmes.
4. La fonction decoupeMinutes modifie directement les valeurs des variables heures
et minutes en mémoire car elle possède leurs adresses dans des pointeurs. La seule
contrainte, un peu gênante je dois le reconnaître, c’est qu’il faut impérativement
mettre une étoile devant le nom des pointeurs si on veut modifier la valeur de
heures et de minutes. Si on n’avait pas fait ça, on aurait modifié l’adresse contenue
dans les pointeurs, ce qui n’aurait servi… à rien.
Résumé sur les pointeurs :
— Chaque variable est stockée à une adresse précise en mémoire.
— Les pointeurs sont semblables aux variables, à ceci près qu’au lieu de stocker un
nombre ils stockent l’adresse à laquelle se trouve une variable en mémoire.

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 13


Chargé du cours : M. Baïmi Badjoua ©UPM

— Si on place un symbole & devant un nom de variable, on obtient son adresse au


lieu de sa valeur (ex. : &age).
— Si on place un symbole * devant un nom de pointeur, on obtient la valeur de la
variable stockée à l’adresse indiquée par le pointeur.
— Les pointeurs constituent une notion essentielle du langage C, mais néanmoins un
peu complexe au début. Il faut prendre le temps de bien comprendre comment ils
fonctionnent car beaucoup d’autres notions sont basées dessus.

1.3 Les tableaux


« Les tableaux sont une suite de variables de même type, situées dans un espace contigu
en mémoire. »

1.3.1 Les tableaux dans la mémoire


Un tableau a une dimension bien précise. Il peut occuper 2, 3, 10, 150, 2 500 cases, c’est
vous qui décidez. La figure suivante est un schéma d’un tableau de 4 cases en mémoire
qui commence à l’adresse 1600.

Figure 1.2 – Représentation d’un tableau en mémoire

Lorsque vous demandez à créer un tableau de 4 cases en mémoire, votre programme


demande à l’OS la permission d’utiliser 4 cases en mémoire. Ces 4 cases doivent être
contiguës, c’est-à-dire les unes à la suite des autres. Comme vous le voyez, les adresses se
suivent : 1600, 1601, 1602, 1603. Il n’y a pas de « trou » au milieu.

Enfin, chaque case du tableau contient un nombre du même type. Si le tableau est de
type int, alors chaque case du tableau contiendra un int. On ne peut pas faire de tableau
contenant à la fois des int et des double par exemple.

En résumé, voici ce qu’il faut retenir sur les tableaux :


— Lorsqu’un tableau est créé, il prend un espace contigu en mémoire : les cases sont
les unes à la suite des autres.

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 14


Chargé du cours : M. Baïmi Badjoua ©UPM

— Toutes les cases d’un tableau sont du même type. Ainsi, un tableau de int contien-
dra uniquement des int, et pas autre chose.

1.3.2 Définir un tableau


Pour commencer, nous allons voir comment définir un tableau de 4 int :

1 int tableau [ 4 ] ;

Voilà, c’est tout. Il suffit donc de rajouter entre crochets le nombre de cases que vous
voulez mettre dans votre tableau. Il n’y a pas de limite (à part peut-être la taille de votre
mémoire, quand même).

Maintenant, comment accéder à chaque case du tableau ? C’est simple, il faut écrire :

1 t a b l e a u [ numeroDeLaCase ]

Attention : un tableau commence à l’indice numéro 0 ! Notre tableau de 4 int a donc les
indices 0, 1, 2 et 3. Il n’y a pas d’indice 4 dans un tableau de 4 cases ! C’est une source
d’erreurs très courantes, souvenez-vous-en.

Si je veux mettre dans mon tableau les mêmes valeurs que celles indiquées sur la figure
précedente, je devrai donc écrire :

1 int tableau [ 4 ] ;
2 t a b l e a u [ 0 ] = 10 ;
3 t a b l e a u [ 1 ] = 23 ;
4 t a b l e a u [ 2 ] = 505 ;
5 tableau [ 3 ] = 8 ;

Quel est le rapport entre pointeur et tableau ? En fait, si vous écrivez juste tableau, vous
obtenez un pointeur. C’est un pointeur sur la première case du tableau. Faites le test :

1 int tableau [ 4 ] ;
2 p r i n t f ( ”%d” , t a b l e a u ) ;

En revanche, si vous indiquez l’indice de la case du tableau entre crochets, vous obtenez
la valeur :

1 int tableau [ 4 ] ;
2 p r i n t f ( ”%d” , t a b l e a u [ 0 ] ) ;

De même pour les autres indices. Notez que comme tableau est un pointeur, on peut
utiliser le symbole * pour connaître la première valeur :

1 int tableau [ 4 ] ;
2 p r i n t f ( ”%d” , ∗ t a b l e a u ) ;

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 15


Chargé du cours : M. Baïmi Badjoua ©UPM

Il est aussi possible d’obtenir la valeur de la seconde case avec *(tableau + 1)(adresse de
tableau + 1).

Les deux lignes suivantes sont donc identiques :

1 t a b l e a u [ 1 ] // Renvoie l a v a l e u r de l a s e c o n d e c a s e ( l a premi è r e c a s e é
tant 0)
2 ∗ ( t a b l e a u + 1 ) // I d e n t i q u e : r e n v o i e l a v a l e u r c o n t e n u e dans l a s e c o n d e
case

En clair, quand vous écrivez tableau[0], vous demandez la valeur qui se trouve à l’adresse
tableau + 0 case (c’est-à-dire 1600).
Si vous écrivez tableau[1], vous demandez la valeur se trouvant à l’adresse tableau + 1
case (c’est-à-dire 1601).
Et ainsi de suite pour les autres valeurs.

1.3.3 Les tableaux à taille dynamique


Le langage C existe en plusieurs versions. Une version récente, appelée le C99, autorise la
création de tableaux à taille dynamique, c’est-à-dire de tableaux dont la taille est définie
par une variable :

1 int t a i l l e = 5 ;
2 int tableau [ t a i l l e ] ;

Or cela n’est pas forcément reconnu par tous les compilateurs, certains planteront sur la
seconde ligne. Le langage C que je vous enseigne depuis le début (appelé le C89) n’autorise
pas ce genre de choses. Nous considèrerons donc que faire cela est interdit.

Nous allons nous mettre d’accord sur ceci : vous n’avez pas le droit d’utiliser une variable
entre crochets pour la définition de la taille du tableau, même si cette variable est une
constante ! Le tableau doit avoir une dimension fixe, c’est-à-dire que vous devez écrire noir
sur blanc le nombre correspondant à la taille du tableau :

1 int tableau [ 5 ] ;

Mais alors… il est interdit de créer un tableau dont la taille dépend d’une variable ?

Non, rassurez-vous : c’est possible, même en C89. Mais pour faire cela, nous utiliserons
une autre technique (plus sûre et qui marche partout) appelée l’allocation dynamique.
Nous verrons cela bien plus loin dans ce chapitre.

1.3.4 Parcourir un tableau


Voici un exemple de parcours d’un tableau :

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 16


Chargé du cours : M. Baïmi Badjoua ©UPM

1 i n t main ( i n t argc , c h a r ∗ argv [ ] )


2 {
3 int tableau [ 4 ] , i = 0 ;
4 t a b l e a u [ 0 ] = 10 ;
5 t a b l e a u [ 1 ] = 23 ;
6 t a b l e a u [ 2 ] = 505 ;
7 tableau [ 3 ] = 8 ;
8 f o r ( i = 0 ; i < 4 ; i ++)
9 {
10 p r i n t f ( ”%d\n” , t a b l e a u [ i ] ) ;
11 }
12 return 0 ;
13 }

1.3.5 Initialiser un tableau


Bon, parcourir le tableau pour mettre 0 à chaque case, c’est de votre niveau maintenant :

1 i n t main ( i n t argc , c h a r ∗ argv [ ] )


2 {
3 int tableau [ 4 ] , i = 0 ;
4 // I n i t i a l i s a t i o n du t a b l e a u
5 f o r ( i = 0 ; i < 4 ; i ++)
6 {
7 tableau [ i ] = 0 ;
8 }
9 // A f f i c h a g e de s e s v a l e u r s pour v é r i f i e r
10 f o r ( i = 0 ; i < 4 ; i ++)
11 {
12 p r i n t f ( ”%d\n” , t a b l e a u [ i ] ) ;
13 }
14 return 0 ;
15 }

1.3.6 Une autre façon d’initialiser


Il faut savoir qu’il existe une autre façon d’initialiser un tableau un peu plus automatisée
en C.

Elle consiste à écrire tableau[4] = valeur1, valeur2, valeur3, valeur4. En clair, vous placez
les valeurs une à une entre accolades, séparées par des virgules :

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 17


Chargé du cours : M. Baïmi Badjoua ©UPM

1 i n t main ( i n t argc , c h a r ∗ argv [ ] )


2 {
3 i n t tableau [ 4 ] = {0 , 0 , 0 , 0} , i = 0 ;
4 f o r ( i = 0 ; i < 4 ; i ++)
5 {
6 p r i n t f ( ”%d\n” , t a b l e a u [ i ] ) ;
7 }
8 return 0 ;
9 }

1.3.7 Passage de tableau à une fonction


Comme je vous l’ai dit, tableau peut être considéré comme un pointeur. On peut donc
l’envoyer à la fonction comme on l’aurait fait avec un vulgaire pointeur :

1 // P r o t o t y p e de l a f o n c t i o n d ’ a f f i c h a g e
2 void a f f i c h e ( i n t ∗ tableau , i n t t a i l l e T a b l e a u ) ;
3
4 i n t main ( i n t argc , c h a r ∗ argv [ ] )
5 {
6 i n t t a b l e a u [ 4 ] = { 1 0 , 1 5 , 3} ;
7 // On a f f i c h e l e contenu du t a b l e a u
8 a f f i c h e ( tableau , 4) ;
9 return 0 ;
10 }
11
12 void a f f i c h e ( i n t ∗ tableau , i n t t a i l l e T a b l e a u )
13 {
14 int i ;
15 f o r ( i = 0 ; i < t a i l l e T a b l e a u ; i ++)
16 {
17 p r i n t f (”%d\n ” , t a b l e a u [ i ] ) ;
18 }
19 }

Résumé sur les tableaux

— Les tableaux sont des ensembles de variables du même type stockées côte à côte
en mémoire.
— La taille d’un tableau doit être déterminée avant la compilation, elle ne peut pas
dépendre d’une variable.
— Chaque case d’un tableau de type int contient une variable de type int.
— Les cases sont numérotées via des indices commençant à 0 : tableau[0], tableau[1],
tableau[2], etc.

1.4 Les chaînes de caractères


Une « chaîne de caractères », c’est un nom programmatiquement correct pour désigner…
du texte, tout simplement ! Une chaîne de caractères, c’est donc du texte que l’on peut

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 18


Chargé du cours : M. Baïmi Badjoua ©UPM

retenir sous forme de variable en mémoire. On pourrait ainsi stocker le nom de l’utilisateur.

Comme nous l’avons dit plus tôt, notre ordinateur ne peut retenir que des nombres. Les
lettres sont exclues. Comment diable les programmeurs font-ils pour manipuler du texte,
alors ? Eh bien ils sont malins, vous allez voir !

1.4.1 Le type char


Le type char est en fait prévu pour stocker… une lettre ! Attention, j’ai bien dit : UNE
lettre. Testons :

1 i n t main ( i n t argc , c h a r ∗ argv [ ] )


2 {
3 c h a r l e t t r e = ’A ’ ;
4 p r i n t f ( ”%d\n” , l e t t r e ) ;
5 return 0 ;
6 }

Afficher un caractère
La fonction printf, qui n’a décidemment pas fini de nous étonner, peut aussi afficher un
caractère. Pour cela, on doit utiliser le symbole %c (c comme caractère) :

1 i n t main ( i n t argc , c h a r ∗ argv [ ] )


2 {
3 c h a r l e t t r e = ’A ’ ;
4 p r i n t f ( ”%c \n” , l e t t r e ) ;
5 return 0 ;
6 }

On peut aussi demander à l’utilisateur d’entrer une lettre en utilisant le %c dans un scanf :

1 i n t main ( i n t argc , c h a r ∗ argv [ ] )


2 {
3 char l e t t r e = 0 ;
4 s c a n f ( ”%c ” , &l e t t r e ) ;
5 p r i n t f ( ”%c \n” , l e t t r e ) ;
6 return 0 ;
7 }

Voici à peu près tout ce qu’il faut savoir sur le type char :
— le type char permet de stocker des nombres allant de -128 à 127, unsigned char des
nombres de 0 à 255 ;
— il y a une table que votre ordinateur utilise pour convertir les lettres en nombres
et inversement, la table ASCII ;
— on peut donc utiliser le type char pour stocker UNE lettre ;
— ’A’ est remplacé à la compilation par la valeur correspondante (65 en l’occurrence).
On utilise donc les apostrophes pour obtenir la valeur d’une lettre.

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 19


Chargé du cours : M. Baïmi Badjoua ©UPM

1.4.2 Les chaînes de caractères sont les tableaux de char


Comme on dit, tout est dans le titre. En effet : une chaîne de caractères n’est rien d’autre
qu’un tableau de type char. Un bête tableau de rien du tout.

Si on crée un tableau :

1 char chaine [ 5 ] ;

et qu’on met dans chaine[0] la lettre ’S’, dans chaine[1] la lettre ’a’… on peut ainsi former
une chaîne de caractères, c’est-à-dire du texte.

La figure suivante vous donne une idée de la façon dont la chaîne est stockée en mémoire
(attention : je vous préviens de suite, c’est un peu plus compliqué que ça en réalité, je
vous explique après pourquoi).

Figure 1.3 – Représentation de chaîne de caractères en mémoire

Comme on peut le voir, c’est un tableau qui prend 5 cases en mémoire pour représenter
le mot « Salut ». Pour la valeur, j’ai volontairement écrit sur le schéma les lettres entre
apostrophes pour indiquer que c’est un nombre qui est stocké, et non une lettre. En réa-
lité, dans la mémoire, ce sont bel et bien les nombres correspondant à ces lettres qui sont
stockés.

Toutefois, une chaîne de caractères ne contient pas que des lettres ! Le schéma de la figure
précedente est en fait incomplet. Une chaîne de caractère doit impérativement contenir un
caractère spécial à la fin de la chaîne, appelé « caractère de fin de chaîne ». Ce caractère

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 20


Chargé du cours : M. Baïmi Badjoua ©UPM

s’écrit ’\0’.

La figure suivante est le schéma correct de la représentation de la chaîne de caractères «


Salut » en mémoire.

Figure 1.4 – Représentation correcte de chaîne de caractères en mémoire

Comme vous le voyez, la chaîne prend 6 caractères et non pas 5, il va falloir s’y faire. La
chaîne se termine par ’\0’, le caractère de fin de chaîne qui permet d’indiquer à l’ordina-
teur que la chaîne se termine là.

Voyez le caractère ’\0’ comme un avantage. Grâce à lui, vous n’aurez pas à retenir la taille
de votre tableau car il indique que le tableau s’arrête à cet endroit. Vous pourrez passer
votre tableau de char à une fonction sans avoir à ajouter à côté une variable indiquant la
taille du tableau.

Cela n’est valable que pour les chaînes de caractères (c’est-à-dire le type char*, qu’on
peut aussi écrire char[]). Pour les autres types de tableaux, vous êtes toujours obligés de
retenir la taille du tableau quelque part.

1.4.3 Création et initialisation de la chaîne


Si on veut initialiser notre tableau chaine avec le texte « Salut », on peut utiliser la mé-
thode manuelle mais peu efficace :

1 c h a r c h a i n e [ 6 ] ; // Tableau de 6 c h a r pour s t o c k e r S−a−l −u−t + l e \0


2 chaine [ 0 ] = ’S ’ ;
3 chaine [ 1 ] = ’ a ’ ;

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 21


Chargé du cours : M. Baïmi Badjoua ©UPM

4 chaine [2] = ’l ’ ;
5 chaine [3] = ’u ’ ;
6 chaine [4] = ’t ’ ;
7 chaine [5] = ’ \0 ’ ;

Voici le code complet qui crée une chaîne « Salut » en mémoire et qui l’affiche :

1 #i n c l u d e <s t d i o . h>
2 #i n c l u d e < s t d l i b . h>
3

4 i n t main ( i n t argc , c h a r ∗ argv [ ] )


5 {
6 c h a r c h a i n e [ 6 ] ; // Tableau de 6 c h a r pour s t o c k e r S−a−l −u−t + l e \0
7 // I n i t i a l i s a t i o n de l a cha î ne ( on é c r i t l e s c a r a c t è r e s un à un en mé
moire )
8 chaine [ 0 ] = ’S ’ ;
9 chaine [ 1 ] = ’ a ’ ;
10 chaine [ 2 ] = ’ l ’ ;
11 chaine [ 3 ] = ’u ’ ;
12 chaine [ 4 ] = ’ t ’ ;
13 c h a i n e [ 5 ] = ’ \0 ’ ;
14 // A f f i c h a g e de l a cha î ne g r â c e au %s du p r i n t f
15 p r i n t f ( ”%s ” , c h a i n e ) ;
16 return 0 ;
17 }

Pour initialiser une chaîne, il existe heureusement une méthode plus simple :

1 i n t main ( i n t argc , c h a r ∗ argv [ ] )


2 {
3 c h a r c h a i n e [ ] = ” S a l u t ” ; // La t a i l l e du t a b l e a u c h a i n e e s t
automatiquement c a l c u l é e
4 p r i n t f ( ”%s ” , c h a i n e ) ;
5 return 0 ;
6 }

1.4.4 Récupération d’une chaîne via un scanf


Ci-après un code qui montre comment récuperer une chaîne de caractères :

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 22


Chargé du cours : M. Baïmi Badjoua ©UPM

1 i n t main ( i n t argc , c h a r ∗ argv [ ] )


2 {
3 c h a r prenom [ 1 0 0 ] ;
4 p r i n t f ( ”Comment t ’ a p p e l l e s −tu p e t i t Zé r o ? ” ) ;
5 s c a n f ( ”%s ” , prenom ) ;
6 p r i n t f ( ” S a l u t %s , j e s u i s heureux de t e r e n c o n t r e r ! ” , prenom ) ;
7 return 0 ;
8 }

1.4.5 Fonctions de manipulation des chaînes


Pensez à inclure string.h dans l’entête de votre fichier.

1 #i n c l u d e <s t r i n g . h>

— La fonction strlen pour avoir la taille de la chaîne :

1 i n t main ( i n t argc , c h a r ∗ argv [ ] )


2 {
3 char chaine [ ] = ” Salut ” ;
4 i n t longueurChaine = 0 ;
5 // On r é cup è r e l a l o n g u e u r de l a cha î ne dans l o n g u e u r C h a i n e
6 longueurChaine = s t r l e n ( chaine ) ;
7 // On a f f i c h e l a l o n g u e u r de l a cha î ne
8 p r i n t f ( ”La c h a i n e %s f a i t %d c a r a c t e r e s de l o n g ” , c h a i n e ,
longueurChaine ) ;
9 return 0 ;
10 }
11

— La fonction strcpy pour copier une chaîne dans une autre :

1 i n t main ( i n t argc , c h a r ∗ argv [ ] )


2 {
3 /∗ On c r é e une cha î ne ” c h a i n e ” q u i c o n t i e n t un peu de t e x t e
4 e t une c o p i e ( v i d e ) de t a i l l e 100 pour ê t r e s û r d ’ a v o i r l a
place
5 pour l a c o p i e ∗/
6 c h a r c h a i n e [ ] = ” Texte ” , c o p i e [ 1 0 0 ] = {0} ;
7 s t r c p y ( c o p i e , c h a i n e ) ; // On c o p i e ” c h a i n e ” dans ” c o p i e ”
8 // S i t o u t s ’ e s t b i e n p a s s é , l a c o p i e d e v r a i t ê t r e i d e n t i q u e à
chaine
9 p r i n t f ( ” c h a i n e vaut : %s \n ” , c h a i n e ) ;
10 p r i n t f ( ” c o p i e vaut : %s \n ” , c o p i e ) ;
11 return 0 ;
12 }
13

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 23


Chargé du cours : M. Baïmi Badjoua ©UPM

— La fonction strcat pour concaténer deux chaînes :

1 i n t main ( i n t argc , c h a r ∗ argv [ ] )


2 {
3 /∗ On c r é e 2 cha î n e s . c h a i n e 1 d o i t ê t r e a s s e z grande pour
accueillir
4 l e contenu de c h a i n e 2 en p l u s , s i n o n r i s q u e de p l a n t a g e ∗/
5 c h a r c h a i n e 1 [ 1 0 0 ] = ” S a l u t ” , c h a i n e 2 [ ] = ” Baïmi ” ;
6 s t r c a t ( c h a i n e 1 , c h a i n e 2 ) ; // On c o n c a t è ne c h a i n e 2 dans c h a i n e 1
7 // S i t o u t s ’ e s t b i e n p a s s é , c h a i n e 1 vaut ” S a l u t Baïmi ”
8 p r i n t f ( ” c h a i n e 1 vaut : %s \n ” , c h a i n e 1 ) ;
9 // c h a i n e 2 n ’ a pas chang é :
10 p r i n t f ( ” c h a i n e 2 vaut t o u j o u r s : %s \n” , c h a i n e 2 ) ;
11 return 0 ;
12 }
13

— ...
Résumé sur les chaînes de caractères :

— Un ordinateur ne sait pas manipuler du texte, il ne connaît que les nombres. Pour
régler le problème, on associe à chaque lettre de l’alphabet un nombre correspon-
dant dans une table appelée la table ASCII.
— Le type char est utilisé pour stocker une et une seule lettre. Il stocke en réalité un
nombre mais ce nombre est automatiquement traduit par l’ordinateur à l’affichage.
— Pour créer un mot ou une phrase, on doit construire une chaîne de caractères. Pour
cela, on utilise un tableau de char.
— Toute chaîne de caractère se termine par un caractère spécial appelé \0 qui signifie
« fin de chaîne ».
— Il existe de nombreuses fonctions toutes prêtes de manipulation des chaînes dans
la bibliothèque string. Il faut inclure string.h pour pouvoir les utiliser.

1.5 Les structures et les énumérations


Le langage C nous permet de faire quelque chose de très puissant : créer nos propres types
de variables. Des « types de variables personnalisés », nous allons en voir deux sortes : les
structures et les énumérations.

Créer de nouveaux types de variables devient indispensable quand on cherche à faire des
programmes plus complexes.

Ce n’est (heureusement) pas bien compliqué à comprendre et à manipuler. Restez atten-


tifs tout de même parce que nous réutiliserons les structures tout le temps dans la suite
de ce cours.

Il faut savoir que les bibliothèques définissent généralement leurs propres types. Vous ne
tarderez donc pas à manipuler un type de variable Fichier ou encore, un peu plus tard,
d’autres de types Fenetre, Audio, Clavier, etc.

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 24


Chargé du cours : M. Baïmi Badjoua ©UPM

1.5.1 Définir une structure


Une structure est un assemblage de variables qui peuvent avoir différents types. Contrai-
rement aux tableaux qui vous obligent à utiliser le même type dans tout le tableau, vous
pouvez créer une structure comportant des variables de types long, char, int et double à
la fois.

Voici un exemple de structure :

1 s t r u c t NomDeVotreStructure
2 {
3 int variable1 ;
4 int variable2 ;
5 int autreVariable ;
6 d o u b l e nombreDecimal ;
7 };

Exemple de structure
Imaginons par exemple que vous vouliez créer une variable qui stocke les coordonnées d’un
point à l’écran. Vous aurez très certainement besoin d’une structure comme cela lorsque
vous ferez des jeux 2D dans la partie suivante, c’est donc l’occasion de s’avancer un peu.

Êtes-vous capables d’écrire une structure Coordonnees qui permette de stocker à la fois
la valeur de l’abscisse (x) et celle de l’ordonnée (y) d’un point ?

Allons, allons, ce n’est pas bien difficile :

1 s t r u c t Coordonnees
2 {
3 i n t x ; // A b s c i s s e s
4 i n t y ; // Ordonn é e s
5 };

Tableaux dans une structure


Les structures peuvent contenir des tableaux. Ça tombe bien, on va pouvoir ainsi placer
des tableaux de char (chaînes de caractères) sans problème !

Allez, imaginons une structure Personne qui stockerait diverses informations sur une per-
sonne :

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 25


Chargé du cours : M. Baïmi Badjoua ©UPM

1 s t r u c t Personne
2 {
3 c h a r nom [ 1 0 0 ] ;
4 c h a r prenom [ 1 0 0 ] ;
5 char a d r e s s e [ 1 0 0 0 ] ;
6
7 i n t age ;
8 i n t g e n r e ; // Bool é en : 1 = g a r ç on , 0 = f i l l e
9 };

1.5.2 Utilisation d’une structure


Voici comment créer une variable de type Coordonnees (la structure qu’on a définie plus
haut) :

1 #i n c l u d e ” main . h” // I n c l u s i o n du . h q u i c o n t i e n t l e s p r o t o t y p e s e t
structures
2
3 i n t main ( i n t argc , c h a r ∗ argv [ ] )
4 {
5 s t r u c t Coordonnees p o i n t ; // Cr é a t i o n d ’ une v a r i a b l e ” p o i n t ” de type
Coordonnees
6 return 0 ;
7 }

Le typedef
Retournons dans le fichier.h qui contient la définition de notre structure de type Coor-
donnees. Nous allons ajouter une instruction appelée typedef qui sert à créer un alias de
structure, c’est-à-dire à dire qu’écrire telle chose équivaut à écrire telle autre chose.

Nous allons ajouter une ligne commençant par typedef juste avant la définition de la
structure :

1 t y p e d e f s t r u c t Coordonnees Coordonnees ;
2 s t r u c t Coordonnees
3 {
4 int x ;
5 int y ;
6 };

En clair, cette ligne (1) dit « Écrire le mot Coordonnees est désormais équivalent à écrire
struct Coordonnees ». En faisant cela, vous n’aurez plus besoin de mettre le mot struct
à chaque définition de variable de type Coordonnees. On peut donc retourner dans notre
main et écrire tout simplement :

1 i n t main ( i n t argc , c h a r ∗ argv [ ] )

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 26


Chargé du cours : M. Baïmi Badjoua ©UPM

2 {
3 Coordonnees p o i n t ; // L ’ o r d i n a t e u r comprend qu ’ i l s ’ a g i t de ” s t r u c t
Coordonnees ” g r â c e au t y p e d e f
4 return 0 ;
5 }

Modifier les composantes de la structure


Maintenant que notre variable point est créée, nous voulons modifier ses coordonnées.
Comment accéder au x et au y de point ? Comme ceci :

1 i n t main ( i n t argc , c h a r ∗ argv [ ] )


2 {
3 Coordonnees p o i n t ;
4 p o i n t . x = 10 ;
5 p o i n t . y = 20 ;
6 return 0 ;
7 }

Si on prend la structure Personne que nous avons vue tout à l’heure et qu’on demande le
nom et le prénom, on devra faire comme ça :

1 i n t main ( i n t argc , c h a r ∗ argv [ ] )


2 {
3 Personne u t i l i s a t e u r ;
4 p r i n t f ( ” Quel e s t v o t r e nom ? ” ) ;
5 s c a n f ( ”%s ” , u t i l i s a t e u r . nom) ;
6 p r i n t f ( ” Votre prenom ? ” ) ;
7 s c a n f ( ”%s ” , u t i l i s a t e u r . prenom ) ;
8 p r i n t f ( ” Vous vous a p p e l e z %s %s ” , u t i l i s a t e u r . prenom , u t i l i s a t e u r . nom
);
9 return 0 ;
10 }

Initialiser une structure


Pour les structures comme pour les variables, tableaux et pointeurs, il est vivement
conseillé de les initialiser dès leur création pour éviter qu’elles ne contiennent « n’im-
porte quoi ». En effet, je vous le rappelle, une variable qui est créée prend la valeur de ce
qui se trouve en mémoire là où elle a été placée. Parfois cette valeur est 0, parfois c’est
un résidu d’un autre programme qui est passé par là avant vous et la variable a alors une
valeur qui n’a aucun sens, comme -84570.

Pour rappel, voici comment on initialise :


— une variable : on met sa valeur à 0 (cas le plus simple) ;
— un pointeur : on met sa valeur à NULL. NULL est en fait un #define situé dans
stdlib.h qui vaut généralement 0, mais on continue à utiliser NULL, par conven-
tion, sur les pointeurs pour bien voir qu’il s’agit de pointeurs et non de variables

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 27


Chargé du cours : M. Baïmi Badjoua ©UPM

ordinaires ;
— un tableau : on met chacune de ses valeurs à 0.
Pour les structures, l’initialisation va un peu ressembler à celle d’un tableau. En effet, on
peut faire à la déclaration de la variable :

1 Coordonnees p o i n t = { 0 , 0} ; // Dans l ’ o r d r e , p o i n t . x = 0 e t p o i n t . y = 0 .

Revenons à la structure Personne (qui contient des chaînes). Vous avez aussi le droit
d’initialiser une chaîne en écrivant juste ”” (rien entre les guillemets). Je ne vous ai pas
parlé de cette possibilité dans la section sur les chaînes, mais il n’est pas trop tard pour
l’apprendre. On peut donc initialiser dans l’ordre nom, prenom, adresse, age et garcon
comme ceci :

1 Personne u t i l i s a t e u r = { ” ” , ” ” , ” ” , 0 , 0} ;

1.5.3 Pointeur de structure


Un pointeur de structure se crée de la même manière qu’un pointeur de int, de double ou
de n’importe quelle autre type de base :

1 Coordonnees ∗ p o i n t = NULL ;

Envoi de la structure à une fonction


Ce qui nous intéresse ici, c’est de savoir comment envoyer un pointeur de structure à une
fonction pour que celle-ci puisse modifier le contenu de la variable.

On va faire ceci pour cet exemple : on va simplement créer une variable de type Coor-
donnees dans le main et envoyer son adresse à initialiserCoordonnees. Cette fonction aura
pour rôle de mettre tous les éléments de la structure à 0.

Notre fonction initialiserCoordonnees va prendre un paramètre : un pointeur sur une


structure de type Coordonnees (un Coordonnees*, donc).

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 28


Chargé du cours : M. Baïmi Badjoua ©UPM

1 i n t main ( i n t argc , c h a r ∗ argv [ ] )


2 {
3 Coordonnees monPoint ;
4 i n i t i a l i s e r C o o r d o n n e e s (&monPoint ) ;
5 return 0 ;
6 }
7
8 v o i d i n i t i a l i s e r C o o r d o n n e e s ( Coordonnees ∗ p o i n t )
9 {
10 // I n i t i a l i s a t i o n de chacun d e s membres de l a s t r u c t u r e i c i
11 }

Ma variable monPoint est donc créée dans le main. On envoie son adresse à la fonction
initialiserCoordonnees qui récupère cette variable sous la forme d’un pointeur appelé point
(on aurait d’ailleurs pu l’appeler n’importe comment dans la fonction, cela n’aurait pas
eu d’incidence).

Bien : maintenant que nous sommes dans initialiserCoordonnees, nous allons initialiser
chacune des valeurs une à une. Il ne faut pas oublier de mettre une étoile devant le nom
du pointeur pour accéder à la variable. Si vous ne le faites pas, vous risquez de modifier
l’adresse, et ce n’est pas ce que nous voulons faire.
Oui mais voilà, problème… On ne peut pas vraiment faire :

1 v o i d i n i t i a l i s e r C o o r d o n n e e s ( Coordonnees ∗ p o i n t )
2 {
3 ∗ point . x = 0 ;
4 ∗ point . y = 0 ;
5 }

Ce serait trop facile… Pourquoi on ne peut pas faire ça ? Parce que le point de séparation
s’applique sur le mot point et non sur *point en entier. Or, nous ce qu’on veut, c’est
accéder à *point pour en modifier la valeur. Pour régler le problème, il faut placer des
parenthèses autour de *point. Comme cela, le point de séparation s’appliquera à *point
et non juste à point :

1 v o i d i n i t i a l i s e r C o o r d o n n e e s ( Coordonnees ∗ p o i n t )
2 {
3 (∗ point ) . x = 0 ;
4 (∗ point ) . y = 0 ;
5 }

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 29


Chargé du cours : M. Baïmi Badjoua ©UPM

Un raccourci pratique et très utilisé

1 (∗ point ) . x = 0 ;
2 // EST EQUIVALENT A
3 p o i n t −>x = 0 ;

Reprenons notre fonction initialiserCoordonnees. Nous pouvons donc l’écrire comme ceci :

1 v o i d i n i t i a l i s e r C o o r d o n n e e s ( Coordonnees ∗ p o i n t )
2 {
3 p o i n t −>x = 0 ;
4 p o i n t −>y = 0 ;
5 }

Retenez bien ce raccourci de la flèche, nous allons le réutiliser un certain nombre de fois. Et
surtout, ne confondez pas la flèche avec le « point ». La flèche est réservée aux pointeurs,
le « point » est réservé aux variables. Utilisez ce petit exemple pour vous en souvenir :

1 i n t main ( i n t argc , c h a r ∗ argv [ ] )


2 {
3 Coordonnees monPoint ;
4 Coordonnees ∗ p o i n t e u r = &monPoint ;
5 monPoint . x = 10 ; // On t r a v a i l l e s u r une v a r i a b l e , on u t i l i s e l e ”
point ”
6 p o i n t e u r −>x = 20 ; // On t r a v a i l l e s u r un p o i n t e u r , on u t i l i s e l a f l è
che
7 return 0 ;
8 }

On modifie la valeur du x à 10 de deux manières différentes, ici : la première fois en


travaillant directement sur la variable, la seconde fois en passant par le pointeur.

1.5.4 Les énumérations


Les énumérations constituent une façon un peu différente de créer ses propres types de
variables.

Une énumération ne contient pas de « sous-variables » comme c’était le cas pour les
structures. C’est une liste de « valeurs possibles » pour une variable. Une énumération ne
prend donc qu’une case en mémoire et cette case peut prendre une des valeurs que vous
définissez (et une seule à la fois).

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 30


Chargé du cours : M. Baïmi Badjoua ©UPM

Voici un exemple d’énumération :

1 t y p e d e f enum Volume Volume ;


2 enum Volume
3 {
4 FAIBLE , MOYEN, FORT
5 };

Vous noterez qu’on utilise un typedef là aussi, comme on l’a fait jusqu’ici.

Pour créer une énumération, on utilise le mot-clé enum. Notre énumération s’appelle ici
Volume. C’est un type de variable personnalisé qui peut prendre une des trois valeurs
qu’on a indiquées : soit FAIBLE, soit MOYEN, soit FORT.

On va pouvoir créer une variable de type Volume, par exemple musique, qui stockera
le volume actuel de la musique. On peut par exemple initialiser la musique au volume
MOYEN :

1 Volume musique = MOYEN;

Voilà qui est fait. Plus tard dans le programme, on pourra modifier la valeur du volume
et la mettre soit à FAIBLE, soit à FORT.

Association de nombres aux valeurs


Vous avez remarqué que j’ai écrit les valeurs possibles de l’énumération en majuscules.
Cela devrait vous rappeler les constantes et les define, non ?

En effet, c’est assez similaire mais ce n’est pourtant pas exactement la même chose. Le
compilateur associe automatiquement un nombre à chacune des valeurs possibles de l’énu-
mération.

Dans le cas de notre énumération Volume, FAIBLE vaut 0, MOYEN vaut 1 et FORT vaut
2. L’association est automatique et commence à 0.
Contrairement au #define, c’est le compilateur qui associe MOYEN à 1 par exemple, et
non le préprocesseur. Au bout du compte, ça revient un peu au même. En fait, quand on
a initialisé la variable musique à MOYEN, on a donc mis la case en mémoire à la valeur
1.

En pratique, est-ce utile de savoir que MOYEN vaut 1, FORT vaut 2, etc. ?

Non. En général ça nous est égal. C’est le compilateur qui associe automatiquement un
nombre à chaque valeur. Grâce à ça, vous n’avez plus qu’à écrire :

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 31


Chargé du cours : M. Baïmi Badjoua ©UPM

1 i f ( musique == MOYEN)
2 {
3 // Jouer l a musique au volume moyen
4 }

Peu importe la valeur de MOYEN, vous laissez le compilateur se charger de gérer les
nombres.

L’intérêt de tout ça ? C’est que de ce fait votre code est très lisible. En effet, tout le
monde peut facilement lire le if précédent (on comprend bien que la condition signifie «
Si la musique est au volume moyen »).

Associer une valeur précise


Pour le moment, c’est le compilateur qui décide d’associer le nombre 0 à la première va-
leur, puis 1, 2, 3 dans l’ordre. Il est possible de demander d’associer une valeur précise à
chaque élément de l’énumération.

Quel intérêt est-ce que ça peut bien avoir ? Eh bien supposons que sur votre ordinateur, le
volume soit géré entre 0 et 100 (0 = pas de son, 100 = 100 % du son). Il est alors pratique
d’associer une valeur précise à chaque élément :

1 t y p e d e f enum Volume Volume ;


2 enum Volume
3 {
4 FAIBLE = 1 0 , MOYEN = 5 0 , FORT = 100
5 };

Ici, le volume FAIBLE correspondra à 10 % de volume, le volume MOYEN à 50 %, etc. On


pourrait facilement ajouter de nouvelles valeurs possibles comme MUET. On associerait
dans ce cas MUET à la valeur… 0 ! Vous avez compris.

Résumé sur les structures et les énumérations


— Une structure est un type de variable personnalisé que vous pouvez créer et utiliser
dans vos programmes. C’est à vous de la définir, contrairement aux types de base
tels que int et double que l’on retrouve dans tous les programmes.
— Une structure est composée de « sous-variables » qui sont en général des variables
de type de base comme int et double, mais aussi des tableaux.
— On accède à une des composantes de la structure en séparant le nom de la variable
et la composante d’un point : joueur.prenom.
— Si on manipule un pointeur de structure et qu’on veut accéder à une des compo-
santes, on utilise une flèche à la place du point : pointeurJoueur->prenom.
— Une énumération est un type de variable personnalisé qui peut seulement prendre
une des valeurs que vous prédéfinissez : FAIBLE, MOYEN ou FORT par exemple.

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 32


Chargé du cours : M. Baïmi Badjoua ©UPM

1.6 L’allocation dynamique de mémoire


Toutes les variables que nous avons créées jusqu’ici étaient construites automatiquement
par le compilateur du langage C. C’était la méthode simple. Il existe cependant une façon
plus manuelle de créer des variables que l’on appelle l’allocation dynamique.

Un des principaux intérêts de l’allocation dynamique est de permettre à un programme


de réserver la place nécessaire au stockage d’un tableau en mémoire dont il ne connaissait
pas la taille avant la compilation. En effet, jusqu’ici, la taille de nos tableaux était fixée
« en dur » dans le code source. Après lecture de ce chapitre, vous allez pouvoir créer des
tableaux de façon bien plus flexible !

Il est impératif de bien savoir manipuler les pointeurs pour pouvoir lire ce chapitre ! Si
vous avez encore des doutes sur les pointeurs, je vous recommande d’aller relire la section
correspondant avant de commencer.

Quand on déclare une variable, on dit qu’on demande à allouer de la mémoire :

1 i n t monNombre = 0 ;

Jusqu’ici, les choses étaient automatiques. Lorsqu’on déclarait une variable, le système
d’exploitation était automatiquement appelé par le programme. Que diriez-vous de faire
cela manuellement ? Non pas par pur plaisir de faire quelque chose de compliqué (même si
c’est tentant !), mais plutôt parce que nous allons parfois être obligés de procéder comme
cela.

Dans cet section, nous allons :


— étudier le fonctionnement de la mémoire (oui, encore !) pour découvrir la taille que
prend une variable en fonction de son type ;
— puis attaquer le sujet lui-même : nous verrons comment demander manuellement
de la mémoire au système d’exploitation. On fera ce qu’on appelle de l’allocation
dynamique de mémoire ;
— enfin, découvrir l’intérêt de faire une allocation dynamique de mémoire en appre-
nant à créer un tableau dont la taille n’est connue qu’à l’exécution du programme.

1.6.1 La taille des variables


Pour connaître la taille d’unint, on devra donc écrire :

1 sizeof ( int ) ;

À la compilation, cela sera remplacé par un nombre : le nombre d’octets que prendinten
mémoire. Chez moi, sizeof(int) vaut 4, ce qui signifie que int occupe 4 octets. Chez vous,
c’est probablement la même valeur, mais ce n’est pas une règle. Testez pour voir, en
affichant la valeur à l’aide d’unprintfpar exemple :

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 33


Chargé du cours : M. Baïmi Badjoua ©UPM

1 p r i n t f ( ” c h a r : %d o c t e t s \n” , s i z e o f ( c h a r ) ) ;
2 p r i n t f ( ” i n t : %d o c t e t s \n” , s i z e o f ( i n t ) ) ;
3 p r i n t f ( ” l o n g : %d o c t e t s \n” , s i z e o f ( l o n g ) ) ;
4 p r i n t f ( ” d o u b l e : %d o c t e t s \n” , s i z e o f ( d o u b l e ) ) ;

Peut-on afficher la taille d’un type personnalisé qu’on a créé (une structure) ?

Oui ! sizeof marche aussi sur les structures !

1 t y p e d e f s t r u c t Coordonnees Coordonnees ;
2 s t r u c t Coordonnees
3 {
4 int x ;
5 int y ;
6 };
7
8 i n t main ( i n t argc , c h a r ∗ argv [ ] )
9 {
10 p r i n t f ( ” Coordonnees : %d o c t e t s \n” , s i z e o f ( Coordonnees ) ) ;
11 return 0 ;
12 }

1.6.2 Une nouvelle façon de voir la mémoire


Jusqu’ici, mes schémas de mémoire étaient encore assez imprécis. On va enfin pouvoir les
rendre vraiment précis et corrects maintenant qu’on connaît la taille de chacun des types
de variables.

Si on déclare une variable de type int :

1 i n t nombre = 18 ;

… et que sizeof(int) indique 4 octets sur notre ordinateur, alors la variable occupera 4
octets en mémoire !

Supposons que la variable nombre soit allouée à l’adresse 1600 en mémoire. On aurait
alors le schéma de la figure suivante.
Si on avait fait la même chose avec un char, on n’aurait alors occupé qu’un seul octet en
mémoire (figure suivante).
Imaginez maintenant un tableau de int ! Chaque « case » du tableau occupera 4 octets.
Si notre tableau fait 100 cases :

1 int tableau [100] ;

on occupera alors en réalité 4 * 100 = 400 octets en mémoire.

Il est important de bien comprendre ces petits calculs pour la suite de cette section.

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 34


Chargé du cours : M. Baïmi Badjoua ©UPM

Figure 1.5 – Nouvelle représentation d’une variable en mémoire

Figure 1.6 – Nouvelle représentation d’une variable de type char en mémoire

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 35


Chargé du cours : M. Baïmi Badjoua ©UPM

1.6.3 Allocation de mémoire dynamique


Entrons maintenant dans le vif du sujet. Je vous rappelle notre objectif : apprendre à
demander de la mémoire manuellement.

On va avoir besoin d’inclure la bibliothèque <stdlib.h>. Si vous avez suivi mes conseils,
vous devriez avoir inclus cette bibliothèque dans tous vos programmes, de toute façon.
Cette bibliothèque contient deux fonctions dont nous allons avoir besoin :

— malloc (« Memory ALLOCation », c’est-à-dire « Allocation de mémoire ») : de-


mande au système d’exploitation la permission d’utiliser de la mémoire ;
— free (« Libérer ») : permet d’indiquer à l’OS que l’on n’a plus besoin de la mémoire
qu’on avait demandée. La place en mémoire est libérée, un autre programme peut
maintenant s’en servir au besoin.

Quand vous faites une allocation manuelle de mémoire, vous devez toujours suivre ces
trois étapes :

— appeler malloc pour demander de la mémoire ;


— vérifier la valeur retournée par malloc pour savoir si l’OS a bien réussi à allouer la
mémoire ;
— une fois qu’on a fini d’utiliser la mémoire, on doit la libérer avec free. Si on ne le fait
pas, on s’expose à des fuites de mémoire, c’est-à-dire que votre programme risque
au final de prendre beaucoup de mémoire alors qu’il n’a en réalité plus besoin de
tout cet espace.

Le principe est exactement le même qu’avec les fichiers : on alloue, on vérifie si l’allocation
a marché, on utilise la mémoire, puis on la libère quand on a fini de l’utiliser.

malloc : demande d’allocation de mémoire


Le prototype de la fonction malloc est assez comique, vous allez voir :

1 void ∗ malloc ( size_t nombreOctetsNecessaires ) ;

La fonction prend un paramètre : le nombre d’octets à réserver. Ainsi, il suffira d’écrire


sizeof(int) dans ce paramètre pour réserver suffisamment d’espace pour stocker un int.

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 36


Chargé du cours : M. Baïmi Badjoua ©UPM

Exemple :

1 i n t ∗ memoireAllouee = NULL ; // On c r é e un p o i n t e u r s u r i n t
2 memoireAllouee = m a l l o c ( s i z e o f ( i n t ) ) ; // La f o n c t i o n m a l l o c i n s c r i t dans
notre pointeur l ’ adresse qui a é t é r e s e r v é e .

À la fin de ce code, memoireAllouee est un pointeur contenant une adresse qui vous a été
réservée par l’OS, par exemple l’adresse 1600 pour reprendre mes schémas précédents.

1.6.4 Tester le pointeur


La fonction malloc a donc renvoyé dans notre pointeur memoireAllouee l’adresse qui a
été réservée pour vous en mémoire. Deux possibilités :

— si l’allocation a marché, notre pointeur contient une adresse ;


— si l’allocation a échoué, notre pointeur contient l’adresse NULL.

On va utiliser une fonction standard qu’on n’avait pas encore vue jusqu’ici : exit(). Elle ar-
rête immédiatement le programme. Elle prend un paramètre : la valeur que le programme
doit retourner (cela correspond en fait au return du main()).

1 i n t main ( i n t argc , c h a r ∗ argv [ ] )


2 {
3 i n t ∗ memoireAllouee = NULL ;
4 memoireAllouee = m a l l o c ( s i z e o f ( i n t ) ) ;
5 i f ( memoireAllouee == NULL) // S i l ’ a l l o c a t i o n a é chou é
6 {
7 e x i t ( 0 ) ; // On a r r ê t e immé d i a t e m e n t l e programme
8 }
9 // On peut c o n t i n u e r l e programme normalement s i n o n
10 return 0 ;
11 }

Si le pointeur est différent de NULL, le programme peut continuer, sinon il faut afficher un
message d’erreur ou même mettre fin au programme, parce qu’il ne pourra pas continuer
correctement s’il n’y a plus de place en mémoire.

1.6.5 free : libérer de la mémoire


Tout comme on utilisait la fonction fclose pour fermer un fichier dont on n’avait plus
besoin, on va utiliser la fonction free pour libérer la mémoire dont on ne se sert plus.

1 void f r e e ( void ∗ pointeur ) ;

La fonction free a juste besoin de l’adresse mémoire à libérer. On va donc lui envoyer
notre pointeur, c’est-à-dire memoireAllouee dans notre exemple. Voici le schéma complet
et final, ressemblant à s’y méprendre à ce qu’on a vu dans le chapitre sur les fichiers :

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 37


Chargé du cours : M. Baïmi Badjoua ©UPM

1 i n t main ( i n t argc , c h a r ∗ argv [ ] )


2 {
3 i n t ∗ memoireAllouee = NULL ;
4 memoireAllouee = m a l l o c ( s i z e o f ( i n t ) ) ;
5 i f ( memoireAllouee == NULL) // On v é r i f i e s i l a mé moire a é t é a l l o u é e
6 {
7 e x i t ( 0 ) ; // E r r e u r : on a r r ê t e t o u t !
8 }
9 // On peut u t i l i s e r i c i l a mé moire
10 f r e e ( memoireAllouee ) ; // On n ’ a p l u s b e s o i n de l a mé moire , on l a l i b è
re
11 return 0 ;
12 }

1.6.6 Exemple concret d’utilisation


On va programmer quelque chose qu’on a appris à faire il y a longtemps : demander
l’âge de l’utilisateur et le lui afficher. La seule différence avec ce qu’on faisait avant,
c’est qu’ici la variable va être allouée manuellement (on dit aussi dynamiquement) plutôt
qu’automatiquement comme auparavant. Alors oui, du coup, le code est un peu plus
compliqué. Mais faites l’effort de bien le comprendre, c’est important :

1 i n t main ( i n t argc , c h a r ∗ argv [ ] )


2 {
3 i n t ∗ memoireAllouee = NULL ;
4 memoireAllouee = m a l l o c ( s i z e o f ( i n t ) ) ; // A l l o c a t i o n de l a mé moire
5 i f ( memoireAllouee == NULL)
6 {
7 exit (0) ;
8 }
9 // U t i l i s a t i o n de l a mé moire
10 p r i n t f ( ” Quel age avez−vous ? ” ) ;
11 s c a n f ( ”%d” , memoireAllouee ) ;
12 p r i n t f ( ” Vous avez %d ans \n” , ∗ memoireAllouee ) ;
13 f r e e ( memoireAllouee ) ; // Lib é r a t i o n de mé moire
14 return 0 ;
15 }

Attention : comme memoireAllouee est un pointeur, on ne l’utilise pas de la même ma-


nière qu’une vraie variable. Pour obtenir la valeur de la variable, il faut placer une étoile
devant : *memoireAllouee (regardez le printf ). Tandis que pour indiquer l’adresse, on a
juste besoin d’écrire le nom du pointeur memoireAllouee (regardez le scanf ). Tout cela a
été expliqué dans le chapitre sur les pointeurs. Toutefois, on met généralement du temps
à s’y faire et il est probable que vous confondiez encore. Si c’est votre cas, vous devez
relire le chapitre sur les pointeurs, qui est fondamental.

Revenons à notre code. On y a alloué dynamiquement une variable de type int. Au final,
ce qu’on a écrit revient exactement au même que d’utiliser la méthode « automatique »
qu’on connaît bien maintenant :

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 38


Chargé du cours : M. Baïmi Badjoua ©UPM

1 i n t main ( i n t argc , c h a r ∗ argv [ ] )


2 {
3 i n t maVariable = 0 ; // A l l o c a t i o n de l a mé moire ( automatique )
4 // U t i l i s a t i o n de l a mé moire
5 p r i n t f ( ” Quel age avez−vous ? ” ) ;
6 s c a n f ( ”%d” , &maVariable ) ;
7 p r i n t f ( ” Vous avez %d ans \n” , maVariable ) ;
8 return 0 ;
9 } // Lib é r a t i o n de l a mé moire ( automatique à l a f i n de l a f o n c t i o n )

En résumé, il y a deux façons de créer une variable, c’est-à-dire d’allouer de la mémoire.


Soit on le fait :

— automatiquement : c’est la méthode que vous connaissez et qu’on a utilisée jus-


qu’ici ;
— manuellement (dynamiquement) : c’est la méthode que je vous enseigne dans cette
section.

1.6.7 Allocation dynamique d’un tableau


Voici ce que fait le programme suivant dans l’ordre :

1. demander à l’utilisateur combien il a d’amis ;


2. créer un tableau deintayant une taille égale à son nombre d’amis (via malloc) ;
3. demander l’âge de chacun de ses amis un à un, qu’on stocke dans le tableau ;
4. afficher l’âge des amis pour montrer qu’on a bien mémorisé tout cela ;
5. à la fin, puisqu’on n’a plus besoin du tableau contenant l’âge des amis, le libérer
avec la fonctionfree.
Ce programme est tout à fait inutile : il demande les âges et les affiche ensuite. J’ai choisi
de faire cela car c’est un exemple « simple » (enfin, si vous avez compris lemalloc).

Je vous rassure : dans la suite du cours, nous aurons l’occasion d’utiliser le malloc pour
des choses bien plus intéressantes que le stockage de l’âge de ses amis !

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 39


Chargé du cours : M. Baïmi Badjoua ©UPM

1 i n t main ( i n t argc , c h a r ∗ argv [ ] )


2 {
3 i n t nombreDAmis = 0 , i = 0 ;
4 i n t ∗ ageAmis = NULL ; // Ce p o i n t e u r va s e r v i r de t a b l e a u apr è s l ’
a p p e l du m a l l o c
5 // On demande l e nombre d ’ amis à l ’ u t i l i s a t e u r
6 p r i n t f ( ” Combien d ’ amis avez−vous ? ” ) ;
7 s c a n f ( ”%d” , &nombreDAmis ) ;
8 i f ( nombreDAmis > 0 ) // I l f a u t qu ’ i l a i t au moins un ami ( j e l e
p l a i n s un peu s i n o n :p )
9 {
10 ageAmis = m a l l o c ( nombreDAmis ∗ s i z e o f ( i n t ) ) ; // On a l l o u e de l a m
é moire pour l e t a b l e a u
11 i f ( ageAmis == NULL) // On v é r i f i e s i l ’ a l l o c a t i o n a march é ou
non
12 {
13 e x i t ( 0 ) ; // On a r r ê t e t o u t
14 }
15 // On demande l ’ â ge d e s amis un à un
16 f o r ( i = 0 ; i < nombreDAmis ; i ++)
17 {
18 p r i n t f ( ” Quel age a l ’ ami numero %d ? ” , i + 1 ) ;
19 s c a n f (”%d ” , &ageAmis [ i ] ) ;
20 }
21 // On a f f i c h e l e s â g e s s t o c k é s un à un
22 p r i n t f ( ” \ n\nVos amis ont l e s a g e s s u i v a n t s :\n ” ) ;
23 f o r ( i = 0 ; i < nombreDAmis ; i ++)
24 {
25 p r i n t f (”%d ans \n ” , ageAmis [ i ] ) ;
26 }
27 // On l i b è r e l a mé moire a l l o u é e avec malloc , on n ’ en a p l u s
besoin
28 f r e e ( ageAmis ) ;
29 }
30 return 0 ;
31 }

Résumé sur l’allocation dynamique

— Une variable occupe plus ou moins d’espace en mémoire en fonction de son type.
— On peut connaître le nombre d’octets occupés par un type à l’aide de l’opérateur
sizeof().
— L’allocation dynamique consiste à réserver manuellement de l’espace en mémoire
pour une variable ou un tableau.
— L’allocation est effectuée avec malloc() et il ne faut surtout pas oublier de libérer
la mémoire avec free() dès qu’on n’en a plus besoin.
— L’allocation dynamique permet notamment de créer un tableau dont la taille est
déterminée par une variable au moment de l’exécution.

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 40


Chargé du cours : M. Baïmi Badjoua ©UPM

Conclusion
Nous avons dans ce chapitre, rappelé les notions de base et complémentaires du langage
C. Ces notions sont très importantes voire indispensables pour la comprehension de ce
cours. Relisez ce chapitre au temps de fois qu’il le faut pour asssimiler tous les concepts
exposés dans ce chapitre. Sans quoi vous aurez des sérieux problèmes pour comprendre la
suite des chapitres.

CHAPITRE 1. RAPPELS ET COMPLÉMENTS DE C 41


CHAPITRE 2
NOTIONS D’ALGORITHME

Introduction
Vous venez d’apprendre les bases d’un langage de programmation ? Vous vous êtes peut-
être rendu compte que parfois, en modifiant un peu votre programme, vous pouvez obtenir
le même résultat mais 2, 10 ou 1000 fois plus vite ?

De telles améliorations ne sont pas le fruit du hasard, ni même dues à une augmentation
de la mémoire vive ou à un changement de processeur : il y a plusieurs manières de pro-
grammer quelque chose et certaines sont incroyablement meilleures que d’autres.

Avec un peu de réflexion, et des outils théoriques de base, vous serez vous aussi en mesure
de faire de bons choix pour vos programmes. À la fin de ce cours, vous serez de meilleurs
développeurs, en mesure de comprendre, corriger et concevoir des programmes plus effi-
caces.

L’un des éléments important à connaitre pour développer des logiciels optimisés (qui
consomme moins de mémoire et qui prennent moins de temps à s’exécuter) est l’algorithme
qui est l’objet de ce chapitre.

2.1 Qu’est-ce qu’un algorithme ?


2.1.1 Définition
Un algorithme est la description précise, sous forme de concepts simples, de la manière
dont on peut résoudre un problème.

Dans la vie de tous les jours, nous avons souvent besoin de résoudre des problèmes. Surtout
si on considère la notion de ”problème” au sens large.

42
Chargé du cours : M. Baïmi Badjoua ©UPM

2.1.2 Omniprésence des algorithmes


Un exemple de problème qui nous concerne tous (oui, même vous) est celui de la cuisine :
vous êtes dans une cuisine, vous trouvez du riz, comment le cuire ? Voici une marche à
suivre simple :

1. remplir une casserole d’eau ;


2. y ajouter une pincée de sel ;
3. la mettre sur le feu ;
4. attendre l’ébullition de l’eau ;
5. mettre le riz dans la casserole ;
6. le laisser cuire 10 à 15 minutes ;
7. égoutter le riz.

J’ai décrit une solution au problème ”il faut faire cuire du riz”, sous forme de concepts
simples. Vous remarquerez qu’il y a pourtant beaucoup de choses implicites : j’ai précisé
que vous étiez au départ en possession du riz, mais il faut aussi une casserole, de l’eau,
etc. On peut se trouver dans des situations spécifiques où tous ces objets ne sont pas
disponibles, et il faudra alors utiliser un autre algorithme (ou commencer par construire
une casserole...).

Les instructions que j’ai utilisées sont ”précises”, mais on pourrait préciser moins de
choses, ou plus. Comment fait-on pour remplir une casserole d’eau, plus précisément ? Si
le cuisinier à qui la recette est destinée ne sait pas interpréter la ligne ”remplir une casse-
role d’eau”, il faudra l’expliquer en termes plus simples (en expliquant comment utiliser
le robinet, par exemple).

De même, quand vous programmez, le degré de précision que vous utilisez dépend de
nombreux paramètres : le langage que vous utilisez, les bibliothèques que vous avez à
disposition, etc.

2.1.3 Rôle privilégié des algorithmes


Si on trouve des algorithmes dans la vie de tous les jours, pourquoi en parle-t-on princi-
palement en informatique ? La raison est très simple : les ordinateurs sont très pratiques
pour effectuer des tâches répétitives. Ils sont rapides, efficaces, et ne se lassent pas.

On peut décrire un algorithme permettant de calculer les décimales de la racine carrée


de deux, qui soit utilisable par un humain. Vous pourrez ainsi calculer, à l’aide d’une
feuille et d’un crayon, les 10 premières décimales (1,4142135624). Mais s’il vous en faut
un million ? Un ordinateur deviendra alors beaucoup plus adapté.

De manière générale, on peut concevoir de nombreux algorithmes comme des méthodes


de traitement d’information : recherche, comparaison, analyse, classement, extraction, les
ordinateurs sont souvent très utiles pour tripoter la masse d’informations qui nous entoure

CHAPITRE 2. NOTIONS D’ALGORITHME 43


Chargé du cours : M. Baïmi Badjoua ©UPM

continuellement.

Vous aurez peut-être pensé au célèbre moteur de recherche Google (qui a initialement
dominé le marché grâce aux capacités de son algorithme de recherche), mais ce genre
d’activités n’est pas restreint au (vaste) secteur d’Internet : quand vous jouez à un jeu de
stratégie en temps réel, et que vous ordonnez à une unité de se déplacer, l’ordinateur a en
sa possession plusieurs informations (la structure de la carte, le point de départ, le point
d’arrivée) et il doit produire une nouvelle information : l’itinéraire que doit suivre l’unité.

2.1.4 Notion de structure de données


En plus de manipuler l’information, il faut aussi la stocker. La manière dont on organise
cette information stockée peut avoir des conséquences très importantes sur leur manipu-
lation.

Concrètement, prenez par exemple un dictionnaire : on peut définir un dictionnaire comme


”un ensemble de mots et leur définition”. Cependant, pourrait-on utiliser correctement un
dictionnaire dont les mots sont placés dans le désordre ? Certainement pas, parce qu’il
serait très difficile de trouver la définition d’un mot que l’on cherche (c’est encore pos-
sible, il suffit de lire le dictionnaire page par page jusqu’à ce qu’on trouve le mot). L’ordre
alphabétique est clairement une solution très efficace pour pouvoir retrouver rapidement
le mot que l’on cherche.

Il y a des liens forts entre les algorithmes (qui décrivent des méthodes) et les structures
de données (qui décrivent une organisation). Typiquement, certaines structures de don-
nées sont indispensables à la mise en place de certaines méthodes, et à l’inverse certains
algorithmes sont nécessaires aux structures de données : par exemple, si on veut rajouter
un mot dans un dictionnaire classé alphabétiquement, on ne peut pas juste l’écrire dans
l’espace libre sur la dernière page, il faut utiliser un algorithme pour l’ajouter au bon
endroit. L’étude des structures de données est donc inséparable de celle des algorithmes,
et vous n’y échapperez pas.

2.2 Découvrez l’intérêt des algorithmes


Une fois n’est pas coutume, commençons par nous demander quel est l’intérêt d’apprendre
l’algorithmique et surtout quels sont les différents sujets couverts par le domaine.

Un algorithme est une manière de résoudre un problème. C’est tout. Vous êtes déçu·e ?
Vous pensiez que vous alliez vous transformer en savant fou à la fin de ce cours ? Pas
vraiment !

Alors pourquoi relions-nous toujours ce mot à l’informatique et aux ordinateurs en parti-


culier ? Tout simplement parce qu’un algorithme est avant tout un ensemble de méthodes
utilisées par un ordinateur pour résoudre un problème. En fait, vous allez réfléchir aux
différentes manières de résoudre un problème, puis faire en sorte que l’ordinateur le fasse
pour vous. Pourquoi ? Ce n’est pas uniquement parce que nous sommes des fainéants !
Un ordinateur a des capacités étendues que nous, humains, ne pouvons égaler.

CHAPITRE 2. NOTIONS D’ALGORITHME 44


Chargé du cours : M. Baïmi Badjoua ©UPM

Avant tout, il ne se plaindra pas si vous lui demandez de réaliser des actions répétitives.
Au contraire ! Déplacer vos 2 000 dernières photos de voyage une à une ? Facile ! Chercher
dans ce PDF de 200 pages toutes les occurrences de l’expression ”max tout puissant” ? Du
gâteau ! Imaginez si vous deviez en faire de même ! Transférer 2 000 photos d’un album
papier à un autre vous ennuierait à mourir...

Une seconde raison, et non des moindres : l’ordinateur va bien plus vite que nous. Il est
plus efficace ! Essayez de calculer mentalement 10 x 20 x 30 x 40 et comparez votre temps
de calcul à celui d’un ordinateur. Imbattable !

2.2.1 C’est logique !


Nous utilisons tous les jours des algorithmes quand nous avons plusieurs possibilités à
étudier et qu’il nous faut faire un choix. Prenons un exemple. Vous allez rendre visite à
votre grand-mère, mais, cornebleu, il y a des travaux sur la route et vous devez changer
votre trajet habituel. Aujourd’hui, vous lanceriez très certainement votre GPS pour que
l’ordinateur, notre ami, vous indique quelle route est la plus courte pour bien vite man-
ger la tarte aux pommes qui doit déjà sortir du four (il y a certaines priorités dans la vie !).

Qu’auriez-vous fait sans ordinateur ? Vous auriez déplié une carte papier et déterminé
l’itinéraire idéal en prenant en compte certains paramètres : vitesse maximale autorisée
sur chaque portion de route, péages, position actuelle. Vous auriez ensuite choisi le trajet
le plus court en fonction de tous ces paramètres.

Bravo, vous pourriez remplacer l’ordinateur ! Mais vous auriez certainement réfléchi dix
bonnes minutes... laissant ainsi à la tarte le temps de refroidir, seule et abandonnée dans
la cuisine de votre grand-mère. Heureusement, des informaticiens ont confectionné un
algorithme et l’ont implémenté dans le petit boîtier sur votre pare-brise !

2.2.2 Les algorithmes que nous connaissons


Pourquoi apprendre l’algorithmique ? Car ses concepts vous ouvrent des portes vers bien
des domaines allant de la recherche en ligne à la conception de jeux vidéo.

Site web
Vous avez certainement remarqué que Facebook adapte votre fil d’actualité en fonction
de votre activité. Votre page d’accueil n’affiche pas les dernières publications de vos amis,
mais bien ce que Facebook considère comme étant le contenu le plus pertinent pour vous
compte tenu de votre activité.

De même, lorsque vous effectuez une recherche sur Google, vous vous attendez à ce que la
page affiche les résultats les plus pertinents et non les derniers sites parus sur le domaine.
Google a donc besoin d’un modèle pour déterminer comment calculer la pertinence de
ces résultats en prenant en compte plusieurs paramètres tels que votre historique de re-
cherche, le nombre de visites sur un site, le nombre de liens pointant vers le site, etc.

CHAPITRE 2. NOTIONS D’ALGORITHME 45


Chargé du cours : M. Baïmi Badjoua ©UPM

Ces deux exemples utilisent des méthodes de machine learning (ou apprentissage auto-
matique) extrêmement intéressantes.

Déplacement
Votre GPS intègre un algorithme qui lui permet de déterminer le plus court chemin entre
votre position actuelle et celle que vous lui avez indiquée. De même, les nombreux sites
de réservation en ligne intègrent un algorithme qui gère les places libres dans un train,
mais également les correspondances et les problèmes éventuels (annulation, par exemple).

Les algorithmes sont également utilisés dans des logiciels de reconnaissance d’image ou
par votre banque lorsque vous effectuez des paiements sur Internet (détection de fraude).
C’est très puissant ! Nous pourrions trouver bien plus d’exemples d’algorithmes intégrés
à notre vie quotidienne. Leur point commun : répondre à une problématique que nous
nous posons par l’utilisation d’un programme.

2.2.3 Qu’est-ce qu’un programme ?


Un programme est un ensemble d’instructions exécutables par un ordinateur et qui permet
à ce dernier de répondre à un problème que nous nous posons. Par exemple, imaginons que
je souhaite écrire une nouvelle version du Seigneur des Anneaux. Je peux bien sûr utiliser
une feuille et un crayon, mais comment la partager avec des amis ? Que se passe-t-il si je
renverse mon café dessus ? Si un ami perd le manuscrit ? Etc. Après avoir pleuré toutes
les larmes de mon corps, et m’être juré de ne plus boire de café ni de fréquenter d’humains
pendant le restant de mes jours, je vais certainement allumer mon ordinateur et utiliser
Microsoft Word.

D’ailleurs, comment fonctionne un programme ? De manière assez simple : il prend un en-


semble de données en entrée, exécute des instructions puis retourne des données en sortie.
Tiens, cela me rappelle la manière dont nous communiquons ! Si mon père me demande
de mettre la table, il va me donner en entrée des assiettes, je vais mettre la table et lui dire
(en sortie) : ”La table est mise !” Reprenons Microsoft Word et l’action d’enregistrer un
document. Lorsque vous cliquez sur ”Enregistrer”, Word prendra en entrée le contenu non
enregistré (texte, images...), l’enregistrera, puis vous affichera un message de confirmation
(en sortie).

Les actions effectuées par un programme sont des instructions. Une instruction peut être
une opération de base, une exécution conditionnelle ou une itération.

— Opérations de base : addition, soustraction, multiplication, division... Exemple :


”tire” + ”-” + ”bouchon” => ”tire-bouchon”
— Exécution conditionnelle : si (condition), alors (fais ça), sinon (fais ça). Exemple :
si je suis connectée, affiche ”Salut Céline !”.
— Itération : répéter une instruction un nombre déterminé de fois. Exemple : affiche
chaque photo de mon album ”La vie est un combat”.

CHAPITRE 2. NOTIONS D’ALGORITHME 46


Chargé du cours : M. Baïmi Badjoua ©UPM

2.3 Conventions pour écrire un algorithme


Historiquement, plusieurs types de notations ont représenté des algorithmes.

— Il y a eu notamment une représentation graphique, avec des carrés, des lo-sanges,


etc. qu’on appelait des organigrammes. Cependant dès que l’algorithme commence
à grossir un peu, ce n’est plus pratique.
— On utilise généralement une série de conventions appelée « pseudo-code », qui
ressemble à un langage de programmation authentique dont on aurait évacuéla
plupart des problèmes de syntaxe. Ce pseudo-code est susceptible de varier légère-
ment d’un livre (ou d’un enseignant) à un autre.

On peut diviser un algorithme en 4 parties : la déclaration des variables, les entrées et


l’initialisation, le traitement, les sorties.

Exemple d’algorithme :

Figure 2.1 – Exemples d’algorithmes

2.4 Types d’instructions


Les ordinateurs, ne sont capables de comprendre que quatre catégories d’instructions. Ces
quatre familles d’instructions sont :

— la lecture
— l’affichage
— l’affectation de variables
— les tests
— les boucles

Un algorithmique exprime les instructions résolvant un problème donné indépendamment


des particularités de tel ou tel langage de programmation. Pour écrire un algorithme
sur un ordinateur, nous avons besoin d’un langage de programmation. « Un langage de
programmation est une convention pour donner des ordres à un ordinateur.» Il existe des

CHAPITRE 2. NOTIONS D’ALGORITHME 47


Chargé du cours : M. Baïmi Badjoua ©UPM

milliers de langage de programmation, ayant chacun ses spécificités. On peut citer par
exemple :

Figure 2.2 – Quelques langages de programmation

Un compilateur est un programme informatique qui transforme un code source écrit dans
un langage de programmation (le langage source) en langage machine (le langage cible).

Dans le cas de langage semi-compilé (ou intermédiaire), le code source est traduit en un
langage intermédiaire, sous forme binaire, avant d’être lui-même interprété ou compilé.

Un interpréteur se distingue d’un compilateur par le fait que, pour exécuter un programme,
les opérations d’analyse et de traduction sont réalisées à chaque exécution du programme
(par un interpréteur) plutôt qu’une fois pour toutes (par un compilateur).

Conclusion
Nous avons vu dans ce cours quelques notions d’algorithme. Il convient de préciser qu’un
algorithme est une suite finie d’instructions, qui une fois exécutée correctement, conduit
à un résultat donné. Pour fonctionner, un algorithme doit donc contenir uniquement des
instructions compréhensibles par celui qui devra l’exécuter.

Nous avons aussi montré l’interêt des algorithmes. En effet, ce sont les algorithmes, une
fois bien redigés qui nous permettrons de développer des logiciels de qualité.

Dans le chapitre suivant, nous allons parlé d’une notion particulière des algorithmes : la
complexité.

CHAPITRE 2. NOTIONS D’ALGORITHME 48


CHAPITRE 3
COMPLEXITÉS D’UN ALGORITHME

Introduction
L’algorithmique est defini comme l’étude des algorithmes. Un algorithme est quant à lui,
une suite d’instructions finies qui décrit comment résoudre un problème particulier en un
temps fini.

Avez-vous déjà ouvert un livre de recettes de cuisine ? ou déjà indiqué un chemin à un


touriste ? Si oui, sans le savoir, vous avez déjà exécuté des algorithmes.

Dans le cas de guide touristique, si l’algorithme est juste, le résultat est le résultat voulu,
et le touriste se retrouve là où il voulait aller. Si l’algorithme est faux, alors, le résultat est
aléatoire, et le touriste est perdu. Il est donc important de comprendre que pour fonction-
ner, un algorithme doit contenir uniquement des instructions compréhensibles par celui
qui devra l’exécuter.

Dans ce chapitre, on s’intéresse essentiellement à la complexité algorithmique, un aspect


très important si on veut concevoir des vrais programmes.

3.1 Algorithme et programme


Un programme (code) : la réalisation (l’implémentation) d’un algorithme au moyen d’un
langage donné (sur une architecture donnée). Il s’agit de la mise en œuvre du principe.

49
Chargé du cours : M. Baïmi Badjoua ©UPM

Figure 3.1 – Mise en oeuvre du principe d’un programme

Le raisonnement à l’origine du code peut être décrit par l’exemple suivant de tâche :
Décider si un tableau L est trié en ordre croissant. Pour ce faire :
— Raisonnement : un tableau L est trié si tous ses éléments sont dans l’ordre croissant.
L trié ⇐⇒ ∀i 0 < i < |L| et L[i] < L[i + 1]
— Algorithme : une fonction vérifiant cette propriété, supposera donc le tableau L,
de taille n, trié au départ et cherchera une contradiction.

1 F o n c t i o n t r i e (L : tab , n : e n t i e r ) : b o o l e e n
2 V a r i a b l e s i , n : e n t i e r ; Ok : b o o l e e n
3 Dé but
4 Ok <−− v r a i
5 pour i de 1 à n f a i r e
6 s i (L [ i ] > L [ i +1]) a l o r s
7 Ok <−− Faux
8 Finsi
9 Finpour
10 Retourner OK
11 Fin t r i e

— Code :

1 b o o l e a n t r i e ( tab L , i n t n )
2 {
3 OK = t r u e ;
4 f o r ( i =0 ; i < n ; i ++)
5 {
6 i f (L [ i ] > L [ i +1])
7 {
8 OK = f a l s e ;
9 }
10 }
11 r e t u r n OK ;
12 }

De ce qui précède, il en résulte une double problèmatique de l’algorithmique. En effet, la


question la plus fréquente qui se pose à chaque programmeur est la suivante : Comment
choisir parmi les différentes approches pour résoudre un problème ? Exemples : Trier un

CHAPITRE 3. COMPLEXITÉS D’UN ALGORITHME 50


Chargé du cours : M. Baïmi Badjoua ©UPM

tableau : algorithme de tri par insertion ou de tri rapide ?…, etc.

Cependant, on attend d’un d’un algorithme qu’il résolve correctement et de manière ef-
ficace le problème à résoudre, quelles que soient les données à traiter. Ce qui peut se
résumer en deux points essentiels :
1. La correction : l’algorithme résout il bien le problème donné ? Il faut donc trouver
une méthode de résolution (exacte ou approchée) du problème.
2. L’efficacité : en combien de temps et avec quelles ressources ? Il est souhaitable
que nos solutions ne soient pas lentes et ne prennent pas de l’espace mémoire
considérable.
Il convient donc de le préciser : ”Savoir résoudre un problème est une chose, le résoudre
efficacement en est une autre !”

3.2 L’efficacité d’un algorithme


L’efficacité d’un algorithme peut être évaluée par :
— Sa rapidité (en terme de temps d’exécution) ;
— Consommation de ressources (espace de stockage, mémoire utilisée).
La théorie de la complexité étudie l’efficacité des algorithmes. On s’intéresse dans ce
chapitre, essentiellement, à l’efficacité en terme de temps d’exécution.

3.3 Théorie de la complexité


3.3.1 Définition et Objectifs
La théorie de la complexité est une branche de l’informatique théorique, elle cherche à
calculer, formellement, la complexité algorithmique nécessaire pour résoudre un problème
P au moyen de l’exécution d’un algorithme A. Elle vise donc à répondre aux besoins
d’efficacité des algorithmes (programmes).

La théorie de la complexité permet de :


— Classer les problèmes selon leur difficulté ;
— Classer les algorithmes selon leur efficacité ;
— Comparer les algorithmes résolvant un problème donné afin de faire un choix sans
devoir les implémenter.

3.3.2 Evaluation de la rapidité d’un algorithme


Pour bien évaluer la rapidité d’un algorithme il faut tenir compte de ce qui suit :
— On ne mesure pas la durée en heure, minute, seconde… cela impliquerait d’implé-
menter les algorithmes qu’ on veut comparer.
— Ces mesures ne seraient pas pertinentes car le même algorithme sera plus rapide
sur une machine plus puissante.
— Au lieu de ça, on utilise des unités de temps abstraite proportionnelles au nombre
d’opérations effectuées.

CHAPITRE 3. COMPLEXITÉS D’UN ALGORITHME 51


Chargé du cours : M. Baïmi Badjoua ©UPM

— Au besoin, on pourra alors adapter ces quantités en fonction de la machine sur


laquelle l’algorithme s’exécute.

3.3.3 Calcul du temps d’exécution


Voici les règles sur lesquelles on se base pour calculer le temps d’exécution d’un algor-
tihme :
— Chaque instruction basique consomme une unité de temps (affectation d’une va-
riable, lecture, écriture, comparaison,…).
— Chaque itération d’une boucle rajoute le nombre d’unités de temps consommés
dans le corps de cette boucle.
— Chaque appel de fonction rajoute le nombre d’unités de temps consommées dans
cette fonction.
— Pour avoir le nombre d’opération effectuées par l’algorithme, on additionne le tout.
Exemple : Calculons le temps d’exécution de la fonction factorielle

L’algorithme suivant calcule : n! = n ∗ (n − 1) ∗ (n − 2) ∗ … ∗ 1 avec 0! = 1

CHAPITRE 3. COMPLEXITÉS D’UN ALGORITHME 52


Chargé du cours : M. Baïmi Badjoua ©UPM

Conclusion
Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a. Vestibulum ante ipsum primis
in faucibus orci luctus et ultrices posuere cubilia Curae ; Donec velit neque, auctor sit amet
aliquam vel, ullamcorper sit amet ligula. Vivamus magna justo, lacinia eget consectetur
sed, convallis at tellus. Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem.
Praesent sapien massa, convallis a pellentesque nec, egestas non nisi. Pellentesque in ipsum
id orci porta dapibus.

CHAPITRE 3. COMPLEXITÉS D’UN ALGORITHME 53


CHAPITRE 4
LES LISTES

Introduction
Pour stocker des données en mémoire, nous avons utilisé des variables simples (type int,
double…), des tableaux et des structures personnalisées. Si vous souhaitez stocker une
série de données, le plus simple est en général d’utiliser des tableaux.

Toutefois, les tableaux se révèlent parfois assez limités. Par exemple, si vous créez un
tableau de 10 cases et que vous vous rendez compte plus tard dans votre programme que
vous avez besoin de plus d’espace, il sera impossible d’agrandir ce tableau. De même, il
n’est pas possible d’insérer une case au milieu du tableau.

Les listes chaînées représentent une façon d’organiser les données en mémoire de manière
beaucoup plus flexible. Comme à la base le langage C ne propose pas ce système de
stockage, nous allons devoir le créer nous-mêmes de toutes pièces. C’est un excellent
exercice qui vous aidera à être plus à l’aise avec le langage.

4.1 Représentation d’une liste chaînée


Qu’est-ce qu’une liste chaînée ? Je vous propose de partir sur le modèle des tableaux.
Un tableau peut être représenté en mémoire comme sur la fig. suivante. Il s’agit ici d’un
tableau contenant des int.

Figure 4.1 – Représentation d’une liste chaînée

54
Chargé du cours : M. Baïmi Badjoua ©UPM

Comme je vous le disais en introduction, le problème des tableaux est qu’ils sont figés.
Il n’est pas possible de les agrandir, à moins d’en créer de nouveaux, plus grands (fig.
suivante). De même, il n’est pas possible d’y insérer une case au milieu, à moins de
décaler tous les autres éléments.

Figure 4.2 – Limites d’un tableau classique en C

Le langage C ne propose pas d’autre système de stockage de données, mais il est possible
de le créer soi-même de toutes pièces. Encore faut-il savoir comment s’y prendre : c’est
justement ce que ce chapitre et les suivants vous proposent de découvrir.

Une liste chaînée est un moyen d’organiser une série de données en mémoire. Cela consiste
à assembler des structures en les liant entre elles à l’aide de pointeurs. On pourrait les
représenter comme ceci :

Figure 4.3 – Représentation simple d’une liste chaînée

Chaque élément peut contenir ce que l’on veut : un ou plusieursint,double… En plus de


cela, chaque élément possède un pointeur vers l’élément suivant (fig. suivante).

Figure 4.4 – Représentation complete d’une liste chaînée

Je reconnais que tout cela est encore très théorique et doit vous paraître un peu flou pour
le moment. Retenez simplement comment les éléments sont agencés entre eux : ils forment
une chaîne de pointeurs, d’où le nom de « liste chaînée ».

NB : Contrairement aux tableaux, les éléments d’une liste chaînée ne sont pas placés côte
à côte dans la mémoire. Chaque case pointe vers une autre case en mémoire qui n’est pas
nécessairement stockée juste à côté.

4.2 Construction d’une liste chaînée


Passons maintenant au concret. Nous allons essayer de créer une structure qui fonctionne
sur le principe que nous venons de découvrir. Je rappelle que tout ce que nous allons

CHAPITRE 4. LES LISTES 55


Chargé du cours : M. Baïmi Badjoua ©UPM

faire ici fait appel à des techniques du langage C que vous connaissez déjà. Il n’y a aucun
élément nouveau, nous allons nous contenter de créer nos propres structures et fonctions
et les transformer en un système logique, capable de se réguler tout seul.

4.2.1 Un élément de la liste


Pour nos exemples, nous allons créer une liste chaînée de nombres entiers. Chaque élément
de la liste aura la forme de la structure suivante :

1 t y p e d e f s t r u c t é l é ment é l é ment ;
2 s t r u c t é l é ment
3 {
4 i n t nombre ;
5 é l é ment ∗ s u i v a n t ;
6 };

Nous avons créé ici un élément d’une liste chaînée, correspondant à la fig. suivante que
nous avons vue plus tôt. Que contient cette structure ?
— Une donnée, ici un nombre de type int : on pourrait remplacer cela par n’importe
quelle autre donnée (un double, un tableau…). Cela correspond à ce que vous voulez
stocker, c’est à vous de l’adapter en fonction des besoins de votre programme.
— Un pointeur vers un élément du même type appelé suivant. C’est ce qui permet de
lier les éléments les uns aux autres : chaque élément « sait » où se trouve l’élément
suivant en mémoire. Comme je vous le disais plus tôt, les cases ne sont pas côte
à côte en mémoire. C’est la grosse différence par rapport aux tableaux. Cela offre
davantage de souplesse car on peut plus facilement ajouter de nouvelles cases par
la suite au besoin.

4.2.2 La structure de contrôle


En plus de la structure qu’on vient de créer (que l’on dupliquera autant de fois qu’il y a
d’éléments), nous allons avoir besoin d’une autre structure pour contrôler l’ensemble de
la liste chaînée. Elle aura la forme suivante :

1 typedef struct Liste Liste ;


2 struct Liste
3 {
4 é l é ment ∗ p r e m i e r ;
5 };

Cette structure Liste contient un pointeur vers le premier élément de la liste. En effet,
il faut conserver l’adresse du premier élément pour savoir où commence la liste. Si on
connaît le premier élément, on peut retrouver tous les autres en « sautant » d’élément en
élément à l’aide des pointeurs suivant.

Nous n’aurons besoin de créer qu’un seul exemplaire de la structureListe. Elle permet de
contrôler toute la liste (fig. suivante).

CHAPITRE 4. LES LISTES 56


Chargé du cours : M. Baïmi Badjoua ©UPM

Figure 4.5 – Représentation exacte de notre liste chaînée

4.2.3 Le dernier élément de la liste


Notre schéma est presque complet. Il manque une dernière chose : on aimerait retenir le
dernier élément de la liste. En effet, il faudra bien arrêter de parcourir la liste à un mo-
ment donné. Avec quoi pourrait-on signifier à notre programme « Stop, ceci est le dernier
élément » ?

Il serait possible d’ajouter dans la structure Liste un pointeur vers le dernier élément.
Toutefois, il y a encore plus simple : il suffit de faire pointer le dernier élément de la
liste vers NULL, c’est-à-dire de mettre son pointeursuivant à NULL. Cela nous permet
de réaliser un schéma enfin complet de notre structure de liste chaînée (fig. suivante).

Figure 4.6 – Schéma complet de notre liste chaînée

4.3 Les fonctions de gestion de la liste


Nous avons créé deux structures qui permettent de gérer une liste chaînée :
— élément, qui correspond à un élément de la liste et que l’on peut dupliquer autant
de fois que nécessaire ;
— Liste, qui contrôle l’ensemble de la liste. Nous n’en aurons besoin qu’en un seul
exemplaire.
C’est bien, mais il manque encore l’essentiel : les fonctions qui vont manipuler la liste
chaînée. En effet, on ne va pas modifier « à la main » le contenu des structures à chaque
fois qu’on en a besoin ! Il est plus sage et plus propre de passer par des fonctions qui
automatisent le travail. Encore faut-il les créer.

CHAPITRE 4. LES LISTES 57


Chargé du cours : M. Baïmi Badjoua ©UPM

À première vue, je dirais qu’on aura besoin de fonctions pour :


— initialiser la liste ;
— ajouter un élément ;
— supprimer un élément ;
— afficher le contenu de la liste ;
— supprimer la liste entière.
On pourrait créer d’autres fonctions (par exemple pour calculer la taille de la liste) mais
elles sont moins indispensables. Nous allons ici nous concentrer sur celles que je viens de
vous énumérer, ce qui nous fera déjà une bonne base. Je vous inviterai ensuite à réaliser
d’autres fonctions pour vous entraîner une fois que vous aurez bien compris le principe.

4.3.1 Initialiser la liste


La fonction d’initialisation est la toute première que l’on doit appeler. Elle crée la struc-
ture de contrôle et le premier élément de la liste.

Je vous propose la fonction ci-dessous, que nous commenterons juste après, bien entendu :

1 Liste ∗ i n i t i a l i s a t i o n ()
2 {
3 L i s t e ∗ l i s t e = malloc ( s i z e o f (∗ l i s t e ) ) ;
4 Element ∗ e l e m e n t = m a l l o c ( s i z e o f ( ∗ e l e m e n t ) ) ;
5
6 i f ( l i s t e == NULL | | e l e m e n t == NULL)
7 {
8 e x i t (EXIT_FAILURE) ;
9 }
10

11 element−>nombre = 0 ;
12 element−>s u i v a n t = NULL ;
13 l i s t e −>p r e m i e r = e l e m e n t ;
14
15 return l i s t e ;
16 }

On commence par créer la structure de contrôle liste.

NB : Notez que le type de données est Liste et que la variable s’appelle liste. La majuscule
permet de les différencier.

On alloue dynamiquement la structure de contrôle avec un malloc. La taille à allouer est


calculée automatiquement avec sizeof(*liste). L’ordinateur saura qu’il doit allouer l’espace
nécessaire au stockage de la structure Liste.

On alloue ensuite de la même manière la mémoire nécessaire au stockage du premier élé-


ment. On vérifie si les allocations dynamiques ont fonctionné. En cas d’erreur, on arrête
immédiatement le programme en faisant appel à exit().

Si tout s’est bien passé, on définit les valeurs de notre premier élément :
— la donnée nombre est mise à 0 par défaut ;

CHAPITRE 4. LES LISTES 58


Chargé du cours : M. Baïmi Badjoua ©UPM

— le pointeur suivant pointe vers NULL car le premier élément de notre liste est
aussi le dernier pour le moment. Comme on l’a vu plus tôt, le dernier élément doit
pointer vers NULL pour signaler qu’il est en fin de liste.
Nous avons donc maintenant réussi à créer en mémoire une liste composée d’un seul
élément et ayant une forme semblable à la fig. suivante.

Figure 4.7 – Liste chaînée avec un seul élément

4.3.2 Ajouter un élément


Ici, les choses se compliquent un peu. Où va-t-on ajouter un nouvel élément ? Au début
de la liste, à la fin, au milieu ?

La réponse est qu’on a le choix. Libre à nous de décider ce que nous faisons. Pour ce
chapitre, je propose que l’on voie ensemble l’ajout d’un élément en début de liste. D’une
part, c’est simple à comprendre, et d’autre part cela me donnera une occasion à la fin
de ce chapitre de vous proposer de réfléchir à la création d’une fonction qui ajoute un
élément à un endroit précis de la liste.

Nous devons créer une fonction capable d’insérer un nouvel élément en début de liste.
Pour nous mettre en situation, imaginons un cas semblable à la fig. suivante : la liste est
composée de trois éléments et on souhaite en ajouter un nouveau au début.

Figure 4.8 – Ajout d’un nouvel élément dans la liste chaînée

Il va falloir adapter le pointeur premier de la liste ainsi que le pointeur suivant de notre
nouvel élément pour « insérer » correctement celui-ci dans la liste. Je vous propose pour
cela ce code source que nous analyserons juste après :

CHAPITRE 4. LES LISTES 59


Chargé du cours : M. Baïmi Badjoua ©UPM

1 v o i d i n s e r t i o n ( L i s t e ∗ l i s t e , i n t nvNombre )
2 {
3 /∗ Cr é a t i o n du n o u v e l é l é ment ∗/
4 Element ∗ nouveau = m a l l o c ( s i z e o f ( ∗ nouveau ) ) ;
5 i f ( l i s t e == NULL | | nouveau == NULL)
6 {
7 e x i t (EXIT_FAILURE) ;
8 }
9 nouveau−>nombre = nvNombre ;
10
11 /∗ I n s e r t i o n de l ’ é l é ment au dé but de l a l i s t e ∗/
12 nouveau−>s u i v a n t = l i s t e −>p r e m i e r ;
13 l i s t e −>p r e m i e r = nouveau ;
14 }

La fonction insertion() prend en paramètre l’élément de contrôle liste (qui contient l’adresse
du premier élément) et le nombre à stocker dans le nouvel élément que l’on va créer.

Dans un premier temps, on alloue l’espace nécessaire au stockage du nouvel élément et


on y place le nouveau nombre nvNombre. Il reste alors une étape délicate : l’insertion du
nouvel élément dans la liste chaînée.

Nous avons ici choisi pour simplifier d’insérer l’élément en début de liste. Pour mettre à
jour correctement les pointeurs, nous devons procéder dans cet ordre précis :
1. faire pointer notre nouvel élément vers son futur successeur, qui est l’actuel premier
élément de la liste ;
2. faire pointer le pointeur premier vers notre nouvel élément.
NB : On ne peut pas suivre ces étapes dans l’ordre inverse ! En effet, si vous faites d’abord
pointer premier vers notre nouvel élément, vous perdez l’adresse du premier élément de
la liste ! Faites le test, vous comprendrez de suite pourquoi l’inverse est impossible.

Cela aura pour effet d’insérer correctement notre nouvel élément dans la liste chaînée (fig.
suivante) !

Figure 4.9 – Notre liste chaînée, après ajout d’un nouvel élément au debut

Cette fonction est courte mais sauriez-vous la réécrire ? Il faut bien comprendre qu’on doit
faire les choses dans un ordre précis :
1. faire pointer premier vers le second élément ;
2. supprimer le premier élément avec unfree.
Si on faisait l’inverse, on perdrait l’adresse du second élément !

CHAPITRE 4. LES LISTES 60


Chargé du cours : M. Baïmi Badjoua ©UPM

4.3.3 Supprimer un élément


De même que pour l’insertion, nous allons ici nous concentrer sur la suppression du pre-
mier élément de la liste. Il est techniquement possible de supprimer un élément précis au
milieu de la liste, ce sera d’ailleurs un des exercices que je vous proposerai à la fin.

La suppression ne pose pas de difficulté supplémentaire. Il faut cependant bien adapter


les pointeurs de la liste dans le bon ordre pour ne « perdre » aucune information.

1 void suppression ( L i s t e ∗ l i s t e )
2 {
3 i f ( l i s t e == NULL)
4 {
5 e x i t (EXIT_FAILURE) ;
6 }
7

8 i f ( l i s t e −>p r e m i e r != NULL)
9 {
10 Element ∗ aSupprimer = l i s t e −>p r e m i e r ;
11 l i s t e −>p r e m i e r = l i s t e −>premier −>s u i v a n t ;
12 f r e e ( aSupprimer ) ;
13 }
14 }

On commence par vérifier que le pointeur qu’on nous envoie n’est pas NULL, sinon on ne
peut pas travailler. On vérifie ensuite qu’il y a au moins un élément dans la liste, sinon il
n’y a rien à faire.

Ces vérifications effectuées, on peut sauvegarder l’adresse de l’élément à supprimer dans


un pointeur aSupprimer. On adapte ensuite le pointeur premier vers le nouveau premier
élément, qui est actuellement en seconde position de la liste chaînée.

Il ne reste plus qu’à supprimer l’élément correspondant à notre pointeur aSupprimer avec
un free (fig. suivante).

Figure 4.10 – Notre liste chaînée, après suppression du premier élément

4.3.4 Afficher la liste chaînée


Pour bien visualiser ce que contient notre liste chaînée, une fonction d’affichage serait
idéale ! Il suffit de partir du premier élément et d’afficher chaque élément un à un en «
sautant » de bloc en bloc.

CHAPITRE 4. LES LISTES 61


Chargé du cours : M. Baïmi Badjoua ©UPM

1 void a f f i c h e r L i s t e ( L i s t e ∗ l i s t e )
2 {
3 i f ( l i s t e == NULL)
4 {
5 e x i t (EXIT_FAILURE) ;
6 }
7
8 Element ∗ a c t u e l = l i s t e −>p r e m i e r ;
9
10 w h i l e ( a c t u e l != NULL)
11 {
12 p r i n t f ( ”%d −> ” , a c t u e l −>nombre ) ;
13 a c t u e l = a c t u e l −>s u i v a n t ;
14 }
15 p r i n t f ( ”NULL\n” ) ;
16 }

Cette fonction est simple : on part du premier élément et on affiche le contenu de chaque
élément de la liste (un nombre). On se sert du pointeur suivant pour passer à l’élément
qui suit à chaque fois.

On peut s’amuser à tester la création de notre liste chaînée et son affichage avec un main :

1 i n t main ( )
2 {
3 L i s t e ∗ maListe = i n i t i a l i s a t i o n ( ) ;
4
5 i n s e r t i o n ( maListe , 4 ) ;
6 i n s e r t i o n ( maListe , 8 ) ;
7 i n s e r t i o n ( maListe , 1 5 ) ;
8 s u p p r e s s i o n ( maListe ) ;
9
10 a f f i c h e r L i s t e ( maListe ) ;
11

12 return 0 ;
13 }

En plus du premier élément (que l’on a laissé ici à 0), on en ajoute trois nouveaux à cette
liste. Puis on en supprime un. Au final, le contenu de la liste chaînée sera donc :

Figure 4.11 – Résultat après exécution de notre liste chaînée

4.4 Travaux Pratiques


Nous venons de faire le tour des principales fonctions nécessaires à la gestion d’une liste
chaînée : initialisation, ajout d’élément, suppression d’élément, etc. Voici quelques autres

CHAPITRE 4. LES LISTES 62


Chargé du cours : M. Baïmi Badjoua ©UPM

fonctions qui manquent et que je vous invite à écrire, ce sera un très bon exercice !

— Insertion d’un élément en milieu de liste : actuellement, nous ne pouvons ajouter


des éléments qu’au début de la liste, ce qui est généralement suffisant. Si toutefois
on veut pouvoir ajouter un élément au milieu, il faut créer une fonction spécifique
qui prend un paramètre supplémentaire : l’adresse de celui qui précèdera notre
nouvel élément dans la liste. Votre fonction va parcourir la liste chaînée jusqu’à
tomber sur l’élément indiqué. Elle y insèrera le petit nouveau juste après.
— Suppression d’un élément en milieu de liste : le principe est le même que pour
l’insertion en milieu de liste. Cette fois, vous devez ajouter en paramètre l’adresse
de l’élément à supprimer.
— Destruction de la liste : il suffit de supprimer tous les éléments un à un !
— Taille de la liste : cette fonction indique combien il y a d’éléments dans votre liste
chaînée. L’idéal, plutôt que d’avoir à calculer cette valeur à chaque fois, serait de
maintenir à jour un entier nbElements dans la structure Liste. Il suffit d’incrémenter
ce nombre à chaque fois qu’on ajoute un élément et de le décrémenter quand on
en supprime un.

Conclusion
En conclusion de ce chapitre très riche en enseignements, on peut retenir ce qui suit :
— Les listes chaînées constituent un nouveau moyen de stocker des données en mé-
moire. Elles sont plus flexibles que les tableaux car on peut ajouter et supprimer
des « cases » à n’importe quel moment.
— Il n’existe pas en langage C de système de gestion de listes chaînées, il faut l’écrire
nous-mêmes ! C’est un excellent moyen de progresser en algorithmique et en pro-
grammation en général.
— Dans une liste chaînée, chaque élément est une structure qui contient l’adresse de
l’élément suivant.
— Il est conseillé de créer une structure de contrôle (du type Liste dans notre cas)
qui retient l’adresse du premier élément.
— Il existe une version améliorée — mais plus complexe — des listes chaînées appelée
« listes doublement chaînées », dans lesquelles chaque élément possède en plus
l’adresse de celui qui le précède.

CHAPITRE 4. LES LISTES 63


CHAPITRE 5
LES PILES ET LES FILES

Introduction
Nous avons découvert avec les listes chaînées un nouveau moyen plus souple que les ta-
bleaux pour stocker des données. Ces listes sont particulièrement flexibles car on peut
insérer et supprimer des données à n’importe quel endroit, à n’importe quel moment.

Les piles et les files que nous allons découvrir ici sont deux variantes un peu particulières
des listes chaînées. Elles permettent de contrôler la manière dont sont ajoutés les nou-
veaux éléments. Cette fois, on ne va plus insérer de nouveaux éléments au milieu de la
liste mais seulement au début ou à la fin.

Les piles et les files sont très utiles pour des programmes qui doivent traiter des données
qui arrivent au fur et à mesure. Nous allons voir en détails leur fonctionnement dans ce
chapitre.

Les piles et les files sont très similaires, mais révèlent néanmoins une subtile différence
que vous allez rapidement reconnaître. Nous allons dans un premier temps découvrir les
piles qui vont d’ailleurs beaucoup vous rappeler les listes chaînées, à quelques mots de
vocabulaire près.

Globalement, ce chapitre sera simple pour vous si vous avez compris le fonctionnement
des listes chaînées. Si ce n’est pas le cas, retournez d’abord au chapitre précédent car nous
allons en avoir besoin.

5.1 Les piles


Imaginez une pile de pièces (fig. suivante). Vous pouvez ajouter des pièces une à une en
haut de la pile, mais aussi en enlever depuis le haut de la pile. Il est en revanche impossible
d’enlever une pièce depuis le bas de la pile. Si vous voulez essayer, bon courage !

64
Chargé du cours : M. Baïmi Badjoua ©UPM

Figure 5.1 – Une illustration des piles

5.1.1 Fonctionnement des piles


Le principe des piles en programmation est de stocker des données au fur et à mesure les
unes au-dessus des autres pour pouvoir les récupérer plus tard. Par exemple, imaginons
une pile de nombres entiers de type int (fig. suivante). Si j’ajoute un élément (on parle
d’empilage), il sera placé au-dessus, comme dans Tetris (fig. suivante).

Figure 5.2 – Empilage d’élément dans une pile

Le plus intéressant est sans conteste l’opération qui consiste à extraire les nombres de
la pile. On parle de dépilage. On récupère les données une à une, en commençant par
la dernière qui vient d’être posée tout en haut de la pile (fig. suivante). On enlève les
données au fur et à mesure, jusqu’à la dernière tout en bas de la pile.

Figure 5.3 – Dépilage d’élément dans une pile

CHAPITRE 5. LES PILES ET LES FILES 65


Chargé du cours : M. Baïmi Badjoua ©UPM

On dit que c’est un algorithme LIFO, ce qui signifie « Last In First Out ». Traduction :
« Le dernier élément qui a été ajouté est le premier à sortir ».

Les éléments de la pile sont reliés entre eux à la manière d’une liste chaînée. Ils possèdent
un pointeur vers l’élément suivant et ne sont donc pas forcément placés côte à côte en
mémoire. Le dernier élément (tout en bas de la pile) doit pointer vers NULL pour indiquer
qu’on a… touché le fond (fig. suivante).

Figure 5.4 – Représentation des éléménts d’une pile

À quoi est-ce que tout cela peut bien servir, concrètement ?

Il y a des programmes où vous avez besoin de stocker des données temporairement pour
les ressortir dans un ordre précis : le dernier élément que vous avez stocké doit être le
premier à ressortir.

Pour vous donner un exemple concret, votre système d’exploitation utilise ce type d’al-
gorithme pour retenir l’ordre dans lequel les fonctions ont été appelées. Imaginez un
exemple :
1. votre programme commence par la fonction main (comme toujours) ;
2. vous y appelez la fonction jouer ;
3. cette fonction jouer fait appel à son tour à la fonction charger ;
4. une fois que la fonction charger est terminée, on retourne à la fonction jouer ;
5. une fois que la fonction jouer est terminée, on retourne au main ;
6. enfin, une fois le main terminé, il n’y a plus de fonction à appeler, le programme
s’achève.
Pour « retenir » l’ordre dans lequel les fonctions ont été appelées, votre ordinateur crée
une pile de ces fonctions au fur et à mesure (fig. suivante).

CHAPITRE 5. LES PILES ET LES FILES 66


Chargé du cours : M. Baïmi Badjoua ©UPM

Figure 5.5 – Empilage et dépilage d’une pile

Voilà un exemple concret d’utilisation des piles. Grâce à cette technique, votre ordinateur
sait à quelle fonction il doit retourner. Il peut empiler 100 fonctions d’affilée s’il le faut, il
retrouvera toujours le main en bas !

5.1.2 Création d’un système de pile


Maintenant que nous connaissons le principe de fonctionnement des piles, essayons d’en
construire une. Comme pour les listes chaînées, il n’existe pas de système de pile intégré
au langage C. Il faut donc le créer nous-mêmes.

Chaque élément de la pile aura une structure identique à celle d’une liste chaînée :

1 t y p e d e f s t r u c t Element Element ;
2 s t r u c t Element
3 {
4 i n t nombre ;
5 Element ∗ s u i v a n t ;
6 };

La structure de contrôle contiendra l’adresse du premier élément de la pile, celui qui se


trouve tout en haut :

1 typedef struct Pile Pile ;


2 struct Pile
3 {
4 Element ∗ p r e m i e r ;
5 };

Nous aurons besoin en tout et pour tout des fonctions suivantes :


— empilage d’un élément ;
— dépilage d’un élément.
Vous noterez que, contrairement aux listes chaînées, on ne parle pas d’ajout ni de sup-
pression. On parle d’empilage et de dépilage car ces opérations sont limitées à un élément
précis, comme on l’a vu. Ainsi, on ne peut ajouter et retirer un élément qu’en haut de la
pile.

On pourra aussi écrire une fonction d’affichage de la pile, pratique pour vérifier si notre
programme se comporte correctement.

CHAPITRE 5. LES PILES ET LES FILES 67


Chargé du cours : M. Baïmi Badjoua ©UPM

Allons-y !

5.1.3 Empilage
Notre fonction empiler doit prendre en paramètre la structure de contrôle de la pile (de
type Pile) ainsi que le nouveau nombre à stocker. Je vous rappelle que nous stockons ici
des int, mais rien ne vous empêche d’adapter ces exemples avec un autre type de données.
On peut stocker n’importe quoi : des double, des char, des chaînes, des tableaux ou même
d’autres structures !

1 v o i d e m p i l e r ( P i l e ∗ p i l e , i n t nvNombre )
2 {
3 Element ∗ nouveau = m a l l o c ( s i z e o f ( ∗ nouveau ) ) ;
4 i f ( p i l e == NULL | | nouveau == NULL)
5 {
6 e x i t (EXIT_FAILURE) ;
7 }
8
9 nouveau−>nombre = nvNombre ;
10 nouveau−>s u i v a n t = p i l e −>p r e m i e r ;
11 p i l e −>p r e m i e r = nouveau ;
12 }

L’ajout se fait en début de pile car, comme on l’a vu, il est impossible de le faire au milieu
d’une pile. C’est le principe même de son fonctionnement, on ajoute toujours par le haut.
De ce fait, contrairement aux listes chaînées, on ne doit pas créer de fonction pour insérer
un élément au milieu de la pile. Seule la fonction empiler permet d’ajouter un élément.

5.1.4 Dépilage
Le rôle de la fonction de dépilage est de supprimer l’élément tout en haut de la pile, ça,
vous vous en doutiez. Mais elle doit aussi retourner l’élément qu’elle dépile, c’est-à-dire
dans notre cas le nombre qui était stocké en haut de la pile.

C’est comme cela que l’on accède aux éléments d’une pile : en les enlevant un à un. On ne
parcourt pas la pile pour aller y chercher le second ou le troisième élément. On demande
toujours à récupérer le premier.

Notre fonction depiler va donc retourner un int correspondant au nombre qui se trouvait
en tête de pile :

CHAPITRE 5. LES PILES ET LES FILES 68


Chargé du cours : M. Baïmi Badjoua ©UPM

1 int depiler ( Pile ∗ pile )


2 {
3 i f ( p i l e == NULL)
4 {
5 e x i t (EXIT_FAILURE) ;
6 }
7
8 i n t nombreDepile = 0 ;
9 Element ∗ e l e m e n t D e p i l e = p i l e −>p r e m i e r ;
10
11 i f ( p i l e != NULL && p i l e −>p r e m i e r != NULL)
12 {
13 nombreDepile = e l e m e n t D e p i l e −>nombre ;
14 p i l e −>p r e m i e r = e l e m e n t D e p i l e −>s u i v a n t ;
15 f r e e ( elementDepile ) ;
16 }
17

18 r e t u r n nombreDepile ;
19 }

On récupère le nombre en tête de pile pour le renvoyer à la fin de la fonction. On modifie


l’adresse du premier élément de la pile, puisque celui-ci change. Enfin, bien entendu, on
supprime l’ancienne tête de pile grâce à free.

5.1.5 Affichage de la pile


Bien que cette fonction ne soit pas indispensable (les fonctions empiler et depiler suffisent
à gérer une pile !), elle va nous être utile pour tester le fonctionnement de notre pile et
surtout pour « visualiser » le résultat.

1 void a f f i c h e r P i l e ( P i l e ∗ p i l e )
2 {
3 i f ( p i l e == NULL)
4 {
5 e x i t (EXIT_FAILURE) ;
6 }
7 Element ∗ a c t u e l = p i l e −>p r e m i e r ;
8
9 w h i l e ( a c t u e l != NULL)
10 {
11 p r i n t f ( ”%d\n” , a c t u e l −>nombre ) ;
12 a c t u e l = a c t u e l −>s u i v a n t ;
13 }
14
15 p r i n t f ( ” \n” ) ;
16 }

Cette fonction étant ridiculement simple, elle ne nécessite aucune explication (et toc !).

CHAPITRE 5. LES PILES ET LES FILES 69


Chargé du cours : M. Baïmi Badjoua ©UPM

En revanche, c’est le moment de faire un main pour tester le comportement de notre pile :

1 i n t main ( )
2 {
3 P i l e ∗ maPile = i n i t i a l i s e r ( ) ;
4
5 e m p i l e r ( maPile , 4) ;
6 e m p i l e r ( maPile , 8) ;
7 e m p i l e r ( maPile , 15) ;
8 e m p i l e r ( maPile , 16) ;
9 e m p i l e r ( maPile , 23) ;
10 e m p i l e r ( maPile , 42) ;
11
12 p r i n t f ( ” \ nEtat de l a p i l e :\n” ) ;
13 a f f i c h e r P i l e ( maPile ) ;
14

15 p r i n t f ( ” Je d e p i l e %d\n” , d e p i l e r ( maPile ) ) ;
16 p r i n t f ( ” Je d e p i l e %d\n” , d e p i l e r ( maPile ) ) ;
17
18 p r i n t f ( ” \ nEtat de l a p i l e :\n” ) ;
19 a f f i c h e r P i l e ( maPile ) ;
20

21 return 0 ;
22 }

On affiche l’état de la pile après plusieurs empilages et une autre fois après quelques
dépilages. On affiche aussi le nombre qui est dépilé à chaque fois que l’on dépile. Le
résultat dans la console est le suivant :

Figure 5.6 – Résultat exécution exemple d’une pile

Vérifiez que vous voyez bien ce qui se passe dans ce programme. Si vous comprenez cela,
vous avez compris le fonctionnement des piles !

CHAPITRE 5. LES PILES ET LES FILES 70


Chargé du cours : M. Baïmi Badjoua ©UPM

5.2 Les files


Les files ressemblent assez aux piles, si ce n’est qu’elles fonctionnent dans le sens inverse !

5.2.1 Fonctionnement des files


Dans ce système, les éléments s’entassent les uns à la suite des autres. Le premier qu’on
fait sortir de la file est le premier à être arrivé. On parle ici d’algorithme FIFO (First In
First Out), c’est-à-dire « Le premier qui arrive est le premier à sortir ».

Il est facile de faire le parallèle avec la vie courante. Quand vous allez prendre un billet
de cinéma, vous faites la queue au guichet (fig. suivante). À moins d’être le frère du
guichetier, vous allez devoir faire la queue comme tout le monde et attendre derrière.
C’est le premier arrivé qui sera le premier servi.

Figure 5.7 – Représentation simple d’une file

En programmation, les files sont utiles pour mettre en attente des informations dans
l’ordre dans lequel elles sont arrivées. Par exemple, dans un logiciel de chat (type mes-
sagerie instantanée), si vous recevez trois messages à peu de temps d’intervalle, vous les
enfilez les uns à la suite des autres en mémoire. Vous vous occupez alors du premier
message arrivé pour l’afficher à l’écran, puis vous passez au second, et ainsi de suite.
Les événements que vous envoie la bibliothèque SDL que nous avons étudiée sont eux
aussi stockés dans une file. Si vous bougez la souris, un événement sera généré pour
chaque pixel dont s’est déplacé le curseur de la souris. La SDL les stocke dans une file
puis vous les envoie un à un à chaque fois que vous faites appel à SDL_PollEvent (ou à
SDL_WaitEvent : oui, c’est bien ! Je vois que ça suit au fond de la classe. ;) ).
En C, une file est une liste chaînée où chaque élément pointe vers le suivant, tout comme
les piles. Le dernier élément de la file pointe vers NULL (fig. suivante).

Figure 5.8 – Représentation complete d’une file

CHAPITRE 5. LES PILES ET LES FILES 71


Chargé du cours : M. Baïmi Badjoua ©UPM

5.2.2 Création d’un système de file


Le système de file va ressembler à peu de choses près aux piles. Il y a seulement quelques
petites subtilités étant donné que les éléments sortent de la file dans un autre sens, mais
rien d’insurmontable si vous avez compris les piles.

Nous allons créer une structure Element et une structure de contrôle File :

1 t y p e d e f s t r u c t Element Element ;
2 s t r u c t Element
3 {
4 i n t nombre ;
5 Element ∗ s u i v a n t ;
6 };
7
8 typedef struct File File ;
9 struct File
10 {
11 Element ∗ p r e m i e r ;
12 };

Comme pour les piles, chaque élément de la file sera de type Element. À l’aide du pointeur
premier, nous disposerons toujours du premier élément et nous pourrons remonter jusqu’au
dernier.

5.2.3 Enfilage
La fonction qui ajoute un élément à la file est appelée fonction « d’enfilage ». Il y a deux
cas à gérer :
— soit la file est vide, dans ce cas on doit juste créer la file en faisant pointer premier
vers le nouvel élément créé ;
— soit la file n’est pas vide, dans ce cas il faut parcourir toute la file en partant
du premier élément jusqu’à arriver au dernier. On rajoutera notre nouvel élément
après le dernier.
Voici comment on peut faire dans la pratique :

CHAPITRE 5. LES PILES ET LES FILES 72


Chargé du cours : M. Baïmi Badjoua ©UPM

1 v o i d e n f i l e r ( F i l e ∗ f i l e , i n t nvNombre )
2 {
3 Element ∗ nouveau = m a l l o c ( s i z e o f ( ∗ nouveau ) ) ;
4 i f ( f i l e == NULL | | nouveau == NULL)
5 {
6 e x i t (EXIT_FAILURE) ;
7 }
8
9 nouveau−>nombre = nvNombre ;
10 nouveau−>s u i v a n t = NULL ;
11

12 i f ( f i l e −>p r e m i e r != NULL) /∗ La f i l e n ’ e s t pas v i d e ∗/


13 {
14 /∗ On s e p o s i t i o n n e à l a f i n de l a f i l e ∗/
15 Element ∗ e l e m e n t A c t u e l = f i l e −>p r e m i e r ;
16 w h i l e ( e l e m e n t A c t u e l −>s u i v a n t != NULL)
17 {
18 e l e m e n t A c t u e l = e l e m e n t A c t u e l −>s u i v a n t ;
19 }
20 e l e m e n t A c t u e l −>s u i v a n t = nouveau ;
21 }
22 e l s e /∗ La f i l e e s t vide , n o t r e é l é ment e s t l e p r e m i e r ∗/
23 {
24 f i l e −>p r e m i e r = nouveau ;
25 }
26 }

Vous voyez dans ce code le traitement des deux cas possibles, chacun devant être géré à
part. La différence par rapport aux piles, qui rajoute une petite touche de difficulté, est
qu’il faut se placer à la fin de la file pour ajouter le nouvel élément. Mais bon, un petit
while et le tour est joué, comme vous pouvez le constater. :-)

5.2.4 Défilage
Le défilage ressemble étrangement au dépilage. Étant donné qu’on possède un pointeur
vers le premier élément de la file, il nous suffit de l’enlever et de renvoyer sa valeur.

CHAPITRE 5. LES PILES ET LES FILES 73


Chargé du cours : M. Baïmi Badjoua ©UPM

1 int de f i le r ( File ∗ f i l e )
2 {
3 i f ( f i l e == NULL)
4 {
5 e x i t (EXIT_FAILURE) ;
6 }
7
8 i n t nombreDefile = 0 ;
9
10 /∗ On v é r i f i e s ’ i l y a q u e l q u e c h o s e à d é f i l e r ∗/
11 i f ( f i l e −>p r e m i e r != NULL)
12 {
13 Element ∗ e l e m e n t D e f i l e = f i l e −>p r e m i e r ;
14
15 n o m b r e D e f i l e = e l e m e n t D e f i l e −>nombre ;
16 f i l e −>p r e m i e r = e l e m e n t D e f i l e −>s u i v a n t ;
17 free ( elementDefile ) ;
18 }
19
20 return nombreDefile ;
21 }

5.2.5 Travaux Pratiques


À vous de jouer ! Il resterait à écrire une fonction afficherFile, comme on l’avait fait pour
les piles. Cela vous permettrait de vérifier si votre file se comporte correctement.

Réalisez ensuite un main pour faire tourner votre programme. Vous devriez pouvoir ob-
tenir un rendu similaire à ceci :

Figure 5.9 – Résultats d’exécution d’une file

À terme, vous devriez pouvoir créer votre propre bibliothèque de files, avec des fichiers
file.h et file.c par exemple. La même chose peut se faire avec pile.c et pile.h ou encore avec
liste_chainee.c et liste_chainee.h

Conclusion
Au terme de ce chapitre il est important de retenir ce qui suit :
— Les piles et les files permettent d’organiser en mémoire des données qui arrivent
au fur et à mesure.

CHAPITRE 5. LES PILES ET LES FILES 74


Chargé du cours : M. Baïmi Badjoua ©UPM

— Elles utilisent un système de liste chaînée pour assembler les éléments.


— Dans le cas des piles, les données s’ajoutent les unes au-dessus des autres. Lorsqu’on
extrait une donnée, on récupère la dernière qui vient d’être ajoutée (la plus récente).
On parle d’algorithme LIFO (Last In First Out).
— Dans le cas des files, les données s’ajoutent les unes à la suite des autres. On
extrait la première donnée à avoir été ajoutée dans la file (la plus ancienne). On
parle d’algorithme FIFO (First In First Out).

CHAPITRE 5. LES PILES ET LES FILES 75


CHAPITRE 6
LES ARBRES BINAIRES

Introduction
Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a. Vestibulum ante ipsum primis
in faucibus orci luctus et ultrices posuere cubilia Curae ; Donec velit neque, auctor sit amet
aliquam vel, ullamcorper sit amet ligula. Vivamus magna justo, lacinia eget consectetur
sed, convallis at tellus. Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem.
Praesent sapien massa, convallis a pellentesque nec, egestas non nisi. Pellentesque in ipsum
id orci porta dapibus.

Conclusion
Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a. Vestibulum ante ipsum primis
in faucibus orci luctus et ultrices posuere cubilia Curae ; Donec velit neque, auctor sit amet
aliquam vel, ullamcorper sit amet ligula. Vivamus magna justo, lacinia eget consectetur
sed, convallis at tellus. Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem.
Praesent sapien massa, convallis a pellentesque nec, egestas non nisi. Pellentesque in ipsum
id orci porta dapibus.

76
CHAPITRE 7
ÉTUDE DE QUELQUES ALGORITHMES

Introduction
Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a. Vestibulum ante ipsum primis
in faucibus orci luctus et ultrices posuere cubilia Curae ; Donec velit neque, auctor sit amet
aliquam vel, ullamcorper sit amet ligula. Vivamus magna justo, lacinia eget consectetur
sed, convallis at tellus. Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem.
Praesent sapien massa, convallis a pellentesque nec, egestas non nisi. Pellentesque in ipsum
id orci porta dapibus.

Conclusion
Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a. Vestibulum ante ipsum primis
in faucibus orci luctus et ultrices posuere cubilia Curae ; Donec velit neque, auctor sit amet
aliquam vel, ullamcorper sit amet ligula. Vivamus magna justo, lacinia eget consectetur
sed, convallis at tellus. Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem.
Praesent sapien massa, convallis a pellentesque nec, egestas non nisi. Pellentesque in ipsum
id orci porta dapibus.

77
CONCLUSION GÉNÉRALE

Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a. Vestibulum ante ipsum primis
in faucibus orci luctus et ultrices posuere cubilia Curae ; Donec velit neque, auctor sit amet
aliquam vel, ullamcorper sit amet ligula. Vivamus magna justo, lacinia eget consectetur
sed, convallis at tellus. Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem.
Praesent sapien massa, convallis a pellentesque nec, egestas non nisi. Pellentesque in ipsum
id orci porta dapibus.

78
RÉFÉRENCES BIBLIOGRAPHIQUES

1 Notes de cours du professeur ;


2 https://www.google.fr/, Moteur de recherche Google ;
3 https://fr.wikipedia.com/, Moteur de recherche Wikipedia ;
4 https://www.commentcamarche.net/, Moteur de recherche CommentCaMarche ;
5 D’autres ressources sont également disponibles sur le Web.

79

Vous aimerez peut-être aussi

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy