Cours C++ by abdelhedi meftah heig-vd suisse

75
1 POO - R. Rentsch 1 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 2 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

description

cours complet de c++

Transcript of Cours C++ by abdelhedi meftah heig-vd suisse

Page 1: Cours C++ by abdelhedi meftah heig-vd suisse

1

POO - R. Rentsch 1

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 2

Références bibliographiques

Apprendre le C++Claude DelannoyEyrolles, 2007

C++ par la pratiqueJean-Cédric Chappelier, Florian SeydouxPPUR, 2004

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

Pierre Donini

Jean-Pierre Molliet

Page 2: Cours C++ by abdelhedi meftah heig-vd suisse

2

Chapitre 1

Introduction

POO - R. Rentsch 4

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).

Page 3: Cours C++ by abdelhedi meftah heig-vd suisse

3

POO - R. Rentsch 5

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 6

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.

Page 4: Cours C++ by abdelhedi meftah heig-vd suisse

4

POO - R. Rentsch 7

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 8

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.

Page 5: Cours C++ by abdelhedi meftah heig-vd suisse

5

POO - R. Rentsch 9

Exemple d'écriture sur cout

#include <iostream>

using namespace std;

int main() {

int n = 1;

float f = 2.0;

double d1 = 3.0;

double d2 = 3.1;

char c = 'a';

char * ch = "Hello";

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;

}

Valeur de n = 1

Valeur de f = 2

Valeur de d1 = 3

Valeur de d2 = 3.1

Valeur de c = a

Valeur de ch = Hello

Valeur de ptr = 0x22ff74

Adresse de ch = 0x443000

Pressez <ENTER> pour terminer...

POO - R. Rentsch 10

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

Page 6: Cours C++ by abdelhedi meftah heig-vd suisse

6

POO - R. Rentsch 11

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 12

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

...

Page 7: Cours C++ by abdelhedi meftah heig-vd suisse

7

POO - R. Rentsch 13

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;

...

return EXIT_SUCCESS;

}

10

10

1.5

1.500000

1.500000e+000

1.500000E+000

1.23

Pressez <ENTER> pour terminer...

POO - R. Rentsch 14

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;

}

Page 8: Cours C++ by abdelhedi meftah heig-vd suisse

8

POO - R. Rentsch 15

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.

#include <iostream>using namespace std;

int main() {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 buffercout << "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;

}

Donnez une chaine se terminant par un point:

Salut les copains.

Resultat de la lecture:

Salutlescopains.

Pressez <ENTER> pour terminer...

POO - R. Rentsch 16

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.

Page 9: Cours C++ by abdelhedi meftah heig-vd suisse

9

POO - R. Rentsch 17

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 18

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'erreurcin.sync(); // vider le buffer

} cout << "Resultat de la lecture:\n";cout << t << endl;cout << "\nPressez <ENTER> pour terminer...";cin.get();return EXIT_SUCCESS;

}

Page 10: Cours C++ by abdelhedi meftah heig-vd suisse

10

POO - R. Rentsch 19

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 20

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();

Page 11: Cours C++ by abdelhedi meftah heig-vd suisse

11

POO - R. Rentsch 21

Vider le buffer d'entrée (2)

Exemple:

int main() {

int n;

char t[80];

cout << "Donnez un entier:\n";

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();

return EXIT_SUCCESS;

}

Sans vider le buffer

Donnez un entier:

1

Donnez une chaine:

Resultat de la lecture:

1

Pressez <ENTER> pour terminer...

En vidant le buffer

Donnez un entier:

1

Donnez une chaine:

abc

Resultat de la lecture:

1 abc

Pressez <ENTER> pour terminer...

Chapitre 2

Fonctions, types énumérés, types structures et gestion dynamique de la mémoire

Page 12: Cours C++ by abdelhedi meftah heig-vd suisse

12

POO - R. Rentsch 23

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 24

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;

}

Page 13: Cours C++ by abdelhedi meftah heig-vd suisse

13

POO - R. Rentsch 25

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 26

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;

}

Page 14: Cours C++ by abdelhedi meftah heig-vd suisse

14

POO - R. Rentsch 27

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 28

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.

Page 15: Cours C++ by abdelhedi meftah heig-vd suisse

15

POO - R. Rentsch 29

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 30

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

Page 16: Cours C++ by abdelhedi meftah heig-vd suisse

16

POO - R. Rentsch 31

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 32

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;

}

