Vous êtes sur la page 1sur 75

Sommaire du cours

 Introduction
 Fonctions, types énumérés, types structure et gestion
dynamique de la mémoire
 Classes et objets
 Surcharge des opérateurs
 Héritage et polymorphisme

POO - R. Rentsch 1

Références bibliographiques

 Apprendre le C++
Claude Delannoy
Eyrolles, 2007

 C++ par la pratique


Jean-Cédric Chappelier, Florian Seydoux
PPUR, 2004

 Supports de cours de mes collègues professeurs à la HEIG-VD


 Pierre Donini
 Jean-Pierre Molliet

POO - R. Rentsch 2

1
Chapitre 1

Introduction

Historique

 Pour des raisons historiques, C++ reste indissociable de C, lui-même


l'étant du système UNIX.

 En 1983, Bjarnes Stroustrup des laboratoires Bell crée C++ (C, lui,
date de 1972), qu'il nomme initialement C with Classes.

 Le langage C++ est normalisé ISO: une première fois en 1998, puis
en 2003.

 Son créateur l'a voulu compatible avec C (s'avère vrai en grande partie
mais pas à 100%). Il en améliore, entre autres, la sécurité.

 L'aspect procédural de C est conservé mais C++ lui ajoute le


paradigme objet (inspiré de Simula).
POO - R. Rentsch 4

2
Principales différences entre C++ et Java (1)
 Modularité: .h (déclaration) et .cpp (implantation). Pas de paquetages.

 Pas de garbage collector. La mémoire doit explicitement être


allouée/désallouée (new/delete).

 Héritage multiple possible. Pas d'interfaces.

 Polymorphisme explicite:
 Par des pointeurs ou des références
 Pas de liaison dynamique par défaut; les méthodes, pour en bénéficier,
doivent être déclarées virtuelles (virtual)

 Fonctions (indépendantes), méthodes et classes amies (friend) pouvant


accéder aux champs privés ou protégés d'une classe donnée

POO - R. Rentsch 5

Principales différences entre C++ et Java (2)

 Surcharge des opérateurs =, + , +=, [], ... possible

 Généricité: fonctions et classes patrons (templates)

 Pas de machine virtuelle: les programmes doivent être recompilés sur


chaque plateforme.

POO - R. Rentsch 6

3
Notre premier programme C++ (1)
#include <iostream>
#include <cstdlib>
using namespace std;

int main() {
int n;
cout << "Entrez une valeur entiere: ";
cin >> n;
cout << "Vous avez entre la valeur " << n << endl;
system("pause");
return EXIT_SUCCESS;
}

 En C, l'utilisation des fonctions printf ou scanf nécessitait l'inclusion du


fichier d'en-tête stdio.h.

 De manière identique, les déclarations nécessaires aux E/S en C++


figurent dans un fichier d'en-tête nommé iostream.

POO - R. Rentsch 7

Notre premier programme C++ (2)

 Toutefois l'utilisation des symboles déclarés dans iostream fait appel à


la notion d'espace de nommage.

 Pour l'instant, il suffit de savoir que ce concept nous oblige à introduire


dans notre programme la déclaration:

using namespace std; // std = espace de nommage standard

 L'affichage d'informations à l'écran est réalisé par le biais de l'opérateur


<<. Celui-ci nous permet d'envoyer la valeur d'une expression sur le
flot cout.
 La lecture d'informations au clavier est réalisée par le biais de
l'opérateur >>. Celui-ci nous permet d'extraire une valeur du flot cin.

POO - R. Rentsch 8

4
Exemple d'écriture sur cout
#include <iostream> Valeur de n = 1
using namespace std; Valeur de f = 2
Valeur de d1 = 3
int main() { Valeur de d2 = 3.1
int n = 1; Valeur de c = a
float f = 2.0; Valeur de ch = Hello
double d1 = 3.0; Valeur de ptr = 0x22ff74
double d2 = 3.1; Adresse de ch = 0x443000
char c = 'a';
char * ch = "Hello"; Pressez <ENTER> pour terminer...
int * ptr = &n;
cout << "Valeur de n = " << n << endl;
cout << "Valeur de f = " << f << endl;
cout << "Valeur de d1 = " << d1 << endl;
cout << "Valeur de d2 = " << d2 << endl;
cout << "Valeur de c = " << c << endl;
cout << "Valeur de ch = " << ch << endl;
cout << "Valeur de ptr = " << ptr << endl;
cout << "Adresse de ch = " << (void *)ch << endl;

cout << "\nPressez <ENTER> pour terminer...";


cin.get();
return EXIT_SUCCESS;
POO - R. Rentsch } 9

