poo-heritage-handout-cpp

Télécharger au format pdf ou txt
Télécharger au format pdf ou txt
Vous êtes sur la page 1sur 14

Un exemple Exemple : classes pour les personnages

class Guerrier class Voleur

string nom string nom


int energie int energie
Oublions un peu les rectangles ...
int duree_vie int duree_vie
Arme arme
rencontrer(Personnage&) rencontrer(Personnage&)
voler(Personnage&)

class Magicien class Sorcier

string nom string nom


int energie int energie
int duree_vie int duree_vie

Baguette baguette Baguette baguette

rencontrer(Personnage&) Baton baton


rencontrer(Personnage&)

Exemple : héritage Héritage

class Personnage Après les notions d’encapsulation et d’abstraction, le troisième aspect essentiel de
string nom la « Programmation Orientée Objet » est la notion d’héritage.
int energie
int duree_vie
void rencontrer(Personnage&)
L’héritage représente la relation «est-un».
Il permet de créer des classes plus spécialisées,
appelées sous-classes, à partir de classes
class Voleur class Magicien class Guerrier
plus générales déjà existantes, appelées super-
void voler(Personnage&) Baguette baguette Arme arme
classes.

Héritage Classe Enrichissement/


class Sorcier Spécialisation

Baton baton Sous-classe Sous-classe Sous-classe


Héritage (2) Héritage : exemple
Lorsqu’une sous-classe C1 est créée à partir d’une super-classe C, Lorsqu’une sous-classe C1 (ici Guerrier ou Voleur) est créée à partir d’une
super-classe C (ici Personnage),
I le type est hérité : un C1 est (aussi) un C
I C1 va hériter de l’ensemble : I le type est hérité : un Guerrier est (aussi) un Personnage :
I des attributs de C Personnage p;
I des méthodes de C Guerrier g;
(sauf les constructeurs et destructeur) // ...
p = g;
+ Les attributs et méthodes de C vont être // ...
disponibles pour C1 sans que l’on ait besoin void afficher(Personnage const&);
de les redéfinir explicitement dans C1. // ...
afficher(g);
I Par ailleurs :
I des attributs et/ou méthodes supplémentaires
peuvent être définis par la sous-classe C1
+ enrichissement
I des méthodes héritées de C peuvent être redéfinies dans C1
+ spécialisation

Héritage : exemple Héritage : exemple


Lorsqu’une sous-classe C1 (ici Guerrier ou Voleur) est créée à partir d’une Lorsqu’une sous-classe C1 (ici Guerrier ou Voleur) est créée à partir d’une
super-classe C (ici Personnage), super-classe C (ici Personnage),
I Guerrier va hériter de l’ensemble des attributs et des méthodes de Personnage I des attributs et/ou méthodes supplémentaires
(sauf les constructeurs et destructeur) peuvent être définis par la sous-classe Guerrier : arme
class Personnage
I des méthodes héritées de Personnage peuvent être redéfinies dans Voleur :
Guerrier g;
string nom rencontrer(Personnage&)
Voleur v;
int energie
int duree_vie
g.rencontrer(v);
void rencontrer(Personnage&)
//...
// dans Guerrier::methode():
energie = //...
class Guerrier

Arme arme
Héritage (3) Transitivité de l’héritage
Par transitivité, les instances d’une sous-classe possèdent :
L’héritage permet donc : I les attributs et méthodes (hors constructeurs/destructeur) de l’ensemble des
I d’expliciter des relations structurelles classes parentes (super-classe, super-super-classe, etc.)
et sémantiques entre classes
Enrichissement par héritage :
I de réduire les redondances de class Personnage

description et de stockage des I crée un réseau de dépendances entre string nom


int energie

propriétés classes, int duree_vie


void rencontrer(Personnage&)

I ce réseau est organisé en une structure