Page 17: Cours C++ by abdelhedi meftah heig-vd suisse

17

POO - R. Rentsch 33

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 34

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

Page 18: Cours C++ by abdelhedi meftah heig-vd suisse

18

POO - R. Rentsch 35

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 36

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

...

}

Page 19: Cours C++ by abdelhedi meftah heig-vd suisse

19

POO - R. Rentsch 37

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 38

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

// référence dans p

Page 20: Cours C++ by abdelhedi meftah heig-vd suisse

20

POO - R. Rentsch 39

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 40

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 normalf(n); // appel avec un seul argumentf(); // 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

Page 21: Cours C++ by abdelhedi meftah heig-vd suisse

21

POO - R. Rentsch 41

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 42

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";

}

Page 22: Cours C++ by abdelhedi meftah heig-vd suisse

22

POO - R. Rentsch 43

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 44

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 !).

Page 23: Cours C++ by abdelhedi meftah heig-vd suisse

23

POO - R. Rentsch 45

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 46

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;}

Page 24: Cours C++ by abdelhedi meftah heig-vd suisse

24

POO - R. Rentsch 47

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 48

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.

Page 25: Cours C++ by abdelhedi meftah heig-vd suisse

25

POO - R. Rentsch 49

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 icidouble 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 inlineinline 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 50

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;

}

Page 26: Cours C++ by abdelhedi meftah heig-vd suisse

26

POO - R. Rentsch 51

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 52

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

Page 27: Cours C++ by abdelhedi meftah heig-vd suisse

27

POO - R. Rentsch 53

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 54

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;

Page 28: Cours C++ by abdelhedi meftah heig-vd suisse

28

POO - R. Rentsch 55

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éfautp2.x = 2; p2.y = 2;point * ptr;ptr = &p2;point & ref = p2; // rappel: initialisation obligatoirecout << "p0.x = " << p0.x << endl;cout << "p1.x = " << p1.x << endl;cout << "p2.x = " << p2.x << endl;cout << "ptr->x = " << ptr->x << endl;cout << "(*ptr).x = " << (*ptr).x << endl;cout << "ref.x = " << ref.x << endl;return 0;

}

p0.x = 0

p1.x = 1

p2.x = 2

ptr->x = 2

(*ptr).x = 2

ref.x = 2

POO - R. Rentsch 56

Les types structure (3)

... mais C++ permet également d'associer des méthodes (ou fonctions membres) à un type structure. Exemple:

#include <iostream>using namespace std;

// déclaration du type pointstruct 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éevoid 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;

}

A noter la séparation entredéclaration et définition !!!

Page 29: Cours C++ by abdelhedi meftah heig-vd suisse

29

POO - R. Rentsch 57

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 58

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)

Page 30: Cours C++ by abdelhedi meftah heig-vd suisse

30

POO - R. Rentsch 59

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 60

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));

Page 31: Cours C++ by abdelhedi meftah heig-vd suisse

31

POO - R. Rentsch 61

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]).En cas d'échec, c'est alors un pointeur NULL qui nous est renvoyé.

POO - R. Rentsch 62

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.

Page 32: Cours C++ by abdelhedi meftah heig-vd suisse

32

Chapitre 3

Classes et objets

POO - R. Rentsch 64

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!!!

Page 33: Cours C++ by abdelhedi meftah heig-vd suisse

33

POO - R. Rentsch 65

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 66

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).

Page 34: Cours C++ by abdelhedi meftah heig-vd suisse

34

POO - R. Rentsch 67

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 68

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++)

Page 35: Cours C++ by abdelhedi meftah heig-vd suisse

35

POO - R. Rentsch 69

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 70

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();

}

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

Appel du constructeur

Point de coord (1, 2)

Point de coord (2, 3)

Appel du destructeur

Page 36: Cours C++ by abdelhedi meftah heig-vd suisse

36

POO - R. Rentsch 71

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 72

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.

Page 37: Cours C++ by abdelhedi meftah heig-vd suisse

37

POO - R. Rentsch 73

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.En général, son initialisation se fait dans la définition de la classe.

POO - R. Rentsch 74

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);

Page 38: Cours C++ by abdelhedi meftah heig-vd suisse

38

POO - R. Rentsch 75

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 76

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

Page 39: Cours C++ by abdelhedi meftah heig-vd suisse

39

POO - R. Rentsch 77

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 qu'aux membres publics de l'argument.

POO - R. Rentsch 78

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)

