Retour à la bibliothèque

Cours sur: Héritage, constructeurs et polymorphisme en C++

Copier ce cours dans Summry
Choisir le contenu du cours
Transcription complète

Algorithme et programmation en C++

2A-S1 2024/2025

SQY :
Cherif BOUTAMMINE
Houda YAHI

Laval :
Alexandre BARBIER

[Image: Logo ESTACA]


I. Héritage

II. Héritage et constructeurs

III. Masquage

IV. Polymorphisme

V. Conteneurs et parcours par itérateur


Introduction à l'héritage

Qu'est-ce que l'héritage ?

  • Mécanisme de la POO permettant de créer une nouvelle classe à partir d'une classe existante.
  • La nouvelle classe hérite :
    • des attributs
    • des méthodes
    • du comportement général

But :

  • Réutilisation du code
  • Spécialisation d'un concept général
  • Hiérarchie logique entre objets

Terminologie :

  • Classe mère / classe de base
  • Classe fille / classe dérivée / sous-classe

[Schéma: Diagramme de classes montrant la relation d'héritage entre Animal et Chien]

Animal
age
poids
manger()
Chien
taille
aboyer()

Exemple : héritage et règles d'accès

Personne.h

class Personne {
private:
    string nom;
    string prenom;
    int age;
public:
    Personne();
    Personne(string n, string p, int a);
    void afficher();
};

Personne.cpp

Personne::Personne()
{
    nom = "inconnu";
    prenom = "inconnu";
    age = 0;
}

Personne::Personne(string n, string p, int a)
{
    nom = n;
    prenom = p;
    age = a;
}

void Personne::afficher()
{
    cout << "Je m'appelle " << nom << " " << prenom << ", j'ai " << age << " ans" << endl;
}

Exemple : héritage et règles d'accès

Personne.h

class Personne {
private:
  string nom;
  string prenom;
  int age;
public:
  Personne();
  Personne(string n, string p, int a);
  void afficher();
};

Etudiant.cpp

Etudiant::Etudiant()
{
  nom = "inconnu";
  prenom = "inconnu";
  age = 0;
  numero = 0;
  ecole = "inconnu";
  annee = 0;
}

Etudiant.h

class Etudiant : public Personne {
private:
  int numero;
  int annee;
  string ecole;
public:
  Etudiant();
  Etudiant(string n, string p, int a, int num, string e, int annee);
  void afficher();
};

Une classe fille ne peut pas accéder aux attributs « private » définis dans la classe mère, même si elle les hérite.


Personne.h

class Personne {
protected:
  string nom;
  string prenom;
  int age;
public:
  Personne();
  Personne(string n, string p, int a);
  void afficher();
};

Etudiant.cpp

Etudiant::Etudiant()
{
  nom = "inconnu";
  prenom = "inconnu";
  age = 0;
  numero = 0;
  ecole = "inconnu";
  annee = 0;
}

Etudiant.h

class Etudiant : public Personne {
private:
  int numero;
  int annee;
  string ecole;
public:
  Etudiant();
  Etudiant(string n, string p, int a, int num, string e, int annee);
  void afficher();
};

Déclarer des attributs en « protected » les rendent inaccessibles depuis l'extérieur de la classe, mais permet aux classes filles d'y accéder directement.


Types d'héritage

En C++ :

  • En héritage public, les attributs et méthodes hérités conservent le même niveau de protection que dans la classe mère.
    class Chien : public Animal
    
  • En héritage protected, les membres publics et protégés de la classe mère deviennent protected dans la classe dérivée ; les membres privés restent inaccessibles.
    class Chien : protected Animal
    
  • En héritage private, les membres publics et protégés de la classe mère deviennent private dans la classe dérivée ; les membres privés restent inaccessibles.
    class Chien : private Animal
    

I. Héritage

II. Héritage et constructeurs

III. Masquage

IV. Polymorphisme

V. Conteneurs et parcours par itérateur


Appel en cascade des constructeurs

Personne.h

class Personne {
protected:
  string nom;
  string prenom;
  int age;
public:
  Personne();
  Personne(string n, string p, int a);
  void afficher();
};

Etudiant.h

class Etudiant : public Personne {
private:
  int numero;
  int annee;
  string ecole;
public:
  Etudiant();
  Etudiant(string n, string p, int a, int num, string e, int annee);
  void afficher();
};

Etudiant.cpp

Etudiant::Etudiant()
{
  nom = "inconnu";
  prenom = "inconnu";
  age = 0;
  numero = 0;
  ecole = "inconnu";
  annee = 0;
}

En réalité les attributs hérités n'ont pas besoin d'être initialisés dans ce constructeur.

Pourquoi donc ?

Appel en cascade des constructeurs

Règle fondamentale :

Lorsqu'un objet dérivé est créé : On appelle d'abord le constructeur de la classe mère puis le constructeur de la classe fille.

Exemple :

Personne::Personne()
{
    nom = "inconnu";
    prenom = "inconnu";
    age = 0;
}
Personne::Personne(string n, string p, int a)
{
    nom = n;
    prenom = p;
    age = a;
}
Etudiant::Etudiant()
{
    numero = 0;
    ecole = "inconnu";
    annee = 0;
}
Etudiant::Etudiant(string n, string p, int a, int num, string e, int annee) : Personne(n, p, a), numero(num)
{
    ecole = e;
    this->annee = annee;
}
  1. Le constructeur par défaut de la classe mère (Personne) est automatiquement appelé en premier, même si on ne l'écrit pas explicitement : Il initialise les attributs hérités : nom, prenom et age.
    Ensuite, le constructeur par défaut de Etudiant s'exécute, et initialise à son tour les attributs qui lui sont spécifiques (numero, ecole, annee).

  2. il est possible de choisir exactement quel constructeur de la classe mère sera utilisé.


Appel en cascade des constructeurs

Exemple :

class Animal {
public:
    Animal() { cout << "Animal construit\n"; }
};

class Chien : public Animal {
public:
    Chien() { cout << "Chien construit\n"; }
};

class Chiwawa : Chien {
public:
    Chiwawa() { cout << "Chiwawa construit\n"; }
};

int main()
{
    Chiwawa c;
    return 0;
}

Affichage

Animal construit
Chien construit
Chiwawa construit


Héritage

I. Héritage
II. Héritage et constructeurs
III. Masquage
IV. Polymorphisme
V. Conteneurs et parcours par itérateur


Masquage des méthodes

Qu'est-ce que le masquage ?

  • Le masquage se produit lorsqu'une méthode d'une classe dérivée possède le même nom qu'une méthode de la classe mère.

Conséquences :

  • La méthode de la classe mère est cachée.
  • C'est celle de la classe fille qui la remplace pour les objets de la classe dérivée.
  • L'appel dépend du type statique de la variable, pas du type réel de l'objet.

Personne.h

class Personne {
protected:
    string nom;
    string prenom;
    int age;
public:
    Personne();
    Personne(string n, string p, int a);
    void afficher();
};

Etudiant.h

class Etudiant : public Personne {
private:
    int numero;
    int annee;
    string ecole;
public:
    Etudiant();
    Etudiant(string n, string p, int a, int num, string e, int annee);
    void afficher();
};

Masquage des méthodes

Pourquoi masquer la méthode afficher() ?

  • Nous souhaitons personnaliser le comportement de afficher() dans la classe Etudiant afin qu'il corresponde mieux aux informations qu'un étudiant doit présenter.
  • Dans cet exemple, les deux méthodes afficher() produisent un résultat similaire, mais en général, la redéfinition peut être très différente, car la classe fille possède des besoins et des informations spécifiques que la classe mère ne connaît pas.

Personne.cpp

void Personne::afficher()
{
    cout << "je m'appelle " << nom << " " << prenom << ", j'ai " << age << " ans" << endl;
}

Etudiant.cpp

void Etudiant::afficher()
{
    cout << "je m'appelle " << nom << " " << prenom << ", j'ai " << age << " ans" << endl;
    cout << "je suis étudiant à " << ecole << endl;
}

Masquage des méthodes

Pourquoi masquer la méthode afficher() ?

  • Nous souhaitons personnaliser le comportement de afficher() dans la classe Etudiant afin qu'il corresponde mieux aux informations qu'un étudiant doit présenter.
  • Dans cet exemple, les deux méthodes afficher() produisent un résultat similaire, mais en général, la redéfinition peut être très différente, car la classe fille possède des besoins et des informations spécifiques que la classe mère ne connaît pas.

Personne.cpp

void Personne::afficher()
{
cout << "je m'appelle " << nom << " " << prenom << ", j'ai " << age << " ans" << endl;
}

Etudiant.cpp

void Etudiant::afficher()
{
cout << "je m'appelle " << nom << " " << prenom << ", j'ai " << age << " ans" << endl;
cout << "je suis étudiant à " <<ecole<< endl;
}

Masquage des méthodes

Bonne pratique

  • Si la méthode afficher() de la classe fille reprend une partie du comportement de la méthode afficher() de la classe mère, il est judicieux d'appeler d'abord la version héritée, puis d'ajouter les compléments spécifiques à la classe fille.
  • Cela permet d'éviter la duplication de code, de conserver le comportement commun, tout en étendant la fonctionnalité pour la classe dérivée.

Personne.cpp

void Personne::afficher()
{
cout << "je m'appelle " << nom << " " << prenom << ", j'ai " << age << " ans" << endl;
}

Etudiant.cpp

void Etudiant::afficher()
{
Personne::afficher(); //appel d'abord de la méthode "afficher" de la mère
cout << "je suis etudiant à " <<ecole<< endl;
}

Masquage des méthodes

int main()
{
Personne p("Alex", "Michel", 25);
p.afficher();
cout << endl << endl;
Etudiant e("Lamoureux", "Robert", 20,1002, "ESTACA",2);
e.afficher();
return 0;
}

Affichage

je m'appelle Alex Michel, j'ai 25 ans
je m'appelle Lamoureux Robert, j'ai 20 ans
je suis etudiant a ESTACA

Polymorphisme

Problématique : stocker différents types d'objets

But :

  • Nous voulons stocker des Personne et des Etudiant dans un même conteneur.

Observation :

  • Un Etudiant est aussi une Personne
  • Donc un Etudiant* peut être stocké dans un Personne*.

Idée :

  • Définir un vector de Personne *:
vector<Personne *> liste;

Problématique : stocker différents types d'objets

int main()
{
vector<Personne *> liste;
Personne* p = new Personne("Ana", "Lopez", 28);
Etudiant* e = new Etudiant("Mark", "Stone", 20, 101, "MIT", 2);
liste.push_back(p);
liste.push_back(e);
liste.push_back(new Personne("Luc", "Martin", 35));
liste.push_back(new Etudiant("Nina", "Khan", 22, 202, "ENSA", 1));
for (int i = 0; i < liste.size(); i++)
liste[i]->afficher();
return 0;
}

Résultat observé :

  • Toujours la méthode Personne::afficher() est appelée, même pour les Etudiant.

Pourquoi ?

  • La méthode afficher() n'est pas virtuelle
  • Donc l'appel dépend du type statique du pointeur
    • ici: Personne*
  • L'appel dépend du type indiqué dans la déclaration du pointeur

Solution :

  • Polymorphisme

Affichage

je m'appelle Ana Lopez, j'ai 28 ans
je m'appelle Mark Stone, j'ai 20 ans
je m'appelle Luc Martin, j'ai 35 ans
je m'appelle Nina Khan, j'ai 22 ans

[Image: Logo ESTACA]

Polymorphisme

Solution :

  • Déclarer la méthode afficher() comme virtuelle dans la classe mère.
class Personne {
protected:
string nom;
string prenom;
int age;
public:
Personne();
Personne(string n, string p, int a);
virtual void afficher();
};

Résultat :

  • Si p pointe vers une Personne
    • appelle Personne::afficher()
  • Si p pointe vers un Etudiant
    • appelle Etudiant::afficher()

Ce qui change :

  • l'appel dépend du type réel de l'objet, pas du type du pointeur.

Affichage

je m'appelle Ana Lopez, j'ai 28 ans
je m'appelle Mark Stone, j'ai 20 ans
je suis etudiant a MIT
je m'appelle Luc Martin, j'ai 35 ans
je m'appelle Nina Khan, j'ai 22 ans
je suis etudiant a ENSA

[Image: Logo ESTACA]

I. Héritage

II. Héritage et constructeurs

III. Masquage

IV. Polymorphisme

V. Conteneurs et parcours par itérateur

[Image: Logo ESTACA]

Parcourir un vector avec un itérateur

Pourquoi utiliser des itérateurs ?

  • Les itérateurs sont plus flexibles que l'indexation, car ils permettent de parcourir tous les conteneurs C++ (pas seulement ceux qui ont des indices), d'interagir précisément avec leurs éléments et d'utiliser les algorithmes génériques de la STL.

Exemple :

vector<int> v = { 1,2,3,4,5 };
//parcours par indice
for (int i = 0; i < v.size(); i++)
cout << v[i] << endl;
//parcours par itérateur
vector<int>::iterator it; // déclare un variable de type itérateur sur un vector d'entier
for (it = v.begin(); it != v.end(); it++)
cout << *it << endl;

[Image: Logo ESTACA]

Parcourir un vector avec un itérateur

[Schéma: Représentation d'un itérateur pointant vers le début d'un vecteur et se déplaçant vers la fin.]

[Image: Logo ESTACA]

Les conteneurs du C++ (vue d'ensemble)

Séquence (ordre garanti)

  • vector → dynamique, accès rapide par index
  • list → liste chaînée (pas d'accès direct)

Associatifs (triés)

  • set → valeurs uniques
  • map → couples (clé, valeur)

Adaptateurs

  • Stack
  • Queue
  • priority_queue

La bibliothèque <algorithm>

Contient les fonctions les plus utiles :

  • sort()
  • find()
  • count()
  • reverse()
  • max_element(), min_element()
  • copy()
  • remove(), erase()
  • ...

Utilisable avec tous les conteneurs, via leurs itérateurs.

Exemple :

sort(v.begin(), v.end()); // tri croissant
sort(v.rbegin(), v.rend()); // tri décroissant
sort(v.begin(), v.end(), cmp); // tri personnalisé
Résumé du cours

I. Héritage

  • Définition : Mécanisme POO créant une classe à partir d'une existante, héritant attributs, méthodes, comportement.
  • But : Réutilisation code, spécialisation, hiérarchie logique.
  • Terminologie : Classe mère (base), classe fille (dérivée, sous-classe).
  • Exemple : Animal (mère) -> Chien (fille).
  • Règles d'accès :
    • Membres private classe mère : Inaccessibles par classe fille.
    • Membres protected classe mère : Accessibles par classe fille, inaccessibles de l'extérieur.
  • Types d'héritage :
    • public : Maintien niveau protection.
    • protected : Public/protected mère deviennent protected fille.
    • private : Public/protected mère deviennent private fille.

II. Héritage et constructeurs

  • Appel en cascade : Création objet dérivé -> appel constructeur mère -> appel constructeur fille.
  • Initialisation : Attributs hérités initialisés par constructeur mère.
  • Choix constructeur mère : Possible via liste d'initialisation (ex: : Personne(n, p, a)).
  • Ordre construction : Hiérarchique, de la classe la plus générale à la plus spécifique (ex: Animal -> Chien -> Chiwawa).

III. Masquage

  • Définition : Méthode classe fille même nom que méthode classe mère.
  • Conséquence : Méthode mère cachée, méthode fille appelée pour objets dérivés.
  • Appel : Dépend du type statique de la variable (pas type réel objet) si méthode non virtuelle.
  • Raison : Personnaliser comportement pour classe fille.
  • Bonne pratique : Appeler méthode mère (ClasseMere::methode()) puis ajouter spécificités fille pour éviter duplication code.

IV. Polymorphisme

  • Problématique : Stocker différents types d'objets (ex: Personne, Etudiant) dans même conteneur (ex: vector<Personne*>).
  • Observation : Etudiant est une Personne, Etudiant* peut être stocké dans Personne*.
  • Problème sans polymorphisme : Appel méthode non virtuelle (ex: afficher()) basé sur type statique du pointeur (Personne*), pas type réel objet.
  • Solution : Déclarer méthode comme virtual dans classe mère (virtual void afficher();).
  • Résultat avec polymorphisme : Appel méthode dépend du type réel de l'objet pointé.

V. Conteneurs et parcours par itérateur

  • Itérateurs :
    • Utilité : Flexibilité, parcours tous conteneurs STL, interaction précise éléments, utilisation algorithmes génériques.
    • Exemple : vector<int>::iterator it; for (it = v.begin(); it != v.end(); it++).
  • Conteneurs STL (vue d'ensemble) :
    • Séquence : vector (dynamique, accès index), list (chaînée).
    • Associatifs : set (uniques), map (clé/valeur).
    • Adaptateurs : stack, queue, priority_queue.
  • Bibliothèque <algorithm> :
    • Fonctions : sort, find, count, copy, etc.
    • Utilisation : Avec tous conteneurs via itérateurs (ex: sort(v.begin(), v.end());).

Outils de révision