Vous êtes sur la page 1sur 119

Cours POO et C ++

CHAPITRE 1

ELEMENTS DE LANGAGE C++

INTRODUCTION

Le langage C++ est un langage évolué et structuré. C’est en ce sens une évolution du langage
C.
Il possède en outre les fonctionnalités de la programmation orienté objet.
Le langage C++ se trouve à la frontière entre le langage C, non objet, et le langage JAVA
conçu d’emblée en orienté objet.

On trouve sur le marché un grand nombre de compilateurs C++ destinés à différents


microprocesseurs ou microcontrôleurs.

Le langage C++ possède assez peu d'instructions, il fait par contre appel à des bibliothèques,
fournies en plus ou moins grand nombre avec le compilateur.

exemples: math.h : bibliothèque de fonctions mathématiques


iostream.h : bibliothèque d'entrées/sorties standard
complex.h : bibliothèque contenant la classe des nombres complexes.

On ne saurait développer un programme en C++ sans se munir de la documentation


concernant ces bibliothèques.
ETAPES PERMETTANT L'EDITION, LA MISE AU POINT,

L'EXECUTION D'UN PROGRAMME

1- Edition du programme source, à l'aide d'un éditeur (traitement de textes). Le nom du


fichier contient l'extension .CPP, exemple: EXI_1.CPP (menu « edit »).

2- Compilation du programme source, c'est à dire création des codes machine destinés au
microprocesseur utilisé. Le compilateur indique les erreurs de syntaxe mais ignore les
fonctions-bibliothèque appelées par le programme.
Le compilateur génère un fichier binaire, non éditable en mode « texte », appelé fichier objet:
EXI_1.OBJ (commande « compile »).

3- Editions de liens: Le code machine des fonctions-bibliothèque est chargé, création d'un
fichier binaire, non éditable en mode texte, appelé fichier exécutable: EXI_1.EXE
(commande « build all »).

4- Exécution du programme (commande « Run » ou « flèche jaune »).


Les compilateurs permettent en général de construire des programmes composés de plusieurs
fichiers sources, d'ajouter à un programme des unités déjà compilées. On dit alors que l’on travaille
par gestion de projet.

Exercice I-1: Editer (EXI_1.CPP), compiler et exécuter le programme suivant:

Le langage C++ distingue les minuscules, des majuscules. Les mots réservés du langage C++
doivent être écrits en minuscules.

On a introduit dans ce programme la notion d’interface homme/machine (IHM).


- L’utilisateur visualise une information sur l’écran,
- L’utilisateur, par une action sur le clavier, fournit une information au programme.

Les instructions sont exécutées séquentiellement, c’est à dire les unes après les autres.
L’ordre dans lequel elles sont écrites a donc une grande importance.
Echanger les 2 premières instructions, puis exécuter le programme.

Modifier maintenant le programme comme ci-dessous, puis le tester :


Dans ce programme, on introduit 3 nouveaux concepts :
- La notion de déclaration de variables : les variables sont les données que manipulera le
programme lors de son exécution. Ces variables sont rangées dans la mémoire vive de
l’ordinateur. Elles peuvent être déclarées au moment où on en a besoin dans le programme.
Pour une meilleure lisibilité, il est conseillé de les déclarer au début (sauf peut-être pour des
variables créées par commodité et qui ne servent que très localement dans le programme).
- La notion d’affectation, symbolisée par le signe =. La source de l’information est à droite du
signe =, la destination à gauche.

a = 10; signifie « a prend la valeur 10 »


s = a + b; signifie « s prend la valeur a + b »
s = s + 5; signifie « la nouvelle valeur de s est égale à l’ancienne + 5 »

- La notion d’opération. Un programme informatique est exécuté séquentiellement, c’est à


dire une instruction après l’autre. Lorsque l’instruction s = a + b est exécutée, a possède la
valeur 10, et b possède la valeur 50.
LES DIFFERENTS TYPES DE VARIABLES

1- Les entiers

Le langage C++ distingue plusieurs types d'entiers:

TYPE DESCRIPTION TAILLE MEMOIRE


int entier standard 4 octets: - 231 
signé n  231-1
unsigned int entier positif 4 octets: 0  n 
232
short entier court 2 octets: - 215 
signé n  215-1
unsigned entier court non 2 octets: 0  n 
short signé 216
char caractère signé 1 octet : - 27  n
 27-1
unsigned caractère non 1 octet : 0 n 
char signé 28

Numération:
- En décimal les nombres s'écrivent tels que,
- En hexadécimal ils sont précédés de 0x.
exemple: 127 en décimal s'écrit 0x7f en hexadécimal.

Remarque: En langage C++, le type char possède une fonction de changement de type vers
un entier:
- Un caractère peut voir son type automatiquement transformé vers un entier de 8 bits
- Il est interprété comme un caractère alphanumérique du clavier.

Exemples:

Les caractères alphanumériques s'écrivent entre ‘ ‘


Le caractère 'b' a pour valeur 98.
Le caractère 22 a pour valeur 22.
Le caractère 127 a pour valeur 127.
Le caractère 257 a pour valeur 1 (ce nombre s'écrit sur 9 bits, le bit de poids fort est perdu).
Quelques constantes caractères:

CARACTERE VALEUR (code ASCII) NOM ASCII

'\n interligne 0x0a LF


'
'\t tabulation 0x09 HT
' horizontale
'\v tabulation 0x0b VT
' verticale
'\r retour chariot 0x0d CR
'
'\f saut de page 0x0c FF
'
'\\ Backslash 0x5c \
'
'\' Cote 0x2c '
'
'\" guillemets 0x22 "
'

Modifier ainsi le programme et le tester :

1- Les réels

Un réel est composé :


- d'un signe,
- d'une mantisse,
- d'un exposant,
Un nombre de bits est réservé en mémoire pour chaque élément.

Le langage C++ distingue 2 types de réels:


TYPE DESCRIPTION TAILLE MEMOIRE

float réel standard 4 octets


double réel double 8 octets
précision

LES INITIALISATIONS

Le langage C++ permet l'initialisation des variables dès leurs déclarations:

char c; est équivalent à char c = 'A';


c = 'A';

int i; est équivalent à int i = 50;


i = 50;

Cette règle s'applique à tous les nombres, char, int, float ... Pour améliorer la lisibilité des
programmes et leur efficacité, il est conseillé de l’utiliser.

SORTIES DE NOMBRES OU DE TEXTE A L'ECRAN

L’OPERATEUR COUT

Ce n'est pas une instruction du langage C++, mais une fonction de la bibliothèque iostream.h.

Exemple: affichage d'un texte:

cout <<"BONJOUR"; // pas de retour à la ligne du curseur après l'affichage


cout <<"BONJOUR\n"; // affichage du texte, puis retour à la ligne du curseur

LES OPERATEURS

Opérateurs arithmétiques sur les réels: + - * / avec la hiérarchie habituelle.

Opérateurs arithmétiques sur les entiers: + - * / (quotient de la division) % (reste de la


division) avec la hiérarchie habituelle.

Exemple particulier: char c, d; c = 'G'; d = c+'a'-'A';


Les caractères sont des entiers sur 8 bits, on peut donc effectuer des opérations. Sur cet
exemple, on transforme la lettre majuscule G en la lettre minuscule g.

Opérateurs logiques sur les entiers:

& ET | OU ^ OU EXCLUSIF ~ COMPLEMENT A UN


« DECALAGE A GAUCHE
» DECALAGE A DROITE.

Exemples: p = n « 3; // p est égale à n décalé de 3 bits à gauche


p = n » 3; // p est égale à n décalé de 3 bits à droite

L'opérateur sizeof(type) renvoie le nombre d'octets réservés en mémoire pour chaque type
d'objet.
Exemple: n = sizeof(char); /* n vaut 1 */
Exercice : n est un entier (n = 0x1234567a), p est un entier (p = 4). Ecrire un programme
qui met à 0 les p bits de poids faibles de n.

Exercice : quels nombres va renvoyer le programme suivant ?

INCREMENTATION - DECREMENTATION

Le langage C++ autorise des écritures simplifiées pour l'incrémentation et la décrémentation


de variables de type entier (int, char, long)

i = i+1; est équivalent à i++;

i = i-1; est équivalent à i--;

OPERATEURS COMBINES

Le langage C++ autorise des écritures simplifiées lorsqu'une même variable est utilisée de chaque
côté du signe = d'une affectation. Ces écritures sont à éviter lorsque l'on débute l'étude du langage
C++ car elles nuisent à la lisibilité du programme.

a = a+b; est a+=


équivalent à b;
a = a-b; est a-=
équivalent à b;
a = a & est a&=
b; équivalent à b;

LES DECLARATIONS DE CONSTANTES


Le langage C++ autorise 2 méthodes pour définir des constantes.
1ere méthode: déclaration d'une variable, dont la valeur sera constante pour toute la
portée de la fonction main.

Exemple :

Dans ce cas, le compilateur réserve de la place en mémoire (ici 4 octets), pour la


variable pi, on ne peut changer la valeur. On peut associer un modificateur« const » à
tous les types.

2eme méthode: définition d'un symbole à l'aide de la directive de compilation #define.

Exemple:

Le compilateur ne réserve pas de place en mémoire, on définit ainsi une équivalence


« lexicale ».
Les constantes déclarées par #define s'écrivent traditionnellement en majuscules, mais ce
n'est pas une obligation.
LES CONVERSIONS DE TYPES

Le langage C++ permet d'effectuer automatiquement des conversions de type sur les

scalaires: Exemple et exercice :


Une conversion de type float --> int ou char peut-être dégradante.
Une conversion de type int ou char --> float est dite non dégradante.

SAISIE DE NOMBRES ET DE CARACTERES AU


CLAVIER

LA FONCTION GETCH

La fonction getch, appartenant à la bibliothèque conio.h permet la saisie clavier d’un


caractère alphanumérique, sans écho écran. La saisie s'arrête dès que le caractère a été
frappé.

La fonction getch n'est pas définie dans la norme ANSI mais elle existe dans les
bibliothèques des compilateurs.

On peut utiliser getch de deux façons:

- sans retour de variable au programme:

Exemple: cout<<"POUR CONTINUER FRAPPER UNE TOUCHE ";


getch();

- avec retour de variable au programme:

Exemple: char alpha;


cout<<"ENTRER UN CARACTERE (ATTENTION PAS DE
RETURN)";
alpha = getch();
cout<<"\nVOICI CE CARACTERE: "<<alpha;
Les parenthèses vides de getch() signifient qu'aucun paramètre n'est passé à cette
fonction par le programme appelant.

L’OPERATEUR CIN

L’opérateur cin, spécifique à C++, appartient à la bibliothèque iostream.h, et permet la


saisie à partir du clavier de n'importe quel type de variable (l’affichage prend en compte
le type de la variable).
La saisie s'arrête avec "RETURN" (c'est à dire LF), les éléments saisis s'affichent à
l'écran (saisie avec écho écran).

Tous les éléments saisis après un caractère d'espacement (espace, tabulation) sont
ignorés.
Exemples: char alpha;
int i; float r;
cin >>alpha; // saisie d'un caractère
cin >>i; // saisie d'un nombre entier en décimal
cin >>r; // saisie d'un nombre réel

Remarque: Si l'utilisateur ne respecte pas le type de la variable, aucune erreur n'est


générée.
Le programme peut se comporter de plusieurs façons :

Exemples: int u;
cin >> u;
Si l'utilisateur saisi un caractère non numérique, sa saisie est ignorée.

char c; cin >>


c;
Si l'utilisateur saisi par exemple 68, le caractère ‘6’ sera affecté à la variable c.
Conséquence : pour une interface homme machine (IHM) d’un produit fini, ne jamais
utiliser « cin ».

Exercice II_1:

Saisir un caractère au clavier, afficher son code ASCII à l'écran. Soigner l'affichage.

Exercice II_2:

Dans une élection, I est le nombre d’inscrits, V le nombre de votants,


P = 100V/I le pourcentage de votants, M = V/2 le nombre de voix pour obtenir la
majorité.
Ecrire un programme qui demande à l’utilisateur de saisir I et V, puis calcule et affiche P et
M.

Exercice II_3:

Saisir 3 réels, calculer et afficher leur moyenne.

LES TESTS ET LES BOUCLES

Un programme écrit en C++ s’exécute séquentiellement, c’est à dire instruction après


instruction.
Ce chapitre explique comment, dans un programme, on pourra :
- ne pas exécuter une partie des instructions, c’est à dire faire un saut dans le
programme,
- revenir en arrière, pour exécuter plusieurs fois de suite la même partie d’un
programme.
L'INSTRUCTION SI ... ALORS ... SINON ...

Il s'agit de l'instruction: si (condition vraie)