class Voleur class Magicien class Guerrier
Attention ! arborescente où chacun des nœuds hérite void voler(Personnage&) Baguette baguette Arme arme
I l’héritage doit être utilisé pour décrire une relation « est-un » ("is-a") des propriétés de l’ensemble des nœuds du
I il ne doit jamais décrire une relation « a-un »/« possède-un » ("has-a") chemin remontant jusqu’à la racine.
class Sorcier

+ ce réseau de dépendances définit une


Baton baton

hiérarchie de classes

Sous-classe, Super-classes Passons à la pratique...


Définition d’une sous-classe en C++ :
Une super-classe :
I est une classe « parente » Syntaxe :
I déclare les attributs/méthodes communs class NomSousClasse : public NomSuperClasse
{
I peut avoir plusieurs sous-classes /* Déclaration des attributs et méthodes
spécifiques à la sous-classe */
Une sous-classe est :
};
I une classe « enfant »
Class FigureGeometrique
I étend une (ou plusieurs) super-classe(s) Exemple :
position
I hérite des attributs, des méthodes et du type de la super-classe class Rectangle : public FigureGeometrique
{
Un attribut/une méthode hérité(e) peut s’utiliser comme si il/elle était déclaré(e) hérite de
//...
dans la sous-classe au lieu de la super-classe (en fonction des droits d’accès, private:
voir plus loin) double largeur; double hauteur; Class Rectangle
}; largeur
+ On évite ainsi la duplication de code hauteur
Pratique : exemple 2

class Personnage {
// ...
};
// ...
class Guerrier : public Personnage {
public:
// constructeurs, etc.
private:
Arme arme;
};
Droit d’accès protected Accès protégé
Le niveau d’accès protégé correspond à une extension du niveau privé
Jusqu’à maintenant, l’accès aux membres (attributs et méthodes) d’une classe
permettant l’accès aux sous-classes.
pouvait être :
Exemple : class Personnage {
I soit public : visibilité totale à l’intérieur et à l’extérieur de la classe
// ...
(mot-clé public) protected:
I soit privé : visibilité uniquement à l’intérieur de la classe int energie;
};
(mot-clé private)
class Guerrier : public Personnage {
Un troisième type d’accès régit l’accès aux attributs/méthodes au sein d’une public:
hiérarchie de classes : // ...
void frapper(Personnage& le_pauvre) {
I l’accès protégé : assure la visibilité des membres d’une classe dans les if (energie > 0) {
classes de sa descendance // frapper le perso
}
Le mot clé est «protected». }
};

Accès protégé : portée (1) Accès protégé : portée (2)


class A {
// ...
protected: int a;
Le niveau d’accès protégé correspond à une extension
private: int prive;
du niveau privé permettant l’accès aux sous-classes... };
mais uniquement dans leur portée (de sous-classe),
et non pas dans la portée de la super-classe class B: public A {
public:
// ...
void f(B autreB, A autreA, int x) {
a = x; // OK A::a est protected => accès possible
prive = x; // Erreur : A::prive est private

a += autreB.prive; // Erreur (même raison)


a += autreB.a ; // OK : dans la même portée (B::)

a += autreA.a ; // INTERDIT ! : this n'est pas de la même


// portée que autreA
}
};
Utilisation des droits d’accès

I Membres publics : accessibles pour les


programmeurs utilisateurs de la classe
I Membres protégés : accessibles aux
programmeurs d’extensions par héritage
de la classe
I Membres privés : pour le programmeur de
la classe : structure interne,
(modifiable si nécessaire sans répercussions
ni sur les utilisateurs ni sur les autres
programmeurs)
Les Guerrier font bande à part Les Guerrier font bande à part : masquage

class Personnage
string nom
I Pour un personnage non-Guerrier : int energie
int duree_vie
void rencontrer(Personnage& le_perso) const { saluer(le_perso); }
void rencontrer(Personnage&)