Manipulateurs (1)
 Les manipulateurs permettent de formater nos affichages.

 Ils sont de deux types:


 Les manipulateurs sans paramètres
 Les manipulateurs avec paramètres (nécessitent l'inclusion de <iomanip>)

 Quelques exemples de manipulateurs sans paramètres


 endl
 permet de passer à la ligne
 oct, dec (défaut), hex
 permettent de fixer en quelle base seront affichées les valeurs entières sur
lesquels ils portent
 showbase, noshowbase (défaut)
 demande à ce que les valeurs octales soient préfixées par 0 et les valeurs
hexadécimales par 0x

POO - R. Rentsch 10

5
Manipulateurs (2)
 Quelques exemples de manipulateurs sans paramètres (suite)
 uppercase, nouppercase (défaut)
 demande à ce que les lettres d'une valeur hexadécimale apparaissent en
majuscules, resp. en minuscules

 Exemple:
#include <iostream>
using namespace std;

int main() {
cout << 10 << " " << hex << showbase << 10 << " "
<< uppercase << 10 << endl;

cout << "\nPressez <ENTER> pour terminer...";


cin.get();
return EXIT_SUCCESS;
} 10 0xa 0XA

Pressez <ENTER> pour terminer...

POO - R. Rentsch 11

Manipulateurs (3)

 Quelques exemples de manipulateurs avec paramètres:


 setw (int)
 Permet de fixer le nombre de positions utilisées pour l'affichage
(uniquement valable pour la prochaine valeur traitée)
 fixed, scientific
 Permet de fixer la manière d'afficher un réel
 setprecision(int)
 Permet de fixer le nombre de chiffres significatifs à afficher
 ...

POO - R. Rentsch 12

6
Manipulateurs (4)

 Exemple:
#include <iostream>
#include <iomanip> // nécess pour les manipulateurs avec param
using namespace std;

int main() {
cout << 10 << endl;
cout << setw(3) << 10 << endl;
cout << 1.5 << endl;
cout << fixed << 1.5 << endl;
cout << scientific << 1.5 << endl;
cout << scientific << uppercase << 1.5 << endl;
cout << fixed << setprecision(2) << 1.234 << endl;

... 10
return EXIT_SUCCESS; 10
1.5
}
1.500000
1.500000e+000
1.500000E+000
1.23

POO - R. Rentsch Pressez <ENTER> pour terminer... 13

printf et scanf restent utilisables en C++

 Exemple:

#include <iostream>
#include <cstdlib>
using namespace std;

int main() {
int n;
printf("Entrez une valeur entiere: ");
scanf("%d", &n);
printf("Vous avez entre la valeur %d\n", n);
system("pause");
return EXIT_SUCCESS;
}

POO - R. Rentsch 14

7
Lecture d'une suite de caractères sur cin (1)
 Contrairement à scanf, la lecture d'un caractère sur cin commence par
"sauter les séparateurs"; aussi n'est-il pas possible de lire directement
ces caractères.
Donnez une chaine se terminant par un point:
Salut les copains.
#include <iostream> Resultat de la lecture:
using namespace std; Salutlescopains.

int main() { Pressez <ENTER> pour terminer...


char t[80]; int i = 0;
cout << "Donnez une chaine se terminant par un point:\n";
do
cin >> t[i];
while (t[i++] != '.');
cin.get(); // ou cin.sync(): vider le buffer
cout << "Resultat de la lecture:\n";
i = 0;
do
cout << t[i];
while (t[i++] != '.');
cout << "\n\nPressez <ENTER> pour terminer...";
cin.get();
return EXIT_SUCCESS;
}
POO - R. Rentsch 15

Lecture d'une suite de caractères sur cin (2)

 La solution à ce problème consiste à utiliser la fonction getline:

istream & getline(char * ch, int taille, char delim = '\n')

 Cette fonction lit des caractères sur le flot l'ayant appelée et les place
dans l'emplacement d'adresse ch.

 Elle s'interrompt lorsque l'une des 2 conditions suivantes est vraie:


 le caractère délimiteur delim a été trouvé; dans ce cas ce caractère n'est
pas recopié en mémoire
 taille – 1 caractères ont été lus

 Dans tous les cas, getline ajoute un caractère nul de fin de chaîne ('\0')
à la suite des caractères lus.

POO - R. Rentsch 16

8
Lecture d'une suite de caractères sur cin (3)

 Exemple:

#include <iostream>
using namespace std;

int main() {
char t[80];
cout << "Donnez une chaine:\n";
cin.getline(t, sizeof(t));
cout << "Resultat de la lecture:\n";
cout << t << endl;
cout << "\nPressez <ENTER> pour terminer...";
cin.get();
return EXIT_SUCCESS; Donnez une chaine:
} Salut les copains
Resultat de la lecture:
Salut les copains

Pressez <ENTER> pour terminer...

POO - R. Rentsch 17

Lecture d'une suite de caractères sur cin (4)


 En fait, le code ci-dessus n'est pas 100% correct, car si l'utilisateur
saisit taille caractères ou plus, une exception est levée (plus
précisément, le bit d'erreur failbit est levé).
Il faudrait donc en toute rigueur écrire:
#include <iostream>
using namespace std;
int main() {
char t[80];
cout << "Donnez une chaine:\n";
cin.getline(t, sizeof(t));
if (cin.fail()) {// Si taille (ou plus) caractères saisis
cin.clear(); // reset des bits d'erreur
cin.sync(); // vider le buffer
}
cout << "Resultat de la lecture:\n";
cout << t << endl;
cout << "\nPressez <ENTER> pour terminer...";
cin.get();
return EXIT_SUCCESS;
}

POO - R. Rentsch 18

9
Lecture d'une suite de caractères sur cin (5)

 Une solution plus simple (mais purement C++) consiste à utiliser la


classe string. A noter qu'alors la longueur de la chaîne saisie peut être
absolument quelconque.

#include <iostream>
using namespace std;

int main() {
string s;
cout << "Donnez une chaine:\n";
getline(cin, s);
cout << "Resultat de la lecture:\n";
cout << s << endl;
cout << "\nPressez <ENTER> pour terminer...";
cin.get();
return EXIT_SUCCESS;
}

POO - R. Rentsch 19

Vider le buffer d'entrée (1)

 Après un cin >> ... un '\n' demeure dans le tampon.

 Pour pouvoir effectuer un getline juste après, il s'agit donc de l'éliminer


(de vider le buffer).

 Pour ce faire, 2 possibilités:

while (cin.get() != '\n'); // ou simplement cin.get() si l'on est


// sûr qu'il n'y a que '\n' à éliminer
cin.sync();

POO - R. Rentsch 20

10
Vider le buffer d'entrée (2)
Sans vider le buffer
 Exemple: Donnez un entier:
1
Donnez une chaine:
int main() { Resultat de la lecture:
int n; 1
char t[80];
cout << "Donnez un entier:\n"; Pressez <ENTER> pour terminer...
cin >> n;
cin.sync(); // vider le buffer
cout << "Donnez une chaine:\n";
cin.getline(t, sizeof(t));
cout << "Resultat de la lecture:\n";
cout << n << " " << t << endl;
cout << "\nPressez <ENTER> pour terminer...";
cin.get(); En vidant le buffer
return EXIT_SUCCESS; Donnez un entier:
} 1
Donnez une chaine:
abc
Resultat de la lecture:
1 abc

POO - R. Rentsch Pressez <ENTER> pour terminer... 21

Chapitre 2

Fonctions, types énumérés,


types structures et gestion
dynamique de la mémoire

11
Quelques rappels... (1)
 Une fonction est un bloc d'instructions éventuellement paramétré par
un ou plusieurs arguments et pouvant fournir un résultat appelé
souvent valeur de retour.
On distingue la définition d'une fonction de son utilisation (ou appel),
cette dernière nécessitant une déclaration (ou prototype)
#include <iostream>
#include <cstdlib>
using namespace std;

int main() {
int f (int); // déclaration de la fonction (prototype)
cout << f(1) << endl; // appel de la fonction
system("pause");
return EXIT_SUCCESS;
}

// définition de la fonction
int f (int n) { // en-tête de la fonction
return ++n; // corps de la fonction
}
POO - R. Rentsch 23

Quelques rappels... (2)

 La déclaration (ou prototype) d'une fonction peut être omise si


la fonction est connue du compilateur, càd que sa définition a
déjà été rencontrée dans le même fichier source.
#include <iostream>
#include <cstdlib>
using namespace std;

// définition de la fonction
int f (int n) {return ++n;}

int main() { // ici la fonction est connue


// -> prototype plus nécess (mais recommandé!)
cout << f(1) << endl;
system("pause");
return EXIT_SUCCESS;
}

POO - R. Rentsch 24

12
Petites différences entre C et C++

 En C++, une fonction sans arguments se définit (en-tête) et se déclare


(prototype) en fournissant une "liste vide" d'arguments comme dans:

float f(); // En C, on peut écrire float f() ou float f (void)

 En C++, une fonction sans valeur de retour se définit (en-tête) et se


déclare (prototype) obligatoirement à l'aide du mot-clé void comme
dans:

void f (int); // En C ANSI, void est facultatif

POO - R. Rentsch 25

Mode de transmission des arguments (1)


 Par défaut, les arguments sont transmis par valeur.
La fonction travaille sur une copie des arguments; les éventuelles
modifications apportées aux arguments formels n'ont aucune incidence
sur les arguments effectifs.

 En cas de transmission par valeur, les arguments effectifs peuvent être


des expressions quelconques.
#include <iostream>
#include <cstdlib>
using namespace std;

int f (int n) {return ++n;}

int main() {
int n = 10;
cout << f(2*n) << endl; // argument de f = expression
system("pause");
return EXIT_SUCCESS;
}
POO - R. Rentsch 26

13
Mode de transmission des arguments (2)

 En faisant suivre du symbole & le type d'un argument dans l'en-tête


d'une fonction (et dans son prototype), on réalise une transmission
par référence.

 Cela signifie que les éventuelles modifications effectuées au sein de la


fonction porteront sur l'argument effectif et non plus sur une copie.

 A noter qu'alors l'argument effectif doit obligatoirement être une lvalue


du même type que l'argument formel correspondant.

 Ces possibilités de transmission par référence s'appliquent aussi à une


valeur de retour.

POO - R. Rentsch 27

Mode de transmission des arguments (3)

 Rappels:
 En C, les arguments et la valeur de retour sont toujours transmis par
valeur. Pour simuler ce qui se nomme "transmission par adresse" dans
d'autres langages, il est alors nécessaire d'utiliser des pointeurs. Cette
dernière possibilité reste utilisable en C++. La transmission par référence
n'existe pas en C.
 En Java, la notion de référence existe mais elle totalement transparente
pour l'utilisateur. De manière plus précise, les variables d'un type primitif
sont transmises par valeur alors que les objets sont transmis par référence.
 En C++, le principal intérêt de la notion de référence est qu'elle permet de
laisser le compilateur mettre en place les "bonnes instructions" pour
garantir un transfert par adresse.

POO - R. Rentsch 28

14
Mode de transmission des arguments (4)
 Voici un exemple, illustrant les trois modes de transmission:
par valeur, par adresse et par référence:
#include <iostream>
using namespace std;

int main() {
void echange (int, int);
int a = 1, b = 2;
cout << "Avant echange: a = " << a << ", b = " << b << endl;
echange(a, b);
cout << "Apres echange: a = " << a << ", b = " << b << endl;
return 0;
}

void echange (int x, int y) { // transmission par valeur


int z;
z = x; x = y; y = z;
} Avant echange: a = 1, b = 2
Apres echange: a = 1, b = 2

POO - R. Rentsch 29

Mode de transmission des arguments (5)


#include <iostream>
using namespace std;

int main() {
void echange (int *, int *);
int a = 1, b = 2;
cout << "Avant echange: a = " << a << ", b = " << b << endl;
echange(&a, &b); // Utilisation oblig de l'opér & (adresse de)
cout << "Apres echange: a = " << a << ", b = " << b << endl;
return 0;
}

void echange (int * x, int * y) { // transmission par adresse


int z;
z = *x; *x = *y; *y = z;
} Avant echange: a = 1, b = 2
Apres echange: a = 2, b = 1

POO - R. Rentsch 30

15
Mode de transmission des arguments (6)
#include <iostream>
using namespace std;

int main() {
void echange (int &, int &);
int a = 1, b = 2;
cout << "Avant echange: a = " << a << ", b = " << b << endl;
echange(a, b); // echange(&a,&b) serait incorrect!!!
cout << "Apres echange: a = " << a << ", b = " << b << endl;
return 0;
}

void echange (int & x, int & y) { // transmission par référence


int z;
z = x; x = y; y = z;
} Avant echange: a = 1, b = 2
Apres echange: a = 2, b = 1

POO - R. Rentsch 31

Précisions sur la notion de référence (1)

 Lorsqu'un argument effectif est transmis par valeur, il est


éventuellement converti dans le type de l'argument formel.
Attention! Contrairement à Java, la conversion peut ici être
dégradante.

#include <iostream>
using namespace std;

int main() {
int f (int);
cout << f(2.5) << endl; // conversion dégradante
return 0;
}

int f (int n) {
return ++n;
}

POO - R. Rentsch 32

16
Précisions sur la notion de référence (2)
 Ces possibilités de conversion n'existent plus dans le cas d'une
transmission par référence: l'argument effectif doit alors être une
lvalue du type de l'argument formel.

void f (int & n);


float x;
...
f(x); // Illégal!

 Cas des arguments effectifs constants:

void f (int & n);


const int c = 2;
f(1); // Illégal!
f(c); // Illégal!

Si les appels précédents étaient acceptés, ils conduiraient à fournir à f l'adresse


d'une constante (1 ou c) dont elle pourrait très bien modifier la valeur.
POO - R. Rentsch 33

Précisions sur la notion de référence (3)

 En revanche si l'argument formel est déclaré const, l'argument effectif


pourra être non seulement une constante mais aussi une expression de
type quelconque dont la valeur sera alors convertie dans une variable
temporaire dont l'adresse sera fournie à la fonction.

void f (const int & n);


const int c = 2;
float x;
f(1); // OK!
f(c); // OK!
f(x); // OK! f reçoit la référence à une variable temporaire
// contenant le résultat de la conversion (ici dégradante)
// de x en int

POO - R. Rentsch 34

17
Précisions sur la notion de référence (4)
 Nous avons déjà dit plus haut qu'il était également possible de
renvoyer une référence en valeur de retour d'une fonction.

 Dès lors qu'une fonction renvoie une référence, il devient possible


d'utiliser son appel comme une lvalue.
int & f ();
int n;
float x;
...
f() = 2*n; // à la référence renvoyé par f on range la valeur de
// l'expression 2*n (de type int)
f() = x; // à la référence renvoyé par f on range la valeur de x
// après conversion en int
 L'intérêt de la transmission par référence d'une valeur de retour
n'apparaîtra pleinement que lorsque nous étudierons la surcharge des
opérateurs. On verra alors que, dans certains cas, il s'avérera
obligatoire qu'un opérateur (en fait une fonction) fournisse une lvalue
en résultat (cas par exemple de l'opérateur []).
POO - R. Rentsch 35

Précisions sur la notion de référence (5)

 Si une fonction renvoie une valeur de retour par référence, elle ne peut
pas renvoyer une constante (sinon il y aurait risque que la fonction
appelante puisse modifier la valeur en question)

int n = 1;
float x = 2.0;
int & f() {
...
return n; // OK
return 5; // Illégal
return x; // Illégal
...
}

POO - R. Rentsch 36

18
Précisions sur la notion de référence (6)

 Par contre si la fonction renvoie une référence à une constante, il est


alors possible de faire un return d'une constante. Dans ce cas, la
fonction renverra la référence à une copie de cette constante, précédée
d'une éventuelle conversion.

int n = 1;
float x = 2.0;
const int & f() {
...
return n; // OK
return 5; // OK Renvoie la référence à une copie temporaire
return x; // OK Renvoie la référence à un int temporaire obtenu
// par conversion de x
...
}

POO - R. Rentsch 37

Précisions sur la notion de référence (7)


 En fait la notion de référence est plus générale que celle d'argument.

 De manière générale, on peut déclarer un identificateur comme


référence d'une autre variable:
int & p = n; // désormais n et p désigne le même emplacement mémoire

 A noter
 qu'il n'est pas possible de déclarer une référence sans l'initialiser
 qu'une fois déclarée (et initialisée) une référence ne peut plus être modifiée
 qu'il n'est pas possible d'initialiser une référence avec une constante:
int & p = 1; // Illégal

Par contre:
const int & p = 1; // OK Génère une variable temporaire
// contenant la valeur 1 et place sa
POO - R. Rentsch // référence dans p 38

19
Précisions sur la notion de référence (8)
#include <iostream>
using namespace std;

int main() {
int n = 1, m = 2;
float x = 3.5;
int & ref = n;
cout << ref << endl; // affiche 1
ref = m; // affecte la valeur m à l'emplacement
// de référence ref
// ref n'est pas modifié!!!
cout << n << endl; // affiche 2
cout << ref << endl; // affiche 2
ref = x; // affecte la valeur de x après conversion
// en int à l'emplacement de référence ref
cout << n << endl; // affiche 3
cout << ref << endl; // affiche 3
return 0;
}

POO - R. Rentsch 39

Arguments par défaut


 Dans la déclaration d'une fonction, il est possible de prévoir pour un ou
plusieurs arguments (obligatoirement les derniers de la liste!) des
valeurs par défaut:
#include <iostream>
using namespace std;
int main() {
void f (int = 0, int = 0); // A noter que les valeurs par défaut
// sont fixées dans le prototype
// (donc par l'utilisateur)
int n = 10, m = 20;
f(n,m); // appel normal
f(n); // appel avec un seul argument
f(); // appel sans argument
return 0;
}
void f (int a, int b) { // ... et non pas dans la définition
// (donc pas par le concepteur)
cout << "a = " << a << ", "
<< "b = " << b << endl; a = 10, b = 20
} a = 10, b = 0
a = 0, b = 0
POO - R. Rentsch 40

20
Surcharge des fonctions (1)

 A l'instar de Java, mais contrairement à C, C++ supporte la surcharge


(ou surdéfinition) des fonctions.

 Les règles de recherche de la "bonne fonction", bien qu'assez intuitives


dans la plupart des cas, sont toutefois relativement complexes en C++.

 Pour l'instant, signalons simplement que cette recherche peut faire


intervenir toutes les conversions usuelles (promotions numériques et
conversions usuelles, mêmes dégradantes) ainsi que les conversions
définies par l'utilisateur (sujet non traité dans ce cours) dans le cas
d'argument de type classe.

POO - R. Rentsch 41

Surcharge des fonctions (2)


#include <iostream>
using namespace std;
int main() {
void f (int);
void f (float);
char c = 'a'; int n = 1; float x = 2.f; double d = 3.0;
f(c); // appel de f(int)
f(n); // appel de f(int)
f(x); // appel de f(float)
// f(d); // Illégal! ... car les 2 fonctions sont possibles!
return 0;
}
void f (int n) {
cout << "appel de f(int)\n";
}
void f (float x) {
cout << "appel de f(float)\n";
}

POO - R. Rentsch 42

21
Surcharge des fonctions (3)

 A noter que la surcharge suivante n'est pas permise (comme en Java):

void f (int);
void f (const int);

 Par contre, la surcharge suivante est autorisée:

void f (int *);


void f (const int *);

 ... tout comme celle-ci:

void f (int &);


void f (const int &);

POO - R. Rentsch 43

Surcharge des fonctions (4)

 De manière plus précise, la résolution de la surcharge (pour une


fonction membre à un argument) se fait, en C++, selon l'algorithme
suivant:
 S'il existe une fonction réalisant une correspondance exacte, la recherche
s'arrête et la fonction trouvée est appelée.
 Si la recherche précédente n'a pas abouti, on effectue une nouvelle
recherche, en faisant intervenir les promotions numériques (par exemple:
char, short -> int et float -> double).
 Si la recherche n'a toujours pas abouti, on fait alors intervenir les
conversions d'ajustement de type (inclus les conversions dégradantes !).

POO - R. Rentsch 44

22
Surcharge des fonctions (5)

#include <iostream>
using namespace std;
int main() {
void f(short);
void f(double);
f(1); // Erreur à la compilation car appel ambigu.
// En effet, 2 conversions d'ajustement de type sont
// possibles:
// int -> short (conversion dégradante!) ou short -> double
// En Java, il n'y aurait eu aucune ambiguité
// (f(double) aurait été appelé)
return 0;
}
void f(short n) {cout << "Appel de f(short)" << endl;}
void f(double x) {cout << "Appel de f(double)" << endl;}

POO - R. Rentsch 45

Surcharge des fonctions (6)

 A noter pour terminer que C++, contrairement à Java, ne fait pas


intervenir la notion de "meilleure méthode" dans son algorithme de
résolution de surcharge.
Rappel: En Java, le compilateur recherche toutes les méthodes acceptables et choisit la
meilleure (au sens, nécessitant "le moins de conversion possible") si elle existe.

#include <iostream>
using namespace std;

int main() {
void f(float);
void f(double);
f(1); // Erreur à la compilation car appel ambigu.
// En Java, il n'y aurait eu aucune ambiguité
// (f(float) aurait été appelé)
return 0;
}

void f(float x) {cout << "Appel de f(float)" << endl;}


void f(double x) {cout << "Appel de f(double)" << endl;}
POO - R. Rentsch 46

23
Les fonctions inline (1)

 Une fonction inline est une fonction dont les instructions sont
incorporées par le compilateur (dans le module objet correspondant) à
chaque appel.

 L'avantage est un gain de temps par rapport à un appel usuel


(changement de contexte, copie des valeurs sur la pile...); en
revanche, les instructions sont générées plusieurs fois (ce qui conduit à
un exécutable de taille plus importante).

 Une fonction inline est obligatoirement définie dans le même fichier


source que celui où on l'utilise.
Corollaire:
une fonction inline ne peut pas être compilée séparément

POO - R. Rentsch 47

Les fonctions inline (2)

 Cette limitation constitue un désavantage notable. En effet, pour


qu'une même fonction inline puisse être partagée par divers
programmes, il faudra obligatoirement la placer dans un fichier en-tête
(header file: .h)

 Dernière remarque: le compilateur peut sans autre, s'il le juge


nécessaire, transformer une fonction inline en fonction usuelle; en
d'autres termes, on n'a pas la garantie que notre "demande" soit
satisfaite par le compilateur.

POO - R. Rentsch 48

24
Les fonctions inline (3)
#include <iostream>
#include <cmath> // pour sqrt
#include <cstdlib>
using namespace std;
int main() {
double norme (double[], int); // Attention! pas de inline ici
double v2[] = {3.0, 4.0};
double v3[] = {1.0, 1.0, sqrt(2.0)};
cout << "Norme de v2 = " << norme(v2, 2) << endl;
cout << "Norme de v3 = " << norme(v3, sizeof(v3)/sizeof(double))
<< endl;
system("pause");
return EXIT_SUCCESS;
}
// fonction inline
inline double norme (double v[], int n) {
double s = 0.0;
for (int i = 0; i < n; i++)
s += v[i]*v[i];
return sqrt(s);
}

POO - R. Rentsch 49

Les fonctions inline (4)


#ifndef NORME_H
#define NORME_H
inline double norme (double v[], int n) {
double s = 0.0;
for (int i = 0; i < n; i++)
s += v[i]*v[i];
return sqrt(s);
}
#endif
-----------------------------------------------------------
#include <iostream>
#include <cmath> // pour sqrt
#include <cstdlib>
using namespace std;
#include "norme.h" // inclusion du fichier en-tête
int main() {
double v2[] = {3.0, 4.0};
double v3[] = {1.0, 1.0, sqrt(2.0)};
cout << "Norme de v2 = " << norme(v2, 2) << endl;
cout << "Norme de v3 = " << norme(v3, 3) << endl;
system("pause");
return EXIT_SUCCESS;
}
POO - R. Rentsch 50

25
Les types énumérés (1)

 La forme générale d'une déclaration enum est la suivante:

enum [nom_de_type] {
identificateur [= expression] [, identificateur [= expression]]
} [identificateur [, identificateur]];

Exemple:
enum saison {printemps, ete, automne, hiver};

 Les identificateurs entre { }, représentent les valeurs du type, ses


constantes.

 L'ordre dans lequel on les énumère fixe la structure d'ordre dans le


type énuméré et la valeur que représentent ses constantes. Par défaut,
la première constante vaut 0, la seconde 1, etc.

POO - R. Rentsch 51

Les types énumérés (2)

 Toutefois, l'expresssion (constante) facultative qui peut suivre un


identificateur de liste, permet de changer ces valeurs par défaut.

Exemple:
enum saison {printemps, ete = 3, automne, hiver = 2};

Ici printemps vaut 0, ete 3, automne 4 et hiver 2.

 Enfin, le ou les identificateurs (facultatifs) qui peuvent suivre l'accolade


fermante, représentent une ou des variables de ce type (énuméré).
enum saison {…} la_saison;

Exemples:
enum saison une_saison; // enum obligatoire en C, facultatif en C++
saison une_saison; // OK en C++, faux en C

POO - R. Rentsch 52

26
Les types énumérés (3)
#include <iostream>
using namespace std;

int main() {
enum saison {printemps, ete, automne, hiver};
saison s;
int n;
s = printemps;
cout << s << endl; // affiche 0
n = printemps; // conversion implicite tjs permise
cout << n << endl; // affiche 0
s = (saison)0; // en C++, conversion explicite obligatoire
// s = 0, serait accepté en C
s = saison(0); // autre forme possible de conversion
s = saison(100); // Hélas possible !!!
cout << s << endl; // affiche 100 !!!
return 0;
}

POO - R. Rentsch 53

Les types structure (1)

 C++ permet de déclarer un type structure, comme par exemple:

struct point {
int x;
int y;
}

 Une fois un tel type défini, il est alors possible de déclarer des
constantes, variables, pointeurs ou encore références du type
correspondant:

const point pc = {1, 1};


point p; // à noter qu'en C++, contrairement à C,
// le mot struct n'est plus nécessaire
point * ptr = &p;
point & ref = p;

POO - R. Rentsch 54

27
Les types structure (2)
#include <iostream>
using namespace std;
struct point {
int x;
int y;
} p0; // p0 = variable statique
// x et y, par défaut, initialisés à 0
int main() {
const point p1 = {1,1};
point p2; // variable automatique
// x et y, pas initialisés à 0 par défaut
p2.x = 2; p2.y = 2;
point * ptr;
ptr = &p2;
point & ref = p2; // rappel: initialisation obligatoire
cout << "p0.x = " << p0.x << endl;
cout << "p1.x = " << p1.x << endl;
cout << "p2.x = " << p2.x << endl; p0.x = 0
cout << "ptr->x = " << ptr->x << endl; p1.x = 1
cout << "(*ptr).x = " << (*ptr).x << endl; p2.x = 2
cout << "ref.x = " << ref.x << endl;
ptr->x = 2
return 0;
} (*ptr).x = 2
ref.x = 2
POO - R. Rentsch 55

Les types structure (3)


 ... mais C++ permet également d'associer des méthodes (ou fonctions
membres) à un type structure. Exemple:
#include <iostream> A noter la séparation entre
using namespace std; déclaration et définition !!!
// déclaration du type point
struct point {
int x, y;
void initialise(int, int); // méthode (fonction membre)
void affiche();
};
// définition des fonctions membres
// :: = opérateur de résolution de portée
void point::initialise (int x, int y) {this->x = x; this->y = y;}
void point::affiche () {
cout << "(" << x << ", " << y << ")" << endl;}
int main() {
point p;
p.initialise(1,1);
p.affiche(); // affiche (1, 1);
return 0;
}
POO - R. Rentsch 56

28
Les types structure (4)
 Il est bien sûr possible de compiler séparément le type point de sorte à
en faire un composant logiciel réutilisable:
#ifndef POINT_H
#define POINT_H

struct point {
int x, y;
void initialise(int, int); // méthode (fonction membre)
void affiche();
};

#endif
--------------------------------------------------------------
#include <iostream>
using namespace std;
#include "point.h"

void point::initialise (int x, int y) {this->x = x; this->y = y;}

void point::affiche () {
cout << "(" << x << ", " << y << ")" << endl;}
POO - R. Rentsch 57

Les types structure (5)

#include <iostream>
using namespace std;
#include "point.h"
int main() {
point p;
p.initialise(1,1);
p.affiche(); // affiche (1, 1);
system("pause");
return 0;
}

 En conclusion, on peut dire que le type struct en C++ est un cas


particulier de type classe où aucune encapsulation n'est
réalisée par défaut (tous les membres sont publics par défaut)

POO - R. Rentsch 58

29
Gestion dynamique de la mémoire (1)

 En C, la gestion dynamique de la mémoire se réalise au moyen des


fonctions de la bibliothèque standard malloc (ou calloc) et free.
Ces fonctions peuvent également être utilisées en C++.

 Mais dans le contexte de la POO, C++ a introduit 2 nouveaux


opérateurs: new et delete, spécialement conçus pour la gestion
dynamique d'objets (tout en étant aussi utilisables avec des variables
"classiques").

 En C++, on utilise donc new et delete de préférence à malloc et free.

POO - R. Rentsch 59

Gestion dynamique de la mémoire (2)

 Exemple:

Avec la déclaration:

int * ptr;

l'instruction:

ptr = new int;

permet d'allouer l'espace mémoire nécessaire pour un élément de type


int et d'affecter à ptr l'adresse correspondante.

En C, on aurait écrit:

ptr = (int *) malloc (sizeof(int));

POO - R. Rentsch 60

30
Gestion dynamique de la mémoire (3)
 De manière générale:

Si type désigne un type absolument quelconque et n une expression


entière quelconque (non négative), l'expression:

new type[n]

alloue la mémoire nécessaire pour n éléments du type indiqué et


fournit en retour, si l'allocation réussit, un pointeur (de type type *)
sur le premier élément.

 En cas d'échec, new déclenche l'exception bad_alloc.


NB
 L'indication [n] est facultative (voir exemple précédent); écrire new type
est équivalent à écrire new type[1].
 Si nous ne souhaitons pas qu'il y ait propagation de bad_alloc, on peut
écrire: new (nothrow) type (ou type[n]).
POO - R. Rentsch
En cas d'échec, c'est alors un pointeur NULL qui nous est renvoyé. 61

Gestion dynamique de la mémoire (4)

 Si l'on désire libérer un emplacement mémoire préalablement alloué


par new, il faut obligatoirement utiliser l'opérateur delete.

 Ainsi l'expression:

delete ptr;

libère l'espace mémoire préalablement alloué par new à l'adresse


indiquée.

 A noter que si l'on fait un new type[n], il est nécessaire de restituer


la mémoire en faisant un delete[], ceci pour n'importe quel type type.

POO - R. Rentsch 62

31
Chapitre 3

Classes et objets

Notion de classe (1)

 On a vu dans le chapitre précédent, qu'en C++, la structure est un cas


particulier de la classe.

 Plus précisément, une classe sera une structure dans laquelle seuls
certains champs et/ou méthodes seront "publics" (càd accessible
depuis l'extérieur de la classe), les autres membres étant "privés"
(encapsulés).
On verra qu'il existe également, tout comme en Java, le type "protégé" (intervenant dans le
cas de classes dérivées, donc de l'héritage)

 Comme pour les structures, C++ distingue la déclaration d'une


classe de sa définition.
Pour rappel, cette distinction n'existe pas en Java!!!

POO - R. Rentsch 64

32
Notion de classe (2)
 La déclaration d'une classe précise quels sont les membres (données
et fonctions) publics et quels sont les membres privés. On utilise pour
cela les mêmes mots-clés qu'en Java: public et private

 La définition d'une classe consiste à fournir les définitions des


fonctions membres (méthodes).
On indique alors le nom de la classe correspondante à l'aide de
l'opérateur de résolution de portée ::

 Une classe peut être munies de un ou plusieurs constructeurs.


Comme en Java, un constructeur porte le même nom que la classe et
ne renvoie pas de valeur (aucune indication de type, pas même void ne
doit figurer dans sa déclaration ou dans sa définition).

 Le constructeur est appelé APRES l'allocation de l'espace mémoire


destiné à l'objet.
POO - R. Rentsch 65

Notion de classe (3)

 Une classe peut aussi être munie d'un destructeur, à savoir une
fonction membre portant le même nom que sa classe mais précédé du
symbole tilde (~).

 Le destructeur est appelé AVANT la libération de l'espace mémoire


associé à l'objet.

 Par définition, un destructeur n'a jamais de paramètres et il ne renvoie


pas de valeur (aucune indication de type, pas même void ne doit
figurer dans sa déclaration ou dans sa définition).

POO - R. Rentsch 66

33
Notion de classe (4)
#include <iostream>
using namespace std;

/* déclaration de la classe point */

class point {
private : // private par défaut si pas mentionné
int x, y;
public :
point(int, int); // constructeur
void deplace(int, int);
void affiche();
~point(); // destructeur
};

NB les mots-clés public et private peuvent apparaître à plusieurs reprises dans la


déclaration d'une classe (pas recommandé toutefois!)

POO - R. Rentsch 67

Notion de classe (5)

IMPORTANT
Contrairement à Java:

 il est interdit d'écrire public class (protected ou private sont également


interdits) en C++

 un champ usuel n'est pas implicitement initialisé à "zéro" en C++


