Beruflich Dokumente
Kultur Dokumente
SUPPORT DE COURS
PROFESSEUR
ADIL SAYOUTI
ANNEE UNIVERSITAIRE
2017 / 2018
Ecole Royale Navale Algorithmique, Structures
1°A Cycle Ingénieur de données et Langage C
V. Organigramme ................................................................................................. 13
1. Définition ............................................................................................................................................................. 13
2. Symboles .............................................................................................................................................................. 13
3. Sélection ............................................................................................................................................................... 14
4. Itération ................................................................................................................................................................ 14
I. Algorithmique et programmation
1. Notion de programme
Si l’on s’intéresse aux applications de l’ordinateur, on s’aperçoit qu’elles sont très nombreuses.
En voici quelques exemples : gestion d’une école, établir le bulletin de paie des employés,
gestion de stocks…
Un ordinateur pour qu’il puisse effectuer des tâches aussi variées il suffit de le programmer.
Effectivement l’ordinateur est capable de mettre en mémoire un programme qu’on lui fournit
puis l’exécuter. Plus précisément, l’ordinateur possède un ensemble limité d’opérations
élémentaires qu’il sait exécuter. Un programme est constitué d’un ensemble de directives,
nommées instructions, qui spécifient les opérations élémentaires à exécuter et la façon dont
elles s’enchaînent.
Pour s’exécuter, un programme nécessite qu’on lui fournisse ce qu’on peut appeler « données ».
En retour, le programme va fournir des « résultats ».
Par exemple un programme de calcul de la moyenne générale d’un étudiant nécessite des
informations suivantes : nom de l’étudiant, la note et le coefficient de chaque matière. Les
résultats seront imprimés sur les bulletins de notes.
Voici quelques exemples de langages de programmation : pascal, Visual Basic, C, C++, java, etc.
L’objectif de ce cours est d’étudier les éléments de base intervenant dans un algorithme
(programme): variables, affectation, lecture, écriture, sélection, itération, structure de données…
3. Présentation du langage C
Le langage C a été créé en 1972 par Denis Ritchie afin de développer un système d’exploitation
(UNIX). Mais ses qualités opérationnelles l’ont vite fait adopter par une large communauté de
programmeurs. Une première définition de C est apparue en 1978 avec l’ouvrage de Kernighan
et Ritchie « The C programming language ».
Le langage C est normalisé par l’ANSI (American National Standards Institute), puis par l’ISO
(International Standards Organization) en 1990 et en 1993 par le CEN (comité européen de
normalisation) « C ANSI » ou « C norme ANSI».
II. Variables
1. Données et variables
1.1. Notion de variable
Dans un programme informatique, on va avoir en permanence besoin de stocker provisoirement
en mémoire des valeurs. Il peut s’agir de données issues du disque dur ou fournies par
l’utilisateur (frappées au clavier). Il peut aussi s’agir de résultats obtenus par le programme,
intermédiaires ou définitifs. Ces données peuvent être de plusieurs types : elles peuvent être des
nombres, du texte, etc. Dès que l’on a besoin de stocker une information (donnée) au cours
d’un programme, on utilise une variable. Une variable est un nom qui sert à repérer un
emplacement donné de la mémoire, c’est à dire que la variable ce n’est qu’une adresse de
mémoire. Cette notion contribue considérablement à faciliter la réalisation des programmes. Elle
permet de manipuler des données sans avoir à se préoccuper de l’emplacement qu’elles
occupent effectivement en mémoire. Pour cela, il vous suffit tout simplement de leur choisir un
nom (identificateur).
Les identificateurs servent à désigner les différents "objets" manipulés par le programme :
variables, fonctions, etc. les identificateurs sont composés d’une suite de caractères
alphanumériques. Le premier d’entre eux étant nécessairement une lettre. La norme ANSI
(American National Standards Institute) prévoit qu’au moins les 31 premières lettres soient
significatives.
Remarque : Les mots-clefs, sont réservés pour le langage lui-même et ne peuvent pas être
utilisés comme identificateurs. Le langage C compte 32 mots-clés :
2. Types de variables
2.1. Type numérique
A. ENTIER
Le type entier désigne l’ensemble des nombres entiers négatifs ou positifs dont les valeurs
varient entre -32 768 à 32 767. On écrit alors : VARIABLES i, j, k : ENTIER
En langage C, la syntaxe de déclaration d’une variable de type entier : int i, j, k ;
La taille d’un entier est égale à 2 octets (processeur 16 bits) et à 4 Octets (processeur 32 bits).
B. REEL
Le type réel comprend les variables numériques qui ont des valeurs réelles. La plage des valeurs
du type réel est : de -3,40x1038 à -1,40x10-45 pour les valeurs négatives et de 1,40x10-45 à
3,40x1038 pour les valeurs positives.
On écrit alors : VARIABLES x, y : REEL
En langage C, la syntaxe de déclaration d’une variable de type réel (taille = 4 octets): float x, y ;
3. Opérateurs et expressions
Un opérateur est un signe qui relie deux variables pour produire un résultat. Une expression est
un ensemble de variables (ou valeurs) reliées par des opérateurs et dont la valeur du résultat de
cette combinaison est unique. Exemple : 6+8 , x+15-y
Le langage C est l'un des langages les plus fournis en opérateurs. Cette richesse se manifeste au
niveau des opérateurs classiques (arithmétiques, relationnels et logiques) et les opérateurs
originaux d'affectation et d'incrémentation.
3.1. Affectation
L’affection est opération qui consiste à attribuer une valeur à une variable. On la notera avec le
signe←. Cette instruction s’écrit : VARIABLE ← valeur
Exemple : Note←14.5
On peut aussi attribuer à une variable la valeur d’une variable ou d’une expression de façon
générale.
On écrit : VARIABLE ← EXPRESSION
Exemple : A←B*2+5
Dans ce cas, l’instruction d’affectation sera exécutée en deux temps :
o D’abord, on calcule la valeur de l’expression
o On affecte la valeur obtenue à la variable à gauche.
On peut même avoir des cas où la variable de gauche qui figure dans l’expression à droite.
Par exemple : A ← A + 5
Dans cet exemple, après l’exécution de l’instruction d’affectation la valeur de la variable A sera
augmentée de 5.
En C, l’affectation est symbolisée par le signe =. Sa syntaxe est la suivante :
variable = expression ;
Le terme de gauche de l’affectation peut être une variable simple, un élément de tableau mais
pas une constante. Cette expression a pour effet d’évaluer expression et d’affecter la valeur
obtenue à variable.
Remarque :
o Il est possible d'initialiser une variable lors de sa déclaration comme dans : int n = 10 ;
o Il est possible de déclarer que la valeur d'une variable ne doit pas changer lors de l'exécution
du programme. Par exemple, avec : Const int n = 20 ;
Exercice.
Quelles seront les valeurs des variables A et B après exécution des instructions ci-après ?
Les deux expressions sont évaluées puis comparées. La valeur rendue est de type int (il n’y a pas
de type booléen en C); elle vaut 1 si la condition est vraie, et 0 sinon.
Attention : à ne pas confondre l’opérateur de test d’égalité == avec l’opérateur d’affection = .
int a = 3, b, c;
b = ++a; /* a et b valent 4 */
c = b++; /* c vaut 4 et b vaut 5 */
4. Entrées/Sorties
Considérons l’algorithme suivant :
VARIABLE A : ENTIER
Début
A ← 12 ^ 2 ;
Fin
Il permet de calculer le carré de 12. Le problème de ce programme, c’est que, si l’on veut calculer
le carré d’un autre nombre que 12, il faut réécrire le programme. C’est pour cela qu’il faut
introduire des instructions qui permettent le dialogue avec l’utilisateur.
En effet, il existe une instruction qui permet à l’utilisateur de faire entrer des valeurs au clavier
pour qu’elles soient utilisées par le programme.
f float écrit en notation "décimale" avec six chiffres après le point (123.456789)
s chaîne de caractères
III. Sélection
1. Introduction
Dans un programme, les instructions sont exécutées séquentiellement, c.à.d. dans l'ordre où elles
apparaissent. Or la puissance et le "comportement intelligent" d'un programme proviennent
essentiellement de la possibilité d'effectuer des sélections (choix) et de se comporter
différemment suivant les circonstances. Les instructions permettant d’effectuer un choix est
appelée instructions alternatives.
2. Sélection simple
La syntaxe de la sélection simple est :
SI condition ALORS
bloc 1 d'instructions ;
SINON
bloc 2 d'instructions ;
FIN SI
Si la condition mentionnée après SI est VRAI, on exécute le bloc1 d'instructions (ce qui figure
après le mot ALORS); si la condition est fausse, on exécute le bloc2 d'instructions (ce qui
figure après le mot SINON).
Exemple : SI a > 0 ALORS
ECRIRE ''valeur positive''
SINON
ECRIRE ''valeur négative''
FIN SI
Dans ce programme, on vérifie si la valeur de a est supérieure à 0, on affichera le message
''valeur positive''. Dans le cas contraire, il sera affiche le message ''valeur négative''.
La structure alternative peut prendre une autre forme possible où l'une des parties du choix est
absente. Elle s'écrit dans ce cas :
SI condition ALORS
bloc d'instructions
FIN SI
3. Sélection imbriquée
Il peut arriver que l'une des parties d'une instruction alternative (sélection) contienne à son tour
une instruction alternative. Dans ce cas, on dit qu'on a des sélection imbriquées les unes dans les
autres. L'instruction d'un if peut contenir un autre if :
if (expression)
instruction contenant d’autres instructions if
Remarque: un else se rapporte toujours au dernier if rencontré auquel un else n’a pas encore
été attribué.
4. Choix multiples
Elle permet d'associer à différentes valeurs discrètes des instructions à exécuter.
Cette instruction peut être réalisée par une cascade de SI SINON , mais elle offre une
présentation et une lecture plus agréable et compréhensive. Sa syntaxe est :
SELON expression
Valeur 1 : action 1
valeur 2 : action 2
…
Valeur N : action N
SINON : action
FIN SELON
Si expression est égale à valeur i, on exécute action i et on passe à la suite de l’algorithme. Sinon
on exécute action et on passe à la suite de l’algorithme.
En langage C, l’instruction switch permet de faire un choix. Sa syntaxe est :
switch (expression )
{
case constante_1:
liste d’instructions_1
break;
case constante_2:
Liste d’instructions_2
break;
...
default:
liste d’instructions_n
break;
}
Si la valeur de expression est égale à l’une des constantes, la liste d’instructions correspondante
est exécutée. Sinon la liste d’instructions_n correspondante à default est exécutée. break
provoque une sortie du bloc.
Exemple :
IV. Itération
1. Introduction
Reprenons le programme (série 2, exercice 1) qui calcule la moyenne générale. L’exécution de ce
programme fournit la moyenne générale des notes uniquement pour un seul étudiant. S’il l’on
veut les moyennes de 120 étudiants, il faut ré-exécuter ce programme 120 fois. Afin d’éviter
cette tâche fastidieuse d’avoir ré-exécuter le programme 120 fois, on peut faire recours à ce
qu’on appelle les structures itératives, appelées aussi les structures répétitives ou boucles.
Les boucles permettent de répéter une série d’instructions tant qu’une condition est vérifiée. Il
existe trois formes de structures répétitives : POUR (for en C), TANT QUE (While en C),
REPETER (do while en C).
Exemple : Réécrivons le programme de la moyenne générale de façon qu’il puisse calculer les
moyennes de 3 étudiants.
3. Boucle indéterministe
Lorsque le nombre d'itérations qu'effectuera la boucle n'est pas connu d'avance ou que le pas est
variable, il existe deux autres types de boucles, à savoir TANT QUE (while en C) et REPETER (do-
--while en C).
Ici, instructions seront exécutées tant que expression est verifiée. Cela signifie donc que
instructions sont toujours exécutées au moins une fois.
Exemple : Un programme qui demande un nombre à l’utilisateur (en affichant la valeur lue) tant
qu’il ne fournit pas une valeur négative.
4. Boucle imbriquée
Une boucle imbriquée est une boucle dans une boucle, une boucle à l'intérieur du corps d'une
autre boucle. Ce qui se passe est que le premier tour de la boucle externe déclenche la boucle
interne, qui s'exécute jusqu'au bout. Puis le deuxième tour de la boucle externe déclenche la
boucle interne une nouvelle fois. Ceci se répète jusqu'à ce que la boucle externe termine. Bien
sûr, un break à l'intérieur de la boucle interne ou externe peut interrompre ce processus.
Exemple : Dans l’exemple ci-dessous, le programme demande à l’utilisateur de saisir le nombre
d’étudiants et le nombre de matières. La boucle externe correspond aux étudiants. La boucle
interne correspond aux matières par étudiant.
V. Organigramme
1. Définition
Un organigramme est une représentation graphique normalisée d’un algorithme. Un
organigramme a les caractéristiques suivantes :
Il comporte des symboles et des liaisons ;
Il est fermé ;
Il comporte un début et une fin.
2. Symboles
Début, Fin
Début ou fin d’un organigramme.
Traitement
Opération ou groupe d'opérations (Instructions) sur des données.
Entrée / Sortie
Lecture d'une information à traiter ou l’affichage d'une information traitée.
Embranchement (Choix)
Test de condition (vraie ou faux) impliquant un choix.
Le sens général de la lecture est de haut vers le bas, et de gauche à droite. Lorsque le sens ainsi
défini n'est pas respecté, L’utilisation des flèches permet d’indiquer le sens utilisé.
3. Sélection
La structure alternative (conditionnelle) SI…. ALORS…..SINON en organigramme est
représentée dans la figure ci-dessous.
………
SI CONDITION
ALORS action 1
SINON action 2
FINSI
4. Itération
1.3. Boucle POUR
Le nombre d’exécutions de la boucle est connu.
………
POUR variable
De valeur initiale
A valeur finale
PARPASDE pas
FAIRE action
FINPOUR
………
……….
TANTQUE condition
FAIRE action
FINTANTQUE
…………
………….
FAIRE action
JUSQU'A condition
FINFAIRE
…………..
Exercice :
Ecrire un programme en C qui permet de résoudre dans R une équation du second degré
AX2+BX+C = 0. L’organigramme est présenté ci-dessous.
Si l’on veut toujours calculer la moyenne des notes d’une classe mais en gardant en mémoire
toutes les notes des élèves pour d’éventuels calculs (par exemple calculer le nombre d’élèves qui
ont des notes supérieurs à la moyenne). Dans ce cas il faudrait alors déclarer autant de variables
qu’il y a d’étudiants. Donc, si l’on a 20 élèves il faut déclarer 20 variables et si l’on a N il faut
déclarer N variables qui n’est pas pratique. Ce qu’il faudrait c’est pouvoir par l’intermédiaire
d’une seule variable stocker plusieurs valeurs de même type. Ce type de variable est appelé
tableau.
Un tableau est un ensemble de valeurs de même type portant le même nom de variable. Chaque
valeur du tableau est repérée par un indice précisant sa position au sein de l'ensemble (exemple
d’un vecteur).
Les types char, int et float sont des types "simples", car, à un instant donné, une variable d'un tel
type contient une seule valeur. Ils s'opposent aux tableaux qui correspondent à des variables
qui, à un instant donné, contiennent plusieurs valeurs de même type.
La déclaration d’un tableau sera via la syntaxe suivante dans la partie des déclarations :
Tableau nom_tableau (nombre) : Type
nom_tableau : désigne le nom du tableau
nombre : désigne le nombre d’éléments du tableau, c.à.d. sa taille
Type : c’est le type du tableau : le type de tous ces éléments
L’accès (en écriture et en lecture) à la i ème valeur d’un tableau se fait à l’aide de la syntaxe
suivante : nom_tableau (indice)
Par exemple si X est un tableau de 10 entiers :
o X (2) ← - 5 place la valeur -5 dans la 3 ème case du tableau.
o Lire X (1) met l’entier saisi par l’utilisateur dans la deuxième case du tableau
o Ecrire X (2) affiche la valeur de la troisième case du tableau
En langage C, la syntaxe de déclaration d’un tableau à une dimension est la suivante:
type_elements nom_tableau [nb_cases] ;
Exemple d’un algorithme qui calcule la somme des éléments d’un tableau :
Variables i, somme : ENTIERS
Tableau T (N) : ENTIER
DEBUT
somme ← 0
POUR i = 1 A N
somme ← somme + T (i)
FIN POUR
Ecrire ‘’La somme de tous les éléments du tableau est : ‘’, somme
FIN
T (3,5)
T (1,2) et T (3,5) sont deux éléments du tableau. Les valeurs des indices sont incluses entre
parenthèses. Le premier sert à repérer le numéro de la ligne, le second repère la colonne.
On accède en lecture ou en écriture à la valeur d’un élément d’un tableau à deux dimensions en
utilisant la syntaxe suivante : Nom_tableau (i , j)
Exemple: soit T un tableau à deux dimensions (3 lignes et 2 colonnes) Tableau T(3,2): Réel
T (2,1) ← -1.2 place la valeur -1.2 dans la case 2,1 du tableau
Soit a une variable de type Réel, a ← T (2,1) place -1.2 dans a.
On peut imaginer le tableau sous la forme d'un rectangle avec taille_dim1 qui représente le
nombre de lignes et taille_dim2 qui représente le nombre de colonnes.
o Affecter des valeurs dans des cases
syntaxe : nom_tableau[numero_case_dim1][numero_case_dim2] = valeur ;
Si on représente ce tableau sous la forme de lignes et de colonnes, la numérotation des cases
s'effectue de 0 à numero_ligne-1 pour les lignes et de 0 à numero_colonne-1 pour les colonnes.
o Accéder à la valeur d'une case d'un tableau
syntaxe : nom_tableau[numero_case_dim1][numero_case_dim2]
o Initialisation des tableaux
Syntaxe : type nom_tableau[N][P] = {val1_1, val1_2, ..., valN+P} ;
ou
type nom_tableau[N][P] = { {val1_1, val1_2, ..., val1_P}, {val2_1, val2_2, ..., val2_P}
..., {valN_1, valN_2, ..., valN_P}};
Exemple : algorithme qui calcule la somme des éléments d’une matrice de 20 lignes et 50
colonnes.
Tableau T (20 , 50) : Réel
Variables i , j : Entiers
Variable som : Réel
DEBUT
som ← 0
POUR i = 1 A 20
POUR j = 1 A 50
som ← som + T (i , j)
FIN POUR
FIN POUR
Ecrire ‘’La somme de tous les éléments du tableau est :’’, som
FIN
3. Chaînes de caractères
Une chaîne de caractères est traitée comme un tableau à une dimension de caractères (vecteur
de caractères). La représentation interne d'une chaîne de caractères est terminée par le symbole
'\0' (NUL). Ainsi, pour un texte de n caractères, nous devons prévoir n+1 octets.
"bonjour" est équivalente au tableau :
Pour les chaînes de caractères, nous pouvons utiliser une initialisation plus confortable en
indiquant simplement une chaîne de caractère constante: char CHAINE[ ] = "Hello";
Lors de l'initialisation par [ ], l'ordinateur réserve automatiquement le nombre d'octets
nécessaires pour la chaîne, c-à-d le nombre de caractères + 1 (ici: 6 octets).
Exemples :
Remarques :
- "x" : chaîne de caractères qui contient deux caractères : la lettre 'x' et le caractère NUL: '\0‘
- 'x' est codé dans un octet et "x" est codé dans deux octet .
Exemple :
module peut faire appel à d'autres modules, leur transmettre des données et recevoir des
données en retour. L'ensemble des modules ainsi reliés doit alors être capable de résoudre le
problème global. Voici quelques avantages d'un programme modulaire:
- Meilleure lisibilité
- Diminution du risque d'erreurs
- Possibilité de tests sélectifs
- Réutilisation de modules déjà existants
- Simplicité de l'entretien
- Favorisation du travail en équipe
Procédure et fonction
La fonction est la seule sorte de module existant en C.
Dans beaucoup de langages, on trouve deux sortes de "modules", à savoir :
o Les "fonctions", assez proches de la notion mathématique correspondante. Notamment,
une fonction dispose d'arguments (en C, une fonction peut ne comporter aucun
argument) qui correspondent à des informations qui lui sont transmises et elle fournit
un unique résultat scalaire (simple). L’appel d’une fonction peut apparaître dans une
expression.
o Les "procédures" (terme langage Pascal) ou "sous-programmes" (terme langage
Fortran ou Basic) qui élargissent la notion de fonction. La procédure ne possède plus de
valeur de retour et son appel ne peut plus apparaître au sein d'une expression. Par
contre, elle dispose toujours d'arguments.
Déclaration
Appel
Définition
Remarque :
- il n'y a pas de point-virgule après définition des paramètres de la fonction.
- Si nous choisissons un nom de fonction qui existe déjà dans une bibliothèque, notre
fonction cache la fonction prédéfinie.
- Si une fonction F fournit un résultat de type T, on dit que la fonction F est de type T.
Exemple :
La fonction MAX est de type int et elle a besoin de deux paramètres de type int.
Remarque : Les variables déclarées au début de la fonction principale main ne sont pas des
variables globales, mais elles sont locales à main.
Exemple :
Résultat :
n et p restent
inchangés
Le pointeur p contient la valeur 32000, qui est l'adresse de la variable b. L'adresse du pointeur p
est 1200. Dans une telle situation, on dit que p pointe sur b.
Dans cet exemple:
- b désigne le contenu de b (124)
- p désigne le contenu de p (32000)
- &b désigne l'adresse de b (ici 32000)
- *p désigne le contenu de la case sur laquelle pointe p (124)
En utilisant des pointeurs, nous écrivons une deuxième fonction « echange »:
VIII. Récursivité
1. Récursivité simple
En mathématiques une fonction récursive est une fonction qui s'appelle elle-même. Il est
possible en langage C de définir une fonction qui s'appelle elle-même, on parlera alors de
fonction récursive.
Prenons l’exemple de la fonction factorielle :
o en mathématiques :
n! = n.(n-1)! ,
pour n ≥ 1
avec 0!=1
o en informatique :
int factorielle ( int n )
{ return n*factorielle(n-1)
}
Exemple :
Remarque : On parle de récursivité croisée lorsque deux fonctions s'appellent l'une l'autre
récursivement.
3. Récursivité et itération
Prenons par exemple le calcul de la factorielle d'un nombre. On peut écrire la fonction factorielle
sous la forme d'une simple boucle, de la manière suivante :
int factorielle(int valeur)
{
int total = 1;
int curValeur;
for (curValeur = 1; curValeur <= valeur; curValeur++)
total *= curValeur;
return total;
}
On a déjà donné une définition récursive de la fonction factorielle. Cette définition est
parfaitement équivalente à la précédente, et peut se traduire en code par une fonction récursive:
Int factorielle(int valeur)
{
if (valeur == 0)
return 1;
else return valeur * factorielle(valeur - 1);
}
On peut remarquer que le code de cette deuxième version est plus simple que la version avec
une boucle, et qu'il peut se lire quasiment comme une définition.
La première version, qui utilise une boucle, est ce que l'on appelle une implémentation itérative
de la fonction factorielle. La deuxième version s'appelle l'implémentation récursive.
4. Occupation de la mémoire
Une grande partie des problèmes peut se résoudre avec une implémentation récursive, comme
avec une implémentation itérative. L'une ou l'autre peut paraître plus ou moins naturelle suivant
le problème. L'implémentation récursive permet souvent d'avoir un programme plus simple,
plus facile à comprendre, donc à débugger. L'implémentation récursive a cependant deux
principaux inconvénients :
o Le premier inconvénient fait que des programmes implémentés avec une fonction récursive
seront souvent légèrement plus lents que leurs équivalents itératifs. Si le moindre gain de
vitesse pour cette partie de votre programme est important, il peut donc être préférable
d'utiliser une implémentation itérative. Dans le cas contraire, la perte de performances peut être
largement compensée par le gain en clarté du code, donc en réduction de risques de laisser des
bugs.
o Le deuxième inconvénient peut être très gênant si le nombre d'appels imbriqués est très
important. Chaque appel de fonction imbriqué utilise une certaine quantité de mémoire, plus ou
moins importante selon le nombre de paramètres et de variables de votre fonction. Cette
mémoire est libérée dès que l'exécution de la fonction se termine, mais dans le cas d'une
fonction récursive, cette quantité de mémoire est multipliée par le nombre d'appels imbriqués à
un moment donné. Si ce nombre d'appels imbriqués peut atteindre des centaines de milliers,
voire des millions, on peut facilement atteindre des méga-octets de mémoire, pour un calcul qui
ne prendrait aucune mémoire avec une fonction itérative.
En algorithmique, la syntaxe de définition (construction) d’un type basé sur une structure est :
TYPE NomduType = STRUCTURE
attribut1 : Type
attribut2 : Type
...
attributn : Type
FIN STRUCTURE
Remarque:
o Une affectation globale n'est possible que si les structures ont été définies avec le même
nom de modèle ; en particulier, elle sera impossible avec des variables ayant une
structure analogue mais définies sous deux noms différents.
o L'affectation globale n'est pas possible entre tableaux. Elle l'est, par contre, entre
structures. Aussi est-il possible, en créant artificiellement une structure contenant un
seul champ qui est un tableau, de réaliser une affectation globale entre tableaux.
Celle-ci réserve les emplacements pour deux enregistrements nommés employe et responsable.
Ces derniers comportent trois champs :
o nom qui est un tableau de 30 caractères,
o prenom qui est un tableau de 20 caractères,
o heures qui est un tableau de 31 flottants.
Ces structures permettent de conserver pour un employé d’une entreprise les informations
suivantes : nom, prénom et nombre d’heures de travail pendant chacun des jours du mois
courant.
La structure point sert à représenter un point d'un plan. Un point est identifié par son nom
(caractère) et ses deux coordonnées x et y. La structure courbe représente un tableau de 40
éléments du type point.
Si i est un entier, la notation : courbe[i].nom
Représente le nom du point de rang i du tableau courbe. Il s'agit donc d'une valeur de type char.
la notation : courbe[i].x
Désigne la valeur du champ x de l'élément de rang i du tableau courbe.
courbe[4] représente l’enregistrement de type point correspondant au cinquième élément
du tableau courbe.
o La gestion statique ne se prête pas aisément à la mise en œuvre de listes chaînées (font
l’objet du paragraphe III), d'arbres binaires,... objets dont ni la structure ni la taille ne
sont généralement connues lors de la compilation du programme.
Les données dynamiques vont permettre de pallier ces défauts en donnant au programmeur
l'opportunité de s'allouer et de libérer de la mémoire au fur et à mesure de ses besoins.
Dans la première partie de ce paragraphe, nous présentons brièvement les pointeurs. Dans la
seconde partie, nous décrivons les outils de base de la gestion dynamique de la mémoire.
2. Pointeurs
2.1. Introduction
Toute variable manipulée dans un programme est stockée quelque part en mémoire centrale.
Cette mémoire est constituée d’octets qui sont identifiés par un numéro qu’on appelle adresse
mémoire. Pour retrouver une variable, il suffit donc de connaître l’adresse de l’octet où elle est
stockée (ou, s’il s’agit d’une variable qui recouvre plusieurs octets contigus, l’adresse du premier
de ces octets). Pour des raisons de lisibilité, on désigne souvent les variables par des
identificateurs, et non par leur adresse. C’est le compilateur qui fait alors le lien entre
l’identificateur d’une variable et son adresse en mémoire. Toutefois, il est parfois très pratique
de manipuler directement une variable par son adresse.
Deux variables différentes ont des adresses différentes. L’affectation i = j; n’opère que sur les
valeurs des variables.
Les variables i et j étant de type int, elles sont stockées sur 4 octets. Ainsi la valeur de i est
stockée sur les octets d’adresse 4831836000 à 4831836003.
L’adresse d’un objet étant un numéro d’octet en mémoire, il s’agit d’un entier (entier long)
quelque soit le type de l’objet considéré. Le format interne de cet entier (16 bits, 32 bits ou 64
bits) dépend des architectures.
L’opérateur & permet d’accéder à l’adresse d’une variable. Toutefois &i n’est pas une Lvalue
mais une constante : on ne peut pas faire figurer &i à gauche d’un opérateur d’affectation.
Pour pouvoir manipuler des adresses, on doit donc recourir à un nouveau type d’objets, les
pointeurs.
Exemple 1:
Dans l’exemple suivant, on définit un pointeur p qui pointe vers un entier i :
int i = 3;
int * p;
p = &i;
On se trouve dans la configuration
Dans un programme, on peut manipuler à la fois p et *p. Ces deux manipulations sont très
différentes. Comparons par exemple les deux programmes suivants:
main() et main()
{ {
int i = 3, j = 6; int i = 3, j = 6;
int *p1, *p2; int *p1, *p2;
p1 = &i; p1 = &i;
p2 = &j; p2 = &j;
*p1 = *p2; p1 = p2;
} }
Remarque :
La somme de deux pointeurs n’est pas autorisée.
Si i est un entier et p est un pointeur sur un objet de type type, l’expression p + i désigne un
pointeur sur un objet de type type dont la valeur est égale à la valeur de p incrémentée de i *
sizeof (type). Il en va de même pour la soustraction, et pour les opérateurs ++ et --.
Si p et q sont deux pointeurs sur des objets de type type, l’expression p - q désigne un entier
dont la valeur est égale à (p - q)/sizeof(type).
Exemple:
le programme suivant main()
{
int i = 3;
int *p1, *p2;
p1 = &i;
p2 = p1 + 1;
printf("p1 = %ld \t p2 = %ld\n",p1,p2);
Affiche p1= 4831835984 p2 = 4831835988
Par contre, le même programme avec des pointeurs sur des objets de type double :
main()
{
double i = 3;
double *p1, *p2;
p1 = &i;
p2 = p1 + 1;
printf("p1 = %ld \t p2 = %ld\n",p1,p2);
} Affiche p1= 4831835984 p2 = 4831835992
printf(" %d \n",*p);
printf("\n ordre decroissant:\n");
For (p = &tab[N-1]; p >= &tab[0]; p--)
printf(" %d \n",*p);
}
A ce stade, *p n’a aucun sens. En particulier, toute manipulation de la variable *p génère une
violation mémoire, détectable à l’exécution par le message d’erreur Segmentation fault.
On a alors
o Comparaison du programme
suivant avec le programme précédent :
main( )
{ int i = 3;
int *p;
p = &i;
}
Ce programme correspond à la situation suivante :
Dans ce dernier cas, les variables i et *p sont identiques (elles ont la même adresse) ce qui
implique que toute modification de l’une modifie l’autre. Ceci n’était pas vrai dans l’exemple
précédent où *p et i avaient la même valeur mais des adresses différentes.
On remarque que le dernier programme ne nécessite pas d’allocation dynamique puisque
l’espace mémoire à l’adresse &i est déjà réservé pour un entier.
Nous avons sur ce schéma la représentation que l'on pourrait faire d'un tableau et d'une liste
chaînée.
o Dans un tableau, la taille est connue, l'adresse du premier élément aussi. Lorsque vous
déclarez un tableau, la variable contiendra l'adresse du premier élément de votre
tableau.
Comme le stockage est contigu, et la taille de chacun des éléments connue, il est
possible d'atteindre directement la case i d'un tableau.
o Pour supprimer ou ajouter un élément à un tableau, il faut créer un nouveau tableau et
supprimer l'ancien. Ce n'est en général pas visible par l'utilisateur, mais c'est ce que
realloc souvent fait. L'adresse du premier élément d'un tableau peut changer après un
realloc, ce qui est tout à fait logique puisque realloc n'aura pas forcement la possibilité de
trouver en mémoire la place nécessaire et contiguë pour allouer votre nouveau tableau.
realloc cherche une place suffisante, recopier votre tableau, et supprimer l'ancien.
o Dans une liste chaînée, la taille est inconnue au départ, la liste peut avoir autant
d'éléments que votre mémoire le permet.
o Il est en revanche impossible d'accéder directement à l'élément i de la liste chainée.
Pour ce faire, il vous faudra traverser les i-1 éléments précédents de la liste.
o Pour déclarer une liste chaînée, il suffit de créer le pointeur qui pointe sur le premier
élément de votre liste chaînée, aucune taille n'est donc à spécifier.
o Il est possible d'ajouter, de supprimer, d'intervertir des éléments d'une liste chaînée
sans avoir à recréer la liste en entier, mais en manipulant simplement leurs
pointeurs.
Une liste chaînée est un type structuré. Chaque élément de la liste est composé de deux parties :
o la valeur (les valeurs) que vous voulez stocker,
o l'adresse de l'élément suivant.
Le dernier élément pointe vers une adresse (notée NULL) pour signifier la fin de la liste.
Le schéma suivant explique comment se passent l'ajout et la suppression d'un élément d'une
liste chaînée.
Le bloc d’instructions ci-dessus nous a permis de créer le type étudiant qui est une (liste chaînée
de réels) structure contenant un réel (moyenne) et un pointeur sur l’élément étudiant (nxt), qui
contiendra l'adresse de l'étudiant suivant.
La déclaration : Typedef struct etudiant etudiant; signifie que etudiant est synonyme de la
structure etudiant.
L’instruction : typedef struct etudiant * llist; permet de créer le type llist (pour linked list = liste
chaînée) qui est un pointeur sur le type étudiant. Le type llist permet de simplifier la déclaration.
Lors de déclaration de la liste chaînée, nous devons déclarer un pointeur sur étudiant,
l'initialiser à NULL, pour pouvoir ensuite allouer de la mémoire pour le premier étudiant.
N'oubliez pas d'inclure stdlib.h afin de pouvoir utiliser la macro NULL.
Voici la fonction qui permet l’ajout d’un étudiant en tête de liste en langage C :
llist ajouterEnTete(llist liste, float note)
{
/* Déclaration et allocation de la mémoire pour l’élément (étudiant) à ajouter */
etudiant * nouvelEtudiant ;
nouvelEtudiant = (etudiant *) malloc(sizeof(etudiant));
/* On attribue la note au nouvel étudiant */
nouvelEtudiant ->moyenne = note;
/* On affecte l'adresse de l'étudiant suivant au nouvel étudiant */
nouvelEtudiant ->nxt = liste;
/* La fonction renvoie la nouvelle liste, c.à.d. le pointeur sur le premier étudiant de la liste*/
return nouvelEtudiant;
}
Voici la fonction qui permet l’ajout d’un étudiant en fin de liste en langage C :
/* On ajoute l’étudiant en fin de liste, c.à.d. cet étudiant pointe sur NULL */
nouvelEtudiant->nxt = NULL;
if(liste == NULL)
{
/* Si la liste est vide, il suffit de renvoyer l'étudiant créé */
return nouvelEtudiant;
}
else
{
/* Sinon, on parcourt la liste à l'aide d'un pointeur temporaire et on
indique que le dernier étudiant de la liste est relié au nouvel étudiant */
etudiant * tmp=liste;
while(tmp->nxt != NULL)
{
tmp = tmp->nxt;
}
tmp->nxt = nouvelEtudiant;
return liste;
}
}
Dans le dernier bloc d’instructions, nous nous déplaçons le long de la liste chaînée grâce au
pointeur tmp. Si l'étudiant pointé par tmp n'est pas le dernier (tmp->nxt != NULL), on avance d'un
rang (tmp = tmp->nxt) en assignant à tmp l'adresse de l'étudiant suivant. Une fois que l'on est au
dernier étudiant, il ne reste plus qu'à le relier au nouvel étudiant.
même à ne pas libérer le premier étudiant avant d'avoir stocké l'adresse du second, sans quoi il
sera impossible de la récupérer.
Voici le code de la fonction « suppression en tête de liste » en langage C :
/* A la sortie de la boucle, tmp pointe sur le dernier élément, et ptmp sur l'avant-dernier. On
indique que l'avant-dernier devient la fin de la liste et on supprime le dernier élément */
ptmp->nxt=NULL;
free(tmp);
/* free(ptmp): à ne pas liberer ptmp, car le pointeur ptmp pointe sur le nouveau dernier element
de la liste --- donc il faut garder ptmp */
return liste;
}
}
XII. Les piles
1. Définition
La pile est une structure de données, qui permet de stocker les données dans l'ordre LIFO
(Last In First Out - en français Dernier Entré Premier Sorti), ce qui signifie que les derniers
éléments à être ajoutés à la pile seront les premiers à être récupérés. Il est possible de comparer
cela à une pile d'assiettes. Lorsqu'on ajoute une assiette en haut de la pile, on retire toujours en
premier celle qui se trouve en haut de la pile, c'est-à-dire celle qui a été ajoutée en dernier, sinon
tout le reste s'écroule.
Les piles ne sont que des cas particuliers de listes chaînées dont les éléments ne peuvent être
ajoutés et supprimés qu'en fin de liste. Les piles peuvent être utilisées dans des algorithmes
d'évaluation d'expressions mathématiques.
Le schéma ci-dessous montre que les différents éléments de la pile (cases) sont placées les unes
sur les autres.
Le 1er élément, qui se trouve au sommet de la pile, nous permet de réaliser l'opération de
récupération (ou suppression) des données situées en haut de la pile. Pour faire cela, une autre
structure sera utilisée. Voici sa composition :
typedef struct RepererListe {
Element *sommet;
int taille;
} Pile;
Le pointeur sommet contient l'adresse du premier élément de la pile. La variable taille contient
le nombre d'éléments. Quelque soit la position dans la liste, le pointeur sommet pointe toujours
vers le 1er élément, qui est en haut de la pile. Le champ taille contient le nombre d'éléments de la
pile.
free (supp_element);
mapile ->taille= mapile -> taile - 1; /* la taille de la pile sera décrémentée d'un élément
*/
}
Remarque :
mapile -> sommet -> donnee nous permet de récupérer la donnée stockée en haut de la pile.
Exemple :
Le programme en C ci-dessous nous permet de :
o Créer une pile d’entiers,
o Demander à l’utilisateur de saisir 3 entiers : entier 1 (le premier saisi), entier2 et entier3
(le dernier saisi).
o Afficher la pile sous la forma suivante :
**********Haut de la PILE************
Entier1
Entier2
Entier3
**********Bas de la PILE************
Le programme :
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <string.h>
typedef struct ElementListe{
int donnee;
struct ElementListe * precedent;
} Element;
typedef struct Repererliste{
Element * sommet;
int taille;
} Pile;
/* empiler (ajouter) un élément dans la pile */
void empiler (Pile * mapile, int donnees);
/* affichage de la pile */
void afficher (Pile * mapile);
/* la fonction principale */
main ()
{
Pile * mapile;
int note;
/* allocation de la mémoire pour mapile */
mapile = (Pile *) malloc (sizeof (Pile));
/* initialisation */
mapile->sommet = NULL;
mapile->taille = 0;
printf ("Entrez un entier : ");
scanf ("%d", ¬e);
empiler (mapile, note);
printf ("La pile (%d éléments): \n",mapile->taille);
printf("\n********** Haut de la PILE **********\n");
afficher(mapile);
printf("********** Bas de la PILE **********\n\n");
printf ("Entrez un entier : ");
scanf ("%d", ¬e);
empiler (mapile, note);
printf ("La pile (%d éléments): \n",mapile->taille);
printf("\n********** Haut de la PILE **********\n");
afficher(mapile);
printf("********** Bas de la PILE ********** \n\n");
printf ("Entrez un entier : ");
scanf ("%d", ¬e);
empiler (mapile, note);
printf ("La pile (%d éléments): \n",mapile->taille);
printf("\n********** Haut de la PILE **********\n");
afficher(mapile);
printf("********** Bas de la PILE **********\n\n");
getch();
}
for(i=0;i<mapile->taille;i++){
printf("\t\t %d \n", courant->donnee);
courant = courant->precedent;
}
}
Exemple :
Le code suivant ouvre le fichier test.txt en mode "r+" (lecture / écriture) :
# include <stdio.h>
# include <stdlib.h>
main()
{
FILE * fichier = NULL; /* déclaration et initialisation d’un pointeur ‘fichier’ de type
FILE */
fichier = fopen ("test.txt", "r+"); /* le pointeur fichier pointe vers ‘test.txt’ */
if (fichier != NULL) /* test de l’ouverture du fichier */
{
// On peut lire et écrire dans le fichier
}
else
{
// On peut afficher un message d'erreur
printf("Impossible d'ouvrir le fichier test.txt");
}
……….. ……….. …………..
}
Dans l’exemple ci-dessus, le fichier test.txt doit être situé dans le même dossier que notre
programme. Le fichier test peut être de n’importe quel type.
Remarque :
Il est possible de placer le fichier à manipuler dans n’importe quel dossier. Pour y accéder, il
suffit de spécifier son chemin. Le code de l’exemple ci-dessous permet d’ouvrir le fichier tset1.txt
situé dans C:\Program Files\devc++. Dans ce cas, le chemin utilisé est appelé chemin absolu (ou
complet).
Le code : fichier = fopen("C:\\Program Files\\devc++\\test1.txt", "r+");
Il est nécessaire de préfixer un antislash dans une chaîne par un antislash afin d’indiquer au
programme qu’il s’agit d’un seul antislash.
2.3.1. fputc
La fonction fputc écrit un caractère à la fois dans le fichier. Son prototype est :
int fputc (int caractere, FILE * pointeurSurFichier);
fopen reçoit deux arguments:
o Le caractère à écrire (de type int qui revient plus ou moins à utiliser un char). Vous
pouvez donc écrire directement 'B' par exemple.
o Le pointeur sur le fichier. Dans notre exemple, notre pointeur s'appelle "fichier".
L'avantage de demander le pointeur de fichier à chaque fois, c'est que vous pouvez
ouvrir plusieurs fichiers en même temps et donc lire et écrire dans chacun de ces
fichiers. Vous n'êtes pas limités à un seul fichier ouvert à la fois.
Exemple : Le code suivant écrit la lettre 'B' dans test1.txt (si le fichier existe, il est remplacé ; si il
n'existe pas, il est créé).
main()
{
FILE * fichier = NULL;
fichier = fopen("test1.txt", "w");
if (fichier != NULL)
{
fputc ('B', fichier); // Ecriture du caractère B
fclose(fichier);
}
}
2.3.2. fputs
fputs permet d’écrire une chaîne de caractères, ce qui est en général plus pratique que d'écrire
caractère par caractère.
Prototype de la fonction :
int fputs (const char * chaine, FILE * pointeurSurFichier);
Exemple :
main()
{
FILE * fichier = NULL;
fichier = fopen("test2.txt", "w");
if (fichier != NULL)
{
fputs("Bonjour tout le monde.", fichier);
fclose(fichier);
}
}
2.3.3. fprintf
fprintf permet d’écriture dans un fichier. Elle s'utilise de la même manière que printf, sauf que
fprintf demande un pointeur de type FILE en premier paramètre.
Exemple :
Voici un code qui demande l’âge de l’utilisateur et l’écrit dans un fichier :
main ( )
{
FILE * fichier = NULL;
int age;
fichier = fopen("test3.txt", "w");
if (fichier != NULL)
{
// On demande l'âge
printf("Quel age avez-vous ? ");
scanf("%d", &age);
// On l'écrit dans le fichier
fprintf(fichier, "Le Monsieur qui utilise le programme, il a %d ans", age);
fclose(fichier);
}
}
2.4.1. fgetc
Voici le prototype de fgetc :
int fgetc (FILE * pointeurDeFichier);
Cette fonction retourne un int : c'est le caractère qui a été lu. Si la fonction n'a pas pu lire de
caractère, elle retourne EOF. fgetc fait avancer le curseur d'un caractère à chaque fois que nous
en lisons un. Si vous appelez fgetc une seconde fois, la fonction lira donc le second caractère, puis
le troisième et ainsi de suite.
Nous pouvons utiliser une boucle pour lire les caractères un par un dans le fichier. La boucle
s'arrête quand fgetc renvoie EOF (qui signifie End Of File, c'est-à-dire "fin du fichier").
Voici le code :
main( )
{
FILE * fichier = NULL;
int caractereActuel ;
fichier = fopen("test4.txt", "r");
if (fichier != NULL)
{
caractereActuel = fgetc(fichier); // On initialise caractereActuel
// Boucle de lecture des caractères un par un
while (caractereActuel != EOF) // On continue tant que fgetc n'a pas retourné
EOF
{
printf("%c", caractereActuel); // On affiche le caractère stocké dans caractereActuel
caractereActuel = fgetc(fichier); // On lit le caractère suivant
}
fclose(fichier);
}
}
2.4.2. fgets
fgets lit une chaîne dans un fichier. Ca nous évite d'avoir à lire tous les caractères un par un. La
fonction lit au maximum une ligne (elle s'arrête au premier \n rencontré). L’utilisation d’une
boucle est nécessaire pour la lecture de plusieurs lignes.
Exemple :
Le code suivant permet de lire et afficher une ligne de taille maximale = 500 caractères.
#define TAILLE_MAX 500 // constante TAILLE_MAX = 500
main ()
{
FILE * fichier = NULL;
char chaine[TAILLE_MAX] = ""; // Chaîne de caractères vide de taille TAILLE_MAX
fichier = fopen("test5.txt", "r");
if (fichier != NULL)
{
fgets(chaine, TAILLE_MAX, fichier);
// On lit maximum TAILLE_MAX caractères du fichier, On stocke la ligne dans "chaine"
printf("%s", chaine); // On affiche la chaîne
fclose(fichier);
}
getch()
}
Pour Lire tout le fichier avec fgets, nous utilisons une boucle. La fonction fgets renvoie NULL
si elle ne parvient pas à lire ce que nous avons demandé. La boucle doit donc s'arrêter dès que
fgets renvoie NULL. Le programme en C ci-dessous lit et affiche tout le contenu du fichier 5, ligne
par ligne.
2.4.3. fscanf
fscanf s'utilise de la même manière que printf. Cette fonction lit dans un fichier qui doit avoir été
écrit d'une manière précise. Supposons que notre fichier ‘test6.txt’ contienne 4 nombres séparés
par un espace : 10 20 30 40
La fonction fscanf nous permet de récupérer chacun de ces nombres dans une variable de type
int. Voici le code :
main( )
{
FILE * fichier = NULL;
int nbre [4] ; // Tableau de 4 entiers
fichier = fopen("test6.txt", "r");
if (fichier != NULL)
{
fscanf(fichier, "%d %d %d%d", & nbre [0], & nbre [1], & nbre [2], & nbre [3]);
printf("Les nombres sont : %d, %d, %d et %d",nbre [0],nbre [1],nbre [2],nbre [3]);
fclose(fichier);
}
getch ();
}
2.5.1. ftell
Cette fonction renvoie la position actuelle du curseur sous la forme d'un long. Le nombre
renvoyé indique la position du curseur dans le fichier. Voici le prototype de ftell :
long ftell (FILE * pointeurSurFichier);
2.5.2. fseek
Le prototype de fseek est le suivant :
int fseek (FILE* pointeurSurFichier, long deplacement, int origine);
La fonction fseek permet de déplacer le "curseur" d'un certain nombre de caractères (indiqué
par deplacement) à partir de la position indiquée par origine. Le nombre deplacement peut
être un nombre positif (pour se déplacer en avant), nul (= 0) ou négatif (pour se déplacer en
arrière). Quant au nombre origine, vous pouvez mettre comme valeur l'une des 3 constantes
listées ci-dessous :
o SEEK_SET : indique le début du fichier.
o SEEK_CUR : indique la position actuelle du curseur.
o SEEK_END : indique la fin du fichier.
Exemples :
o Le code suivant place le curseur 2 caractères après le début :
fseek (fichier, 2, SEEK_SET);
o Le code suivant place le curseur 4 caractères avant la position courante :
fseek (fichier, -4, SEEK_CUR);
o Le code suivant place le curseur à la fin du fichier :
fseek (fichier, 0, SEEK_END);
2.5.3. rewind
Cette fonction est équivalente à utiliser fseek pour nous renvoyer à la position 0 dans le fichier.
Voici son prototype :
void rewind(FILE * pointeurSurFichier);
2.6.1. rename
La fonction rename permet de renommer un fichier. La fonction renvoie 0 si elle réussit à
renommer le fichier. Voici son prototype :
int rename(const char * ancienNom, const char * nouveauNom);
Exemple : main( )
{
rename ("test_10.txt", "test_20.txt");
}
2.6.2. remove
La fonction remove permet de supprimer un fichier sans demander de confirmation. Il est
supprimé du disque dur. Voici son prototype : int remove(const char * fichierASupprimer);
Exemple : main ( )
{
remove ("test_30.txt"); // ce code supprime le fichier test_30.txt
}