Page 40: Cours C++ by abdelhedi meftah heig-vd suisse

40

POO - R. Rentsch 79

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); // constructeurbool 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 80

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

Page 41: Cours C++ by abdelhedi meftah heig-vd suisse

41

POO - R. Rentsch 81

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 82

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);

Page 42: Cours C++ by abdelhedi meftah heig-vd suisse

42

POO - R. Rentsch 83

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 84

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

};

Page 43: Cours C++ by abdelhedi meftah heig-vd suisse

43

POO - R. Rentsch 85

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'objet1 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

void affiche() const {x++;} // interdit car méthode constante

POO - R. Rentsch 86

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

Page 44: Cours C++ by abdelhedi meftah heig-vd suisse

44

POO - R. Rentsch 87

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 88

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

Page 45: Cours C++ by abdelhedi meftah heig-vd suisse

45

POO - R. Rentsch 89

Classe d'allocation desobjets en C++ (3)

#include <iostream>using namespace std;class point {

int x, y;public :

point(int x = 0, int y = 0) : x(x), y(y) { // constructeurcout << "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;

}

Constr. du point (1,1)

--- debut de main ---

Constr. du point (0,0)

Boucle: iteration no 1

Constr. du point (2,2)

Destr. du point (2,2)

Boucle: iteration no 2

Constr. du point (3,3)

Destr. du point (3,3)

--- fin de main ---

Destr. du point (0,0)

Destr. du point (1,1)

POO - R. Rentsch 90

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;

Page 46: Cours C++ by abdelhedi meftah heig-vd suisse

46

POO - R. Rentsch 91

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 fonctionil 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 typeExemple: point p1 = p2;

POO - R. Rentsch 92

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").

Page 47: Cours C++ by abdelhedi meftah heig-vd suisse

47

POO - R. Rentsch 93

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éesDans 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 94

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)

Page 48: Cours C++ by abdelhedi meftah heig-vd suisse

48

POO - R. Rentsch 95

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 96

Le constructeur de recopie (6)

void f (bidon b) {

cout << "Appel de f\n";

}

int main() {

bidon b(3);

f(b);

return 0;

}

Constructeur: ptr = 0x3d4a08

Appel de f

Destructeur: ptr = 0x3d4a08

Destructeur: ptr = 0x3d4a08

Constructeur: ptr = 0x3d4a08

Constr de recopie: ptr = 0x3d4a28

Appel de f

Destructeur: ptr = 0x3d4a28

Destructeur: ptr = 0x3d4a08

sans constructeur de recopie

avec constructeur de recopie

Page 49: Cours C++ by abdelhedi meftah heig-vd suisse

49

POO - R. Rentsch 97

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 98

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 ::

Page 50: Cours C++ by abdelhedi meftah heig-vd suisse

50

POO - R. Rentsch 99

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 Aclass C {

...};// Membres publics de la classe A...

};

int main() {A a;A::B b; // Interdit la classe interne B est privéeA::C c; // OK...

}

POO - R. Rentsch 100

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++:

Page 51: Cours C++ by abdelhedi meftah heig-vd suisse

51

POO - R. Rentsch 101

Classes imbriquées (internes) (4)

#include <iostream>using namespace std;

class Cercle {private :

class Centre { // classe interne privéeint 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 += dxc->setX(c->getX() + dx); c->setY(c->getY() + dy);

}private :

Centre * c;float r;

};

POO - R. Rentsch 102

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)

Page 52: Cours C++ by abdelhedi meftah heig-vd suisse

52

Chapitre 4

Surcharge des opérateurs

POO - R. Rentsch 104

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.

Page 53: Cours C++ by abdelhedi meftah heig-vd suisse

53

POO - R. Rentsch 105

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

pas une obligation stricte

POO - R. Rentsch 106

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 amievoid 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.affiche("a"); b.affiche("b"); c.affiche("c");point d = a + b + c;d.affiche("d");return 0;

}

a(1, 1)

b(2, 2)

c(3, 3)

d(6, 6)

Page 54: Cours C++ by abdelhedi meftah heig-vd suisse

54

POO - R. Rentsch 107

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 108

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");c.affiche("c");point d = a + b + c;d.affiche("d");return 0;

}

a(1, 1)

b(2, 2)

c(3, 3)

d(6, 6)

Page 55: Cours C++ by abdelhedi meftah heig-vd suisse

55

POO - R. Rentsch 109

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 110

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.

Page 56: Cours C++ by abdelhedi meftah heig-vd suisse