(il contient donc une valeur indéfinie)

 un champ usuel ne peut pas être initialisé explicitement en C++


(impossible donc d'écrire, par exemple, private int x = 1 en C++)

POO - R. Rentsch 68

34
Notion de classe (6)
/* Définition des fonctions membres de la classe point */

point::point(int x, int y) { // :: = opér. de résolution de portée


cout << "Appel du constructeur\n";
this->x = x; this->y = y;
}

void point::deplace(int dx, int dy) {


x += dx; y += dy;
}

void point::affiche() {
cout << "Point de coord (" << x << ", " << y << ")" << endl;
}

point::~point() {
cout << "Appel du destructeur\n";
}

POO - R. Rentsch 69

Notion de classe (7)


/* Programme utilisant la classe point */

int main() {
point p(1,2); // A noter la différence avec Java!!!
// Tout comme en Java, à partir du moment où
// une classe possède un constructeur, il n'est
// plus possible de créer un objet sans fournir
// les arguments requis par le constructeur
p.affiche();
p.deplace(1,1);
p.affiche(); Appel du constructeur
} Point de coord (1, 2)
Point de coord (2, 3)
Appel du destructeur

NB Tout le code ci-dessus se trouve dans le même fichier source. Il est bien sûr possible de
compiler notre classe point séparément du programme utilisateur (ceci en vue par ex de
pouvoir réutiliser notre classe point). Pour ce faire, il suffit de "délocaliser" la déclaration
de notre classe point dans un fichier en-tête .h et sa définition dans un fichier .cpp

POO - R. Rentsch 70

35
Affectation d'objets (1)
 C++ autorise l'affectation d'un objet d'un type donné à un autre
objet du même type. Exemple:
point p1 (1,1);
point p2 (2,2);
p1 = p2;

 Dans ce cas, il y a recopie des valeurs des champs (publics ou


privés)
NB Les fonctions membres, elles, n'ont aucune raison d'être concernées par la recopie

 Toutefois si, parmi ces champs, se trouvent des pointeurs, les


emplacements pointés ne seront pas soumis à cette recopie; il s'agit
donc d'une copie "superficielle" et non "profonde".

 Si un tel effet est nécessaire (cas fréquent en pratique!), il ne pourra


être obtenu qu'en surchargeant l'opérateur d'affectation (voir suite du
cours)
POO - R. Rentsch 71