alors {BLOC 1 D'INSTRUCTIONS}
sinon {BLOC 2 D'INSTRUCTIONS}

Organigramme:

oui condition non

vraie

bloc 1 d' bloc 2 d'


instructions instructions

suite du programme

Syntaxe en C: if (condition)
{
............; // bloc 1 d'instructions
............;
............;
}
else
{
............; // bloc 2 d'instructions
............;
............;
}
suite du programme ...

Si la condition est vraie, seul le bloc1 d’instructions est exécuté, si elle est fausse, seul le bloc2 est
exécuté.
Dans tous les cas, la suite du programme sera exécutée.

Le bloc "sinon" est optionnel: si (condition)


alors {BLOC D'INSTRUCTIONS}

oui non
condition
vraie

bloc d'
instructions

suite du programme
Syntaxe en C: if (condition)
{
............; // bloc d'instructions
............;
............;
}
suite du programme...

Si la condition est vraie, seul le bloc1 d’instructions est exécuté, si elle est fausse, on passe
directement à la suite du programme.

Remarque: les {} ne sont pas nécessaires lorsque les blocs ne comportent qu'une seule
instruction.
LES OPERATEURS LOGIQUES

condition d'égalité: if " si a est égal à


(a==b) b "
condition de non if " si a est
égalité: (a!=b) différent de b "
conditions de relation if if (a<=b) if (a>b) if
d'ordre: (a<b) (a>=b)

plusieurs conditions devant être vraies simultanément, ET LOGIQUE:


if ((expression1) && (expression2)) " si l'expression1 ET l'expression2 sont vraies "

une condition devant être vraie parmi plusieurs, OU LOGIQUE


if ((expression1) || (expression2)) " si l'expression1 OU l'expression2 est vraie "

condition fausse if (!(expression1)) " si l'expression1 est fausse "

Toutes les combinaisons sont possibles entre ces tests.

Exercice III-1: L'utilisateur saisit un caractère, le programme teste s'il s'agit d'une lettre
majuscule, si oui il renvoie cette lettre en minuscule, sinon il renvoie un message d'erreur.

Exercice III-2: Dans une élection, I est le nombre d’inscrits, V le nombre de votants, Q le
quorum, P = 100V/I le pourcentage de votants, M = V/2 + 1 le nombre de voix pour obtenir la
majorité absolue.
Le quorum est le nombre minimum de votants pour que le vote soit déclaré valable.
Ecrire un programme qui
1- demande à l’utilisateur de saisir I, Q et V,
2- teste si le quorum est atteint,
3- si oui calcule et affiche P, M, sinon affiche un message d’avertissement.

L'INSTRUCTION REPETER ... TANT QUE ...

Il s'agit de l'instruction: exécuter {BLOC D'INSTRUCTIONS}


tant que (condition vraie)
Organigramme:

bloc d'
instructions

non
condition
vraie
oui

suite du programme

Syntaxe en C: do
{
............; // bloc d'instructions
............;
............;
}
while (condition);
suite du programme ...

Le test se faisant après, le bloc est exécuté au moins une fois.

Remarque: les {} ne sont pas nécessaires lorsque le bloc ne comporte qu'une seule instruction.

LA BOUCLE TANT QUE ... FAIRE ...

Il s'agit de l'instruction: tant que (condition vraie)


Exécuter {BLOC D'INSTRUCTIONS}
Organigramme:

oui non
condition
vraie

bloc d'
instructions

Syntaxe en C: while (condition)


{
............; // bloc d'instructions
............;
............;
}
suite du programme ...

Le test se fait d'abord, le bloc d'instructions n'est pas forcément exécuté.

Remarque: les {} ne sont pas nécessaires lorsque le bloc ne comporte qu'une seule instruction.

Remarque: On peut rencontrer la construction suivante: while (expression); terminée par un ;


et sans la présence du bloc d'instructions. Cette construction signifie: "tant que l'expression
est vraie attendre". Ceci ne doit être exploité que par rapport à des événements externes
au programme (attente de la frappe du clavier par exemple).

L'INSTRUCTION POUR ...

Il s'agit de l'instruction:

pour (instruction1; condition; instruction2)


{BLOC D'INSTRUCTIONS}
Organigramme:

instruction1

condition
vraie
non
oui

bloc d'instructions

instruction2 suite du programme

Syntaxe en C:

for(instruction1; condition; instruction2)


{
............; // bloc d'instructions
............;
............;
}
suite du programme ...

Remarques:
Il s’agit d’une version enrichie du « while ».
Les {} ne sont pas nécessaires lorsque le bloc ne comporte qu'une seule instruction.
Les 3 instructions du for ne portent pas forcément sur la même variable.
Une instruction peut être omise, mais pas les ;

Exemples: for(int i = 0 ; i<10 ; i++)


{
............; // bloc d'instructions
............;
............;
}
suite du programme ...

correspond à l'organigramme suivant:


i= 0

non
i < 10

oui
bloc d'instructions
suite du
programme

i=i+1

La boucle for(;;)
{
............; // bloc d'instructions
............;
............;
}
est une boucle infinie (répétition infinie du bloc d'instructions).

Utilisation de variables différentes:

resultat = 0;
for(int i = 0 ; resultat<30 ; i++)
{
............; // bloc d'instructions
............;
............;
resultat = resultat + 2*i;
}

Exercice III-3:
Ecrire un programme permettant de saisir un entier n, de calculer n!, puis de l’afficher.
Utiliser une boucle do ...while puis while puis for.
Quelle est la plus grande valeur possible de n, si n est déclaré int, puis unsigned int?

Exercice III-4:
La formule récurrente ci-dessous permet de calculer la racine du nombre 2 :
U0 = 1
Ui = ( Ui-1+ 2/Ui-1 ) / 2
Ecrire un programme qui saisit le nombre d’itérations, puis calcule et affiche la racine de 2.
L'INSTRUCTION AU CAS OU ... FAIRE ...

L'instruction switch permet des choix multiples uniquement sur des entiers (int) ou des
caractères (char).

Syntaxe:

switch(variable de type char ou int) au cas où la variable vaut:


{
case valeur1: ......; - cette valeur1: exécuter ce bloc d'instructions.
.......;
break; - se brancher à la fin du bloc case.
valeur2:........; - cette valeur2: exécuter ce bloc d'instructions.
........;
break; - se brancher à la fin du bloc case.
.
. etc ...
.
default: .......; - aucune des valeurs précédentes: exécuter ce bloc
........; d'instructions, pas de "break" ici.

le bloc "default" n'est pas obligatoire.

L’instruction switch correspond à une cascade d’instructions if ...else

Exemple:
Cette instruction est commode pour fabriquer des "menus":

char choix;
cout<<"LISTE PAR GROUPE TAPER 1\n";
cout<<"LISTE ALPHABETIQUE TAPER 2\n";
cout<<"POUR SORTIR TAPER S\n";
cout<<"\nVOTRE CHOIX : ";
cin >> choix;
switch(choix)
{
case '1': ........ ;
.......;
break;

case '2': ....... ;


......;
break;

case 'S': cout<<"\nFIN DU PROGRAMME .... ";


break;
default; cout<<"\nCE CHOIX N'EST PAS PREVU "; // pas de break ici
}

COMPLEMENT SUR LES TESTS

En langage C++, une expression nulle de type entier (int) est fausse, une expression non
nulle de type entier (int) est vraie.

Exemples:

int a,b,c,delta; est int


équivalent à a,b,c,delta;
delta = b*b- delta = b*b-
4*a*c; 4*a*c;
if(delta != if(delta)
0) { ....... }
{ ....... }

int est int


a,b,c,delta; équivalent à a,b,c,delta;
delta = b*b- delta = b*b-
4*a*c; 4*a*c;
if(delta == if(!delta)
0) {.......}
{ ....... }

En langage C++, le type booléen a été introduit. Il prend les valeurs TRUE ou FALSE.
Par exemple :
bool test ;
test = (x<45) ;
if ( test == TRUE)
{……..}

ou plus simplement ou aussi


if( (x<45) == TRUE) ) if (x<45) // !!!!!
EXERCICES RECAPITULATIFS

Exercice III_5: résoudre ax2 + bx +c = 0.

Exercice III_6: La fonction kbhit appartient à la bibiothèque conio.h. Une fonction


équivalente peut exister avec d'autres compilateurs. La fonction kbhit teste si un caractère a
été frappé au clavier. Tant que ce n'est pas vrai kbhit renvoie 0 (ceci signifie que la valeur
retournée par la fonction kbhit est 0).

Exemple: while(kbhit() == 0) // tant qu'aucun caractère n'a été frappé exécuter la


boucle
{ ..... }

Cette écriture est équivalente à:


while(!kbhit()) // tant que kbhit est faux, exécuter la boucle
{.....} Ecrire un programme qui affiche le carré des entiers 1, 2, 3 ................... , toutes les 500 ms tant
qu'aucun caractère n'a été frappé au clavier. Générer la temporisation à l’aide d’une boucle for
et d’un décompteur.

LES POINTEURS
Que ce soit dans la conduite de process, ou bien dans la programmation orientée objet,
l’usage des pointeurs est extrêmement fréquent en C++.

L'OPERATEUR ADRESSE &

L'opérateur adresse & indique l'adresse d'une variable en mémoire.

Exemple: int i = 8;
cout<<"VOICI i:"<<i;
cout<<"\nVOICI SON ADRESSE EN HEXADECIMAL:"<<&i;

Exercice V_1: Exécuter l’exemple précédent, et indiquer les cases-mémoire occupées par la variable i.

LES POINTEURS

Définition: Un pointeur est une adresse. On dit que le pointeur pointe sur une variable
dont le type est défini dans la déclaration du pointeur. Il contient l’adresse de la
variable.

DECLARATION DES POINTEURS

Une variable de type pointeur se déclare à l'aide du type de l'objet pointé précédé du symbole
*.

Exemple: char pc est un pointeur sur un


*pc; objet de type char
int *pi; pi est un pointeur sur un
objet de type int
float pr est un pointeur sur un
*pr; objet de type float

Excercice V_1: Modifier l’exercice 1 comme ci-dessous, et conclure :

int i = 8;
int *p; // déclaration d’un pointeur sur entier
cout<<"VOICI i : "<<i;
cout<<"\nVOICI SON ADRESSE EN HEXADECIMAL : "<<&i;

p = &i; // p contient l’adresse de i, ont peut dire qu’il pointe sur i


cout<<"\nVOICI SON ADRESSE EN HEXADECIMAL : "<<p;
MANIPULATION DE LA CASE MEMOIRE

En dehors de la partie déclarative, l'opérateur * désigne en fait le contenu de la case


mémoire pointée.

Excercice V_1: Modifier l’exercice 1 comme ci-dessous, et conclure :

int i = 8;
int *p;
cout<<"VOICI i :"<<i;
cout<<"\nVOICI SON ADRESSE EN HEXADECIMAL :"<<&i;

p = &i;
cout<<"\nVOICI SON ADRESSE EN HEXADECIMAL :"<<p;
cout<<"\nVOICI i :"<<*p;

*p = 23;
cout<<"\nVOICI i:"<<i;
cout<<"\nVOICI i:"<<*p;

ARITHMETIQUE DES POINTEURS

On peut essentiellement déplacer un pointeur dans un plan mémoire à l'aide des opérateurs
d'addition, de soustraction, d'incrémentation, de décrémentation. On ne peut le déplacer que
d'un nombre de cases mémoire multiple du nombre de cases réservées en mémoire pour
la variable sur laquelle il pointe.

pi 0x00 pc 0x41 int i;


pc+1 char c;
0x00 int *pi;
char *pc;
0x00 pi = &i;
0x00 pc = &c;
*pi = 0;
pi+1 *pc = 'A';
Exemples:

int *pi; // pi pointe sur un objet de type entier


float *pr; // pr pointe sur un objet de type réel
char *pc; // pc pointe sur un objet de type caractère

*pi = 421; // 421 est le contenu de la case mémoire p et des 3 suivantes


*(pi+1) = 53; // on range 53, 4 cases mémoire plus loin
*(pi+2) = 0xabcd; // on range 0xabcd 8 cases mémoire plus loin
*pr = 45.7; // 45,7 est rangé dans la case mémoire r et les 3 suivantes
*pc = 'j'; // le contenu de la case mémoire c est le code ASCII de 'j'

AFFECTATION D’UNE VALEUR A UN POINTEUR ET ALLOCATION DYNAMIQUE

Lorsque l'on déclare une variable char, int, float ..... un nombre de cases mémoire bien défini
est réservé pour cette variable. En ce qui concerne les pointeurs, l’allocation de la case-
mémoire pointée obéit à des règles particulières :

Exemple:
char *pc;
Si on se contente de cette déclaration, le pointeur pointe « n’importe où ». Son usage, tel que,
dans un programme peut conduire à un « plantage » du programme ou du système
d’exploitation si les mécanismes de protection ne sont pas assez robustes.
L’initialisation du pointeur n’ayant pas été faite, on risque d’utiliser des adresses non
autorisées ou de modifier d’autres variables.

Exercice V_2 : Tester le programme ci-dessous et conclure

char *pc; // normalement, il faudrait réserver !


*pc = 'a'; // le code ASCII de a est rangé dans la case mémoire pointée par pc
*(pc+1) = 'b'; // le code ASCII de b est rangé une case mémoire plus loin
*(pc+2) = 'c'; // le code ASCII de c est rangé une case mémoire plus loin
*(pc+3) = 'd'; // le code ASCII de d est rangé une case mémoire plus loin
cout<<"Valeurs :"<<*pc<<" "<<*(pc+1)<<" "<<*(pc+2)<<" "<<*(pc+3);

1ère méthode : utilisation de l’opérateur adresse :

C’est la méthode qui a été utilisée dans l’exercice V_1. Dans ce cas, l’utilisation de pointeurs
n’apporte pas grand-chose par rapport à l’utilisation classique de variables.
2ème méthode : affectation directe d’une adresse :

Cette méthode est réservée au contrôle de processus, quand on connaît effectivement la valeur
de l’adresse physique d’un composant périphérique.
Elle ne fonctionne pas sur un PC, pour lequel les adresses physiques ne sont pas dans le
même espace d’adressage que les adresses mémoires.

Il faut utiliser l'opérateur de "cast", jeu de deux parenthèses.


char *pc;
pc = (char*)0x1000; // p pointe sur l'adresse 0x1000

3ème méthode : allocation dynamique de mémoire :

C’est la méthode la plus utilisée et qui donne pleinement leur intérêt aux pointeurs par rapport
aux variables classiques.

Via l’opérateur new, natif dans le C++, on réserve, pendant l’exécution du programme, de la
place dans la mémoire pour l’objet (ou la variable) pointé. L’adresse de base est choisie par le
système lors de l’exécution, en fonction du système d’exploitation.
Le programmeur n’a à se soucier que de la quantité de cases mémoire dont il a besoin.

Exercice V_2 : Modifier l’exercice V_2 comme ci-dessous :

char *pc;
pc = new char[4]; // réservation de place dans la mémoire pour 4 char

*pc = 'a'; // le code ASCII de a est rangé dans la case mémoire pointée par pc
*(pc+1) = 'b'; // le code ASCII de b est rangé une case mémoire suivante
*(pc+2) = 'c'; // le code ASCII de c est rangé une case mémoire suivante
*(pc+3) = 'd'; // le code ASCII de d est rangé une case mémoire suivante

cout<<"Valeurs :"<<*pc<<" "<<*(pc+1)<<" "<<*(pc+2)<<" "<<*(pc+3);

delete pc; // libération de la place

Remarques :

- L’allocation dynamique peut se faire n’importe quand dans le programme (avant d’utiliser le
pointeur !).
- L’opérateur delete libère la place réservée quand elle devient inutile.
- L’allocation dynamique devrait se faire dès la déclaration du pointeur avec la syntaxe
suivante :
char *pc = new char[4];
- Si on a besoin d’une seule case mémoire, on écrira :
char *pc; ou bien char *pc = new char;
pc = new char ;
Réservation de place pour d’autres types de pointeurs :

char *pc;
int *pi,*pj;
float *pr;
pc = new char[10]; // on réserve de la place pour 10 caractères, soit 10 cases mémoires
pi = new int[5]; // on réserve de la place pour 5 entiers, soit 20 cases mémoire
pj = new int; // on réserve de la place pour 1 entier, soit 4 cases mémoire
pr = new float[6]; // on réserve de la place pour 6 réels, soit 24 cases mémoire

delete pc; // on libère la place précédemment réservée pour c


delete pi; // on libère la place précédemment réservée pour i
delete pr; // on libère la place précédemment réservée pour r

Comme précédemment, l’allocation dynamique peut être faite dès la déclaration du pointeur,
elle est préférable :
int *pi = new int[5];

Exercice V_3:

adr1 et adr2 sont des pointeurs pointant sur des réels. La variable pointée par adr1 vaut -
45,78; la variable pointée par adr2 vaut 678,89. Ecrire un programme qui affiche les valeurs
de adr1, adr2 et de leur contenu.

L'opérateur de "cast", permet d'autre part, à des pointeurs de types différents de pointer sur la
même adresse.

Exemple: char *pc; // pc pointe sur un objet de type caractère


int *pi; // pi pointe sur un objet de type entier
pi = new int; // allocation dynamique pour i
pc = (char*)i; // c et i pointent sur la même adresse, c sur un caractère

Exercice V_4:

adr_i est un pointeur de type entier; la variable pointée i vaut 0x41424344. A l'aide d'une
conversion de type de pointeur, écrire un programme montrant le rangement des 4 octets en
mémoire.

Exercice V_5:

Saisir un texte de 10 caractères. Ranger les caractères en mémoire. Lire le contenu de la


mémoire et compter le nombre de lettres e.
Exercice V_6:

Saisir 6 entiers et les ranger à partir de l'adresse adr_deb. Rechercher le maximum, l'afficher
ainsi que sa position. La place nécessaire dans la mémoire peut-être le résultat d’un calcul :

int taille; char


*image;
cout<<"\nQuelle est la taille de l’image ? (en octets)";
cin>>taille;
image = new char[taille];

Exercice V_7:

Reprendre l’exercice V_6 mais en demandant à l’utilisateur combien de nombres il souhaite


traiter, et en réservant en mémoire la place nécessaire.

CORRIGE DES EXERCICES

Exercice V_3:

Exercice V_4:
L'analyse de l'exécution de ce programme, montre que les microprocesseurs INTEL rangent en
mémoire d'abord les poids faibles d'une variable.
Exercice V_5:
Exercice V_6:
Exercice V_7:
LES TABLEAUX ET LES CHAINES DE CARACTERES

LES TABLEAUX DE NOMBRES (INT ou FLOAT)

Les tableaux correspondent aux vecteurs et matrices en mathématiques. Un tableau est


caractérisé par sa taille et par le type de ses éléments.

Les tableaux à une dimension:

Déclaration: type nom[dim]; Exemples: int compteur[10];


float nombre[20];

Cette déclaration signifie que le compilateur réserve dim places en mémoire pour ranger
les éléments du tableau.
Exemples:
int compteur[10]; le compilateur réserve des places en mémoire pour 10 entiers, soit 40
octets.
float nombre[20]; le compilateur réserve des places en mémoire pour 20 réels, soit 80
octets.

Remarque: dim est nécessairement une EXPRESSION CONSTANTE (expression qui peut
contenir des valeurs ou des variables constantes – c.f. modificateur const). Ce ne peut être en
aucun cas une combinaison des variables du programme1.

Utilisation: Un élément du tableau est repéré par son indice. En langage C et C++ les tableaux
commencent à l'indice 0. L'indice maximum est donc dim-1.

Exemples:

compteur[2] = 5;
nombre[i] = 6.789;
cout<<compteur[i];
cin>>nombre[i];

Il n’est pas nécessaire de définir tous les éléments d'un tableau. Toutefois, les valeurs
non initialisées contiennent alors des valeurs quelconques.

Exercice VI_1: Saisir 10 réels, les ranger dans un tableau. Calculer et afficher leur moyenne
et leur écart-type.

1
Sauf s’il s’agit d’une allocation dynamique en exploitant l’opérateur « new » - c.f. Cours n° 5.
Les tableaux à plusieurs dimensions:

Tableaux à deux dimensions:

Déclaration: type nom[dim1][dim2]; Exemples: int compteur[4][5];


float nombre[2][10]; Utilisation:
Un élément du tableau est repéré par ses indices. En langage C++ les tableaux commencent aux
indices 0. Les indices maxima sont donc dim1-1, dim2-1.

Exemples:

compteur[2][4] = 5;
nombre[i][j] = 6.789;
cout<<compteur[i][j];
cin>>nombre[i][j];

Il n’est pas nécessaire de définir tous les éléments d'un tableau. Les valeurs non
initialisées contiennent alors des valeurs quelconques.

Exercice VI_2: Saisir une matrice d'entiers 2x2, calculer et afficher son déterminant.

Tableaux à plus de deux dimensions:

On procède de la même façon en ajoutant les éléments de dimensionnement ou les indices


nécessaires.

INITIALISATION DES TABLEAUX

On peut initialiser les tableaux au moment de leur déclaration:

Exemples:

int liste[10] = {1,2,4,8,16,32,64,128,256,528};

float nombre[4] = {2.67,5.98,-8.0,0.09};

int x[2][3] = {{1,5,7},{8,4,3}}; // 2 lignes et 3 colonnes


TABLEAUX ET POINTEURS

En déclarant un tableau, on dispose d’un pointeur (adresse du premier élément du tableau).


Le nom d’un tableau est un pointeur sur le premier élément.

Les tableaux à une dimension:

Les écritures suivantes sont équivalentes:

/* Allocation /* Allocation Déclaration


dynamique pendant automatique pendant la
l’exécution du compilation du programme
programme */ */
int *tableau = new int tableau[10];
int[10];
*tableau tableau[0] le 1er élément
*(tableau+i) tableau[i] un élément
d’indice i
tableau &tableau[0] adresse du 1er
élément
(tableau + i) &(tableau[i]) adresse d'un
élément i

Il en va de même avec un tableau de réels (float).

Remarques:
- La déclaration d'un tableau entraîne automatiquement la réservation de places en mémoire.
C’est aussi le cas si on utilise un pointeur et l’allocation dynamique en exploitant l’opérateur
new.
- On ne peut pas libérer la place réservée en mémoire pour un tableau créé en allocation
automatique (la réservation étant réalisée dans la phase de compilation – en dehors de
l’exécution). Par contre, en exploitant l’allocation dynamique via un pointeur, la primitive
delete libère la mémoire allouée.

Les tableaux à plusieurs dimensions:

Un tableau à plusieurs dimensions est un pointeur de pointeur.

Exemple: int t[3][4]; t est un pointeur de 3 tableaux de 4 éléments ou bien de 3 lignes à 4


éléments.
Les écritures suivantes sont équivalentes:

t[0] &t[0][0] t adresse du 1er élément

t[1] &t[1][0] adresse du 1er élément de la


2e ligne
t[i] &t[i][0] adresse du 1er élément de la
ième ligne
t[i]+1 &(t[i][0])+ adresse du 1er élément de la
1 ième +1 ligne

t[0]
t[0][0]

t[0][2]

t[1]

REPARTITION DES ELEMENTS EN MEMOIRE

Le tableau a été déclaréainsi: int t[3][4];


t[2]

t[2][3]

Exercice VI_3:

Un programme contient la déclaration suivante:


int tab[10] = {4,12,53,19,11,60,24,12,89,19};
Compléter ce programme de sorte d'afficher les adresses des éléments du tableau.

Exercice VI_4:

Un programme contient la déclaration suivante:


int tab[20] = {4,-2,-23,4,34,-67,8,9,-10,11, 4,12,-53,19,11,-60,24,12,89,19};
Compléter ce programme de sorte d'afficher les éléments du tableau avec la présentation
suivante:

4 -2 -23 4 34
-67 8 9 -10 11
4 12 -53 19 11
-60 24 12 89 19
LES CHAINES DE CARACTERES

En langage C++, les chaînes de caractères sont des tableaux de caractères. Leur
manipulation est donc analogue à celle d'un tableau à une dimension:

Déclaration: char nom[dim]; ou bien char *nom=new char[dim];

Exemple: char texte[20]; ou bien char *texte=new char[20];


Il faut toujours prévoir une place de plus pour une chaîne de caractères. En effet, en C ou en C++,
une chaîne de caractère se termine toujours par le caractère NUL ('\0'). Ce caractère permet de
détecter la fin de la chaîne lorsque l’on écrit des programmes de traitement.

Affichage à l'écran:
On utilise l’opérateur cout :
char texte[10] = « BONJOUR »;
cout<<"VOICI LE TEXTE:"<<texte;

Saisie:
On utilise l’opérateur cin :
char texte[10];
cout<<"ENTRER UN TEXTE: ";
cin>> texte;

Remarque: cin ne permet pas la saisie d'une chaîne comportant des espaces : les caractères
saisis à partir de l'espace ne sont pas pris en compte (l'espace est un délimiteur au même titre
que LF).
A l'issue de la saisie d'une chaîne de caractères, le compilateur ajoute '\0' en mémoire après le
dernier caractère.

Exercice VI_5:

Saisir une chaîne de caractères, afficher les éléments de la chaîne caractère par caractère.

Exercice VI_6:

Saisir une chaîne de caractères. Afficher le nombre de lettres e de cette chaîne.

Fonctions permettant la manipulation des chaînes :

Les bibliothèques fournies avec les compilateurs contiennent de nombreuses fonctions de


traitement des chaînes de caractères. En BORLAND /autre IDE C++, elles appartiennent
aux bibliothèques string.h ou stdlib.h. En voici quelques exemples:
Générales (string.h):

Nom : strcat
Prototype : void *strcat(char *chaine1, char *chaine2);
Fonctionnement : concatène les 2 chaînes, résultat dans chaine1, renvoie l'adresse de chaine1.
Exemple d’utilisation :

char texte1[30] = "BONJOUR "; // remarquer l'espace après le R char


texte2[20]= "LES AMIS";
strcat(texte1,texte2);
// texte2 est inchangée, texte1 vaut maintenant "BONJOUR LES AMIS"

Nom : strlen
Prototype : int strlen(char *chaine);
Fonctionnement : envoie la longueur de la chaîne ('\0' non comptabilisé).
Exemple d’utilisation :

char texte1[30] = "BONJOUR";


int L = strlen(texte1);
cout<< "longueur de la chaîne : "<<L; // L vaut 7

Nom: strrev
Prototype : void *strrev(char *chaine);
Fonctionnement : inverse la chaîne et, renvoie l'adresse de la chaîne inversée. Exemple
d’utilisation :

char texte1[10] = "BONJOUR";


strrev(texte1); // texte1 vaut maintenant "RUOJNOB"

Comparaison (string.h):

Nom : strcmp
Prototype : int strcmp(char *chaine1, char *chaine2);
Fonctionnement : renvoie un nombre:
- positif si chaine1 est supérieure à chaine2 (au sens de l'ordre alphabétique)
- négatif si chaîne1 est inférieure à chaine2
- nul si les chaînes sont identiques.
Cette fonction est utilisée pour classer des chaînes de caractères par ordre alphabétique.
Exemple d’utilisation :

char texte1[30] = "BONJOUR ";


char texte2[20]= "LES AMIS";
int n = strcmp(texte1,texte2); // n est positif

Copie (string.h):

Nom : strcpy
Prototype : void *strcpy(char *chaine1,char *chaine2);
Fonctionnement : recopie chaine2 dans chaine1 et renvoie l'adresse de chaîne1.
Exemple d’utilisation :

char texte2[20] = "BONJOUR ";


char texte1[20];
strcpy(texte1,texte2);
// texte2 est inchangée, texte1 vaut maintenant "BONJOUR "

Recopie (string.h):

Ces fonctions renvoient l'adresse de l'information recherchée en cas de succès, sinon le pointeur
NULL (c'est à dire le pointeur dont la valeur n’a jamais été initialisée).

void *strchr(chaine, caractere); recherche le caractère dans la chaîne.


void *strrchr(chaine, caractere); idem en commençant par la fin.
void *strstr(chaine, sous-chaine); recherche la sous-chaîne dans la chaîne.

Exemple d’utilisation :

char texte1[30] ;
cout<< "SAISIR UN TEXTE :";
cin>>texte1;
if( strchr(texte1,A)!=NULL) cout<<"LA LETTRE A EXISTE DANS CE TEXTE ";
else cout<<<< "LA LETTRE A NEXISTE PAS DANS CE TEXTE ";

Conversions (stdlib.h):

int atoi(char *chaine); convertit la chaîne en entier


float atof(char *chaine); convertit la chaîne en réel
Exemple d’utilisation :

char texte[10]; int n;


cout<<"ENTRER UN TEXTE: "; cin>>texte; n
= atoi(texte) ;
cout<<n;
// affiche 123 si texte vaut "123", affiche 0 si texte vaut "bonjour" void *itoa(int n, char
*chaîne, int base);
// convertit un entier en chaîne:
// base: base dans laquelle est exprimé le nombre,
// cette fonction renvoie l'adresse de la chaîne.

Exemple d’utilisation :

char texte[10]; int


n=12;
itoa(12,texte,10); // texte vaut "12"

Pour tous ces exemples, la notation void* signifie que la fonction renvoie un pointeur
(l'adresse de l'information recherchée), mais que ce pointeur n'est pas typé. On peut ensuite
le typer à l'aide de l'opérateur cast.

Exemple:

char *adr;
char texte[10] = "BONJOUR";
adr=(char*)strchr(texte,'N');//adr pointe sur l'adresse de la lettre N

Exercice VI_7:

L'utilisateur saisit le nom d'un fichier. Le programme vérifie que celui-ci possède l'extension
.PAS

Exercice VI_8:

Un oscilloscope à mémoire programmable connecté à un PC renvoie l'information suivante


sous forme d'une chaîne de caractères terminée par '\0'au PC:
"CHANNELA 0 10 20 30 40 30 20 10 0 -10 -20 -30 -40 -30 -20 -10 -0"
Afficher sur l'écran la valeur des points vus comme des entiers. On simulera la présence de
l'oscilloscope en initialisant une chaîne de caractères char mesures [100].
Chapitre2 : Les entrées/sorties et les fonctions C++

1. Bibliothèque d’entrée/sortie :

Les opérations standards d’entrée et de sortie sont fournies par trois flots (streams), désignés
par les variables suivantes :
 cin (ou std ::cin) : désigne le flot d’entrée standard (typiquement, votre clavier),
 cout (ou std ::cout): désigne le flot de sortie standard (typiquement, la fenêtre
d’exécution sur votre écran),
 cerr (ou std ::cerr): désigne le flot standard des messages d’erreur.

Pour utiliser les utiliser les entrées/sortie standard il faut inclure le fichier d’entête « iostream »
#include <iostream>
Les opérateurs « << » et «>> » assurent :
 le transfert de l’information
 le formatage de l’information
L’opérateur « << », également appelée opérateur d’insertion, sera utilisé pour réaliser des
écritures sur un flux de données, tandis que l’opérateur « >> », ou opérateur d’extraction,
permettra de réaliser la lecture d’une nouvelle donnée dans le flux d’entrée. Ces deux opérateurs
renvoient tous les deux le flux de données utilisé, ce qui permet de réaliser plusieurs opérations
d’entrée / sortie successivement sur le même flux.
Exemple :
#include <iostream>
using namespace std;
int main(void)
{
int i;
// Lit un entier :
cin >> i;
// Affiche cet entier et le suivant :
cout << i << " " << i+1 << endl;
return 0;
}

10
De plus, la librairie standard définie ce que l’on appelle des manipulateurs permettant de réaliser
des opérations simples sur les flux d’entrée / sortie. Le manipulateur le plus utilisé est sans nul

11
doute le manipulateur endl, qui, comme son nom l’indique, permet de signaler une fin de ligne
et d’effectuer un retour de chariot lorsqu’il est employé sur un flux de sortie.
Bien entendu, la bibliothèque iostream fournit de nombreuses autres fonctionnalités
d’entrée/sortie, et la bibliothèque fstream fournit les fonctionnalités de manipulation de
fichiers.

Un tampon, également appelé cache, est une zone mémoire dans laquelle les opérations
d’écriture et de lecture se font et dont le contenu est mis en correspondance avec les données
d’un média physique sous-jacent. Les mécanismes de cache ont essentiellement pour but
d’optimiser les performances des opérations d’entrée / sortie. En effet, l’accès à la mémoire
cache est généralement beaucoup plus rapide que l’accès direct aux supports physiques ou aux
médias de communication. Les opérations effectuées par le programme se font donc, la plupart
du temps, uniquement au niveau du tampon, et ce n’est que dans certaines conditions que les
données du tampon sont effectivement transmises au média physique. Le gain en performance
peut intervenir à plusieurs niveaux. Les cas les plus simples étant simplement lorsqu’une
donnée écrite est écrasée peu de temps après par une autre valeur (la première opération
d’écriture n’est alors jamais transmise au média) ou lorsqu’une donnée est lue plusieurs fois (la
même donnée est renvoyée à chaque lecture). Bien entendu, cela suppose que les données
stockées dans le tampon soient cohérentes avec les données du média, surtout si les données
sont accédées au travers de plusieurs tampons. Tout mécanisme de gestion de cache permet
donc de vider les caches (c’est-à-dire de forcer les opérations d’écriture) et de les invalider
(c’est-à-dire de leur signaler que leurs données sont obsolètes et qu’une lecture physique doit
être faite si on cherche à y accéder).
Les mécanismes de mémoire cache et de tampon sont très souvent utilisés en informatique, à
tous les niveaux. On trouve des mémoires cache dans les processeurs, les contrôleurs de disque,
les graveurs de CD, les pilotes de périphériques des systèmes d’exploitation et bien entendu
dans les programmes.
Chacun de ces caches contribue à l’amélioration des performances globales en retardant au
maximum la réalisation des opérations lentes et en optimisant les opérations de lecture et
d’écriture (souvent en les effectuant en groupe, ce qui permet de réduire les frais de
communication ou d’initialisation des périphériques). Il n’est donc absolument pas surprenant
que la librairie standard C++ utilise elle aussi la notion de tampon dans toutes ses classes
d’entrée / sortie...
 les manipulateurs « hex », « oct » et « dec » permettent la réalisation des entrées/sorties

12
des entiers respectivement en base hexadécimale, en base octale et en base décimale.
 Les manipulateurs « fixed », « scientific » activent la représentation respective en
virgule fixe et virgule flottante des nombres flottant (réels).
Ces manipulateurs ne nécessitent pas l’importation du fichier d’entête « iomanip ».
Exemple :
#include <iostream>
using namespace std;

int main() {
int n = 75;
double d = 34.56789120654987;
cout << n << endl;
cout << hex << n << endl;
cout << oct << n << endl;
cout << dec << n << endl;
cout << d << endl;
cout << scientific << d << endl;
cout << fixed << d << endl;
return 0;
}

 Le manipulateur « setbase(base) » joue le même rôle que les manipulateurs « hex »,


« oct » et « dec » qui ne sont réellement que des alias de celui-ci.
int a = 195;
cout << "valeur en base 16 = " << setbase(16) << a << endl;
//affiche : valeur en base 16 = C3
cout << "valeur en base 10 = " << setbase(10) << a << endl;
//affiche : valeur en base 10 = 195
cout << "valeur en base 8 = " << setbase(8) << a << endl;
//affiche : valeur en base 8 = 303

 Le manipulateur « setw(widh) » : indique que les prochaines données doivent être


écrites sur les nombres de caractères indiqués en ajoutant des caractères espaces avant.
int a = 15;
cout << "valeur de a = " << setw(5) << a << endl;
//affiche : la valeur de a = 15

 Le manipulateur « setfill(char) » permet de spécifier le caractère à utiliser pour


compléter les données utilisant le manipulateur « setw »

13
int a = 15;
cout << "valeur de a = " << setfill(‘x’) << setw(5) << a << endl;
//affiche : la valeur de a = xxx15

 Le manipulateur « setprecision(digits) » spécifie que les prochains nombres à virgule


flottante doivent êtres écrits avec la précision donnée (le nombre de chiffres à écrire
après la virgule)
double f = 3.56789 ;
cout << setprecision(4) << f << endl;
//affiche : 3.5679
cout << setprecision(10) << f << endl;
//affiche : 3.5678900000

2. Les fonctions en C++ :

Le C++ ne permet de faire que des fonctions, pas de procédures. Une procédure peut être faite
en utilisant une fonction ne renvoyant pas de valeur (ou en ignorant la valeur retournée).
La définition des fonctions se fait comme suit :
type identificateur(paramètres)
{
... /* Instructions de la fonction. */
}
Où :
 type est le type de la valeur renvoyée,
 identificateur est le nom de la fonction,
 et paramètres est une liste de paramètres.

Toute fonction doit être déclarée avant d’être appelée pour la première fois (prototype). La
définition d’une fonction peut faire office de déclaration.
Il peut se trouver des situations où une fonction doit être appelée dans une autre fonction définie
avant elle. Comme cette fonction n’est pas définie au moment de l’appel, elle doit être déclarée.
Le rôle des déclarations est donc de signaler l’existence des fonctions aux compilateurs afin de
les utiliser, tout en reportant leur définition de ces fonctions plus loin ou dans un autre fichier.
La syntaxe de la déclaration d’une fonction (ou prototype) est la suivante :

14
type identificateur (paramètres) ;
Exemple :
int Min(int a, int b); /* Déclaration de la fonction minimum
*/
/* Prototype */
/* Fonction principale. */
int main(void)
{
int i = Min(2,3); /*Appel à la fonction Min, déjà déclarée.*/
return 0;
}
/* Définition de la fonction min. */
int Min(int i, int j)
{
if (i<j) return i;
else return j;
}
 valeur par défaut :
En C++, il est possible de donner des valeurs par défaut aux paramètres dans une déclaration,
et ces valeurs peuvent être différentes de celles que l’on peut trouver dans une autre déclaration.
Dans ce cas, les valeurs par défaut utilisées sont celles de la déclaration visible lors de l’appel
de la fonction.
Exemple :
#include <iostream>

using namespace std;


int nombresDeSecondes(int heures, int minutes = 0, int secondes = 0);
int main() {
cout << "Hello, World!" << endl;
cout << nombresDeSecondes(3) << endl;
return 0;
}
int nombresDeSecondes(int heures, int minutes, int secondes)
{
int total = 0;
total = heures * 3600;
total += minutes * 60;
total += secondes;
return total;
}

 Passage de paramètre par référence : passer un paramètre par référence a les mêmes
avantages que le passage d’un paramètre par pointeur. Celui-ci est également
modifiable. La différence est que l’opérateur de référencement n’est pas utilisé car il est
déjà d’une référence.
Le passage de paramètre par référence utilise la même syntaxe similaire au passage par pointeur

15
dans la déclaration de la fonction, en utilisant « & » au lieu de « * ».
Exemple
#include <iostream>

using namespace std;


void incrementer(int& val);

int main() {
int a = 5;
cout << " a =" << a << endl;
incrementer(a);
cout << " a =" << a << endl;
return 0;
}
void incrementer (int& val)
{
val++;
}

 Fonction de type « inline » :


Le C++ dispose du mot-clé inline, qui permet de modifier la méthode d’implémentation des
fonctions. Placé devant la déclaration d’une fonction, il propose au compilateur de ne pas
instancier cette fonction. Cela signifie que l’on désirerait que le compilateur remplace l’appel
d’une fonction par le code correspondant.
Si la fonction est grosse ou si elle est appelée souvent, le programme devient plus gros, puisque
la fonction est réécrite à chaque fois qu’elle est appelée. En revanche, il devient nettement plus
rapide, puisque les mécanismes d’appel de fonctions, de passage des paramètres et de la valeur
de retour sont ainsi évités.
De plus, il faut connaître les restrictions des fonctions inline :
 elles ne peuvent pas être récursives ;
 elles ne sont pas instanciées, donc on ne peut pas faire de pointeur sur une fonction
inline.
Exemple :
#include <iostream>

using namespace std;


inline int Max (int , int);
int main() {

cout << Max (5,12) << endl;


return 0;
}
inline int Max(int i, int j)
{
if (i>j)

16
return i;
else
return j;
}

 Surcharge des fonctions :


Il est interdit en C de définir plusieurs fonctions qui portent le même nom. En C++, cette
interdiction est levée, moyennant quelques précautions. Le compilateur peut différencier deux
fonctions en regardant le type des paramètres qu’elle reçoit. La liste de ces types s’appelle la
signature de la fonction.
En revanche, le type du résultat de la fonction ne permet pas de l’identifier, car le résultat peut
ne pas être utilisé ou peut être converti en une valeur d’un autre type avant d’être utilisé après
l’appel de cette fonction.
Il est donc possible de faire des fonctions de même nom (on dit que ce sont des fonctions
surchargées) si et seulement si toutes les fonctions portant ce nom peuvent être distinguées par
leurs signatures.
La fonction qui sera appelée sera choisie parmi les fonctions de même nom, et ce sera celle dont
la signature est la plus proche des valeurs passées en paramètre lors de l’appel.
Exemple :
#include <iostream>
using namespace std;

int double_it(int entier) {


cout << "Appel de double_it pour un entier." << endl;
return 2*entier;
}

double double_it(double double_entier) {


cout << "Appel de double_it pour un double." << endl;
return 2*double_entier;
}

int main () {
int int_var = 6;
double dbl_var = 6.1;
cout << int_var << " est la moitié de : " << double_it(int_var) << endl;
cout << dbl_var << " est la moitié de : " << double_it(dbl_var) << endl;

return 0;
}
Le choix de la fonction appelée est fait en fonction du type des paramètres. Pour la valeur 6 la
fonction utilisée est la première car 6 est un entier. Ensuite, pour 6.1, qui peut être représenté
par un double, c'est la deuxième forme de double_it qui est utilisée.

17
 Fonction avec expression constante :
Le standard C++11 généralise la notion d'expressions constantes pour inclure les appels à des
fonctions simples (fonctions constexpr).
Une fonction est une fonction expression constante si :
 Elle renvoie une valeur (le type void n'est pas autorisé)
 Le corps de la fonction est composé d'une seule instruction, l'instruction return :
return expr;
 Elle est déclarée avec le mot clé constexpr.
Exemple :
//fonction à expression constante
constexpr int add(int x, int y) {
return x + y;
}
// Constante calculée à base d’une fonction à expression constante
constexpr double var = 2 * add(3, 4);

 Pointeur sur fonction :


Les pointeurs de fonction sont des pointeurs comme des pointeurs de variables, mais qui
pointent sur l’adresse d’une fonction. On doit garder à l’esprit qu’un programme qui est exécuté
prend un certain espace dans la mémoire principale. L’exécutable et les variables utilisés sont
tous deux placés dans la mémoire principale.
Ainsi une fonction dans le code du programme est comme, par exemple un champ de caractère,
rien de plus qu’une adresse. La seule chose importante est comment l’on décrit, ou comment le
compilateur décrit sur quelle zone mémoire pointe le pointeur.
Il y est possible par exemple de passer des pointeurs de fonctions en paramètre à une fonction,
ou de retourner un pointeur de fonction.
Voici trois exemples de pointeurs de fonction :

int (*pf1) (void);


pf1 pointe sur une fonction qui a int pour type de retour et qui n'a pas de paramètres.

double (*pf2) (double, double);


pf2 pointe sur une fonction qui renvoie un double et qui a deux double en paramètres.

void (*pf3) ();

18
pf3 pointe sur une fonction qui ne renvoie rien. On ne connait pas les paramètres de la fonction
(cela vient de l'ancienne formulation des fonctions en C).
Dans la définition du pointeur de fonction, la parenthèse est obligatoire (la priorité de l'opérateur
* est trop faible).
Prenons par exemple, la fonction suivante :

double max (double a, double b) {


return (a>b)? a:b;
}

On associe pointeur de fonction et fonction comme cela : (les deux lignes sont équivalentes)
pf2 = max /* forme la plus portable */
pf2 = &max; /* forme la plus "cohérente" */
Maintenant (*pf2) (10.5,21.0); , pf2(10.5,21.0) et max(10.5,21.0); donnent le même résultat.
La forme avec * est préconisée.
Prototype d'une fonction qui a pour paramètre un pointeur de fonction :
void appliquer (double, double, double (*)(double, double));
void appliquer (double a, double b, double (*pf) (double, double));

Exemple :
Dans l’exemple suivant, on s’intéresse à la tâche qui effectue l’une des quatre opérations
arithmétiques basiques.
// 1.2 Exemple introductif : Comment remplacer la commande switch ?
// Réaliser une des quatre opérations basiques spécifiées par les caractères ’+’, ’-’ ’*’ ou ’/’
// Les quatre opérations arithmétiques une de ces fonctions est sélectionnée par une routine //
// contenant l’une commande switch ou un pointeur de fonction
float Plus (float a, float b) { return a+b; }
float Minus (float a, float b) { return a-b; }
float Multiply (float a, float b) { return a*b; }
float Divide (float a, float b) { return a/b; }
// solution utilisant la commande switch (opCode) spécifie quel opération exécuter
void Switch(float a, float b, char opCode)
{
float result;

19
switch(opCode)
{
case ’+’ : result = Plus (a, b); break;
case ’-’ : result = Minus (a, b); break;
case ’*’ : result = Multiply (a, b); break;
case ’/’ : result = Divide (a, b); break;
}
cout << "switch: 2+5=" << result << endl;
// Affiche le résultat
}
// Solution avec les pointeurs de fonction (pt2Func) est un pointeur de fonction qui pointe sur
// une fonction qui prend deux nombres à virgule flottante en argument et qui retourne un
nombre à virgule flottante. Le pointeur de fonction ‘‘spécifie’’ quelle opération exécuter.
void Switch_With_Function_Pointer(float a, float b, float
(*pt2Func)(float, float))
{
float result = pt2Func(a, b);
cout << "switch replaced by function pointer: 2-5=";
// Affiche le resultat
cout << result << endl;
}
// exécute le code d’exemple
void Replace_A_Switch()
{
cout << endl << "Executing function ’Replace_A_Switch’" << endl;
Switch(2, 5, ’+’);
Switch_With_Function_Pointer(2, 5, &Minus);
}
Le template std::function est un adaptateur générique de fonction. En d'autres termes,
une instance de std::function permet de manipuler une cible appelable (i.e. pour laquelle
operator() est défini) , par exemple : une lambda (fonction anonyme), un bind (application
partielle de fonction), un foncteur (objet-fonction), une fonction...

 Lambda calcul (fonctions anonymes):

20
En C++11, une expression lambda, souvent appelée lambda, est un moyen pratique de définir
un objet de fonction anonyme à l'emplacement où il est appelé ou passé comme argument à une
fonction. Les expressions lambda sont généralement utilisées pour encapsuler quelques lignes
de code qui sont passées à des algorithmes ou méthodes asynchrones.
Syntaxe générale :
[capture](parametres)->type-retour{corps}
[capture](parametres){corps}
[capture]{corps}
Exemple :
#include <iostream>
using namespace std;

int main() {

auto func = [] { cout << "Hello, World!" << endl; };


func();
return 0;
}

Exemple :
// function example
#include <iostream> // std::cout
#include <functional> // std::function, std::negate

// a function:
int half(int x) {return x/2;}

// a function object class:


struct third_t {
int operator()(int x) {return x/3;}
};

// a class with data members:


struct MyValue {
int value;
int fifth() {return value/5;}
};

21
int main () {
std::function<int(int)> fn1 = half; // function
std::function<int(int)> fn2 = &half; // function pointer
std::function<int(int)> fn3 = third_t(); // function object
std::function<int(int)> fn4 = [](int x){return x/4;}; // lambda expression
std::function<int(int)> fn5 = std::negate<int>(); // standard function
object

std::cout << "fn1(60): " << fn1(60) << '\n';


std::cout << "fn2(60): " << fn2(60) << '\n';
std::cout << "fn3(60): " << fn3(60) << '\n';
std::cout << "fn4(60): " << fn4(60) << '\n';
std::cout << "fn5(60): " << fn5(60) << '\n';

// stuff with members:


std::function<int(MyValue&)> value = &MyValue::value; // pointer to data
member
std::function<int(MyValue&)> fifth = &MyValue::fifth; // pointer to member
function

MyValue sixty {60};

std::cout << "value(sixty): " << value(sixty) << '\n';


std::cout << "fifth(sixty): " << fifth(sixty) << '\n';

return 0;
}

22
Chapitre 3 : Notions de base en Programmation Orientée
Objets

1. Notion d’objet et classe :

Théoriquement, il y a une nette distinction entre les données et les opérations qui leur sont
appliquées. En tout cas, les données et le code ne se mélangent pas dans la mémoire de
l’ordinateur, sauf cas très particuliers.
Cependant, l’analyse des problèmes à traiter se présente d’une manière plus naturelle si l’on
considère les données avec leurs propriétés. Les données constituent les variables, et les
propriétés les opérations qu’on peut leur appliquer. De ce point de vue, les données et le code
sont logiquement inséparables, même s’ils sont placés en différents endroits de la mémoire de
l’ordinateur.

Ces considérations conduisent à la notion d’objet. Un objet est un ensemble de données et sur
lesquelles des procédures peuvent être appliquées. Ces procédures ou fonctions applicables
aux données sont appelées méthodes. La programmation d’un objet se fait donc en donnant les
données de l’objet et en définissant les procédures qui peuvent lui être appliquées.

Il se peut qu’il y ait plusieurs objets identiques, dont les données ont bien entendu des valeurs
différentes, mais qui utilisent le même jeu de méthodes. On dit que ces différents objets
appartiennent à la même classe d’objet. Une classe constitue donc une sorte de type, et les
objets de cette classe en sont des instances.

La classe définit donc la structure des données, alors appelées champs ou variables d’instances,
que les objets correspondants auront, ainsi que les méthodes de l’objet. À chaque instanciation,
une allocation de mémoire est faite pour les données du nouvel objet créé. L’initialisation de
l’objet nouvellement créé est faite par une méthode spéciale, le constructeur. Lorsque l’objet
est détruit, une autre méthode est appelée : le destructeur. L’utilisateur peut définir ses propres
constructeurs et destructeurs d’objets si nécessaire.

23
Figure1 : exemple d’objet

2. Déclaration d’une classe :

class nomClasse
{
private :
// Déclarations des données et fonctions-membres privées

public :
// Déclarations des données et fonctions-membres publiques
} ;

Exemple :
//fichier « Etudiant.h »
class Etudiant{
private:
char nom[20];
int age;
float note_DS, note_Exam, moyenne_mat;
public:
float Moyenne ();
void Affiche();
};

//fichier « Etudiant.cpp »
24
#include <iostream>
#include "Etudiant.h"
using namespace std;

Etudiant::Etudiant() {
cout << "donner le nomm :"<< endl;
cin >> Etudiant::nom;
cout << "donner note DS :"<< endl;
cin >> Etudiant::note_DS;
cout << "donner note Examen :"<< endl;
cin >> Etudiant::note_Exam;
}
float Etudiant::Moyenne() {
moyenne_mat = (note_DS + (2 * note_Exam)) / 3.0;
return moyenne_mat;
}
void Etudiant::Affiche() {
cout << "la moyenne de " << Etudiant::nom << " est :" <<
Etudiant::moyenne_mat;
}

Démarche « DDU » :
En C++, la programmation d’une classe se fait en trois phases : déclaration, définition,
utilisation.
 Déclaration : c’est la partie interface de la classe. Elle se fait dans un fichier dont le
nom se termine par .h
 Définition : c’est la partie implémentation de la classe. Elle se fait dans un fichier dont
le nom se termine par .cpp. Ce fichier contient les définitions des fonctions-
membres de la classe, c’est-à-dire le code complet de chaque fonction.
 Utilisation : elle se fait dans un fichier dont le nom se termine par.cpp (programme
principal)
Rappelons que la directive d’inclusion #include permet d’inclure un fichier de déclarations
dans un autre fichier : on écrira #include <bib.h> s’il s’agit d’un fichier standard livré
avec le compilateur C++, ou #include "untel.h" s’il s’agit d’un fichier écrit par nous-
mêmes.

25
Figure 2 : démarche DDU

3. Encapsulation des données :

Les données des objets peuvent être protégées : c’est-à-dire que seules les méthodes de l’objet
peuvent y accéder. Ce n’est pas une obligation, mais cela accroît la fiabilité des programmes.
Si une erreur se produit, seules les méthodes de l’objet doivent être vérifiées. De plus, les
méthodes consti-tuent ainsi une interface entre les données de l’objet et l’utilisateur de l’objet
(un autre programmeur).
Cet utilisateur n’a donc pas à savoir comment les données sont gérées dans l’objet, il ne doit
utiliser que les méthodes. Les avantages sont immédiats : il ne risque pas de faire des erreurs
de programmation en modifiant les données lui-même, l’objet est réutilisable dans un autre
programme parce qu’il a une interface standardisée, et on peut modifier l’implémentation
interne de l’objet sans avoir à refaire tout le programme, pourvu que les méthodes gardent le
même nom et les mêmes paramètres.
Cette notion de protection des données et de masquage de l’implémentation interne aux
utilisateurs de l’objet constitue ce que l’on appelle l’encapsulation.
Pour réaliser l’encapsulation, on utilise les mots-clés suivants :
 public : les accès sont libres ;

26
 private : les accès sont autorisés dans les fonctions de la classe seulement ;
 protected : les accès sont autorisés dans les fonctions de la classe et de ses
descendantes seulement. Le mot-clé protected n’est utilisé que dans le cadre de
l’héritage des classes (le prochain chapitre détaillera ce point).

Par défaut les membres d’une classe sont privés.


On accède aux membres des objets en C++ comme on accède aux membres des structures en C
à travers l’opérateur point « . ».
D’autre part, dans la définition d’une fonction-membre, on doit ajouter <nom_classe>::
devant le nom de la fonction. Etudiant::Moyenne()
Les règles suivantes sont appliquées pour identifier l’accès aux attributs et méthodes membres :
 Quand des membres d'un objet apparaissent dans une expression écrite dans une
fonction du même objet on dit que ce dernier fait un accès à ses propres membres.
 Un membre public d'une classe peut être accédé partout où il est visible ;
 un membre privé ne peut être accédé que depuis une fonction membre de la classe

4. Constructeur et destructeur :

Le constructeur se définit comme une méthode normale. Cependant, pour que le compilateur
puisse la reconnaître en tant que constructeur, les deux conditions suivantes doivent être
vérifiées :
 elle doit porter le même nom que la classe ;
 elle ne doit avoir aucun type, pas même le type void.
Un constructeur est appelé automatiquement lors de l’instanciation de l’objet.
Les constructeurs pourront avoir des paramètres. Ils peuvent donc être surchargés. Cela signifie
qu’en pratique, on connaît le contexte dans lequel un objet est créé.
Les constructeurs qui ne prennent pas de paramètres, ou dont tous les paramètres ont une valeur
par défaut, remplacent automatiquement les constructeurs par défaut définis par le
compilateur lorsqu’il n’y a aucun constructeur dans les classes.

Exemple :
class Point {
private:
int x, y;

27
public:
Point::Point(int a, int b) {
//validation de a et b
x = a; y = b;
}
Point::Point(int a) {
//validation de a
x = a; y = 0;
}
Point::Point() {
x = y = 0;
}

};

Un constructeur est toujours appelé lorsqu'un objet est créé, soit explicitement, soit
implicitement. Les appels explicites peuvent être écrits sous deux formes :
Point a(3, 4);
Point b = Point(5, 6);
Dans le cas d'un constructeur avec un seul paramètre, on peut aussi adopter une forme qui
rappelle l'initialisation des variables de types primitifs.
Point e = 7; // équivaut à : Point e = Point(7)

28
Un objet alloué dynamiquement est lui aussi toujours initialisé, au mois implicitement. Dans
beaucoup de cas il peut, ou doit, être initialisé explicitement. Cela s'écrit :
Point *pt;
...
pt = new Point(1, 2);
Les constructeurs peuvent aussi être initialisés pour initialiser des objets temporaires,
anonymes. En fait, chaque fois qu'un constructeur est appelé, un objet nouveau est créé, même
si cela ne se passe pas à l'occasion de la définition d'une variable.
L'appel d'un constructeur dans une expression comportant un signe « = » peut prêter à
confusion, à cause de sa ressemblance avec une affectation. Or, en C++, l'initialisation et
l'affectation sont deux opérations distinctes, du moins lorsqu'elles concernent des variables d'un
type classe : l'initialisation consiste à donner une première valeur à une variable au moment où
elle commence à exister ; l'affectation consiste à remplacer la valeur courante d'une variable
par une autre valeur ; les opérations mises en œuvre par le compilateur, constructeur dans un
cas, opérateur d'affectation dans l'autre, ne sont pas les mêmes.
Point a;
Point b = Point(5, 6);
a = b;

 Constructeur par copie


Le constructeur par copie d'une classe C est un constructeur dont le premier paramètre est de
type « C & » (référence sur un C) ou « const C & » (référence sur un C constant) et dont les
autres paramètres, s'ils existent, ont des valeurs par défaut. Ce constructeur est appelé lorsqu'un
objet est initialisé en copiant un objet existant. Cela arrive parfois explicitement, mais souvent

29
implicitement, notamment chaque fois qu'un objet est passé comme paramètre par valeur à une
fonction ou rendu comme résultat par valeur (c.-à-d. autre qu'une référence) d'une fonction.
Si le programmeur n'a pas défini de constructeur de copie pour une classe, le compilateur
synthétise un constructeur par copie consistant en la recopie de chaque membre d'un objet dans
le membre correspondant de l'autre objet. Si ces membres sont de types primitifs ou des
pointeurs, cela revient à faire la copie « bit à bit » d'un objet sur l'autre.
Exemple :
A titre d'exemple le programme suivant introduit une nouvelle variété de point ; à chacun est
associée une étiquette qui est une chaîne de caractères :
class PointNomme {
private:
int x, y;
char *label;

public:
PointNomme(int a, int b, char *s = "") {
x = a; y = b;
label = new char[strlen(s) + 1];
strcpy(label, s);
}

};

Soit a est une variable de type Point préalablement construite.


Point b = a;

Bien entendu, la copie du pointeur n'a pas dupliquée la chaîne pointée : les deux objets, l'original
et la copie, partagent la même chaîne. Très souvent, ce partage n'est pas souhaitable, car difficile
à gérer et dangereux : toute modification de l'étiquette d'un des deux points se répercutera

30
immédiatement sur l'autre.
Pour résoudre ce problème il faut équiper notre classe d'un constructeur par copie :
class PointNomme {
...
PointNomme(PointNomme &p) {
x = p.x; y = p.y;
label = new char[strlen(p.label) + 1];
strcpy(label, p.label);
}
...
};

L’appel se fait donc :


b = PointNomme(a) ;

 Construction des objets membres


Lorsque des membres d'une classe sont à leur tour d'un type classe on dit que la classe a des
objets membres.
L'initialisation d'un objet de la classe nécessite alors l'initialisation de ces objets membres. Il en
est toujours ainsi, indépendamment du fait que l'on emploie ou non un constructeur explicite,
et qu'à son tour ce constructeur appelle ou non explicitement des constructeurs des objets
membres.
Lorsque les objets membres n'ont pas de constructeurs par défaut, une syntaxe spéciale permet
de préciser les arguments des constructeurs des membres :
NomDeLaClasse(paramètres)
: membre(paramètres), ... membre(paramètres) {
corps du constructeur
}
A titre d'exemple, imaginons que notre classe Point ne possède pas de constructeur sans
arguments, et qu'on doive définir une classe Segment ayant deux points pour membres (un
segment est déterminé par deux points).
Voici comment on devra écrire son constructeur :
class Segment {

30
Point origine, extremite;
int epaisseur;
public:
Segment(int ox, int oy, int ex, int ey, int ep)
: origine(ox, oy), extremite(ex, ey) {
epaisseur = ep;
}
...
};

Autre version :
class Segment {
...
Segment(int ox, int oy, int ex, int ey, int ep) {
origine = Point(ox, oy); // Version erronée
extremite = Point(ex, ey);
epaisseur = ep;
}
...
};

Mais il faut comprendre que cette version est très maladroite, car faite de deux affectations
(les deux lignes qui forment le corps du constructeur ne sont pas des déclarations). Ainsi, au
lieu de se limiter à initialiser les membres origine et extrémité, on procède successivement à
 la construction d’origine et extrémité en utilisant le constructeur sans arguments de la
classe Point,
 la construction de deux points anonymes, initialisés avec les valeurs de ox, oy, ex et
ey,
 l'écrasement des valeurs initiales d’origine et extrémité par les deux points ainsi
construits.

 Données membres constantes

Une donnée membre d'une classe peut être qualifiée const. Il est alors obligatoire de
l'initialiser lors de la construction d'un objet, et sa valeur ne pourra par la suite plus être
modifiée.
A titre d'exemple voici une nouvelle version de la classe Segment, dans laquelle chaque objet
reçoit, lors de sa création, un « numéro de série » qui ne doit plus changer au cours de la vie de
l'objet :

class Segment {

31
Point origine, extremite;
int epaisseur;
const int numeroDeSerie;
public:
Segment(int x1, int y1, int x2, int y2, int ep, int num);
};
Constructeur, version erronée :
Segment::Segment(int x1, int y1, int x2, int y2, int ep, int num)
: origine(x1, y1), extremite(x2, y2) {
epaisseur = ep;
numeroDeSerie = num; // ERREUR : tentative de modification d'une constante
}
Constructeur, version correcte, en utilisant la syntaxe de l'initialisation des objets membres :
Segment::Segment(int x1, int y1, int x2, int y2, int ep, int num)
: origine(x1, y1), extremite(x2, y2), numeroDeSerie(num) {
epaisseur = ep;
}

 Fonctions membres constante :


Le mot const placé à la fin de l'en-tête d'une fonction membre indique que l'état de l'objet à
travers lequel la fonction est appelée n'est pas changé du fait de l'appel. C'est une manière de
déclarer qu'il s'agit d'une fonction de consultation de l'objet, non d'une fonction de
modification :
class Point {
... e
void placer(int a, int b); // modifie l'objet
float distance(Point p) const; // ne modifie pas l'objet
...
};
Il est conseillé de qualifier const toute fonction qui peut l'être cela élargit son champ
d'application.
La qualification const d'une fonction membre fait partie de sa signature. Ainsi, on peut
surcharger une fonction membre non constante par une fonction membre constante ayant,
appart cela, le même en-tête. La fonction non constante sera appelée sur les objets non
constants, la fonction constante sur les objets constants.

32
 Destructeur

De la même manière qu'il y a des choses à faire pour initialiser un objet qui commence à exister,
il y a parfois des dispositions à prendre lorsqu'un objet va disparaitre.
Un destructeur est une fonction membre spéciale. Il a le même nom que la classe, précédé du
caractère « ~ ». Il n'a pas de paramètre, ni de type de retour. Il y a donc au plus un destructeur
par classe.
Le destructeur d'une classe est appelé lorsqu'un objet de la classe est détruit, juste avant que la
mémoire occupée par l'objet soit récupérée par le système.
Par exemple, voici le destructeur qu'il faut ajouter à notre classe PointNomme. Sans ce
destructeur, la destruction d'un point n'entrainerait pas la libération de l'espace alloué pour son
étiquette :

class PointNomme {
...
~PointNomme() {
delete [] label;
}
...
};

Si le programmeur n'a pas écrit de destructeur pour une classe, le compilateur en synthétise un,
de la manière suivante :
o si la classe n'a ni objets membres ni classes de base, alors il s'agit du destructeur trivial
qui consiste à ne rien faire,
o si la classe a des classes de base ou des objets membres, le destructeur synthétise
consiste à appeler les destructeurs des données membres et des classes de base, dans
l'ordre inverse de l'appel des constructeurs correspondants.

5. Membres statiques :

Chaque objet d'une classe possède son propre exemplaire de chaque membre ordinaire (membre
non statique) de la classe :
 pour les données membres, cela signifie que de la mémoire nouvelle est allouée lors de
la création de chaque objet ;

33
 pour les fonctions membres, cela veut dire qu'elles ne peuvent être appelées qu'en
association avec un objet (on n'appelle pas « la fonction f » mais « la fonction f sur
l'objet x »).
A l'opposé de cela, les membres statiques, signalés par la qualification static précédant leur
déclaration, sont partagés par tous les objets de la classe. De chacun il n'existe qu'un seul
exemplaire par classe, quel que soit le nombre d'objets de la classe.
Les données et fonctions membres non statiques sont donc ce que dans d'autres langages
orientés objets on appelle variables d'instance et méthodes d'instance, tandis que les données
et fonctions statiques sont appelées dans ces langages variables de classe et méthodes de classe.

 Données membres statiques :

class Point {
int x, y;
public:
static int nombreDePoints;
Point(int a, int b) {
x = a; y = b;
nbrPoints++;
}
};
Chaque objet Point possède ses propres exemplaires des membres x et y mais, quel que soit le
nombre de points existants à un moment donné, il existe un seul exemplaire du membre
nombreDePoints.
La ligne mentionnant nombreDePoints dans la classe Point est une simple « annonce ».
Il faut encore créer et initialiser cette donnée membre (ce qui, pour une donnée membre non
statique, est fait par le constructeur lors de la création de chaque objet). Cela se fait par une
formule analogue à une définition de variable, écrite dans la portée globale, même s'il s'agit de
membres privés :
int Point::nombreDePoints = 0;
(la ligne ci-dessus doit être écrite dans un fichier « .cpp » , non dans un fichier « .h »). L'accès
à un membre statique depuis une fonction membre de la même classe s'écrit comme l'accès à
un membre ordinaire (voyez l'accès à nombreDePoints fait dans le constructeur Point ci-
dessus).
L'accès à un membre statique depuis une fonction non membre peut se faire à travers un objet,
n'importe lequel, de la classe :

34
Point a, b, c;
...
cout << a.nombreDePoints << "\n";
Mais, puisqu'il y a un seul exemplaire de chaque membre statique, l'accès peut s'écrirai aussi
indépendamment de tout objet, par une expression qui met bien en évidence l'aspect « variable
de classe » des données membres statiques :
cout << Point::nombreDePoints << "\n";

 méthode membre statique


Une fonction membre statique n'est pas attachée à un objet. Par conséquent :
 elle ne dispose pas du pointeur this,
 de sa classe, elle ne peut référencer que les fonctions et les membres statiques.

Par exemple, voici la classe Point précédente, dans laquelle le membre nombreDePoints a
été rendu privé pour en empêcher toute modification intempestive. Il faut donc fournir une
fonction pour en consulter la valeur, nous l'avons appelée combien :
class Point {
int x, y;
static int nombreDePoints;
public:
static int combien() {
return nombreDePoints;
}
Point(int a, int b) {
x = a; y = b;
nbrPoints++;
}
};
Pour afficher le nombre de points existants on devra maintenant écrire une expression comme
(a étant de type Point) :
cout << a.combien() << "\n";
ou, encore mieux, une expression qui ne fait pas intervenir de point particulier :
cout << Point::combien() << "\n";

35
6. Fonctions amis :

Une fonction amie d'une classe C est une fonction qui, sans être membre de cette classe, a le
droit d'accéder à tous ses membres, aussi bien publics que privés.
Une fonction amie doit être déclarée ou définie dans la classe qui accorde le droit d'accès,
précédée du mot réservé friend. Cette déclaration doit être écrite indirectement parmi les
membres publics ou parmi les membres privés :
class Tableau {
int *tab, nbr;
friend void afficher(const Tableau &);
public:
Tableau(int nbrElements);
...
};
et, plus loin, ou bien dans un autre fichier :
void afficher(const Tableau &t) {
cout << '[';
for (int i = 0; i < t.nbr; i++)
cout << ' ' << t.tab[i];
cout << ]" ;"
}

 Classes amis :

Une classe amie d'une classe C est une classe qui a le droit d'accéder à tous les membres de C.
Une telle classe doit être déclarée dans la classe C (la classe qui accorde le droit d'accès),
précédé du mot réservé friend, indirectement parmi les membres privés ou parmi les
membres publics de C.
Exemple : les deux classes Maillon et Pile suivantes implémentent la structure de données pile
(structure « dernier entrée premier sorti ») d'entiers :
class Maillon {
int info;
Maillon *suivant;
Maillon(int i, Maillon *s) {
info = i; suivant = s;

36
}
friend class Pile;
};
class Pile {
Maillon *top;
public:
Pile() {
top = 0;
}
bool vide() {
return top == 0;
}
void empiler(int x) {
top = new Maillon(x, top);
}
int sommet() {
return top->info;
}
void depiler() {
Maillon *w = top;
top = top->suivant;
delete w;
}
};
On notera la particularité de la classe Maillon ci-dessus : tous ses membres sont privés, et la
classe Pile est son amie (on dit que Maillon est une classe « esclave » de la classe Pile).
Autrement dit, seules les piles ont le droit de créer et de manipuler des maillons ; le reste du
système n'utilise que les piles et leurs opérations publiques, et n'a même pas à connaitre
l'existence des maillons.

7. Surcharge des opérateurs :

En C++ on peut redéfinir la sémantique des opérateurs du langage, soit pour les étendre à des
objets, alors qui n'étaient initialement définis que sur des types primitifs, soit pour changer
l'effet d'opérateurs prédéfinis sur des objets. Cela s'appelle surcharger des opérateurs.
Il n'est pas possible d'inventer de nouveaux opérateurs ; seuls des opérateurs déjà connus du
compilateur peuvent être surchargés. Tous les opérateurs de C++ peuvent être surchargés, sauf
les cinq suivants :
. .* :: ? : sizeof
Il n'est pas possible de surcharger un opérateur appliqué uniquement à des données de type
standard : un opérande au moins doit être d'un type classe.

37
Une fois surchargés, les opérateurs gardent leur pluralité, leur priorité et leur associativité
initiales. En revanche, ils perdent leur éventuelle commutativité et les éventuels liens
sémantiques avec d'autres opérateurs.
Par exemple, la sémantique d'une surcharge de ++ ou <= n'a pas à être liée avec celle de + ou
<.
Surcharger un opérateur revient à définir une fonction ; tout ce qui a été dit à propos de la
surcharge des fonctions s'applique donc à la surcharge des opérateurs.
Plus précisément, pour surcharger un opérateur ÷ (ce signe représente un opérateur quelconque)
il faut définir une fonction nommée operator÷. Ce peut être une fonction membre d'une classe
ou bien une fonction indépendante. Si elle n'est pas membre d'une classe, alors elle doit avoir
au moins un paramètre d'un type classe.

 Surcharge d'un opérateur par une fonction membre


Si la fonction operator÷ est membre d'une classe, elle doit comporter un paramètre de moins
que la pluralité de l'opérateur : le premier opérande sera l'objet à travers lequel la fonction a être
appelée. Ainsi, sauf quelques exceptions :
 « obj÷ » ou « ÷obj » équivalent à « obj.operator÷() »
 « obj1 ÷ obj2 » équivaut à « obj1.operator ÷ (obj2) »
Exemple :
class Point {
int x, y;
public:
Point(int = 0, int = 0);
int X() const { return x; }
int Y() const { return y; }
Point operator+(const Point) const; //surcharge de + par une fonction
membre
...
};
Point Point::operator+(const Point q) const {
return Point(x + q.x, y + q.y);
}
Utilisation :
Point p, q, r;

38
...
r = p + q; // compris comme : r = p.operator+(q);

 Surcharge d'un opérateur par une fonction non membre


Si la fonction operator÷ n'est pas membre d'une classe, alors elle doit avoir un nombre de
paramètres égal à la pluralité de l'opérateur. Dans ce cas :
 « obj÷ » ou « ÷obj » équivalent à « obj.operator÷() »
 « obj1 ÷ obj2 » équivaut à « operator ÷ (obj1, obj2) »
Exemple :
Point operator+(const Point p, const Point q) { // surcharge de + par une
return Point(p.X() + q.X(), p.Y() + q.Y()); // fonction non membre
}
Utilisation :
Point p, q, r;
...
r = p + q; // compris maintenant comme : r = operator+(p, q);

A cause des conversions implicites, la surcharge d'un opérateur binaire symétrique par une
fonction non membre, comme la précédente, est en général préférable, car les deux opérandes
y sont traités symétriquement. Exemple :
Point p, q, r;
int x, y;
Surcharge de l'opérateur + par une fonction membre :
r = p + q; // Oui : r = p.operator+(q);
r = p + y; // Oui : r = p.operator+(Point(y));
r = x + q; // Erreur : x n'est pas un objet
Surcharge de l'opérateur + par une fonction non membre :
r = p + q; // Oui : r = operator+(p, q);
r = p + y; // Oui : r = operator+(p, Point(y));
r = x + q; // Oui : r = operator+(Point(p), q);
Lorsque la surcharge d'un opérateur est une fonction non membre, on a souvent intérêt, ou
nécessite, à en faire une fonction amie. Par exemple, si la classe Point n'avait pas possède les
« accesseurs » publics X() et Y(), on aurait dû surcharger l'addition par une fonction amie :
class Point {

39
int x, y;
public:
Point(int = 0, int = 0);
friend Point operator+(const Point, const Point);
...
};
Point operator+(const Point p, Point q) {
return Point(p.x + q.x, p.y + q.y);
}
Les deux surcharges de + comme celles montrées ci-dessus, par une fonction membre et par
une fonction non membre, ne peuvent pas être définies en même temps dans un même
programme ; si tel était le cas, une expression comme p + q serait trouvée ambigüe par le
compilateur.

40
Chapitre 4 : Héritage et Hiérarchies des classes

Le mécanisme de l'héritage consiste en la définition d'une classe par réunion des membres d'une
ou plusieurs classes préexistantes, dites classes de base directes, et d'un ensemble de membres
spécifiques de la classe nouvelle, appelée alors classe dérivée. La syntaxe est :
class classe : dérivation classe, dérivation classe, ... dérivation classe {
déclarations et définitions des membres spécifiques de la nouvelle classe
}
Où dérivation est un des mots-clés private, protected ou public.

Par exemple, voici une classe Tableau (tableau « amélioré », en ce sens que la valeur de
l'indice est contrôlée lors de chaque accès) et une classe Pile qui ajoute à la classe Tableau
une donnée membre exprimant le niveau de remplissage de la pile et trois fonctions membres
qui encapsulent le comportement particulier (« dernier entrée premier sorti ») des piles :
class Tableau {
int *tab;
int maxTab;
public:
int &operator[](int i) {
//contrôle de la valeur de i
return tab[i];
}
int taille() { return maxTab; }
...
};

class Pile : private Tableau { // Une Pile est un Tableau


int niveau; // avec des choses en plus
public:
void empiler(int) {
(*this)[niveau++] = x;
}
int depiler() {
return (*this)[--niveau];
}
int taille() {
return niveau;
}
...
};

41
Etant donnée une classe C, une classe de base de C est soit une classe de base directe de C, soit
une classe de base directe d'une classe de base de C.
L'héritage est appelé simple s'il y a une seule classe de base directe, il est dit multiple sinon. En
C++, l'héritage peut être multiple.
Dans la notion d'héritage il y a celle de réunion de membres. Ainsi, du point de vue de
l’occupation de la mémoire, chaque objet de la classe dérivée contient un objet de la classe de
base :

Figure 1 : un objet Tableau et un objet Pile

Pour parler de l'ensemble des membres hérités (par exemple, tab et maxTab) d'une classe de
base B qui se trouvent dans une classe dérivée D on dit souvent le sous-objet B de l'objet D.

Pour la visibilité des membres (qui n'est pas l'accessibilité,) il faut savoir que la classe dérivée
détermine une portée imbriquée dans la portée de la classe de base. Ainsi, les noms des membres
de la classe dérivée masquent les noms des membres de la classe de base.
Ainsi, si une Pile est un objet Pile, dans un appel comme :
unePile.taille() // la valeur de unePile.niveau
la fonction taille() de la classe Tableau et celle de la classe Pile (c'est-à-dire les fonctions
Tableau::taille() et Pile::taille()) ne sont pas en compétition pour la
surcharge, car la deuxième rend tout simplement la première invisible. L'opérateur de résolution
de portée permet de remédier à ce masquage (sous réserve qu'on ait le droit d'accès au membre
taille de la classe Tableau) :
unePile.Tableau::taille() // la valeur de unePile.nbr

 Héritage et accessibilité des membres

En plus des membres publics et privés, une classe C peut avoir des membres protégés.
Annoncés par le mot clé protected, ils représentent une accessibilité intermédiaire car ils
sont accessibles par les fonctions membres et amies de C et aussi par les fonctions membres et
amies des classes directement dérivées de C.

42
Les membres protégés sont donc des membres qui ne font pas partie de l'interface de la classe,
mais dont on a jugé que le droit d'accès serait nécessaire ou utile aux concepteurs des classes
dérivées.
Imaginons, par exemple, qu'on veuille comptabiliser le nombre d'accès faits aux objets de nos
classes Tableau et Pile.
Il faudra leur ajouter un compteur, qui n'aura pas à être public mais qui devra être accessible
aux membres de la classe Pile, si on veut que les accès aux piles qui ne mettent en œuvre
aucun membre des tableaux, comme dans vide(), soient bien comptés. Imaginons, par
exemple, qu'on veuille comptabiliser le nombre d'accès faits aux objets de nos classes Tableau
et Pile. Il faudra leur ajouter un compteur, qui n'aura pas à être public
mais qui devra être accessible aux membres de la classe Pile, si on veut que les accès aux piles
qui ne mettent en œuvre aucun membre des tableaux, comme dans vide(), soient bien comptés.
class Tableau {
int *tab;
int maxTab;
protected:
int nbrAcces;
public:
Tableau(int t) {
nbrAcces = 0;
tab = new int[maxTab = t];
}
int &operator[](int i) {
contr^ole de la valeur de i
nbrAcces++;
return tab[i];
}
};
class Pile : private Tableau {
int niveau;
public:
Pile(int t): Tableau(t), niveau(0) { }
bool vide() {
nbrAcces++;
return niveau == 0;
}
void empiler(int x) {
(*this)[niveau++] = x;
}
int depiler() {
return (*this)[--niveau];

43
}
};

 Héritage privé, protégé, public

En choisissant quel mot-clé indique la dérivation, parmi private, protected ou public,


le programmeur détermine l'accessibilité dans la classe dérivée des membres de la classe de
base. On notera que :
 cela concerne l'accessibilité des membres, non leur présence (les objets de la
classe dérivée contiennent toujours tous les membres de toutes les classes de
base),
 la dérivation ne peut jamais servir à augmenter l'accessibilité d'un membre.
Conséquence de ces deux points, les objets des classes dérivées ont généralement des membres
inaccessibles : les membres privés de la classe de base sont présents dans les objets de la classe
dérivée, mais il n'y a aucun moyen d'y faire référence.

Syntaxe : class classeDérivée : private classeDeBase { ... }

Le mot clé private est optionnel car l'héritage privé est l'héritage par défaut. C'est la forme
la plus restrictive d'héritage.
Dans l'héritage privé, l'interface de la classe de base disparait (l'ensemble des membres publics
cessent d'être publics). Autrement dit, on utilise la classe de base pour réaliser l'implémentation
de la classe dérivée, mais on s'oblige à écrire une nouvelle interface pour la classe dérivée.
Cela se voit dans l'exemple déjà donné des classes Tableau et Pile. Il s'agit d'héritage privé,
car les tableaux ne fournissent que l'implémentation des piles, non leur comportement.

Figure 2 : un héritage privé

Exemple d'emploi d'un tableau :


Tableau t(taille max souhaitée);

44
...
t[i] = x;
etc.
Emploi d'une pile :
Pile p(taille max souhaitée);
p[i] = x; // ERREUR : l'opérateur [] est inaccessible
p.e mpiler(x); // Oui
...
cout << t[i]; // ERREUR : l'opérateur [] est inaccessible
cout << p.depiler(); // Oui

Syntaxe : class classeDérivée : protected classeDeBase { ... }

Cette forme d'héritage, moins souvent utilisée que les deux autres, est similaire à l'héritage privé
(la classe de base fournit l'implémentation de la classe dérivée, non son interface) mais on
considère ici que les détails de l'implémentation, c.-à-d. les membres publics et protégés de la
classe de base, doivent rester accessibles aux concepteurs d'éventuelles classes dérivées de la
classe dérivée.

Figure 3 : un héritage protégé

Syntaxe : class classeDérivée : public classeDeBase { ... }

Dans l'héritage public l'interface est conservée : tous les éléments du comportement public de
la classe de base font partie du comportement de la classe dérivée. C'est la forme d'héritage la
plus fréquemment utilisée, car la plus utile et la plus facile à comprendre : dire que D dérivé
publiquement de B c'est dire que, vu de l'extérieur, tout D est une sorte de B, ou encore que tout
ce qu'on peut demander à un B on peut aussi le demander à un D.

45
La plupart des exemples d'héritage que nous examinerons seront des cas de dérivation
publique.

Figure 3 : un héritage public

 Redéfinition des fonctions membres

Lorsqu'un membre spécifique d'une classe dérivée a le même nom qu'un membre d'une classe
de base, le premier masque le second, et cela quels que soient les rôles syntaxiques (constante,
type, variable, fonction, etc.) de ces membres.
La signification de ce masquage dépend de la situation :
 il peut s'agir des noms de deux fonctions membres de même signature,
 il peut s'agir de fonctions de signatures différentes, ou de membres dont l'un n'est
pas une fonction.
S'il ne s'agit pas de deux fonctions de même signature, on a une situation maladroite, génératrice
de confusion, dont on ne retire en général aucun bénéfice. En particulier, le masquage d'une
donnée membre par une autre donnée membre ne fait pas économiser la mémoire, puisque les
deux membres existent dans chaque objet de la classe dérivée.
En revanche, le masquage d'une fonction membre de la classe de base par une fonction membre
de même signature est une démarche utile et justifiée. On appelle cela une redéfinition de la
fonction membre.
La justification est la suivante : si la classe D dérivé publiquement de la classe B, tout D peut
être vu comme une sorte de B, c'est-à-dire un B « amélioré » (augmente des membres d'autres
classes de base, ou de membres spécifiques) ; il est donc naturel qu'un D réponde aux requêtes
qu'on peut soumettre à un B, et qu'il y réponde de façon améliorée. D'où l'intérêt de la
redéfinition : la classe dérivée donne des versions analogues mais enrichies des fonctions de
la classe de base.

46
Souvent, cet enrichissement concerne le fait que les fonctions redéfinies accèdent à ce que la
classe dérivée a de plus que la classe de base.
Exemple : un Pixel est un point amélioré (c.-à-d. augmente d'un membre supplémentaire, sa
couleur). L'affichage d'un pixel consiste à l'afficher en tant que point, avec des informations
Additionnelles. Le code suivant reflet tout cela :
class Point {
int x, y;
public:
Point(int, int);
void afficher() { // a±chage d'un Point
cout << '(' << x << ',' << y << ')';
}
...
};
class Pixel : public Point {
char *couleur;
public:
Pixel(int, int, char *);
void afficher() { // affichage d'un Pixel
cout << '[';
Point::afficher(); // affichage en tant que Point
cout << ';' << couleur << ']';
}
...
};

Utilisation :

Point pt(2, 3);


Pixel px(4, 5, "rouge");
...
pt.afficher(); // affichage obtenu : (2,3)
px.afficher(); // affichage obtenu : [(4,5) ;rouge]

 Création et destruction des objets dérivés

Lors de la construction d'un objet, ses sous-objets hérités sont initialisées selon les constructeurs
des classes de base correspondantes. S'il n'y a pas d'autre indication, il s'agit des constructeurs
par défaut.
Si des arguments sont requis, il faut les signaler dans le constructeur de l'objet selon une syntaxe
voisine de celle qui sert à l'initialisation des objets membres :

47
classe(paramètres):classeDeBase(paramètres), ...
classeDeBase(paramètres) {
corps du constructeur
}
De la même manière, lors de la destruction d'un objet, les destructeurs des classes de base
directes sont toujours appelés. Il n'y a rien à écrire, cet appel est toujours implicite.
Exemple :
class Tableau {
int *table;
int nombre;
public:
Tableau(int n) {
table = new int[nombre = n];
}
~Tableau() {
delete [] table;
}
...
};
class Pile : private Tableau {
int niveau;
public:
Pile(int max)
: Tableau(max) {
niveau = 0;
}
~Pile() {
if (niveau > 0)
erreur("Destruction d'une pile non vide");
} // ceci est suivi d'un appel de ~Tableau()
...
};

 Héritage multiple
Une classe peut hériter de plusieurs classes.

Le constructeur de A doit appeler les constructeurs de B et C dans le même ordre qu’ils sont
déclarés suivant la syntaxe :

48
Les destructeurs sont appelés dans l’ordre inverse
 Si B et C ont chacun une variable de même nom, disons x, elle seront différenciées dans
A à l’aide de l’opérateur de résolution de portée. B::x et A::x désigneront
respectivement la variable x de A et celle de B.

49
Chapitre 5 : Polymorphisme, méthodes virtuelles et
abstraction

Introduction :

Nous savons qu’il est possible de redéfinir les méthodes d’une classe mère dans une classe fille.
Lors de l’appel d’une fonction ainsi redéfinie, la fonction appelée est la dernière fonction
définie dans la hiérarchie de classe. Pour appeler la fonction de la classe mère alors qu’elle a
été redéfinie, il faut préciser le nom de la classe à laquelle elle appartient. Bien que simple, cette
utilisation de la redéfinition des méthodes peut poser des problèmes.
Supposons qu’une classe B hérite de sa classe mère A. Si A possède une méthode f() appelant
une autre méthode g() redéfinie dans la classe fille B, que se passe-t-il lorsqu’un objet de
classe B appelle la méthode f ? La méthode f appelée étant celle de la classe A, elle appellera
la méthode g de la classe A. Par conséquent, la redéfinition de g ne sert à rien dès qu’on l’appelle
à partir d’une des fonctions d’une des classes mères.

Une première solution consisterait à redéfinir la méthode f dans la classe B. Mais ce n’est ni
élégant, ni efficace. Il faut en fait forcer le compilateur à ne pas faire le lien dans la fonction f

50
de la classe A avec la fonction g de la classe A. Il faut que f appelle soit la fonction g de la
classe A si elle est appelée par un objet de la classe A, soit la fonction g de la classe B si elle est
appelée pour un objet de la classe B. Le lien avec l’une des méthodes g ne doit être fait qu’au
moment de l’exécution, c’est-à-dire qu’on doit faire une édition de liens dynamique. Le C++
permet de faire cela. Pour cela, il suffit de déclarer virtuelle la fonction de la classe de base qui
est redéfinie dans la classe fille, c’est-à-dire la fonction g.

1. Méthodes virtuelles

Une méthode virtuelle est une méthode précédée par le mot-clé virtual dans la classe de
base.
Les méthodes virtuelles sont des méthodes qui sont appelées selon la vraie classe de l’objet qui
l’appelle. Les objets qui contiennent des méthodes virtuelles peuvent être manipulés en tant
qu’objets des classes de base, tout en effectuant les bonnes opérations en fonction de leur type.
Ils apparaissent donc comme étant des objets de la classe de base et des objets de leur classe
complète indifféremment, et on peut les considérer soit comme les uns, soit comme les autres.
Un tel comportement est appelé polymorphisme (c’est-à-dire qui peut avoir plusieurs aspects
différents).
Une classe possédant des fonctions virtuelles est dite classe polymorphe. La qualification
virtual devant la redéfinition d'une fonction virtuelle est facultative : les redéfinitions d'une
fonction virtuelle sont virtuelles d'office.

Exemple :
Class Polygone {
...
void toto(double a) {
double b=f(a);
.....}
virtual double f(double a);
...
Class Triangle:public Polygone{
.....
double f(double a);
}

51
Si dans le programme principal, on écrit :
Polygone p;
...
p.toto();
La méthode toto appellera la méthode f de Polygone car p est de type Polygone.
Si par contre, on écrit :
Triangle t;
...
t.toto();
t étant de type Triangle (qui dérive de Polygone), on peut appeler la fonction toto de la classe
Polygone. Mais, ici, la méthode toto appellera la fonction f de la classe Triangle. Cela est dû
au fait que la fonction f de la classe Polygone est virtuelle. Si elle n’était pas virtuelle la fonction
toto appellerait systématiquement la fonction f de la classe Polygone.

2. Méthodes virtuelles pures - classes abstraites

Une méthode virtuelle pure est une méthode qui est déclarée mais non définie dans une classe.
Elle est définie dans une des classes dérivées de cette classe.
Une classe abstraite est une classe comportant au moins une méthode virtuelle pure.
Étant donné que les classes abstraites ont des méthodes non définies, il est impossible
d’instancier des objets pour ces classes. En revanche, on pourra les référencer avec des
pointeurs.
Pour déclarer une méthode virtuelle pure dans une classe, il suffit de faire suivre sa déclaration
de « = 0 ». La fonction doit également être déclarée virtuelle :
virtual type nom (paramètres) = 0 ;
= 0 signifie ici simplement qu’il n’y a pas d’instance de cette méthode dans cette classe.
Exemple :
Class Polygone {
...
virtual void CalcAire()=0;
...
}
Utilisation :
Polygone p; // ERREUR : création d’une instance de la classe abstraite Polygone
Polygone *pt; // Oui : c’est un pointeur

52
pt = new Polygone; // ERREUR : création d’une instance de la classe abstraite Polygone
Triangle t; // Oui : la classe Triangle n’est pas abstraite
pt = new Triangle; // Oui

Le mécanisme des méthodes virtuelles pures et des classes abstraites permet de créer des classes
de base contenant toutes les caractéristiques d’un ensemble de classes dérivées, pour pouvoir
les manipuler avec un unique type de pointeurs. En effet, les pointeurs des classes dérivées sont
compatibles avec les pointeurs des classes de base, on pourra donc référencer les classes
dérivées avec des pointeurs sur les classes de base, donc avec un unique type sous-jacent : celui
de la classe de base. Cependant, les méthodes des classes dérivées doivent exister dans la classe
de base pour pouvoir être accessibles à travers le pointeur sur la classe de base. C’est ici que
les méthodes virtuelles pures apparaissent. Elles forment un moule pour les méthodes des
classes dérivées, qui les définissent. Bien entendu, il faut que ces méthodes soient déclarées
virtuelles, puisque l’accès se fait avec un pointeur de classe de base et qu’il faut que ce soit la
méthode de la classe réelle de l’objet (c’est-à-dire la classe dérivée) qui soit appelée.

3. Polymorphisme et identification dynamique du type

Le C++ est un langage fortement typé. Malgré cela, il se peut que le type exact d’un objet soit
inconnu à cause de l’héritage. Par exemple, si un objet est considéré comme un objet d’une
classe de base de sa véritable classe, on ne peut pas déterminer a priori quelle est sa véritable
nature. Cependant, les objets polymorphiques (qui, rappelons-le, sont des objets disposant de
méthodes virtuelles) conservent des informations sur leur type dynamique, à savoir leur
véritable nature. En effet, lors de l’appel des méthodes virtuelles, la méthode appelée est la
méthode de la véritable classe de l’objet.

a. Conversion standard vers une classe de base :


Si la classe D dérive publiquement de la classe B alors les membres de B sont membres de D.
Autrement dit, les membres publics de B peuvent être atteints à travers un objet D. Ou encore :
tous les services offerts par un B sont offerts par un D.
Par conséquent, là où un B est prévu, on doit pouvoir mettre un D. C'est la raison pour laquelle
la conversion (explicite ou implicite) d'un objet de type D vers le type B, une classe de base
accessible de D, est définie, et a le statut de conversion standard.
Exemple :

53
class Point { // point « géométrique »
int x, y;
public:
Point(int, int);
...
};
class Pixel : public Point { // pixel = point coloré
char *couleur;
public:
Pixel(int, int, char *);
...
};

Utilisation :
Pixel px(1, 2, "rouge");
Point pt = px; // un pixel a été mis là où un point était
// attendu : il y a conversion implicite
De manière interne, la conversion d'un D vers un B est traitée comme l'appel d'une fonction
membre de D qui serait publique, protégée ou privée selon le mode (ici : public) dont D dérive
de B.
Ainsi, la conversion d'une classe dérivée vers une classe de base privée ou protégée existe mais
n'est pas utilisable ailleurs que depuis l'intérieur de la classe dérivée. Bien entendu, le cas le
plus intéressant est celui de la dérivation publique.
Selon qu'elle s'applique à des objets ou à des pointeurs (ou des références) sur des objets, la
conversion d'un objet de la classe dérivée vers la classe de base recouvre deux réalités très
différentes :
1. Convertir un objet D vers le type B c'est lui enlever tous les membres qui ne font pas
partie de B (les membres spécifiques et ceux hérités d'autres classes). Dans cette
conversion il y a perte effective d'information :

Figure : Conversion de oD, un objet D, en un objet B

2. Au contraire, convertir un D* en un B* (c'est-à-dire un pointeur sur D en un pointeur


sur B) ne fait perdre aucune information. Pointé à travers une expression de type B*,
l'objet n'offre que les services de la classe B, mais il ne cesse pas d'être un D avec tous

54
ses membres :

Figure : Conversion de pD, un pointeur sur un D, en un pointeur sur un B

Par exemple, supposons posséder deux fonctions de prototypes :


void fon1(Point pt);
void fon2(Point *pt);
et supposons qu'elles sont appelées de la manière suivante :
Pixel pix = Pixel(2, 3, "rouge")
...
fon1(pix);
fon2(&pix);
Le statut de l'objet pix en tant que paramètre de ces deux fonctions est tout à fait diffèrent.
L'objet passé à fon1 comme argument est une version tronquée de pix, alors que le pointeur
passé à fon2 est l'adresse de l'objet pix tout entier :
void fon1(Point pt) {
La valeur de pt est un Point ; il n'a pas de couleur. Il y avait peut-être à l'origine un
Pixel mais, ici, aucune conversion (autre que définie par l'utilisateur) ne peut faire
un Pixel à partir de pt.
}
void fon2(Point *pt) {
Il n'est rien arrivé à l'objet dont l'adresse a servi à initialiser pt lors de l'appel de cette
fonction. Si on peut garantir que cet objet est un Pixel, l'expression suivante (placée
sous la responsabilité du programmeur) a un sens :
((Pixel *) pt)->couleur ...
}
Pour ce qui nous occupe ici, les références (pointeurs gères de manière interne par le
compilateur) doivent être considérées comme des pointeurs. Nous pouvons donc ajouter un
troisième cas :

55
void fon3(Point &rf) {
Il n'est rien arrivé à l'objet qui a servi à initialiser rf lors de l'appel de cette fonction.
Si on peut garantir que cet objet est un Pixel, l'expression suivante (placée sous la
responsabilité du programmeur) a un sens :
((Pixel &) rf).couleur ...
}

b. Type statique, type dynamique, généralisation :


Considérons la situation suivante :
class B {
...
};
class D : public B {
...
};
D unD;
...
B unB = unD;
B *ptrB = &unD;
B &refB = unD;
Les trois affectations ci-dessus sont légitimes ; elles font jouer la conversion standard d'une
classe vers une classe de base. Le type de unB ne soulève aucune question (c'est un B), mais
les choses sont moins claires pour les types de ptrB et refB. Ainsi ptrB, par exemple, pointe-t-
il un B ou un D ?
o on dit que le type statique de *ptrB (ce que ptB pointe) et de refB est B, car c'est ainsi
que ptB et refB ont été déclarées ;
o on dit que le type dynamique de *ptrB et de refB est D, car tel est le type des valeurs
effectives de ces variables.
Le type statique d'une expression d’écoulé de l'analyse du texte du programme ; il est connu à
la compilation.
Le type dynamique, au contraire, est déterminé par la valeur courante de l'expression, il peut
changer durant l'exécution du programme.

c. Identification dynamique du type

56
Le coût du polymorphisme, en espace mémoire, est d'un pointeur par objet, quel que soit le
nombre de fonctions virtuelles de la classe. Chaque objet d'une classe polymorphe comporte un
membre de plus que ceux que le programmeur voit : un pointeur vers une table qui donne les
adresses qu'ont les fonctions virtuelles pour les objets de la classe en question.
D'où l’idée de profiter de l'existence de ce pointeur pour ajouter au langage, sans coût
supplémentaire, une gestion des types dynamiques qu'il faudrait sinon écrire au coup par coup
(probablement µa l'aide de fonctions virtuelles). Fondamentalement, ce mécanisme se compose
des deux opérateurs dynamic_cast et typeid.

 L'opérateur « dynamic_cast » :
Le transtypage dynamique permet de convertir une expression en un pointeur ou une référence
d’une classe, ou un pointeur sur void. Il est réalisé à l’aide de l’opérateur dynamic_cast.
Cet opérateur impose des restrictions lors des transtypages afin de garantir une plus grande
fiabilité :
o il effectue une vérification de la validité du transtypage ;
o il n’est pas possible d’éliminer les qualifications de constance (pour cela, il faut utiliser
l’opérateur const_cast).
Syntaxe : dynamic_cast<type> (expression)
où type désigne le type cible du transtypage, et expression l’expression à transtyper.

Exemple :
class Animal {
...
virtual void uneFonction() { // il faut au moins une fonction virtuelle
... // (car il faut des classes polymorphes)
}
};
class Mammifere : public Animal {
...
};
class Chien : public Mammifere {
...
};

57
class Caniche : public Chien {
...
};
class Chat : public Mammifere {
...
};
class Reverbere {
...
};
Chien medor;
Animal *ptr = &medor;
...
Mammifere *p0 = ptr;
// ERREUR (à la compilation) : un Animal n'est pas forcément un Mammifère
Mammifere *p1 = dynamic_cast<Mammifere *>(ptr);
// OK : p1 reçoit une bonne adresse, car Médor est un mammifère
Caniche *p2 = dynamic_cast<Caniche *>(ptr);
// OK, mais p2 reçoit 0, car Médor n'est pas un caniche
Chat *p3 = dynamic_cast<Chat *>(ptr);
// OK, mais p3 reçoit 0, car Médor n'est pas un chat non plus
Reverbere *p4 = dynamic_cast<Reverbere *>(ptr);
// OK, mais p4 reçoit 0, car Médor n'est pas un réverbère

L'opérateur dynamic_cast s'applique également aux références. L'explication est la même,


sauf qu'en cas d'impossibilité d'effectuer la conversion, cet opérateur lance l'exception bad
cast :
Animal &ref = medor;
Mammifere &r1 = dynamic_cast<Mammifere &>(ref); // Oui
Caniche &r2 = dynamic_cast<Caniche &>(ref); // exception bad cast lancée
// non attrapée, elle est fatale
 L'opérateur « static_cast » :
Contrairement au transtypage dynamique, le transtypage statique n’effectue aucune vérification
des types dynamiques lors du transtypage. Il est donc nettement plus dangereux que le
transtypage dynamique.

58
Cependant, contrairement au transtypage C classique, il ne permet toujours pas de supprimer
les qualifications de constance.
Le transtypage statique s’effectue à l’aide de l’opérateur static_cast, dont la syntaxe est
exactement la même que celle de l’opérateur dynamic_cast :
static_cast <type> (expression)
où type et expression ont les mêmes signification que pour l’opérateur dynamic_cast.
Contrairement à l’opérateur dynamic_cast, l’opérateur static_cast permet donc
d’effectuer les conversions entre les types autres que les classes définies par l’utilisateur.
Aucune vérification de la validité de la conversion n’a lieu cependant (comme pour le
transtypage C classique).
Exemple :
int sum = 1000;
int count = 21;
double average1 = sum/count;
cout<<"Before conversion = "<<average1<<endl;
double average2 = static_cast<double>(sum)/count;
cout<<"After conversion = "<<average2<<endl;

 L'opérateur « const_cast » :
La suppression des attributs de constance et de volatilité peut être réalisée grâce à l’opérateur
const_cast. Cet opérateur suit exactement la même syntaxe que les opérateurs
dynamic_cast et static_cast :
const_cast <type> (expression)
L’opérateur const_cast peut travailler essentiellement avec des références et des pointeurs.
Il permet de réaliser les transtypages dont le type destination est moins contraint que le type
source vis-à-vis des mots-clés const et volatile.
En revanche, l’opérateur const_cast ne permet pas d’effectuer d’autres conversions que les
autres opérateurs de transtypage (ou simplement les transtypages C classiques) peuvent réaliser.
Par exemple, il est impossible de l’utiliser pour convertir un flottant en entier. Lorsqu’il travaille
avec des références, l’opérateur const_cast vérifie que le transtypage est légal en
convertissant les références en pointeurs et en regardant si le transtypage n’implique que les
attributs const et volatile. const_cast ne permet pas de convertir les pointeurs de
fonctions.

59
Exemple :
class CCTest {
public:
void setNumber( int );
void printNumber() const;
private:
int number;
};

void CCTest::setNumber( int num ) { number = num; }

void CCTest::printNumber() const {


cout << "\nBefore: " << number;
const_cast< CCTest * >( this )->number--;
cout << "\nAfter: " << number;
}

int main() {
CCTest X;
X.setNumber( 8 );
X.printNumber();
}

 L'opérateur « reinterpret_cast » :

L’opérateur de transtypage le plus dangereux est reinterpret_cast. Sa syntaxe est la


même que celle des autres opérateurs de transtypage dynamic_cast, static_cast et
const_cast :
reinterpret_cast <type> (expression)
Cet opérateur permet de réinterpréter les données d’un type en un autre type. Aucune
vérification de la validité de cette opération n’est faite.
Exemple :
#include <cstdint>
#include <cassert>
#include <iostream>
int f() { return 42; }
int main()
{
int i = 7;

// pointer to integer and back


uintptr_t v1 = reinterpret_cast<uintptr_t>(&i); // static_cast is an
error
std::cout << "The value of &i is 0x" << std::hex << v1 << '\n';
int* p1 = reinterpret_cast<int*>(v1);
assert(p1 == &i);

// pointer to function to another and back

60
void(*fp1)() = reinterpret_cast<void(*)()>(f);
// fp1(); undefined behavior
int(*fp2)() = reinterpret_cast<int(*)()>(fp1);
std::cout << std::dec << fp2() << '\n'; // safe

// type aliasing through pointer


char* p2 = reinterpret_cast<char*>(&i);
if(p2[0] == '\x7')
std::cout << "This system is little-endian\n";
else
std::cout << "This system is big-endian\n";

// type aliasing through reference


reinterpret_cast<unsigned int&>(i) = 42;
std::cout << i << '\n';
}

 L'opérateur « typeid » :

Le C++ fournit l’opérateur typeid afin de récupérer les informations de type des expressions.
Sa syntaxe est la suivante :
Typeid (expression)
où expression est l’expression dont il faut déterminer le type.
Le résultat de l’opérateur typeid est une référence sur un objet constant de classe
type_info.
Les informations de type récupérées sont les informations de type statique pour les types non
polymorphiques. Cela signifie que l’objet renvoyé par typeid caractérisera le type de
l’expression fournie en paramètre, que cette expression soit un sous-objet d’un objet plus dérivé
ou non.
Exemple :
Animal *ptr = new Caniche;
cout << typeid(ptr).name() << '\n';
cout << typeid(*ptr).name() << '\n';
cout << "L'animal pointé par ptr "
<< (typeid(*ptr) == typeid(Chien) ? "est" : "n'est pas")
<< " un chien\n";
cout << "L'animal pointé par ptr est un "
<< typeid(*ptr).name() << "\n";
Affichage obtenu :
Animal *
Chien

61
L'animal pointé par ptr n'est pas un chien
L'animal pointé par ptr est un Caniche

62
Chapitre 6 : Bibliothèque STL

Le C++ possède une bibliothèque standard (SL pour Standard Library) qui est composée, entre
autre, d’une bibliothèque de flux, de la bibliothèque standard du C, de la gestion des exceptions,
..., et de la STL (Standard Template Library : bibliothèque de modèles standard).
La STL fournit un certain nombre de conteneurs pour gérer des collections d’objets : les
tableaux (vector), les listes (list), les ensembles (set), les piles (stack), et beaucoup
d’autres ...
On distingue différents types de conteneurs :
 Les conteneurs de séquence :
 vector : accès direct à n’importe quel élément
 list : accès séquentiel avant arrière.
 deque : associe les 2 précédents.
 Les conteneurs associatifs :
 set : recherche rapide, aucun doublon autorisé.
 multiset : recherche rapide, doublons autorisés.
 map : recherche rapide par clé
 Les adaptateurs de conteneur :
 stack : pile ou LIFO : dernier entré, premier sorti.
 queue : file ou FIFO : premier entré, premier sorti.
 priority_queue : l’élément de priorité la plus grande est toujours le premier
élément sorti.
Les conteneurs de la STL ont été conçus de façon à fournir des fonctionnalités similaires, c’est
pourquoi, ils contiennent un certain nombre de méthodes et d’opérateurs communs :
Constructeur par size Retourne le nombre actuel
défaut d’éléments dans le conteneur
Constructeur de Constructeur qui initialise max_size Retourne le nombre
copie le conteneur comme une maximum d’éléments d’un
copie d’un conteneur conteneur
existant de même type
destructeur Les 7 operator Retourne true ou false selon la
= < <= > >= == comparaison du premier

64
!= conteneur avec le second.
empty Retourne true s’il n’y a pas swap Effectue l’échange de 2
d’élément dans le conteneurs
conteneur

Un conteneur (container) est un objet qui contient d’autres objets. Il fournit un moyen de
gérer les objets contenus (au minimum ajout, suppression, parfois insertion, tri, recherche, ...)
ainsi qu’un accès à ces objets qui dans le cas de la STL consiste très souvent en un itérateur.
Pour utiliser ces classes conteneurs, il faut les en têtes suivantes ; elles sont totalement intégrées
à l’espace de noms std (namespace std).
<vector> <queue> contient à la fois des queue et les
<list> priority_queue
<dequeue> <map> contient à la fois les map et les
<stack> multimap
<bitset> <set> contient à la fois les set et les multiset

1. Tableau dynamique (vector) :


Un vector est un tableau dynamique où il est particulièrement aisé d’accéder directement
aux divers éléments par un index, et d’en ajouter ou en retirer à la fin.
A la manière des tableaux de type C, l’espace mémoire alloué pour un objet de type vector est
toujours continu, ce qui permet des algorithmes rapides d’accès aux divers éléments.

Aux méthodes communes des classes conteneurs s’ajoute ces méthodes spécifiques :

65
push_back, pop_back, front, back, at, capacity.

Exemple :
#include <iostream>
#include <vector>
using namespace std;
int main()
{
cout << "test Vector " << endl;
vector<int> monVect;
int fibo1=0, fibo2=1;
monVect.push_back (fibo1);
for (int i =0 ; i < 10; i++)
{
int fibo = fibo2 + fibo1;
monVect.push_back (fibo);
fibo1 = fibo2;
fibo2 = fibo;
}
cout << "Premier element " << monVect.front() << endl;
for (int i=0; i < monVect.size(); i++){
cout << "Element no " << i << " " << monVect.at(i) << endl;
}
cout << "Dernier element " << monVect.back() << endl;
cout << "copie de vector " << endl;
vector<int> monVect2(monVect);
cout << "Capacite " << monVect2.capacity() << " Nombre elements
" <<
monVect2.size() << endl;
monVect.clear();
cout << " Apres clear : Capacite " << monVect.capacity() << "
Nombreelts " << monVect.size() << endl;
int i= monVect2.size() -1;
while (!monVect2.empty()) {
cout << "retire element " << i << " " << monVect2[i] <<
endl; //ou monVect2.at(i)
i--;
monVect2.pop_back();
}
}
Résultat :

66
Les itérateurs (iterator) sont une généralisation des pointeurs : ce sont des objets qui
pointent sur d’autres objets.
Comme son nom l’indique, les itérateurs sont utilisés pour parcourir une série d’objets de
telle façon que si on incrémente l’itérateur, il désignera l’objet suivant de la série.
Exemple :
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> v2(4, 100); // un vecteur de 4 entiers initialisés avec la valeur 100
cout << "Le vecteur v2 contient " << v2.size() << "entiers : ";
// utilisation d’un itérateur pour parcourir le vecteur v2
for (vector<int>::iterator it = v2.begin(); it != v2.end(); ++it)
cout << " " << *it;
cout << "\n";
return 0;
}

2. Les listes chainées (list) :


La classe list fournit une structure générique de listes doublement chaînées (c’est-à-dire que
l’on peut parcourir dans les deux sens) pouvant éventuellement contenir des doublons.

Exemple :
#include <iostream>
#include <list>
using namespace std;
int main()
{
list<int> lst; // une liste vide
lst.push_back( 5 );
lst.push_back( 6 );
lst.push_back( 1 );
lst.push_back( 10 );
lst.push_back( 7 );
lst.push_back( 8 );

67
lst.push_back( 4 );
lst.push_back( 5 );
lst.pop_back(); // enleve le dernier élément et supprime l’entier 5
cout << "La liste lst contient " << lst.size() << " entiers : \n";
// utilisation d’un itérateur pour parcourir la liste lst
for (list<int>::iterator it = lst.begin(); it != lst.end(); ++it)
cout << " " << *it;
cout << "\n";
// afficher le premier élément
cout << "Premier element : " << lst.front() << "\n";
// afficher le dernier élément
cout << "Dernier element : " << lst.back() << "\n";
// parcours avec un itérateur en inverse
for ( list<int>::reverse_iterator rit = lst.rbegin(); rit !=
lst.rend(); ++rit )
{
cout << " " << *rit;
}
cout << "\n";
return 0;
}

Résultat :
La liste lst contient 7 entiers :
5 6 1 10 7 8 4
Premier element : 5
Dernier element : 4
4 8 7 10 1 6 5

3. La table associative (map)


Une table associative map permet d’associer une clé à une donnée.
La map prend au moins deux paramètres :
– le type de la clé (dans l’exemple ci-dessous, une chaîne de caractères string)
– le type de la donnée (dans l’exemple ci-dessous, un entier non signé unsigned int)

Exemple :
#include <iostream>
#include <iomanip>
#include <map>
#include <string>
using namespace std;
int main()
{
map<string,unsigned int> nbJoursMois;
nbJoursMois["janvier"] = 31;

68
nbJoursMois["février"] = 28;
nbJoursMois["mars"] = 31;
nbJoursMois["avril"] = 30;
//...
cout << "La map contient " << nbJoursMois.size() << "elements : \n";
for (map<string,unsigned int>::iterator it=nbJoursMois.begin();
it!=nbJoursMois.end(); ++it)
{
cout << it->first << " -> \t" << it->second << endl;
}
cout << "Nombre de jours du mois de janvier : " <<
nbJoursMois.find("janvier")->second <<"\n";
// affichage du mois de janvier
cout << "Janvier : \n" ;
for (int i=1; i <= nbJoursMois["janvier"]; i++)
{
cout << setw(3) << i;
if(i%7 == 0)
cout << endl;
}
cout << endl;
return 0;
}
Résultat :
La map contient 4 elements :
avril -> 30
février -> 28
janvier -> 31
mars -> 31
Nombre de jours du mois de janvier : 31
Janvier :
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31

4. Les Set :
Un set<T> contient des éléments de type T triées automatiquement. T doit définir un ordre, par
défaut on utilise l’opérateur < mais une autre fonction peut être utilisée.
Set possède quelques opérations spécifiques à la recherche dans le conteneur :
 count(elem) retourne le nombre d’éléments avec valeur égale à elem.
 find(elem) retourne le premier élément avec valeur elem

 lower_bound(elem), upper_bound(elem) retournene la position du premier élément >=

69
elem, resp. <= elem.
Dans un set il n’y a pas de duplication.
Exemple :
#include <set>
#include <iostream>

int main() {
std::set<int> s; // équivaut à std::set<int, std::less<int> >
s.insert(2); // s contient 2
s.insert(5); // s contient 2 5
s.insert(2); // le doublon n'est pas inséré
s.insert(1); // s contient 1 2 5
std::set<int>::const_iterator sit (s.begin()), send(s.end());
for(;sit!=send;++sit)
std::cout << *sit << ' ';
std::cout << std::endl;
return 0;
}

5. Les piles et les files :


A la différence avec les conteneurs vector, les piles & les files n’offrent pas d’accès direct aux
éléments mais uniquement par les iterator.
Les priority_queue ont le même fonctionnement que les queues mais l’élément retiré le premier
est celui de plus haute priorité.
Exemple :
#include <iostream>
#include <stack>
#include <queue>
using namespace std;
int main() {
cout << "test des classes stack, queue " << endl;
stack <int> pile;
queue <double>file;
cout << "remplissage de la pile " << endl;
for (int i=0; i < 5; i++)
{
pile.push(i);
cout << "sommet " << pile.top() << endl;
}
cout << "Vidage de la pile " << endl;
while (pile.empty() == false)
{
cout << "tete " << pile.top() << endl;
pile.pop();
}
cout << "remplissage de la file par l'arriere " << endl;
for (int i=0; i < 5; i++)
{
file.push(i);

70
cout << "avant " << file.front() << " arriere " <<
file.back() << endl;
}
cout << "Sauvegarde de la file " << endl;
queue <double> file2(file);
cout << "Vidage de la file par l'avant " << endl;
while (file.empty() == false)
{
cout << "avant " << file.front() << " arriere " <<
file.back() << endl;
file.pop();
}
return 0;
}
Résultat :
test des classes tete 3 avant 0 arriere 4
stack, queue tete 2 Sauvegarde de la
remplissage de la tete 1 file
pile tete 0 Vidage de la file
sommet 0 remplissage de la par l'avant
sommet 1 file par l'arriere avant 0 arriere 4
sommet 2 avantavant 0 avant 1 arriere 4
sommet 3 arriere 0 avant 2 arriere 4
sommet 4 avant 0 arriere 1 avant 3 arriere 4
Vidage de la pile avant 0 arriere 2 avant 4 arriere 4
tete 4 avant 0 arriere 3

6. Les algorithmes :
La STL fournit environs 70 algorithmes destinés à manipuler des conteneurs. Les algorithmes
opèrent sur les conteneurs uniquement via les itérateurs.
Il est également possible de créer ses propres algorithmes qui opèrent d’une manière semblable
à ceux de la STL.
Un algorithme tel que find (), par exemple, localise un élément et retourne un itérateur vers
cet élement. Si l ‘élement n’est pas trouvé, find () retourne l’iterateur end() qui est le
dernier élément du conteneur.
L’algorithme find () est disponible avec tous les conteneurs de la STL.
On distingue les algorithmes mutables, c’est à dire qui modifient les éléments du conteneur.
copy () partition() replace_copy() stable_partition()
copy_backward() random_suffle() replace_copy_if() swap()

fill() remove() replace_if() swap_ranges()

71
fill_n() remove-copy() reverse() transform()

generate() remove_copy_if() reverse_copy() unique()

generate_n() remove_if() rotate() unique_copy()

iter_swap() replace() rotate_copy()

Les algorithmes non mutables


adjacent-find () equal() find_end() mismatch()

count() find() find_first_of() search()


count_if() find_each() find_if() search_n()

et les algorithmes numériques (fichier d’en-tête <numeric>)

accumulate () inner_product() partial_sum() adjacent_difference()

72
Les flux d’entrée / sortie

Un flux(ou stream) est une abstraction logicielle représentant un flot de données entre :
 une source produisant de l’information
 une cible consommant cette information.
Il peut être représenté comme un buffer et des mécanismes associés à celui−ci et il prend en
charge, quand le flux est créé, l’acheminement de ces données. Un flux est associé à un fichier
ou à un périphérique.
Les flux d'entrée sont de type istream, comme par exemple cin.
 Le clavier est un istream qui se nomme std::cin .
Les flux de sortie sont de type ostream, comme par exemple cout.
 L'écran est un ostream qui se nomme std::cout.
 La sortie d'erreur est un ostream qui se nomme std::cerr .
La classe iostream permet de combiner flux de sortie et flux d'entrée
.
1. Les classes de flux d’entrée / sortie

Les classes de la librairie d’entrée / sortie de la librairie standard se subdivisent en deux


catégories distinctes :
 La première catégorie regroupe les classes de gestion des tampons d’entrée / sortie. Ces
classes sont au nombre de trois : la classe template basic_stringbuf, qui permet
de réaliser des tampons pour les flux orientés chaînes de caractères, la classe template
basic_filebuf, qui prend en charge les tampons pour les flux orientés fichiers, et
leur classe de base commune, la classe template basic_streambuf.
Le rôle de ces classes est principalement d’optimiser les entrées / sorties en intercalant des
tampons d’entrée / sortie au sein même du programme. Ce sont principalement des classes
utilitaires, qui sont utilisées en interne par les autres classes de la librairie d’entrée / sortie.

 La deuxième catégorie de classes est de loin la plus complexe, puisqu’il s’agit des
classes de gestion des flux eux-mêmes. Toutes ces classes dérivent de la classe template
basic_ios (elle-même dérivée de la classe de base ios_base, qui définit tous les
types et les constantes utilisées par les classes de flux).

72
La classe basic_ios fournit les fonctionnalités de base des classes de flux et, en particulier,
elle gère le lien avec les tampons d’entrée / sortie utilisés par le flux. De cette classe de base
dérivent des classes spécialisées respectivement pour les entrées ou pour les sorties. Ainsi, la
classe template basic_istream prend en charge toutes les opérations des flux d’entrée et la
classe basic_ostream toutes les opérations des flux de sortie. Enfin, la librairie standard
définit la classe template basic_iostream, qui regroupe toutes les fonctionnalités des
classes basic_istream et basic_ostream et dont dérivent toutes les classes de gestion
des flux mixtes.

Les Classes déclarées dans iostream.h et permettant la manipulation des périphériques


standards :

73
 istream_withassign, ostream_withassign et iostream_withassign
: classes dérivées respectivement de istream, ostream et iostream et qui
surchargent l'opérateur d'affectation.

2. Les flux d’entrée / sortie sur fichiers


Les classes déclarées dans fstream.h permettant la manipulation des fichiers.

 fstreambase : classe de base pour les classes dérivées ifstream, ofstream et


fstream. Elle-même est dérivée de ios et contient un objet de la classe filebuf
(dérivée de streambuf)
 ifstream : classe permettant d'effectuer la lecture à partir des fichiers disque
 ofstream : classe permettant d'effectuer l'écriture dans des fichiers sur disque
 fstream : classe permettant d'effectuer des entrées/sorties à partir des fichiers

Les classes déclarées dans strstream.h permettant de simuler des opérations


d’entrées/sorties avec des tampons en mémoire centrale. Elles opèrent de la même façon que
les fonctions du langage C sprintf() et sscanf() :

 strstreambase : classe de base pour les classes istrstream, ostrstream

74
et strstream. Elle contient un objet de la classe strstreambuf (dérivée de
streambuf).
 istrstream : classe dérivée de strstreambase et de istream permettant la
lecture dans un tampon mémoire (à la manière de la fonction sscanf)
 ostrstream : classe dérivée de strtreambase et de ostream permettant
l'écriture dans un tampon mémoire (à la manière de la fonction sprintf)
 strstream : classe dérivée deistrstream et de iostream permettant la lecture
et l'écriture dans un tampon mémoire

Lors de la manipulation des fichiers on distingue :


 Les fichiers textes : contiennent des informations sous la forme de caractères (en général
en utilisant le code ASCII). La source d’un programme C++ est un exemple de fichier
texte.
 Les fichiers binaires : contiennent directement la représentation mémoire des
informations. Un fichier binaire peut aussi être vu comme une séquence d’octets. Un
exemple de fichier binaire est le résultat de la compilation d’un programme C++
(programme exécutable).

1) Ouverture de fichier:
La méthode open() permet d'ouvrir le fichier et d'associer un flux avec ce dernier.
void open(const char *nom, int mode, int prot=filebuf::openprot);
 nom : nom du fichier à ouvrir
 mode : mode d'ouverture du fichier
Celui-ci indique de quelle façon les données sont lues ou écrites, et ce qui se passe au moment
de l'ouverture du flux. On l'utilise surtout pour les fichiers. Voici la liste de ces bits :
 ios::in : ouverture en lecture (par défaut pour ifstream).
 ios::out : ouverture en écriture (par défaut pour ofstream).
 ios::app : ajout des données en fin de fichier.
 ios::ate : positionnement à la fin du fichier.
 ios::trunc : supprime le contenu du fichier, s'il existe déjà ; cette suppression est
automatique pour les fichiers ouverts en écriture, sauf si ios::ate ou ios::app a été
précisé dans le mode

75
 ios::nocreate : ne crée pas le fichier s'il n'existe pas déjà ; une erreur (bit
ios::failbit positionné) est produite dans le cas où le fichier n'existe pas encore
 ios::noreplace : pour une ouverture en écriture, si ni ios::ate ni ios::app
ne sont positionnés, le fichier n'est pas ouvert s'il existe déjà, et une erreur est produite
 ios::binary : Fichier binaire, ne faire aucun formatage

 prot : il définit les droits d’accès au fichier (par défaut les permissions de lecture/écriture
sont positionnées) dans le cas d’une ouverture avec création (sous UNIX).

Exemple : Utilisation de flux d’entrée / sortie sur un fichier


#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main()
{
// Lit les données :
int i;
double d, e;
cout << "Entier Reel Reel : ";
cin >> i >> d >> e;
// Enregistre ces données dans un fichier :
ofstream f("fichier.txt",ios::out );
if (f.is_open())
{
cout << "OK"<< endl;
f << "Les donnees lues sont : " <<
i << " " << d << " " << e << endl;
f.close();
}

return 0;
}

Repositionnement du pointeur de fichier dans un flux d’entrée / sortie


#include <iostream>
#include <fstream>
#include <string>
using namespace std;

76
int main(void)
{
// Ouvre le fichier de données :
fstream f("fichier.txt", ios_base::in | ios_base::out |
ios_base::trunc);
if (f.is_open())
{
// Écrit les données :
f << 2 << " " << 45.32 << " " << 6.37 << endl;
// Replace le pointeur de fichier au début :
f.seekg(0);
// Lit les données :
int i;
double d, e;
f >> i >> d >> e;
cout << "Les données lues sont : " <<
i << " " << d << " " << e << endl;
// Ferme le fichier :
f.close();
}
return 0;

Exercice :
1) Ecrire un programme C++ qui permet de saisir et de mémoriser dans un fichier nommé
répertoire, le nom et le numéro de téléphone de 5 personnes.
2) Ecrire un programme C++ qui permet d’afficher à l’écran le contenu du fichier
répertoire, en sautant une ligne entre chaque personne.

Solution :
#include <iostream>
#include <fstream>
#include <string>
#include <iomanip>
using namespace std;
int main() {
string nom;
int tel;
ofstream repfile("repertoire.txt");
// ou ofstrem repfile;
// repfile.open ()= ("C:/test_cpp/repertoire.txt");
if (repfile) {
for (int i = 0; i < 5; i++)
{
cout << "Personne N°" << i + 1 << endl;
cout << "Nom: ";
cin >> nom;

77
repfile << nom << " ";
cout << "Telephone: ";
cin >> tel;
repfile << tel << endl;
}
return 0;
}
else
cout << "ERREUR: Impossible d'ouvrir le fichier." <<
endl;
repfile.close();
return 0;
}

#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
ifstream repfile("repertoire.txt");
string nom;
int tel; // ou string line;
if (repfile) {
repfile >> nom >> tel; //getline ( repfile,line );
while (!repfile.eof())
//Tant qu'on n'est pas à la fin, on ramène les
lignes
{
cout << nom << " " << tel << endl; //cout<<line;
repfile >> nom >> tel; //getline ( repfile,line
);
}
}
else
cerr << "ERREUR" << endl;
repfile.close();
return 0;
}

78
Les flux d’entrée / sortie

Un flux(ou stream) est une abstraction logicielle représentant un flot de données entre :
 une source produisant de l’information
 une cible consommant cette information.
Il peut être représenté comme un buffer et des mécanismes associés à celui−ci et il prend en
charge, quand le flux est créé, l’acheminement de ces données. Un flux est associé à un fichier
ou à un périphérique.
Les flux d'entrée sont de type istream, comme par exemple cin.
 Le clavier est un istream qui se nomme std::cin .
Les flux de sortie sont de type ostream, comme par exemple cout.
 L'écran est un ostream qui se nomme std::cout.
 La sortie d'erreur est un ostream qui se nomme std::cerr .
La classe iostream permet de combiner flux de sortie et flux d'entrée
.
1. Les classes de flux d’entrée / sortie

Les classes de la librairie d’entrée / sortie de la librairie standard se subdivisent en deux


catégories distinctes :
 La première catégorie regroupe les classes de gestion des tampons d’entrée / sortie. Ces
classes sont au nombre de trois : la classe template basic_stringbuf, qui permet
de réaliser des tampons pour les flux orientés chaînes de caractères, la classe template
basic_filebuf, qui prend en charge les tampons pour les flux orientés fichiers, et
leur classe de base commune, la classe template basic_streambuf.
Le rôle de ces classes est principalement d’optimiser les entrées / sorties en intercalant des
tampons d’entrée / sortie au sein même du programme. Ce sont principalement des classes
utilitaires, qui sont utilisées en interne par les autres classes de la librairie d’entrée / sortie.

 La deuxième catégorie de classes est de loin la plus complexe, puisqu’il s’agit des
classes de gestion des flux eux-mêmes. Toutes ces classes dérivent de la classe template
basic_ios (elle-même dérivée de la classe de base ios_base, qui définit tous les
types et les constantes utilisées par les classes de flux).

72
La classe basic_ios fournit les fonctionnalités de base des classes de flux et, en particulier,
elle gère le lien avec les tampons d’entrée / sortie utilisés par le flux. De cette classe de base
dérivent des classes spécialisées respectivement pour les entrées ou pour les sorties. Ainsi, la
classe template basic_istream prend en charge toutes les opérations des flux d’entrée et la
classe basic_ostream toutes les opérations des flux de sortie. Enfin, la librairie standard
définit la classe template basic_iostream, qui regroupe toutes les fonctionnalités des
classes basic_istream et basic_ostream et dont dérivent toutes les classes de gestion
des flux mixtes.

Les Classes déclarées dans iostream.h et permettant la manipulation des périphériques


standards :

73
 istream_withassign, ostream_withassign et iostream_withassign
: classes dérivées respectivement de istream, ostream et iostream et qui
surchargent l'opérateur d'affectation.

2. Les flux d’entrée / sortie sur fichiers


Les classes déclarées dans fstream.h permettant la manipulation des fichiers.

 fstreambase : classe de base pour les classes dérivées ifstream, ofstream et


fstream. Elle-même est dérivée de ios et contient un objet de la classe filebuf
(dérivée de streambuf)
 ifstream : classe permettant d'effectuer la lecture à partir des fichiers disque
 ofstream : classe permettant d'effectuer l'écriture dans des fichiers sur disque
 fstream : classe permettant d'effectuer des entrées/sorties à partir des fichiers

Les classes déclarées dans strstream.h permettant de simuler des opérations


d’entrées/sorties avec des tampons en mémoire centrale. Elles opèrent de la même façon que
les fonctions du langage C sprintf() et sscanf() :

 strstreambase : classe de base pour les classes istrstream, ostrstream

74
et strstream. Elle contient un objet de la classe strstreambuf (dérivée de
streambuf).
 istrstream : classe dérivée de strstreambase et de istream permettant la
lecture dans un tampon mémoire (à la manière de la fonction sscanf)
 ostrstream : classe dérivée de strtreambase et de ostream permettant
l'écriture dans un tampon mémoire (à la manière de la fonction sprintf)
 strstream : classe dérivée deistrstream et de iostream permettant la lecture
et l'écriture dans un tampon mémoire

Lors de la manipulation des fichiers on distingue :


 Les fichiers textes : contiennent des informations sous la forme de caractères (en général
en utilisant le code ASCII). La source d’un programme C++ est un exemple de fichier
texte.
 Les fichiers binaires : contiennent directement la représentation mémoire des
informations. Un fichier binaire peut aussi être vu comme une séquence d’octets. Un
exemple de fichier binaire est le résultat de la compilation d’un programme C++
(programme exécutable).

1) Ouverture de fichier:
La méthode open() permet d'ouvrir le fichier et d'associer un flux avec ce dernier.
void open(const char *nom, int mode, int prot=filebuf::openprot);
 nom : nom du fichier à ouvrir
 mode : mode d'ouverture du fichier
Celui-ci indique de quelle façon les données sont lues ou écrites, et ce qui se passe au moment
de l'ouverture du flux. On l'utilise surtout pour les fichiers. Voici la liste de ces bits :
 ios::in : ouverture en lecture (par défaut pour ifstream).
 ios::out : ouverture en écriture (par défaut pour ofstream).
 ios::app : ajout des données en fin de fichier.
 ios::ate : positionnement à la fin du fichier.
 ios::trunc : supprime le contenu du fichier, s'il existe déjà ; cette suppression est
automatique pour les fichiers ouverts en écriture, sauf si ios::ate ou ios::app a été
précisé dans le mode

75
 ios::nocreate : ne crée pas le fichier s'il n'existe pas déjà ; une erreur (bit
ios::failbit positionné) est produite dans le cas où le fichier n'existe pas encore
 ios::noreplace : pour une ouverture en écriture, si ni ios::ate ni ios::app
ne sont positionnés, le fichier n'est pas ouvert s'il existe déjà, et une erreur est produite
 ios::binary : Fichier binaire, ne faire aucun formatage

 prot : il définit les droits d’accès au fichier (par défaut les permissions de lecture/écriture
sont positionnées) dans le cas d’une ouverture avec création (sous UNIX).

Exemple : Utilisation de flux d’entrée / sortie sur un fichier


#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main()
{
// Lit les données :
int i;
double d, e;
cout << "Entier Reel Reel : ";
cin >> i >> d >> e;
// Enregistre ces données dans un fichier :
ofstream f("fichier.txt",ios::out );
if (f.is_open())
{
cout << "OK"<< endl;
f << "Les donnees lues sont : " <<
i << " " << d << " " << e << endl;
f.close();
}

return 0;
}

Repositionnement du pointeur de fichier dans un flux d’entrée / sortie


#include <iostream>
#include <fstream>
#include <string>
using namespace std;

76
int main(void)
{
// Ouvre le fichier de données :
fstream f("fichier.txt", ios_base::in | ios_base::out |
ios_base::trunc);
if (f.is_open())
{
// Écrit les données :
f << 2 << " " << 45.32 << " " << 6.37 << endl;
// Replace le pointeur de fichier au début :
f.seekg(0);
// Lit les données :
int i;
double d, e;
f >> i >> d >> e;
cout << "Les données lues sont : " <<
i << " " << d << " " << e << endl;
// Ferme le fichier :
f.close();
}
return 0;

Exercice :
1) Ecrire un programme C++ qui permet de saisir et de mémoriser dans un fichier nommé
répertoire, le nom et le numéro de téléphone de 5 personnes.
2) Ecrire un programme C++ qui permet d’afficher à l’écran le contenu du fichier
répertoire, en sautant une ligne entre chaque personne.

Solution :
#include <iostream>
#include <fstream>
#include <string>
#include <iomanip>
using namespace std;
int main() {
string nom;
int tel;
ofstream repfile("repertoire.txt");
// ou ofstrem repfile;
// repfile.open ()= ("C:/test_cpp/repertoire.txt");
if (repfile) {
for (int i = 0; i < 5; i++)
{
cout << "Personne N°" << i + 1 << endl;
cout << "Nom: ";
cin >> nom;

77
repfile << nom << " ";
cout << "Telephone: ";
cin >> tel;
repfile << tel << endl;
}
return 0;
}
else
cout << "ERREUR: Impossible d'ouvrir le fichier." <<
endl;
repfile.close();
return 0;
}

#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
ifstream repfile("repertoire.txt");
string nom;
int tel; // ou string line;
if (repfile) {
repfile >> nom >> tel; //getline ( repfile,line );
while (!repfile.eof())
//Tant qu'on n'est pas à la fin, on ramène les
lignes
{
cout << nom << " " << tel << endl; //cout<<line;
repfile >> nom >> tel; //getline ( repfile,line
);
}
}
else
cerr << "ERREUR" << endl;
repfile.close();
return 0;
}

78

Vous aimerez peut-être aussi