Académique Documents
Professionnel Documents
Culture Documents
CHAPITRE 1
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.
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.
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 »).
Le langage C++ distingue les minuscules, des majuscules. Les mots réservés du langage C++
doivent être écrits en minuscules.
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.
1- Les entiers
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:
1- Les réels
LES INITIALISATIONS
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.
L’OPERATEUR COUT
Ce n'est pas une instruction du langage C++, mais une fonction de la bibliothèque iostream.h.
LES OPERATEURS
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.
INCREMENTATION - DECREMENTATION
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.
Exemple :
Exemple:
Le langage C++ permet d'effectuer automatiquement des conversions de type sur les
LA FONCTION GETCH
La fonction getch n'est pas définie dans la norme ANSI mais elle existe dans les
bibliothèques des compilateurs.
L’OPERATEUR CIN
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
Exemples: int u;
cin >> u;
Si l'utilisateur saisi un caractère non numérique, sa saisie est ignorée.
Exercice II_1:
Saisir un caractère au clavier, afficher son code ASCII à l'écran. Soigner l'affichage.
Exercice II_2:
Exercice II_3:
Organigramme:
vraie
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.
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
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.
bloc d'
instructions
non
condition
vraie
oui
suite du programme
Syntaxe en C: do
{
............; // bloc d'instructions
............;
............;
}
while (condition);
suite du programme ...
Remarque: les {} ne sont pas nécessaires lorsque le bloc ne comporte qu'une seule instruction.
oui non
condition
vraie
bloc d'
instructions
Remarque: les {} ne sont pas nécessaires lorsque le bloc ne comporte qu'une seule instruction.
Il s'agit de l'instruction:
instruction1
condition
vraie
non
oui
bloc d'instructions
Syntaxe en C:
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 ;
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).
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:
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;
En langage C++, une expression nulle de type entier (int) est fausse, une expression non
nulle de type entier (int) est vraie.
Exemples:
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)
{……..}
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++.
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.
Une variable de type pointeur se déclare à l'aide du type de l'objet pointé précédé du symbole
*.
int i = 8;
int *p; // déclaration d’un pointeur sur entier
cout<<"VOICI i : "<<i;
cout<<"\nVOICI SON ADRESSE EN HEXADECIMAL : "<<&i;
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;
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.
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.
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.
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.
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
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
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.
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 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 :
Exercice V_7:
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
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:
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.
Exemples:
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.
t[0]
t[0][0]
t[0][2]
t[1]
t[2][3]
Exercice VI_3:
Exercice VI_4:
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:
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:
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 :
Nom : strlen
Prototype : int strlen(char *chaine);
Fonctionnement : envoie la longueur de la chaîne ('\0' non comptabilisé).
Exemple d’utilisation :
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 :
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 :
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 :
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).
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):
Exemple d’utilisation :
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:
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;
}
13
int a = 15;
cout << "valeur de a = " << setfill(‘x’) << setw(5) << a << endl;
//affiche : la valeur de a = xxx15
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>
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>
int main() {
int a = 5;
cout << " a =" << a << endl;
incrementer(a);
cout << " a =" << a << endl;
return 0;
}
void incrementer (int& val)
{
val++;
}
16
return i;
else
return j;
}
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);
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 :
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...
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() {
Exemple :
// function example
#include <iostream> // std::cout
#include <functional> // std::function, std::negate
// a function:
int half(int x) {return x/2;}
21
int main () {
std::function<int(int)> fn1 = half; // function
std::function<int(int)> fn2 = ½ // 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
return 0;
}
22
Chapitre 3 : Notions de base en Programmation Orientée
Objets
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
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
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).
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;
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);
}
};
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);
}
...
};
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.
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;
}
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.
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";
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.
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.
38
...
r = p + q; // compris comme : r = p.operator+(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; }
...
};
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 :
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
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
}
};
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.
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
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.
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.
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 :
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.
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.
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.
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 :
54
ses membres :
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 ...
}
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
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;
};
int main() {
CCTest X;
X.setNumber( 8 );
X.printNumber();
}
L'opérateur « reinterpret_cast » :
60
void(*fp1)() = reinterpret_cast<void(*)()>(f);
// fp1(); undefined behavior
int(*fp2)() = reinterpret_cast<int(*)()>(fp1);
std::cout << std::dec << fp2() << '\n'; // safe
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
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;
}
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
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
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;
}
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()
71
fill_n() remove-copy() reverse() transform()
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
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.
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.
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
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).
return 0;
}
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
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.
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.
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
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).
return 0;
}
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