Affectation d'objets (2)

 Rappel:
En Java, l'affectation entre objets ne consiste pas en une recopie
("superficielle") des valeurs des champs... mais d'une recopie de
référence: après affectation, on se retrouve, en effet, en présence de 2
références sur un même objet.

POO - R. Rentsch 72

36
Champs statiques (1)
 A l'instar de Java, C++ permet de déclarer un champ statique:
class c {
static int n; // initialisation explicite interdite!
...
}

 Rappels
 un champ statique n'existe qu'en un seul exemplaire indépendamment des
objets (instances) de la classe (càd même si aucun objet de la classe n'a
encore été créé)
 un champ statique peut donc être vu comme une sorte de variable globale
dont la portée est limitée à la classe dans laquelle il se trouve déclaré.

 En C++, un champ static (même privé) doit être initialisé


explicitement à l'extérieur de la déclaration de la classe en
utilisant l'opérateur de résolution de portée :: (int c::n = 1, par ex)
pour spécifier sa classe.
POO - R. Rentsch En général, son initialisation se fait dans la définition de la classe. 73

Champs statiques (2)


 Par ailleurs, contrairement à ce qui se produit avec une variable
statique ordinaire, un champ statique n'est pas initialisé à 0 par
défaut en C++.
 Pour rappel, en Java, un champ statique est initialisé par défaut à 0.

 Les champs statiques constants


class c {
static const int n = 1; // OK
static const string msg = "Hello!"; // pas OK !!!
...
}

peuvent être initialisés explicitement dans la déclaration de la classe...