I Pour un Guerrier
void rencontrer(Personnage& le_pauvre) const { frapper(le_pauvre); } class Voleur class Magicien class Guerrier
void voler(Personnage&) Baguette baguette Arme arme
void rencontrer(Personnage&)

Faut-il re-concevoir toute la hiérarchie ?


+ Non, on ajoute simplement une méthode rencontrer(Personnage&) spéciale class Sorcier
dans la sous-classe Guerrier
Baton baton

Masquage dans une hiérarchie Masquage dans une hiérarchie (2)


Personnage
....
void rencontrer(Personnage& autre) {
saluer(autre); }

I Masquage : un identificateur qui en cache un autre


I Situations possibles dans une hiérarchie : Guerrier : public Personnage
...
void rencontrer(Personnage& autre) {
I Même nom d’attribut ou de méthode utilisé sur plusieurs niveaux frapper(autre); }

I Peu courant pour les attributs


I Très courant et pratique pour les méthodes
La méthode rencontrer de Guerrier masque celle de Personnage
I Un objet de type Guerrier n’utilisera donc jamais la méthode rencontrer de
la classe Personnage
I Vocabulaire OO :
I Méthode héritée = méthode générale, méthode par défaut
I Méthode qui masque la méthode héritée = méthode spécialisée
Accès à une méthode masquée Accès à une méthode masquée (2)

Pour accéder aux attributs/méthodes masqué(e)s de la


I Il est parfois souhaitable d’accéder à une méthode/un attribut masqué(e)
I on utilise l’opérateur de résolution de portée
I Exemple :
I Le Guerrier commence par rencontrer le personnage comme le fait n’importe
I Syntaxe : NomClasse::méthode ou attribut
quel personnage (il le salue) avant de le frapper ! I Exemple :
I Code désiré : class Guerrier : public Personnage {
1. Personnage non-Guerrier : //...
I Méthode générale (rencontrer de Personnage) void rencontrer (Personnage& perso) {
Personnage::rencontrer(perso); // salutation d'usage !!
2. Personnage Guerrier :
frapper(perso);
I Méthode spécialisée (rencontrer de Guerrier) }
I Appel à la méthode générale depuis la méthode spécialisée };
Constructeurs et héritage Constructeurs et héritage : appel explicite
Lors de l’instanciation d’une sous-classe, il faut initialiser : L’invocation du constructeur de la super-classe se fait au début de la section
d’appel aux constructeurs des attributs.
I les attributs propres à la sous-classe
I les attributs hérités des super-classes Syntaxe :

MAIS... SousClasse(liste de paramètres)


: SuperClasse(Arguments),
...il ne doit pas être à la charge du concepteur des sous-classes de réaliser attribut1(valeur1),
lui-même l’initialisation des attributs hérités ...
attributN(valeurN)
L’accès à ces attributs pourrait notamment être interdit ! (private) {
// corps du constructeur
L’initialisation des attributs hérités doit donc se faire au niveau des classes où ils }
sont explicitement définis.
Lorsque la super-classe admet un constructeur par défaut, l’invocation explicite de
Solution : l’initialisation des attributs hérités doit se faire en invoquant les ce constructeur dans la sous-classe n’est pas obligatoire
constructeurs des super-classes. + le compilateur se charge de réaliser l’invocation du constructeur par défaut

Constructeurs et héritage : exemple 1 Constructeurs et héritage : exemple 2


