Blog ENI : Toute la veille numérique !
🐠 -25€ dès 75€ 
+ 7 jours d'accès à la Bibliothèque Numérique ENI. Cliquez ici
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici
  1. Livres et vidéos
  2. Langage C++
  3. Héritage
Extrait - Langage C++ De l'héritage C au C++ moderne (avec programmes d'illustration) (2e édition)
Extraits du livre
Langage C++ De l'héritage C au C++ moderne (avec programmes d'illustration) (2e édition) Revenir à la page d'achat du livre

Héritage

Principe

L’héritage consiste à ajouter des champs ou des méthodes à une classe existante en définissant une nouvelle classe qui prolonge la première. La classe initiale est appelée classe de base, classe mère ou superclasse, et l’autre classe dérivée, classe fille ou sous-classe. En résumé, un objet d’une classe dérivée contient :

  • ses propres champs et ceux de la classe mère, hors les champs private,

  • ses propres méthodes et celles de la classe mère, hors les méthodes private.

Seul l’opérateur private permet de réduire l’accès aux données et méthodes de la classe de base depuis une classe héritée.

Du point de vue de la modélisation, nous pouvons avoir un système d’ensembles et de sous-ensembles avec cette particularité informatique que le sous-ensemble est plus large que l’ensemble de base parce qu’il lui ajoute des données et des fonctions. Le sous-ensemble précise l’ensemble de base en l’orientant dans un certain sens.

Par exemple, à partir d’une classe vehicule, nous pouvons dériver une classe voiture, une classe camion, une classe deuxroues. Les voitures sont des vehicules avec des spécificités propres, différentes de celles des camions et des deux-roues. La classe voiture elle-même peut être décomposée ensuite en plusieurs types de voitures avec une classe pour les voitures familiales, une classe pour les voitures de course, une autre classe pour les utilitaires. De même, à partir de la classe camion, il peut y avoir plusieurs ensembles dérivés, chacun précisant une catégorie de camion. Idem pour les deux-roues. Chaque classe dérivée implémente un nouveau...

Définir une classe dérivée

Soit la classe de base suivante :

class nomBase 
{ 
    // données membres 
    // fonctions membres 
}; 

La syntaxe pour écrire une classe dérivée est :

class herit : [accès public/protected/private] nomBase 
{ 
    // données membres 
    // fonctions membres 
}; 

Pour un objet de la classe herit, les données et fonctions membres de la classe herit s’ajoutent aux données et fonctions membres de la classe base.

Exemple

#include <iostream> 
 
// CLASSE DE BASE 
class base 
{ 
public: 
    int v1; 
    base() : v1(0) {} 
    base(int v) : v1(v) {} 
    void affiche() { std::cout << "base v1 : " << v1 << '\n'; } 
}; 
 