s'ils sont de type primitif mais pas s'ils sont de type classe (dans ce
dernier cas, tout champ static constant (même privé) doit être initialisé
explicitement à l'extérieur de la déclaration de la classe)

NB A noter qu'il n'est pas possible d'écrire ci-dessus: static const int n(1);
POO - R. Rentsch 74

37
Propriétés des fonctions membres (1)
Surcharge et arguments par défaut
 Il s'agit simplement de la généralisation aux fonctions des possibilités
offertes par C++ pour les fonctions "ordinaires" (voir chapitre
précédent)

Fonctions membres inline


 Il s'agit ici aussi de la généralisation aux fonctions membre d'une
possibilité offerte aux fonctions ordinaires... à une petite nuance près.

 Pour rendre en ligne une fonction membre, on peut


 soit fournir directement la définition de la fonction dans la déclaration
même de la classe; dans ce cas le qualificatif inline n'a pas à être utilisé
class c {
...
int f (int n) {...} // fonction membre en ligne
...
}
POO - R. Rentsch 75

Propriétés des fonctions membres (2)

Fonctions membres inline (suite)


 soit procéder comme pour une fonction ordinaire, en fournissant une
définition en dehors de la déclaration de la classe; dans ce cas, le
qualificatif inline doit figurer à la fois devant la déclaration et devant l'en-
tête:

class c {
...
inline int f(int); // le qualificatif inline doit figurer ici
...
}

inline int c::f(int n) {...} // ... et ici

POO - R. Rentsch 76

38
Propriétés des fonctions membres (3)

Objets transmis en arguments d'une fonction membre


 Une fonction membre reçoit implicitement l'adresse de l'objet
l'ayant appelé.
Au sein d'une fonction membre, this représente un pointeur sur
l'objet ayant appelé ladite méthode.

 Mais il est aussi possible de lui transmettre explicitement un argument


(ou plusieurs) du type de sa classe, ou d'un type d'une autre classe.

 Dans le premier cas, la fonction membre aura accès aux membres


privés de l'argument en question (car, en C++ (ainsi qu'en Java),
l'unité d'encapsulation est la classe elle-même et non l'objet.

 Dans le second cas, par contre, la fonction membre n'aura accès


POO - R. Rentsch
qu'aux membres publics de l'argument. 77

Propriétés des fonctions membres (4)

Objets transmis en arguments d'une fonction membre (suite)


 Un tel argument peut être transmis par valeur, adresse ou référence.

 Avec la transmission par valeur, il y a recopie des valeurs des


champs dans un emplacement local à la fonction appelée.
Des problèmes peuvent survenir dès lors que l'objet transmis en
argument contient des pointeurs sur des parties dynamiques.
Ils seront réglés par l'emploi d'un constructeur de recopie (voir suite
du cours)

POO - R. Rentsch 78

39
Propriétés des fonctions membres (5)
 Exemple
#include <iostream>
using namespace std;
class point {
int x, y; // champs privés
public :
point(int, int); // constructeur
bool coincide(const point &); // const car la fonction membre
// n'a aucune raison de modifier
// le point référencé
};
point::point(int x, int y) {
this->x = x; this->y = y;
}
bool point::coincide(const point & p) {
return (p.x == x && p.y == y);
}

POO - R. Rentsch 79

Propriétés des fonctions membres (6)


int main() {
point p1(1,2);
point p2(1,2);
point p3(1,3);
cout << boolalpha
<< "p1 coincide avec p2 = " << p1.coincide(p2) << endl
<< "p1 coincide avec p3 = " << p1.coincide(p3) << endl;
return 0;
}

p1 coincide avec p2 = true


p1 coincide avec p3 = false

POO - R. Rentsch 80

40
Propriétés des fonctions membres (7)

Fonctions membres fournissant un objet en retour


 Une fonction membre peut fournir comme valeur de retour un objet de
sa classe ou d'une autre classe.

 La transmission peut ici aussi se faire par valeur, adresse ou référence.

 La transmission par valeur implique une recopie; le même


problème que celui évoqué plus haut pour les objets comportant des
pointeurs sur des parties dynamiques peut donc ici aussi se poser.

 Quant aux transmissions par adresse ou référence, elles


doivent être utilisées avec précaution, dans la mesure où il s'agit
souvent de l'adresse d'un objet alloué automatiquement càd dont la
durée de vie coïncide avec celle de la fonction.
POO - R. Rentsch 81

Propriétés des fonctions membres (8)

 Exemple
point symetrique() { // valeur de retour transmise par valeur
// OK! pas de problème
point p;
p.x = -x; p.y = -y;
return p;
}

point & symetrique() { // valeur de retour transmise par référence


// Gros problème... car au sortir de la
// fonction l'objet p n'existe plus
point p;
p.x = -x; p.y = -y;
return p;
}

NB Dans les 2 cas, il serait possible de simplifier l'écriture en écrivant simplement :

return point(-x,-y);

POO - R. Rentsch 82

41
Propriétés des fonctions membres (9)
Fonctions membres statiques
 Comme en Java, si une fonction membre a une action indépendante
d'un quelconque objet de sa classe, on peut la déclarer static.

static void f(); // déclaration

 Une telle fonction peut être appelée indépendamment de toute objet


en préfixant son nom par le nom de sa classe suivie de l'opérateur de
résolution de portée ::

maclasse::f(); // appel

 A noter que l'attribut static ne doit figurer que dans la déclaration de la


fonction membre et non dans sa définition:

static maclasse::void f() {} // définition

POO - R. Rentsch 83

Propriétés des fonctions membres (10)

Fonctions membres constantes


 En C++, on peut déclarer des objets constants en utilisant le
qualificatif const

 Dans ce cas, seules les fonctions membres déclarées (et


définies) avec ce même qualificatif peuvent s'appliquer à un
objet constant

class point {
private :
int x, y;
public :
point(int = 0, int = 0); // constructeur
void deplace(int, int);
void affiche() const;
~point(); // destructeur
};
POO - R. Rentsch 84

42
Propriétés des fonctions membres (11)
Fonctions membres constantes (suite)
 Avec:
point a;
const point b;

b.affiche(); // OK car affiche déclarée const


b.deplace(); // Erreur à la compilation!
// deplace pas applicable à un objet constant
a.affiche(); // OK

 Plus généralement, le fait de spécifier que affiche est constante a


deux conséquences:
 Elle est utilisable pour un objet constant (cf exemple ci-dessus)
 Les instructions figurant dans sa définition ne doivent en aucun cas modifier
la valeur des membres (non statiques1) de l'objet
1 la valeur des membres statiques peut, par contre, être modifiée en vertu du fait qu'un membre
statique n'est pas associé à un objet spécifique

POO - R. Rentsch void affiche() const {x++;} // interdit car méthode constante 85

Propriétés des fonctions membres (12)

Fonctions membres constantes (suite)


 La notion de fonction membre constante n'existe pas en Java

 Il est possible de surcharger une fonction membre en se fondant sur la


présence ou l'absence du qualificatif const:

void affiche(); // méthode I


void affiche() const; // méthode II

Avec:

point a;
const point b;

a.affiche(); // appellera la méthode I


b.affiche(); // appellera la méthode II

POO - R. Rentsch 86

43
Classe d'allocation des objets en C++ (1)

 En C, une variable peut être créée de 2 manières:


 par une déclaration: elle est alors de classe automatique ou statique;
sa durée de vie est parfaitement définie par l'emplacement de sa
déclaration
 en faisant appel à des fonctions de gestion dynamique de la mémoire
(malloc, calloc, free); elle est alors dite dynamique; sa durée de vie est
contrôlée par le programme

 En C++, on retrouve ces trois classes à la fois pour les variables et


pour les objets, avec cette différence que la gestion dynamique fait
appel aux opérateurs new et delete.

POO - R. Rentsch 87

Classe d'allocation des objets en C++ (2)

 Rappel:
 les objets automatiques sont créés par une déclaration soit dans une
fonction, soit dans un bloc et sont détruits au sortir de cette fonction ou de
ce bloc
 Les objets statiques sont créés par une déclaration située en dehors de
toute fonction ou par une déclaration précédée du mot-clé static (dans une
fonction ou dans un bloc). Ils sont créés avant l'entrée dans la fonction
main et détruits après la fin de son exécution

POO - R. Rentsch 88

44
Constr. du point (1,1)
--- debut de main ---

Classe d'allocation des Constr. du point (0,0)


Boucle: iteration no 1
Constr. du point (2,2)
objets en C++ (3) Destr. du point (2,2)
Boucle: iteration no 2
Constr. du point (3,3)
Destr. du point (3,3)
#include <iostream> --- fin de main ---
using namespace std; Destr. du point (0,0)
class point { Destr. du point (1,1)
int x, y;
public :
point(int x = 0, int y = 0) : x(x), y(y) { // constructeur
cout << "Constr. du point (" << x << "," << y << ")\n";
};
~point() { // destructeur
cout << "Destr. du point (" << x << "," << y << ")\n";}
};
point a(1,1); // un objet statique
int main() {
cout << "--- debut de main ---\n";
point b; // un objet automatique (NB b() serait incorrect);
for (int i = 0; i < 2; i++) {
cout << "Boucle: iteration no " << i + 1 << endl;
point b(i+2, i+2); // un objet automatique créé dans un bloc
}
cout << "--- fin de main ---\n";
return 0;
}

POO - R. Rentsch 89

Classe d'allocation des objets en C++ (4)

 Les objets dynamiques sont créés au moyen de l'opérateur new,


auquel on fournit, le cas échéant, les valeurs destinées au
constructeur:
point * ptr;
ptr = new point(1,2);

 L'accès aux membres d'un objet dynamique se fait comme pour les
variables:
ptr->affiche();
(*ptr).affiche();

 Les objets dynamiques n'ont pas de durée de vie définie a priori. Ils
sont détruits en utilisant l'opérateur delete (ou delete[]):
delete ptr;

POO - R. Rentsch 90

45
Le constructeur de recopie (1)

 On a vu que C++ garantit l'appel du constructeur pour un objet créé


par une déclaration ou par new ... ce qui nous assure qu'un objet ne
peut pas être créé sans avoir été correctement initialisé.

 Il existe cependant des circonstances dans lesquelles il est nécessaire


de construire un objet, même si le programmeur n'a pas prévu de
constructeur pour cela.

 C'est le cas dans les 3 situations suivantes:


 transmission d'un objet par valeur en argument d'appel d'une fonction
il est alors nécessaire de créer, dans un emplacement local à la fonction, un objet qui soit une copie
de l'argument effectif
 transmission d'un objet par valeur en valeur de retour d'une fonction
 initialisation d'un objet lors de sa déclaration avec un autre objet du même
type
Exemple: point p1 = p2;
POO - R. Rentsch 91

Le constructeur de recopie (2)

 On regroupe ces 3 situations sous le nom d'initialisation par


recopie.

 Une initialisation par recopie d'un objet est donc la création


d'un objet par recopie d'un objet existant.

 Pour réaliser une telle initialisation, C++ a prévu d'utiliser un


constructeur particulier, appelé constructeur de recopie

 Si le programmeur n'a pas prévu explicitement un tel constructeur dans


sa classe, un constructeur de recopie par défaut est généré
automatiquement par le compilateur.
Attention toutefois: le constructeur de recopie par défaut se contente
de faire une copie superficielle de chacun des membres (d'où risque de
problèmes sérieux avec des "champs pointeurs").
POO - R. Rentsch 92

46
Le constructeur de recopie (3)

 L'en-tête d'un constructeur de recopie doit obligatoirement être de


l'une de ces deux formes:
(ici on suppose que la classe concernée est la classe point)

point (point &)

point (const point &) forme conseillée!

 A noter que:
 C++ impose au constructeur de recopie que son unique argument soit