Si la classe parente n’admet pas de constructeur par défaut, l’invocation explicite Autre exemple (qui ne fait pas la même chose) :
d’un de ses constructeurs est obligatoire dans les constructeurs de la sous-classe class FigureGeometrique {
protected: Position position;
+ La sous-classe doit admettre au moins un constructeur explicite. public:
Exemple : class FigureGeometrique { /* Note : le constructeur par défaut par défaut de FigureGeometrique
protected: Position position; * appelle le constructeur par défaut de Position.
public: */
FigureGeometrique(double x, double y) : position(x, y) {} // ...
// ... };
};
class Rectangle : public FigureGeometrique {
class Rectangle : public FigureGeometrique { protected: double largeur; double hauteur;
protected: double largeur; double hauteur; public:
public: Rectangle(double l, double h)
Rectangle(double x, double y, double l, double h) : largeur(l), hauteur(h)
: FigureGeometrique(x,y), largeur(l), hauteur(h) {} {}
// ... // ...
}; };
Encore un exemple Constructeurs et héritage : résumé (1)

Il n’est pas nécessaire d’avoir


des attributs supplémentaires...

class Carre : public Rectangle {


public: 1. Chaque constructeur d’une sous-classe doit appeler un des constructeurs de la
Carre(double taille) super-classe
: Rectangle(taille, taille)
{} 2. L’appel est la 1re instruction
/* Et c'est tout !
(sauf s'il y avait des manipulateurs,
il faudrait alors sûrement aussi les
redéfinir)
*/
};

Constructeurs et héritage : résumé (2)

Et si l’on oublie l’appel à un constructeur de la super-classe ?


I Appel automatique au constructeur par défaut de la super-classe
I Pratique parfois, mais erreur si le constructeur par défaut n’existe pas

Rappel : le constructeur par défaut est particulier


I Il existe par défaut pour chaque classe qui n’a aucun autre constructeur
I Il disparaît dès qu’il y a un autre constructeur

Pour éviter des problèmes avec les hiérarchies de classes, dans un premier temps :
I Toujours déclarer au moins un constructeur
I Toujours faire l’appel à un constructeur de la super-classe
Ordre d’appel des constructeurs Ordre d’appel des destructeurs
Hiérarchie de classes Constructeurs Instance Les destructeurs sont toujours appelés dans l’ordre inverse (/symétrique) des
constructeurs.
Classe A A(...) 1
a1 m1(...) : a1(...),
a2 a2(...)
a1
Par exemple dans l’exemple précédent, lors de la destruction d’un C, on aura appel
m2(...) {} et exécution de :
a2
I C::˜C()
Classe B B(...) 2 I B::˜B()
b1 m3(...) : A(...),
m4(...) b1(...) b1 I A::˜A()
a1
{}
a2 (et dans cet ordre)
(puisque les constructeurs avaient été appelés dans l’ordre
Classe C C(...)
: B(...),
3 I A::A()
c1 m5(...)
c2 c1(...), c2(...) b1 c1 I B::B()
{} a1
instanciation : a2 c2 I C::C()
C mon_c(...); mon_c )

Héritage et constructeur de copie Héritage des constructeurs


Les constructeurs ne sont, en général, pas hérités
mais en on peut demander leur héritage en utilisant le mot clé « using ».
Le constructeur de copie d’une sous-classe doit invoquer explicitement le
contructeur de copie de la super-classe On récupère alors tous les constructeurs de la super-classe,
i.e. on peut construire la sous-classe avec les mêmes arguments, mais...
+ Sinon c’est le constructeur par défaut de la super-classe qui est appelé ! Attention ! ces constructeurs n’initalisent donc pas les attributs spécifiques de la
sous-classe.
Exemple : C’est donc très risqué, et je vous conseille de ne l’utiliser que pour des
Rectangle(Rectangle const& autre) sous-classes n’ayant pas de nouvel attribut (et si c’est approprié) !
: FigureGeometrique(autre),
Exemple :
largeur(autre.largeur),
hauteur(autre.hauteur) class A { class B : public A {
{} public: using A::A;
A(int); /* existent alors maintenant
A(double, double); B::B(int)
// ... et B::B(double, double) */
}; };
Petit rappel Petit rappel (2)
Nous avons vu qu’il existe en C++, des Dans certains cas, les versions minimales par défaut des méthodes
méthodes particulières permettant : constructeurs/destructeurs ne sont pas adaptées : exemple du comptage des
instances (cf. semaine passée).
I d’initialiser les attributs d’un objet en
début de vie : Autre exemple :
constructeurs
I de copier un objet dans un autre Le constructeur de copie par défaut
objet : réalise une copie membre à membre
constructeurs de copie des attributs
I de libérer les ressources utilisées par → copie de surface
un objet en fin de vie :
destructeurs Ceci pose typiquement problème
lorsque certains attributs de la
Une version par défaut, minimale, de ces méthodes est automatiquement classe sont des pointeurs.
générée si on ne les définit pas explicitement.
Examinons pourquoi sur un exemple concret...

Exemple Exemple
Que se passe-t-il lorsqu’on invoque la fonction suivante ?
void afficher_largeur(Rectangle tmp)
Soit une autre définition possible (farfelue, mais possible !) de classe Rectangle : {
cout << "Largeur: " << tmp.getLargeur() << endl;
class Rectangle { }
private:
double* largeur; // aïe, un pointeur !
double* hauteur;
public:
Rectangle(double l, double h)
: largeur(new double(l)), hauteur(new double(h)) {}
~Rectangle() { delete largeur; delete hauteur; }
double getLargeur() const;
double getHauteur() const;
// ...
};
Exemple (2) Exemple (3)
Voilà ce qui se produit concrètement :
Avant la copie

void afficher_largeur(Rectangle tmp) { // une copie !..


r largeur tmp ?
cout << "Largeur: " << tmp.getLargeur() << endl; ?
hauteur
} // destruction de tmp...
Après la copie
I Lorsque afficher_largeur a fini de s’exécuter, l’objet tmp est
automatiquement détruit par le destructeur de la classe Rectangle r
largeur
tmp

I le destructeur va libérer la mémoire pointée par les champs largeur et hauteur

hauteur de tmp
Après la destruction de tmp
Attention ! cette portion de mémoire est aussi utilisée par r dans un appel comme
largeur
afficher_largeur(r) ! r tmp
hauteur

+ (gros risque de) Segmentation Fault lors de la prochaine utilisation de r ! !

+ il faut redéfinir le constructeur de copie de sorte à ce qu’il duplique


véritablement les champs concernés → copie profonde

Exemple (4) Exemple : Définition complète de la classe


Une bonne solution consiste alors à redéfinir le constructeur de copie : class Rectangle {
public:
Rectangle(const Rectangle& obj)
Rectangle(double l, double h)
: largeur(new double(*(obj.largeur))) ,
: largeur(new double(l)), hauteur(new double(h)) {}
hauteur(new double(*(obj.hauteur)))
Rectangle(const Rectangle& obj);
{}
Avant la copie
~Rectangle();
// Note: il faudrait aussi redefinir operator= !
r
largeur
tmp ?
private:
?
double* largeur; double* hauteur;
hauteur
};
// constructeur de copie
Après la copie
Rectangle::Rectangle(const Rectangle& obj)
: largeur(new double(*(obj.largeur))),
tmp
r
hauteur(new double(*(obj.hauteur)))
{}
// destructeur
Après la desctruction de tmp void Rectangle::~Rectangle() {
tmp
delete largeur;
r delete hauteur;
}

Il faudra aussi penser à redéfinir l’opérateur =


Pour conclure
I Si une classe contient des pointeurs,
penser à la copie profonde (au moins se
poser la question) :
I constructeur de copie ;
I surcharge de l’opérateur = ;
I destructeur.

I Remarque : si l’on redéfinit le constructeur de copie d’une sous-classe, penser


à explicitement mettre l’appel au constructeur de copie de la super-classe
(sinon c’est le constructeur par défaut de la super-classe qui est appelé ! !)
Exemple :
Rectangle(const Rectangle& obj)
: FigureGeometrique(obj), // ...etc.
{ // ...etc.
}

Vous aimerez peut-être aussi