// CLASSE DÉRIVÉE 
class herit : public base 
{ 
public: 
    int v2; 
    herit(int v2) :v2(v2) {} 
    void affiche2(); 
}; 
// 
void herit::affiche2() 
{ 
    std::cout << "base v1 : " << v1 << '\n'; 
    std::cout << "herit v2 : " << v2 << '\n'; 
} 
// 
int main() 
{ 
    base b(5); 
    b.affiche();    // imprime 
                    // base v1 : 5 
 
    herit h(100); ...

Appeler explicitement un constructeur de la classe de base

Il y a possibilité d’appeler le constructeur de la classe de base. Il suffit de faire suivre le constructeur de la classe dérivée par un deux-points (:) et le constructeur souhaité de la classe de base. Voici trois constructeurs ajoutés à notre classe herit :

class herit : public base 
{ 
public: 
    int v2; 
 
public: 
    herit() : base(), v2(0) {} 
    herit(int v2) : base(rand() % 10), v2(v2) {} 
    herit(int v1, int v2) : base(v1), v2(v2) {} 
 
    void affiche2(); 
}; 

Le premier met les deux variables à 0.

Le second initialise la variable v2 de l’objet avec une valeur passée à la déclaration et la variable v1 de la classe de base avec une valeur aléatoire entre 0 et 9.

Le troisième initialise les variables v1 et v2 avec des valeurs passées à la déclaration. Par exemple :

int main() 
{ 
    srand(time(nullptr)); 
 
    herit h1; 
    h1.affiche2(); // imprime 0 0 
 
    herit h2(10); 
    h2.affiche2(); // un nombre entre 0 et 9 et 10 
 
    herit h3(20, 30); 
    h3.affiche2(); // 20 et 30 
 
    std::cin.get(); 
    return 0; 
} 

Redéfinition de données ou de fonctions

Un autre point important est de pouvoir réutiliser les mêmes noms pour certaines fonctions ou données membres dans une classe dérivée. Ici, par exemple, nous avons deux fonctions affiche(), une pour la classe de base et une pour la classe dérivée.

#include <iostream> 
#include<ctime> 
 
class base 
{ 
public: 
    int v1; 
 
    base() : v1(0) {} 
    base(int v) : v1(v) {} 
 
    void affiche() { std::cout << "base v1 : " << v1 << '\n'; } 
}; 
// 
class herit : public base 
{ 
public: 
    int v2; 
 
    herit() : base(), v2(0) {} 
    herit(int v2) : base(rand() % 10), v2(v2) {} 
    herit(int v1, int v2) : base(v1), v2(v2) {} 
 
    void affiche(); 
}; 
// 
void herit::affiche() 
{ 
    std::cout << "base v1 : " << v1 << '\n'; 
    std::cout << "herit v2 : " << v2 << '\n'; 
} 
// 
int main() 
{ 
    srand(time(nullptr)); 
 
    base b(5); 
    b.affiche(); // 5 
 
    herit h(10, 20); 
    h.affiche(); // 10 20 
 
    std::cin.get(); 
    return 0; 

Au moment de l’appel, c’est la fonction membre la plus proche de l’objet qui a la priorité. Dans l’exemple...

Spécifier un membre de la classe de base

En cas de redéfinition de variables ou de fonctions dans une classe dérivée, il peut être nécessaire de forcer l’accès à la variable ou la fonction de même nom dans la classe de base. Pour ce faire, la syntaxe est :

nomClassBase :: nomVar 
nomClassBase :: nomFonct 

Dans l’exemple ci-dessous, la classe de base et sa dérivée possèdent chacune une variable val et une fonction affiche. Dans la classe herit, val tout court correspond à val de herit et affiche tout court correspond à affiche de herit. En revanche, base::val donne val de la classe base et base::affiche() appelle la fonction affiche de la classe base :

#include <iostream> 
class base 
{ 
public: 
    int val; 
 
    base() : val(0) {} 
    base(int v) : val(v) {} 
 
    void affiche() { std::cout << "val de base : " << val << '\n'; } 
}; 
// 
class herit : public base 
{ 
public: 
    int val; 
 
    herit() : val(0) { base::val = 0; } // val de base 
    herit(int v1, int v2) : val(v2) { base::val = v1; } 
 
    void affiche(); 
}; 
// 
void herit::affiche() 
{ 
    base::affiche(); // affiche de base 
    std::cout << "val de herit : " << val << '\n'; 
} 
// 
int main() 
{ 
    base b(5); 
    b.affiche();  // 5 
 
    herit h(10, 20); 
    h.affiche();  //...

Droits d’accès locaux de la classe héritée

Il reste à se pencher sur les droits d’accès aux membres d’une classe dérivée et, à partir de la classe dérivée, aux membres de la classe de base.

Pour une classe de base, nous avons vu des membres :

  • public : accès interne à la classe et à partir des objets de cette classe.

  • protected : accès interne à la classe et aux classes dérivées, mais fermé aux objets.

  • private : accès en interne à la classe, mais fermé aux classes dérivées et aux objets.

Par exemple :

#include <iostream> 
class base 
{ 
private: 
    int v1; 
 
public: 
    base() { v1 = 0; } 
    base(int v) { v1 = v; } 
 
    void affiche() { std::cout << "base v1 : " << v1 << '\n'; } 
}; 
 
int main() 
{ 
    base b(5); 
    b.affiche(); 
 
    b.v1 = 10;  // erreur à la compilation, 
                // v1 inaccessible si private ou protected 
    return 0; 
} 

La variable v1 est inaccessible à l’objet b si elle est déclarée private ou protected

Ces restrictions demeurent depuis une classe dérivée. Avec v1 private :

class base 
{  
    private : 
    int v1; 
 
    public : 
    base(){v1=0;} 
    base(int v) {v1=v;} ...

Droits d’accès globaux de la classe héritée

Par ailleurs, un droit d’accès s’intercale entre la classe de base et la classe dérivée. Il concerne globalement l’accès aux éléments de la classe de base depuis les objets de la classe fille ansi que les classes dérivées de la classe fille. Mais attention, il n’agit pas depuis la classe fille elle-même pour laquelle les accès aux membres de la classe de base restent inchangés.

Soit une classe de base :

class base 
{ 
    // membres base 
}; 

La classe dérivée peut avoir globalement un accès public :

class deriv : public base 
{ 
    // membres deriv 
} 

ou protected :

class deriv : protected base 
{ 
    // membres deriv 
} 

ou private :

class deriv : private base 
{  
    // membres deriv 
} 

aux membres de la classe de base.

La règle est qu’ils ne peuvent qu’être restreints. Il n’est pas possible d’élargir les droits, il est seulement possible de les diminuer. Les restrictions concernent uniquement les objets de la classe fille ainsi que ses propres dérivées.

Si l’accès global est :

  • public : rien ne change.

  • protected : ce qui est public en base devient protected depuis un objet de la classe dérivée ou d’une classe héritée de la classe dérivée.

  • private : ce qui est public ou protected en base devient private depuis un objet de la classe dérivée ou d’une classe héritée de la classe dérivée.

Soit la variable v1 publique dans la base. Si la classe de base est mentionnée public pour la classe dérivée :...

Héritage multiple

1. Principe et syntaxe

Une classe peut hériter de plusieurs classes simultanément. Il suffit de le préciser à la définition de la classe dérivée. Soit par exemple une classe B qui dérive des classes A1 et A2, la situation est la suivante :

images/RI21-im1.png

Elle se traduit par la syntaxe suivante :

class A1 { . . . }; 
class A2 { . . . }; 
class B : [public, protected ou private] A1, [public, ...] A2 
{ 
    . . . 
}; 

B hérite de A1 et A2. Tout ce qui a été vu de l’héritage est vrai avec un héritage multiple. Juste deux précisions. Dans la classe dérivée, les constructeurs des classes de base peuvent être appelés en liste (séparés chacun par une virgule), et quel que soit l’ordre de la liste, l’ordre d’appel est donné par l’ordre des déclarations des classes de base, dans l’exemple ci-dessus A1 puis A2.

2. Exemple : InDominusRex

Le film Jurassic World fournit avec l’invention du Indominus rex un bon exemple d’héritage multiple. Le Indominus rex est une nouvelle espèce hybride de dinosaure qui mélange des gènes provenant principalement de tyrannosaurus, de vélociraptor, de carnautaurus, de seiche et d’une espèce de Rainette. Pour chaque espèce nécessaire à la réalisation de l’hybride, nous créons une classe et le Indominus rex est obtenu avec un héritage multiple.

Pour cette illustration, les classes sont très simples : une propriété, un constructeur et une méthode. Voici la classe Tyrannosaurus :

class Tyrannosaurus 
{ 
    int dents; 
public: 
    Tyrannosaurus(int dents)...

Comment identifier un héritage

1. Distinction entre héritage et association

Dans un héritage, l’explication généralement fournie est celle d’une classe qui évolue de façon qualitative dans un double mouvement : spécialisation en descendant de la base vers la classe héritée et généralisation en remontant de la classe héritée vers la base. Par exemple :

images/chap2-im3.png

On voit qu’un félin est un mammifère et est un animal. C’est différent de dire qu’un félin est composé d’un félin, d’un mammifère et d’un animal comme s’il y avait trois objets distincts. En réalité, le félin est un seul objet. Il est un seul animal dont les caractéristiques sont augmentées de ce qui est propre aux mammifères et de ce qui est propre aux félins.

En revanche, une association est un ensemble d’objets différents reliés entre eux, faiblement dans le cas d’une agrégation et très fortement si c’est une composition. Par exemple, un cheval qui possède une selle constitue une agrégation et les deux sont dissociables. Un cheval à une tête constitue une composition et la dissociation des deux provoque la fin des deux.

Dans le cas de l’héritage, il n’y a pas de dissociation possible qui entraîne ou non la fin de l’objet concerné. Nous ne pouvons pas dissocier félin de mammifère ni mammifère d’animal.

Toutefois, nous pouvons toujours essayer de voir le sens induit par une permutation entre héritage et composition. Par exemple, le couple moteur-voiture est une composition. Quel serait le sens du couple moteur-voiture sous forme d’héritage ?

Si la voiture hérite du moteur, ça...