transmis par référence (appels récursifs sinon!!!)
 Les 2 formes peuvent se surchargées
Dans un tel cas, la première forme serait utilisée en cas d'initialisation d'un objet par
un objet quelconque (càd non constant), tandis que la seconde serait utilisée en cas
d'initialisation d'un objet par un objet constant

POO - R. Rentsch 93

Le constructeur de recopie (4)

 A noter que (suite):


 L'affectation n'est PAS une situation d'initialisation par recopie
... car les 2 objets existent déjà:

point p1(...);
point p2(...);
p1 = p2; // affectation, pas initialisation par recopie!

Bien que l'affectation et l'initialisation par recopie possèdent un traitement


par défaut analogue (copie superficielle), la prise en compte d'une copie
profonde passe par des mécanismes différents:
- définition d'un constructeur de recopie pour l'initialisation
- surcharge de l'opérateur = dans le cas de l'affectation
(cf suite du cours)

POO - R. Rentsch 94

47
Le constructeur de recopie (5)
Exemple

#include <iostream>
using namespace std;
class bidon {
int size;
double * ptr;
public :
bidon (int n) { // constructeur
ptr = new double[size = n];
for (int i = 0; i < size; i++) ptr[i] = i;
cout << "Constructeur: ptr = " << ptr << "\n";
}
bidon (const bidon & b) { // constructeur de recopie
ptr = new double[size = b.size];
for (int i = 0; i < size; i++) ptr[i] = b.ptr[i];
cout << "Constr de recopie: ptr = " << ptr << "\n";
}
~bidon () { // destructeur
cout << "Destructeur: ptr = " << ptr << "\n";
delete[] ptr;
}
};
POO - R. Rentsch 95

Le constructeur de recopie (6)


void f (bidon b) {
cout << "Appel de f\n";
}

int main() { sans constructeur de recopie


bidon b(3);
Constructeur: ptr = 0x3d4a08
f(b); Appel de f
return 0; Destructeur: ptr = 0x3d4a08
} Destructeur: ptr = 0x3d4a08

avec constructeur de recopie


Constructeur: ptr = 0x3d4a08
Constr de recopie: ptr = 0x3d4a28
Appel de f
Destructeur: ptr = 0x3d4a28
Destructeur: ptr = 0x3d4a08

POO - R. Rentsch 96

48
Le constructeur de recopie (7)

 Lorsqu'un objet comporte des objets membres, la recopie (par défaut)


se fait membre à membre; en d'autres termes, si l'un des membres est
lui-même un objet, on le recopiera en appelant son propre constructeur
de recopie (par défaut ou celui défini dans la classe correspondante).

 La construction par recopie (par défaut) d'un objet sera donc


satisfaisante dès lors qu'il ne contient pas de pointeurs sur des parties
dynamiques.

 En revanche, si l'objet contient des pointeurs, il faudra le munir


d'un constructeur de recopie approprié.

POO - R. Rentsch 97

Classes imbriquées (internes) (1)

 A l'instar de Java, il est possible, en C++, de déclarer une classe à


l'intérieur d'une autre classe.

 La visibilité d'une classe interne est déterminée par les mots-clés


private ou public (tout comme les membres).

 Une classe interne privée n'est visible que par sa classe externe.

 Une classe interne publique peut être instanciée à l'extérieur de la


classe externe, moyennant l'utilisation de l'opérateur de résolution de
portée ::

POO - R. Rentsch 98

49
Classes imbriquées (internes) (2)
class A {
private :
// Déclaration d'une classe interne privée à la classe A
class B {
...
};
// Membres privés de la classe A
...
public :
// Déclaration d'une classe interne publique à la classe A
class C {
...
};
// Membres publics de la classe A
...
};
int main() {
A a;
A::B b; // Interdit la classe interne B est privée
A::C c; // OK
...
}

POO - R. Rentsch 99

Classes imbriquées (internes) (3)

 Malheureusement, la notion de classe interne s'avère beaucoup moins


"souple" en C++ qu'en Java.

 Contrairement à Java, il est impossible en C++ à un objet d'une classe