56

POO - R. Rentsch 111

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 112

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!).

Page 57: Cours C++ by abdelhedi meftah heig-vd suisse

57

POO - R. Rentsch 113

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 114

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.

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.

3

3

b

a

Page 58: Cours C++ by abdelhedi meftah heig-vd suisse

58

POO - R. Rentsch 115

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:

l'affectation:

b = a;

conduit à:

3

a

2

b

3

a

3

b

POO - R. Rentsch 116

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

Page 59: Cours C++ by abdelhedi meftah heig-vd suisse

59

POO - R. Rentsch 117

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 118

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;

}

Page 60: Cours C++ by abdelhedi meftah heig-vd suisse

60

POO - R. Rentsch 119

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 120

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)

...

};

Page 61: Cours C++ by abdelhedi meftah heig-vd suisse

61

POO - R. Rentsch 121

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 122

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.

Page 62: Cours C++ by abdelhedi meftah heig-vd suisse

62

POO - R. Rentsch 123

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 124

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!).

Page 63: Cours C++ by abdelhedi meftah heig-vd suisse

63

POO - R. Rentsch 125

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)

Chapitre 5

Héritage et polymorphisme

Page 64: Cours C++ by abdelhedi meftah heig-vd suisse

64

POO - R. Rentsch 127

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 128

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++.

Page 65: Cours C++ by abdelhedi meftah heig-vd suisse

65

POO - R. Rentsch 129

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 protégés dans la classe dérivée; les autres membres conservent leur statut

POO - R. Rentsch 130

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é.

Page 66: Cours C++ by abdelhedi meftah heig-vd suisse

66

POO - R. Rentsch 131

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

Considérons la situation suivante:

Il existe deux conversions implicites:

d'un objet d'un type dérivé dans un objet de type basea = bb = a est illégale!

d'un pointeur sur une classe dérivée en un pointeur sur une classe de baseada = adbadb = ada est illégale... mais on peut forcer la conversion: adb = (B*) ada

class A

{

...

};

class B : public A

{

...

};

A a;

B b;

A * ada;

B * adb;

POO - R. Rentsch 132

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;

}};

Page 67: Cours C++ by abdelhedi meftah heig-vd suisse

67

POO - R. Rentsch 133

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();

return 0;

}

Je suis un point

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 134

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.

Page 68: Cours C++ by abdelhedi meftah heig-vd suisse

68

POO - R. Rentsch 135

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;

B b;

b.f(c);

b.f(n);

}

Si on laisse les commentaires devant f de BAppel de f(char) de A

Appel de f(int) de A

Si on enlève les commentaires devant f de BAppel de f(int) de B

Appel de f(int) de B

POO - R. Rentsch 136

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:

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

class A

{

...

public :

void f (...);

...

};

A * pta;

B * ptb;

class B : public A

{

...

public :

void f (...);

...

};

Page 69: Cours C++ by abdelhedi meftah heig-vd suisse

69

POO - R. Rentsch 137

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 138

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:

l'instruction pta->f(...) appellera la fonction f de la classe correspondant réellement au type de l'objet pointé par pta.

class A

{

...

public :

virtual void f (...);

...

};

A * pta;

class B : public A

{

...

public :

void f (...);

...

};

Page 70: Cours C++ by abdelhedi meftah heig-vd suisse

70

POO - R. Rentsch 139

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 surchargée pouvant être ou ne pas être virtuelle).

POO - R. Rentsch 140

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.

Page 71: Cours C++ by abdelhedi meftah heig-vd suisse

71

POO - R. Rentsch 141

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 142

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

Ma couleur est : 3

Je suis un point de couleur

Mes coord sont : 2 2

Ma couleur est : 3

Je suis un point

Mes coord sont : 2 2

Conclusion:

En C++, 2 ingrédients sont nécessaires au

polymorphisme:

- des méthodes virtuelles

- un appel indirect par pointeur ou référence

Page 72: Cours C++ by abdelhedi meftah heig-vd suisse

72

POO - R. Rentsch 143

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 144

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.

Page 73: Cours C++ by abdelhedi meftah heig-vd suisse

73

POO - R. Rentsch 145

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 146

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).

Page 74: Cours C++ by abdelhedi meftah heig-vd suisse

74

POO - R. Rentsch 147

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 148

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;

}

Page 75: Cours C++ by abdelhedi meftah heig-vd suisse

75

POO - R. Rentsch 149

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

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é.

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