externe d'accéder aux membres privés d'un objet de sa classe interne
(l'inverse est également interdit).

 L'exemple du cercle implémenté au moyen d'une classe interne Centre


que nous avons examiné en Java (chapitre "Les classes et les objets)
devra donc être écrit comme suit en C++:

POO - R. Rentsch 100

50
Classes imbriquées (internes) (4)
#include <iostream>
using namespace std;
class Cercle {
private :
class Centre { // classe interne privée
int x, y;
public :
Centre(int x, int y) : x(x), y(y) {}
void affiche() {cout << "(" << x << ", " << y << ")\n";}
int getX() {return x;}
int getY() {return y;}
void setX(int x) {this->x = x;}
void setY(int y) {this->y = y;}
};
public :
Cercle(int x, int y, float r) : r(r) {c = new Centre(x,y);}
~Cercle() {delete c;}
void affiche() {cout << "Cercle de rayon " << r
<< " et de centre "; c->affiche();}
void deplace(int dx, int dy) {
// Ici, il est impossible d'écrire, par ex, c.x += dx
c->setX(c->getX() + dx); c->setY(c->getY() + dy);
}
private :
Centre * c;
POO - R. Rentsch float r; 101
};

Classes imbriquées (internes) (5)

int main() {
Cercle cercle(1, 1, 5.5f);
cercle.affiche();
cercle.deplace(1,1);
cercle.affiche();
}

Cercle de rayon 5.5 et de centre (1, 1)


Cercle de rayon 5.5 et de centre (2, 2)

POO - R. Rentsch 102

51
Chapitre 4

Surcharge des opérateurs

Introduction (1)

 C++ permet de surcharger les opérateurs existants, càd de leur


donner une nouvelle signification lorsqu'ils portent (partiellement ou en
totalité) sur des objets de type classe.

 Java, rappel, ne permet pas la surcharge d'opérateurs.

 Il s'agit là d'une technique très puissante puisqu'elle va nous permettre


de créer, par le biais des classes, des types à part entière, càd munis
comme les types de base, d'opérateurs parfaitement intégrés.

 La notation opératoire qui en résulte présente l'avantage d'être


beaucoup plus concise et lisible qu'une notation fonctionnelle.

POO - R. Rentsch 104

52
Introduction (2)

 Par exemple, si l'on se définit une classe complexe destinée à


représenter des nombres complexes, il est possible de donner une
signification à des expressions comme:

a+b a–b a * b ... a et b étant des objets de type complexe

 Il nous suffit pour cela de surcharger les opérateurs +, -, *, etc en


spécifiant le rôle précis qu'on souhaite leur attribuer.

 De manière plus précise, pour surcharger un opérateur existant op, on


définit une fonction nommée operator op1:
 soit sous forme d'une fonction indépendante (généralement "amie"
d'une ou de plusieurs classes)
 soit sous forme d'une fonction membre d'une classe
1 on peut placer un ou plusieurs espaces entre le mot operator et l'opérateur, mais ce n'est
POO - R. Rentsch pas une obligation stricte 105

Exemple avec fonction amie (1)


#include <iostream>
using namespace std;
class point {
int x, y;
public :
point(int x = 0, int y = 0) : x(x), y(y) {}
friend point operator + (point, point); // fonction amie
void affiche(char * info) {
cout << info << "(" << x << ", " << y << ")\n";
}
};
point operator + (point a, point b) {
point p;
p.x = a.x + b.x; p.y = a.y + b.y;
return p;
}
int main() {
point a(1,1), b(2,2), c(3,3); a(1, 1)
a.affiche("a"); b.affiche("b"); c.affiche("c"); b(2, 2)
point d = a + b + c; c(3, 3)
d.affiche("d"); d(6, 6)
return 0;
}
POO - R. Rentsch 106

53
Exemple avec fonction amie (2)
 Quelques remarques à propos de l'exemple précédent:
 Une expression telle que a + b est interprétée par le compilateur comme:
operator + (a, b)
 Une expression telle que a + b + c est évaluée en tenant compte des règles
de priorité et l'associativité usuelle de l'opérateur +
 Ci-dessus nous avons choisi d'utiliser une transmission par valeur pour les
arguments et la valeur de retour de la fonction opérateur +.
Il est bien sûr possible d'envisager une transmission par référence... mais
pour les arguments seulement1.
1 Le point p étant créé localement, il est détruit au sortir de la fonction!

On pourrait donc écrire aussi:


point operator + (point &, point &);

ou encore, si l'on cherche à protéger nos arguments contre d'éventuelles


modifications:

point operator + (const point &, const point &);


POO - R. Rentsch 107

Exemple avec fonction membre (1)


#include <iostream>
using namespace std;
class point {
int x, y;
public :
point(int x = 0, int y = 0) : x(x), y(y) {}
point operator + (point a) { // fonction membre
point p;
p.x = x + a.x; p.y = y + a.y; // ou p.x = this->x + a.x ...
return p;
}
void affiche(char * info) {
cout << info << "(" << x << ", " << y << ")\n";
}
};
int main() {
point a(1,1), b(2,2), c(3,3);
a.affiche("a");
b.affiche("b"); a(1, 1)
c.affiche("c"); b(2, 2)
point d = a + b + c; c(3, 3)
d.affiche("d"); d(6, 6)
return 0;
}
POO - R. Rentsch 108

54
Exemple avec fonction membre (2)

 Quelques remarques à propos de l'exemple précédent:


 Une expression telle que a + b est interprétée cette fois par le compilateur
comme:
a.operator + (b)
 Ici aussi nous avons choisi d'utiliser une transmission par valeur pour les
arguments et la valeur de retour de la fonction opérateur +.
Il est bien sûr possible d'envisager une transmission par référence... mais
pour les arguments seulement1.
1 Le point p étant créé localement, il est détruit au sortir de la fonction!

On pourrait donc écrire aussi:

point operator + (point &);

ou encore, si l'on cherche à protéger nos arguments contre d'éventuelles


modifications:

point operator + (const point &);


POO - R. Rentsch 109

Possibilités et limites de la surcharge


d'opérateurs (1)

 Le symbole suivant le mot-clé operator doit obligatoirement être un


opérateur déjà défini pour les types de base. Il n'est pas possible de
créer de nouveaux symboles.

 A noter que tous les opérateurs prédéfinis peuvent être surchargés


sauf (il y a toujours des exceptions à la règle!) '.', '::' et '?:'

 La pluralité de l'opérateur initial (unaire, binaire) doit être conservée.

 Les opérateurs surchargés conservent leur priorité et leur associativité


habituelles.

POO - R. Rentsch 110

55
Possibilités et limites de la surcharge
d'opérateurs (2)

 Un opérateur surchargé doit toujours posséder au moins un opérande


de type classe (on ne peut donc pas modifier la sémantique des
opérateurs usuels, càd opérant sur les types de base).

 Il doit donc s'agir (rappel):


 soit d'une fonction membre, auquel cas elle dispose obligatoirement d'un
argument implicite du type de sa classe (this)
 soit d'une fonction amie possédant au moins un argument de type classe

 Tous les opérateurs ne peuvent pas être définis sous l'une ou l'autre
des 2 formes ci-dessus; certains opérateurs doivent être
obligatoirement définis comme fonction membre.
Il s'agit des opérateurs:

=, [], (), ->, new et delete


POO - R. Rentsch 111

Possibilités et limites de la surcharge


d'opérateurs (3)

 Attention! Si vous surchargez les opérateurs = et + ... cela ne vous


dispense pas pour autant de la surcharge de l'opérateur +=

 A noter que les opérateurs << et >> d'écriture et de lecture sur un flot
peuvent très bien être surchargés (c'est même fréquent!).

POO - R. Rentsch 112

56
Surcharge de l'opérateur = (1)

 Retour sur la notion du constructeur de recopie:

Au chapitre 3, nous avions considéré la classe suivante:

#include <iostream>
using namespace std;

class bidon {
int size;
double * ptr;
public :
bidon (int n) {
...
};

POO - R. Rentsch 113

Surcharge de l'opérateur = (2)

 Si f était une fonction à un argument du type bidon, les instructions


suivantes:
bidon b(3);
f(b);

posaient problème: l'appel de f conduisait à la création, par recopie de


b, d'un nouvel objet a. Nous étions alors en présence de 2 objets a et b
comportant un pointeur vers le même emplacement.
3
b

3
a

 Une solution consistait à définir un constructeur de recopie chargé


d'effectuer non seulement la recopie de l'objet lui-même, mais aussi
celle de sa partie dynamique dans un nouvel emplacement.
POO - R. Rentsch 114

57
Surcharge de l'opérateur = (3)

 L'affectation d'objets du type bidon posent les mêmes problèmes.


Ainsi avec cette déclaration:
bidon a(3), b(2);

qui correspond à la situation suivante:


3
a

2
b

l'affectation:

b = a;

3
conduit à: a

3
b

POO - R. Rentsch 115

Surcharge de l'opérateur = (4)

 Le problème est effectivement voisin de celui de la construction par


recopie. Voisin, mais pas identique car il y a des différences:
 On peut se retrouver en présence d'une affectation d'un objet sur lui-même
 Avant l'affectation, il existe ici 2 objets complets.
Dans le cas de la construction par recopie, il n'existait qu'un seul
emplacement dynamique, le second étant à créer.
On va se retrouver ici avec l'ancien emplacement dynamique de b. Or s'il
n'est plus référencé par b, est-on sûr qu'il n'est plus référencé par ailleurs?

3
a

3
b

POO - R. Rentsch 116

58
Surcharge de l'opérateur = (5)
 Un peu brutalement voici le code de notre opérateur = surchargé.
Pour rappel, C++ impose qu'il soit implémenté sous la forme d'une
fonction membre: b devient le première opérande (this) et a devient le
second opérande (ici v).
Nous discuterons plus loin du choix des modes de transmission pour
l'argument et pour la valeur de retour.
bidon & operator = (const bidon & v) {
if (this != &v) { // Si l'adresse des 2 objets diffère
// donc pas affectation du type b = b
// libération de l'emplacement pointé par b
delete[] ptr;
// création dynamique d'un nouvel emplacement dans
// lequel on recopie les valeurs de l'emplacement pointé
// par a et mise en place des valeurs des membres données
// de b
ptr = new double[size = v.size];
for (int i = 0; i < size; i++) ptr[i] = v.ptr[i];
}
return *this;
}
POO - R. Rentsch 117

Surcharge de l'opérateur = (6)

 … ou mieux :

bidon & operator = (const bidon & v) {


if (this != &v) {
if (size != v.size() {
delete[] ptr;
ptr = new double[size = v.size];
}
for (int i = 0; i < size; i++)
ptr[i] = v.ptr[i];
}
return *this;
}

POO - R. Rentsch 118

59
Forme canonique d'une classe (1)

 Dès lors qu'une classe dispose de pointeurs sur des parties


dynamiques, il est nécessaire de munir la classe des 4 fonctions
membres suivantes au moins:

 constructeur
 constructeur de recopie
 destructeur (chargé de libérer les emplacements dynam. créés par l'objet)
 opérateur d'affectation

POO - R. Rentsch 119

Forme canonique d'une classe (2)

 Voici un canevas récapitulatif correspondant à ce minimum qu'on


nomme souvent "classe canonique":

class T {
public :
T (...); // constructeurs autres
// que par recopie
T (const T &); // constructeur de recopie
// (forme conseillée)
~T (); // destructeur
T & operator = (const T &); // affectation
// (forme conseillée)
...
};

POO - R. Rentsch 120

60
Forme canonique d'une classe (3)

 Bien que ce ne soit pas obligatoire, il est recommandé:


 d'employer const pour l'argument du constructeur de recopie et celui de
l'affectation dans la mesure où ces fonctions n'ont aucune raison de
modifier les valeurs des objets correspondants
 de prévoir une valeur de retour1 à l'opérateur d'affectation, seul moyen de
gérer correctement les affectations multiples (du type a = b = c)
1 void serait possible... mais ne permettrait que les affectations simples (a = b)

 L'argument et la valeur de retour de l'affectation peuvent être transmis


par valeur ou par référence. Cependant, il faut se rappeler qu'une
transmission par valeur entraîne l'appel du constructeur de recopie...
ce qui nuit à l'efficacité du code. D'autre part, dès lors que les objets
sont de taille importante, la transmission par référence s'avère plus
efficace.

POO - R. Rentsch 121

Surcharge de l'opérateur [] (1)


 Reprenons l'exemple de notre classe bidon:
class bidon {
int size;
double * ptr;
...
};

 On aimerait ici surcharger l'opérateur [] de telle sorte que a[i] nous


permette d'accéder au ième élément de l'emplacement pointé par ptr.

 La seule précaution à prendre consiste à faire en sorte que cette


notation puisse être utilisée non seulement dans une expression (cas
trivial), mais aussi à gauche d'une affectation, càd comme lvalue.

 Pour que a[i] puisse être une lvalue, il est nécessaire que la valeur de
retour fournie par l'opérateur [] soit transmise par référence.
POO - R. Rentsch 122

61
Surcharge de l'opérateur [] (2)

 D'autre part (rappel), C++ nous impose de surcharger l'opérateur []


sous la forme d'une fonction membre.

 Le prototype de notre opérateur sera donc:

double & operator [] (int);

 Si nous nous contentons de renvoyer l'élément cherché, sans effectuer


de contrôle sur la validité de la position, le corps de notre opérateur se
réduit simplement à:

return ptr[i];

POO - R. Rentsch 123

Surcharge de l'opérateur [] (3)

 Nous avons vu que seules les fonctions membres munies du qualificatif


const pouvaient être appliquées à un objet constant.

 Tel que nous l'avons conçu, notre opérateur [] ne permet donc pas
d'accéder à un objet constant (même s'il ne s'agit ici que d'un pur
accès en lecture).

 On pourrait penser qu'il suffit dès lors de rajouter const à notre


opérateur []. Le problème c'est que comme la valeur de retour est
transmise par référence, il serait alors possible de modifier l'objet
constant (ce qui n'est pas le but!).

POO - R. Rentsch 124

62
Surcharge de l'opérateur [] (4)

 Moralité: il s'avère préférable de se définir un second opérateur []


destiné uniquement aux objets constants en faisant en sorte qu'il
puisse consulter l'objet mais non le modifier.

double operator [] (int) const;

Dès lors:
const bidon b(2);
...
double d = b[0]; // OK (consultation)
b[0] = 1.0; // Erreur à la compilation (tentative de modif)

POO - R. Rentsch 125

Chapitre 5

Héritage et polymorphisme

63
Introduction (1)
 C++ autorise:
 l'héritage simple (une classe dérivée n'a qu'une classe de base)
 l'héritage multiple (une classe dérivée a plusieurs classes de base)

 Nous n'examinerons dans ce chapitre que le cas de l'héritage simple.

 Pour rappel (voir partie du cours Java) le concept d'héritage constitue


l'un des fondements de la POO. En particulier, il est à la base des
possibilités de réutilisation de composants logiciels (en l'occurrence, de
classes).

 Ce mécanisme nous autorise à définir une nouvelle classe (dite classe


dérivée ou sous-classe) à partir d'une classe existante (dite classe de
base ou super-classe). La classe de base "héritera" des "potentialités"
de la classe de base, tout en lui en ajoutant de nouvelles, et cela sans
qu'il soit nécessaire de remettre en question la classe de base.
POO - R. Rentsch 127

Introduction (2)

 En C++, une classe dérivée (ici B) d'une classe de base (ici A) se


déclare selon le schéma suivant:

class B : public A // ou private A ou encore protected A


{
// définition des membres supplémentaires (données ou fonctions)
// ou redéfinition des membres existants dans A (données ou
// fonctions)
};

 Avec public A, on parle de dérivation publique; avec private, on parle


de dérivation privée; avec protected, on parle de dérivation protégée.

 En Java, on peut interdire à une classe de donner naissance à une


classe dérivée en la qualifiant avec le mot-clé final; une telle possibilité
n'existe pas en C++.
POO - R. Rentsch 128

64
Modalités d'accès à la classe de base (1)
 Les membres privés d'une classe de base ne sont jamais accessibles
aux fonctions membre de sa classe dérivée (tout comme en Java).

 Outre les statuts public ou privé, il existe un statut "protégé" (il n'existe
pas, par contre, de statut "avec droit de paquetage" comme en Java)

 Un membre protégé se comporte comme un membre privé pour un


utilisateur quelconque de la classe dérivée, mais comme un membre
public pour la classe dérivée.

 Comme dit en introduction, il existe trois sorte de dérivations en C++:


 publique: les membres de la classe de base conservent leur statut dans la
classe dérivée (c'est la situation usuelle);
 privée: tous les membres de la classe de base deviennent privées dans la
classe dérivée;
 protégée: les membres publics de la classe de base deviennent membres
POO - R. Rentsch
protégés dans la classe dérivée; les autres membres conservent leur statut129

Modalités d'accès à la classe de base (2)

 Lorsqu'un membre (donnée ou fonction) est redéfini dans une classe


dérivée, il reste toujours possible (soit dans les fonctions membres de
cette classe, soit pour un client de cette classe) d'accéder aux membres
de même nom de la classe de base.

 Il suffit pour cela d'utiliser l'opérateur de résolution de portée ::, sous


réserve, bien sûr, qu'un tel accès soit autorisé.

POO - R. Rentsch 130

65
Compatibilité entre objets d'une classe de base
et une classe dérivée

 Considérons la situation suivante:


class A class B : public A
{ {
... ...
}; };

A a;
B b;
A * ada;
B * adb;

 Il existe deux conversions implicites:


 d'un objet d'un type dérivé dans un objet de type base
a=b
b = a est illégale!
 d'un pointeur sur une classe dérivée en un pointeur sur une classe de base
ada = adb
adb = ada est illégale... mais on peut forcer la conversion: adb = (B*) ada
POO - R. Rentsch 131

Exemple de situation d'héritage simple (1)


Exemple
#include <iostream>
using namespace std;
class point {
protected :
int x, y;
public :
point(int x, int y) : x(x), y(y) {}
void affiche() {
cout << "Je suis un point\n"
<< " Mes coord sont : " << x << " " << y << endl;
}
};
class pointcol : public point {
unsigned short couleur;
public :
pointcol(int x, int y, short c) : point(x, y), couleur(c) {}
void affiche() {
cout << "Je suis un point de couleur\n"
<< " Mes coord sont : " << x << " " << y << endl
<< " Ma couleur est : " << couleur << endl;
}
POO - R. Rentsch }; 132

66
Exemple de situation d'héritage simple (2)
int main() {
point p(1,1);
p.affiche();
point * adp = &p;
adp->affiche();

pointcol pc(2,2,3);
pc.affiche();
pointcol * adpc = &pc;
adpc->affiche(); Je suis un point
return 0; Mes coord sont : 1 1
} Je suis un point
Mes coord sont : 1 1
Je suis un point de couleur
Mes coord sont : 2 2
Ma couleur est : 3
Je suis un point de couleur
Mes coord sont : 2 2
Ma couleur est : 3

POO - R. Rentsch 133

Surcharge, redéfinition et héritage (1)

 Nous avons vu qu'une fonction membre peut être surchargée.

 Attention! Lorsqu'une fonction membre est définie dans une classe


dérivée, elle masque TOUTES les fonctions membres de même nom de
la classe de base (en fait de toutes les classes ascendantes)

 Autrement dit, la recherche d'une fonction surchargée se fait


dans une seule portée, soit celle de la classe concernée, soit celle de
la classe de base, mais jamais dans les deux à la fois.

POO - R. Rentsch 134

67
Surcharge, redéfinition et héritage (2)
#include <iostream>
using namespace std;

class A {
public :
void f (char c) {cout << "Appel de f(char) de A\n";}
void f (int n) {cout << "Appel de f(int) de A\n";}
};

class B : public A {
public :
// void f (int n) {cout << "Appel de f(int) de B\n";}
};

int main() {
char c = 'a';
int n = 1; Si on laisse les commentaires devant f de B
B b; Appel de f(char) de A
b.f(c); Appel de f(int) de A
b.f(n);
} Si on enlève les commentaires devant f de B
Appel de f(int) de B
Appel de f(int) de B
POO - R. Rentsch 135

Typage statique vs dynamique (1)


 Rappel: les règles de compatibilité entre une classe de base et une
classe dérivée permettent d'affecter à un pointeur sur une classe de
base la valeur d'un pointeur sur une classe dérivée.

 Toutefois, par défaut, le type des objets pointés est défini à la


compilation. Par exemple, avec:
class A class B : public A
{ {
... ...
public : public :
void f (...); void f (...);
... ...
}; };

A * pta;
B * ptb;

une affectation telle que pta = ptb est autorisée.


POO - R. Rentsch 136

68
Typage statique vs dynamique (2)
 Néanmoins, quel que soit le contenu de pta (autrement dit quelque
soit l'objet pointé par pta), pta->f(...) appelle toujours la
fonction f de A.

 Ceci vient du fait que C++ réalise ce que l'on nomme une ligature
statique ou encore un typage statique.

 Le type d'un objet (pointé) y est déterminé au moment de la


compilation. Dans ces conditions, le mieux que puisse faire le
compilateur est effectivement de considérer que l'objet pointé a le type
du pointeur.

 Pour pouvoir obtenir l'appel de la méthode correspondant au type de


l'objet pointé, il est évidemment nécessaire que le type de l'objet ne
soit pris en compte qu'au moment de l'exécution. On parle alors de
ligature dynamique ou de typage dynamique ou encore de
polymorphisme.
POO - R. Rentsch 137

Fonctions virtuelles (1)


 L'emploi des fonctions virtuelles permet d'éviter les problèmes
inhérents au typage statique.

 Lorsqu'une fonction est déclarée virtuelle (mot-clé virtual) dans une


classe, les appels à une telle fonction ou à n'importe laquelle de ses
redéfinitions dans des classes dérivées sont "résolus" à l'exécution,
selon le type de l'objet concerné. Par exemple, avec:
class A class B : public A
{ {
... ...
public : public :
virtual void f (...); void f (...);
... ...
}; };

A * pta;

l'instruction pta->f(...) appellera la fonction f de la classe


POO - R. Rentsch
correspondant réellement au type de l'objet pointé par pta. 138

69
Fonctions virtuelles (2)

Quelques règles relatives aux fonctions virtuelles

 Le mot-clé virtual ne s'emploie qu'une fois pour une fonction


membre donnée; plus précisément, il ne doit pas accompagner les
redéfinitions de cette fonction dans les classes dérivées.

 Seule une fonction membre peut être déclarée virtuelle.

 Un constructeur ne peut pas être virtuel; un destructeur peut l'être.

 Une fonction déclarée virtuelle dans une classe de base peut ne pas
être redéfinie dans ses classes dérivées.

 Une fonction virtuelle peut être surchargée (chaque fonction


POO - R. Rentsch surchargée pouvant être ou ne pas être virtuelle). 139

Fonctions virtuelles (3)

Rappels

 En Java, la ligature des fonctions est toujours dynamique.

 La notion de fonction virtuelle n'existe pas; tout se passe en fait


comme si toutes les fonctions (méthodes) étaient virtuelles.

POO - R. Rentsch 140

70
Fonctions virtuelles (4)
Exemple
#include <iostream>
using namespace std;
class point {
protected :
int x, y;
public :
point(int x, int y) : x(x), y(y) {}
virtual void affiche() {
cout << "Je suis un point\n"
<< " Mes coord sont : " << x << " " << y << endl;
}
};
class pointcol : public point {
short couleur;
public :
pointcol(int x, int y, short c) : point(x, y), couleur(c) {}
void affiche() {
cout << "Je suis un point de couleur\n"
<< " Mes coord sont : " << x << " " << y << endl
<< " Ma couleur est : " << couleur << endl;
}
POO - R. Rentsch }; 141

Fonctions virtuelles (5)


int main() {
point p(1,1);
point * adp = &p;
adp->affiche();
pointcol pc(2,2,3);
pointcol * adpc = &pc;
adpc->affiche();
adp = adpc; // adp pointe sur un objet de type pointcol
// mais reste fondamentalement de type point
adp->affiche();
p = pc;
p.affiche(); // pas de polymorphisme!
return 0;
Je suis un point
}
Mes coord sont : 1 1
Je suis un point de couleur
Mes coord sont : 2 2
Conclusion:
Ma couleur est : 3
En C++, 2 ingrédients sont nécessaires au
Je suis un point de couleur
polymorphisme:
Mes coord sont : 2 2
- des méthodes virtuelles
Ma couleur est : 3
- un appel indirect par pointeur ou référence
Je suis un point
POO - R. Rentsch Mes coord sont : 2 2 142

71
Fonctions virtuelles pures (1)

 Une fonction virtuelle pure se déclare avec une initialisation à zéro


comme dans:

virtual void affiche() = 0;

 Lorsqu'une classe comporte au moins une fonction virtuelle pure, elle


est considérée comme "abstraite", càd qu'il n'est plus possible de
créer des objets de son type.
NB Le mot-clé abstract n'existe pas en C++

 Une fonction déclarée virtuelle pure dans une classe de base peut ne
pas être déclarée dans une classe dérivée et, dans ce cas, elle est à
nouveau implicitement fonction virtuelle pure de cette classe dérivée.

POO - R. Rentsch 143

Fonctions virtuelles pures (2)

Rappels

 En Java, on peut définir explicitement une classe abstraite en


utilisant le mot-clé abstract.

 On y précise alors (toujours en utilisant le mot-clé abstract) les


méthodes qui doivent obligatoirement être redéfinies dans les classes
dérivées.

POO - R. Rentsch 144

72
Identification de type à l'exécution (1)

 La norme ANSI a introduit dans C++ un mécanisme permettant de


connaître (identifier et comparer) lors de l'exécution du programme, le
type d'une variable, d'une expression ou d'un objet.

 Cette possibilité a surtout été introduite pour être utilisée dans des
situations de polymorphisme.

 Plus précisément, il est possible, à l'exécution, de connaître le


véritable type d'un objet désigné par un pointeur ou par une
référence.
NB En Java, on utilise, rappel, la méthode getClass()

POO - R. Rentsch 145

Identification de type à l'exécution (2)

 Pour ce faire, il existe un opérateur à un opérande typeid fournissant


en résultat un objet de type prédéfini typeinfo.

 La classe typeinfo contient la fonction membre name(), laquelle


fournit une chaîne de caractères représentant le nom du type.

 Ce nom n'est pas imposé par la norme; il peut dépendre de


l'environnement, mais on est sûr que deux types différents n'auront
jamais le même nom.

 La classe typeinfo dispose aussi de deux opérateurs == et !=


permettant de comparer deux types (cf exemple plus loin).

POO - R. Rentsch 146

73
Identification de type à l'exécution (3)

Exemple

#include <iostream>
#include <typeinfo>
using namespace std;

class point {
public :
virtual void affiche() {} // ici vide
// utile pour le polymorphisme
};

class pointcol : public point {


public :
void affiche() {}
};

POO - R. Rentsch 147

Identification de type à l'exécution (4)


int main() {
point p;
pointcol pc;
point * adp;

adp = &p;
cout << "type de adp : " << typeid(adp).name() << endl;
cout << "type de *adp : " << typeid(*adp).name() << endl;

adp = &pc;
cout << "type de adp : " << typeid(adp).name() << endl;
cout << "type de *adp : " << typeid(*adp).name() << endl;

point p2;
point * adp2 = &p2;

cout << "Les objets pointes par adp et adp2 sont de ";
if (typeid(*adp) == typeid(*adp2))
cout << "meme type\n";
else
cout << "type different\n";

return 0;
POO - R. Rentsch } 148

74
Identification de type à l'exécution (5)

type de adp : P5point


type de *adp : 5point
type de adp : P5point
type de *adp : 8pointcol
Les objets pointes par adp et adp2 sont de meme type

On notera bien, que, pour typeid, le type du pointeur adp reste bien
point *. En revanche, le type de l'objet pointé (*adp) est bien
déterminé par la nature exacte de l'objet pointé.

POO - R. Rentsch 149

75

Vous aimerez peut-être aussi