• 50 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 19/02/2019

Découvrez l'héritage

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Nous allons maintenant découvrir une des notions les plus importantes de la POO : l'héritage.
Qu'on se rassure, il n'y aura pas de mort. ;-)

L'héritage est un concept très important qui représente une part non négligeable de l'intérêt de la programmation orientée objet. Bref, cela ne rigole pas. Ce n'est pas le moment de s'endormir au fond, je vous ai à l'œil !

Dans ce chapitre nous allons réutiliser notre exemple de la classePersonnage, que nous simplifierons beaucoup pour nous concentrer uniquement sur ce qui est important. En clair, nous ne garderons que le strict minimum, histoire d'avoir un exemple simple mais que vous connaissez déjà.

Allez, bon courage : cette notion n'est pas bien dure à comprendre, elle est juste très riche.

Exemple d'héritage simple

Vous devez vous dire que le terme « Héritage » est étrange dans le langage de la programmation. Mais vous allez le voir, il n'en est rien.
Alors c'est quoi l'héritage ? C'est une technique qui permet de créer une classe à partir d'une autre classe. Elle lui sert de modèle, de base de départ. Cela permet d'éviter d'avoir à réécrire un même code source plusieurs fois.

Comment reconnaître un héritage ?

C'est la question à se poser. Certains ont tellement été traumatisés par l'héritage qu'ils en voient partout, d'autres au contraire (surtout les débutants) se demandent à chaque fois s'il y a un héritage à faire ou pas. Pourtant ce n'est pas « mystique », il est très facile de savoir s'il y a une relation d'héritage entre deux classes.

Comment ? En suivant cette règle très simple : Il y a héritage quand on peut dire : « A est un B ».

Pas de panique, ce ne sont pas des maths.
Et afin de vous persuader, je vais prendre un exemple très simple : on peut dire « Un guerrier est un personnage » ou encore « Un magicien est un personnage ». On peut donc définir un héritage : « la classe Guerrierhérite dePersonnage», « la classe Magicienhérite dePersonnage».

Pour être sûr que vous compreniez bien, voici quelques exemples supplémentaires et corrects d'héritage :

  • une voiture est un véhicule (Voiturehérite deVehicule) ;

  • un bus est un véhicule (Bushérite deVehicule) ;

  • un moineau est un oiseau (Moineauhérite d'Oiseau) ;

  • un corbeau est un oiseau (Corbeauhérite d'Oiseau) ;

  • un chirurgien est un docteur (Chirurgienhérite deDocteur) ;

  • un diplodocus est un dinosaure (Diplodocushérite deDinosaure) ;

  • etc.

En revanche, vous ne pouvez pas dire « Un dinosaure est un diplodocus », ou encore « Un bus est un oiseau ». Donc, dans ces cas là, on ne peut pas faire d'héritage ou, plus exactement, cela n'aurait aucun sens

Avant de voir comment réaliser un héritage en C++, il faut que je pose l'exemple sur lequel on va travailler.

Notre exemple : la classePersonnage

Petit rappel : cette classe représente un personnage d'un jeu vidéo de type RPG (jeu de rôle). Il n'est pas nécessaire de savoir jouer ou d'avoir joué à un RPG pour suivre mon exemple. J'ai simplement choisi celui-là car il est plus ludique que la plupart des exemples barbants que les profs d'informatique aiment utiliser (Voiture, Bibliotheque, Universite, PompeAEssence… ).

Nous allons un peu simplifier notre classePersonnage. Voici ce sur quoi je vous propose de partir :

#ifndef DEF_PERSONNAGE
#define DEF_PERSONNAGE
 
#include <iostream>
#include <string>
 
class Personnage
{
    public:
        Personnage();
        void recevoirDegats(int degats);
        void coupDePoing(Personnage &cible) const;
 
    private:
        int m_vie;
        std::string m_nom;
};
 
#endif

Notre Personnagea un nom et une quantité de vie.
On n'a mis qu'un seul constructeur, celui par défaut. Il permet d'initialiser le Personnageavec un nom et lui donne 100 points de vie.
Le Personnagepeut recevoir des dégâts, via la méthode recevoirDegatset en distribuer, via la méthodecoupDePoing().

À titre informatif, voici l'implémentation des méthodes dans Personnage.cpp:

#include "Personnage.h"
 
using namespace std;
 
Personnage::Personnage() : m_vie(100), m_nom("Jack")
{
 
}
 
void Personnage::recevoirDegats(int degats)
{
    m_vie -= degats;
}
 
void Personnage::coupDePoing(Personnage &cible) const
{
    cible.recevoirDegats(10);
}

Rien d'extraordinaire pour le moment.

La classe Guerrierhérite de la classePersonnage

Intéressons-nous maintenant à l'héritage : l'idée est de créer une nouvelle classe qui soit une sous-classe dePersonnage. On dit que cette classe hérite dePersonnage.

Pour cet exemple, je vais créer une classe Guerrierqui hérite dePersonnage. La définition de la classe, dansGuerrier.h, ressemble à ceci :

#ifndef DEF_GUERRIER
#define DEF_GUERRIER
 
#include <iostream>
#include <string>
#include "Personnage.h"
//Ne pas oublier d'inclure Personnage.h pour pouvoir en hériter !
 
class Guerrier : public Personnage
//Signifie : créer une classe Guerrier qui hérite de la classe Personnage
{
 
};
 
#endif

Grâce à ce qu'on vient de faire, la classe Guerriercontiendra de base tous les attributs et toutes les méthodes de la classePersonnage.
Dans un tel cas, la classe Personnageest appelée la classe « Mère » et la classeGuerrierla classe « Fille ».

Mais quel intérêt de créer une nouvelle classe si c'est pour qu'elle contienne les mêmes attributs et les mêmes méthodes ?

Attendez, justement ! Le truc, c'est qu'on peut rajouter des attributs et des méthodes spéciales dans la classeGuerrier. Par exemple, on pourrait rajouter une méthode qui ne concerne que les guerriers, du genre frapperCommeUnSourdAvecUnMarteau(bon ok, c'est un nom de méthode un peu long, je l'avoue, mais l'idée est là).

#ifndef DEF_GUERRIER
#define DEF_GUERRIER
 
#include <iostream>
#include <string>
#include "Personnage.h"
 
class Guerrier : public Personnage
{
    public:
        void frapperCommeUnSourdAvecUnMarteau() const;
        //Méthode qui ne concerne que les guerriers
};
 
#endif

Schématiquement, on représente la situation comme à la figure suivante :

Un héritage entre classes
Un héritage entre classes

Le schéma se lit de bas en haut, c'est-à-dire que «Guerrierhérite dePersonnage».
Guerrierest la classe fille, Personnageest la classe mère. On dit que Guerrierest une « spécialisation » de la classePersonnage. Elle possède toutes les caractéristiques d'unPersonnage(de la vie, un nom, elle peut recevoir des dégâts) mais elle possède en plus des caractéristiques propres au GuerriercommefrapperCommeUnSourdAvecUnMarteau().

Vous commencez à comprendre le principe ? En C++, quand on a deux classes qui sont liées par la relation « est un », on utilise l'héritage pour mettre en évidence ce lien. UnGuerrier« est un »Personnageamélioré qui possède une méthode supplémentaire.

Ce concept n'a l'air de rien comme cela mais croyez-moi, cela fait la différence ! Vous n'allez pas tarder à voir tout ce que cela a de puissant lorsque vous pratiquerez, plus loin dans le cours.

La classe Magicienhérite aussi dePersonnage

Tant qu'il n'y a qu'un seul héritage, l'intérêt semble encore limité. Mais multiplions un peu les héritages et les spécialisations et nous allons vite voir tout l'intérêt de la chose.

Par exemple, si on créait une classe Magicienqui hérite elle aussi dePersonnage? Après tout, unMagicienest unPersonnage, donc il peut récupérer les mêmes propriétés de base : de la vie, un nom, donner un coup de poing, etc.
La différence, c'est que le Magicienpeut aussi envoyer des sorts magiques, par exemple bouleDeFeuetbouleDeGlace. Pour utiliser sa magie, il a une réserve de magie qu'on appelle « Mana » (cela fait un attribut à rajouter). Quand Manatombe à zéro, il ne peut plus lancer de sort.

#ifndef DEF_MAGICIEN
#define DEF_MAGICIEN
 
#include <iostream>
#include <string>
#include "Personnage.h"
 
class Magicien : public Personnage
{
    public:
        void bouleDeFeu() const;
        void bouleDeGlace() const;
 
    private:
        int m_mana;
};
 
#endif

Je ne vous donne pas l'implémentation des méthodes (le.cpp) ici, je veux juste que vous compreniez et reteniez le principe :

Deux classes héritent d'une même classe
Deux classes héritent d'une même classe

Notez que, sur le schéma, je n'ai représenté que les méthodes des classes mais les attributs (vie,nom… ) sont eux aussi hérités !

Et le plus beau, c'est qu'on peut faire une classe qui hérite d'une classe qui hérite d'une autre classe !
Imaginons qu'il y ait deux types de magiciens : les magiciens blancs, qui sont des gentils qui envoient des sorts de guérison, et les magiciens noirs qui sont des méchants qui utilisent leurs sorts pour tuer des gens (super exemple, j'en suis fier).

Multiples héritages
Multiples héritages

Et cela pourrait continuer longtemps comme cela. Vous verrez dans la prochaine partie sur la bibliothèque C++ Qt qu'il y a souvent cinq ou six héritages qui sont faits à la suite. C'est vous dire si c'est utilisé !

La dérivation de type

Imaginons le code suivant :

Personnage monPersonnage;
Guerrier monGuerrier;
 
monPersonnage.coupDePoing(monGuerrier);
monGuerrier.coupDePoing(monPersonnage);

Si vous compilez, cela fonctionne. Mais si vous êtes attentifs, vous devriez vous demander pourquoi cela a fonctionné, parce que normalement cela n'aurait pas dû ! … Non, vous ne voyez pas ?

Allez, un petit effort. Voici le prototype decoupDePoing(il est le même dans la classePersonnageet dans la classeGuerrier, rappelez-vous) :

void coupDePoing(Personnage &cible) const;

Quand on fait monGuerrier.coupDePoing(monPersonnage);, on envoie bien en paramètre unPersonnage.
Mais quand on fait monPersonnage.coupDePoing(monGuerrier);, cela marche aussi et le compilateur ne hurle pas à la mort alors que, selon toute logique, il le devrait ! En effet, la méthode coupDePoingattend un Personnageet on lui envoie un Guerrier. Pourquoi diable cela fonctionne-t-il ?

Eh bien… c'est justement une propriété très intéressante de l'héritage en C++ que vous venez de découvrir là : on peut substituer un objet de la classe fille à un pointeur ou une référence vers un objet de la classe mère. Ce qui veut dire, dans une autre langue que le chinois, qu'on peut faire cela :

Personnage *monPersonnage(0);
Guerrier *monGuerrier = new Guerrier();
 
monPersonnage = monGuerrier; // Mais… mais… Ça marche !?

Les deux premières lignes n'ont rien d'extraordinaire : on crée un pointeur Personnagemis à 0 et un pointeur Guerrierqu'on initialise avec l'adresse d'un nouvel objet de typeGuerrier.
Par contre, la dernière ligne est assez surprenante. Normalement, on ne devrait pas pouvoir donner à un pointeur de type Personnageun pointeur de typeGuerrier. C'est comme mélanger les torchons et les serviettes, cela ne se fait pas.

Alors oui, en temps normal le compilateur n'accepte pas d'échanger des pointeurs (ou des références) de types différents. Mais Personnageet Guerrierne sont pas n'importe quels types :Guerrierhérite dePersonnage. Et la règle à connaître, c'est justement qu'on peut affecter un élément enfant à un élément parent ! En fait c'est logique puisque Guerrierest unPersonnage.

Cela nous permet donc de placer un élément dans un pointeur (ou une référence) de type plus général.
C'est très pratique dans notre cas lorsqu'on passe une cible en paramètre :

void coupDePoing(Personnage &cible) const;

Notre méthode coupDePoingest capable de faire mal à n'importe quelPersonnage! Qu'il soit Guerrier,Magicien,MagicienBlanc,MagicienNoirou autre, c'est unPersonnageaprès tout, donc on peut lui donner un coupDePoing.

Je reconnais que c'est un peu choquant au début mais on se rend compte qu'en réalité, c'est très bien fait. Cela fonctionne, puisque la méthode coupDePoingse contente d'appeler des méthodes de la classePersonnage(recevoirDegats) et que ces méthodes se trouvent forcément dans toutes les classes filles (Guerrier,Magicien).

Si vous ne comprenez pas, relisez-moi et vous devriez saisir pourquoi cela fonctionne.

Eh bien non, moi je ne comprends pas ! Je ne vois pas pourquoi cela marche si on faitobjetMere = objetFille;. Là on affecte la fille à la mère or la fille possède des attributs que la mère n'a pas. Cela devrait coincer ! L'inverse ne serait-il pas plus logique ?

Je vous rassure, j'ai mis des mois avant d'arriver à comprendre ce qui se passait vraiment (comment cela, vous n'êtes pas rassurés ?).

Votre erreur est de croire qu'on affecte la fille à la mère or ce n'est pas le cas : on substitue un pointeur (ou une référence). Ce n'est pas du tout pareil. Les objets restent comme ils sont dans la mémoire, on ne fait que diriger le pointeur vers la partie de la fille qui a été héritée. La classe fille est constituée de deux morceaux : les attributs et méthodes héritées de la mère d'une part, et les attributs et méthodes qui lui sont propres d'autre part. En faisant objetMere = objetFille;, on dirige le pointeur objetMerevers les attributs et méthodes hérités uniquement (figure suivante).

La dérivation de type
La dérivation de type

Je peux difficilement pousser l'explication plus loin, j'espère que vous allez comprendre. Sinon, pas de panique, j'ai survécu plusieurs mois en programmation C++ sans bien comprendre ce qui se passait et je n'en suis pas mort (mais c'est mieux si vous comprenez !).

En tout cas, sachez que c'est une technique très utilisée, on s'en sert vraiment souvent en C++ ! Vous découvrirez cela par la pratique, dans la prochaine partie de ce livre, en utilisant Qt.

Héritage et constructeurs

Vous avez peut-être remarqué que je n'ai pas encore parlé des constructeurs dans les classes filles (Guerrier,Magicien… ). C'est justement le moment de s'y intéresser.

On sait que Personnagea un constructeur (par défaut) défini comme ceci dans le.h:

Personnage();

… et son implémentation dans le .cpp:

Personnage::Personnage() : m_vie(100), m_nom("Jack")
{
 
}

Comme vous le savez, lorsqu'on crée un objet de typePersonnage, le constructeur est appelé avant toute chose.

Mais maintenant, que se passe-t-il lorsqu'on crée par exemple un Magicienqui hérite dePersonnage? Le Magiciena le droit d'avoir un constructeur lui aussi ! Est-ce que cela ne risque pas d'interférer avec le constructeur dePersonnage? Il faut pourtant appeler le constructeur de Personnagesi on veut que la vie et le nom soient initialisés !

En fait, les choses se déroule dans l'ordre suivant :

  1. Vous demandez à créer un objet de typeMagicien;

  2. Le compilateur appelle d'abord le constructeur de la classe mère (Personnage) ;

  3. Puis, le compilateur appelle le constructeur de la classe fille (Magicien).

En clair, c'est d'abord le constructeur du « parent » qui est appelé, puis celui du fils, et éventuellement celui du petit-fils (s'il y a un héritage d'héritage, comme c'est le cas avecMagicienBlanc).

Appelez le constructeur de la classe mère

Pour appeler le constructeur de Personnageen premier, il faut y faire appel depuis le constructeur deMagicien. C'est dans un cas comme cela qu'il est indispensable de se servir de la liste d'initialisation (vous savez, tout ce qui suit le symbole deux-points dans l'implémentation).

Magicien::Magicien() : Personnage(), m_mana(100)
{
 
}

Le premier élément de la liste d'initialisation indique de faire appel en premier lieu au constructeur de la classe parentePersonnage. Puis on réalise les initialisations propres auMagicien(comme l'initialisation du mana à 100).

Transmission de paramètres

Le gros avantage de cette technique est que l'on peut « transmettre » les paramètres du constructeur deMagicienau constructeur dePersonnage. Par exemple, si le constructeur de Personnageprend un nom en paramètre, il faut que le Magicienaccepte lui aussi ce paramètre et le fasse passer au constructeur dePersonnage:

Magicien::Magicien(string nom) : Personnage(nom), m_mana(100)
{
 
}

Bien entendu, si on veut que cela marche, il faut aussi surcharger le constructeur dePersonnagepour qu'il accepte un paramètre string!

Personnage::Personnage(string nom) : m_vie(100), m_nom(nom)
{
 
}

Et voilà comment on fait « remonter » des paramètres d'un constructeur à un autre pour s'assurer que l'objet se crée correctement.

Schéma résumé

Pour bien mémoriser ce qui se passe, rien de tel qu'un schéma résumant tout ceci, n'est-ce pas ?

Ordre d'appel des constructeurs
Ordre d'appel des constructeurs

Il faut bien entendu le lire dans l'ordre pour en comprendre le fonctionnement. On commence par demander à créer unMagicien. « Oh mais c'est un objet » se dit le compilateur, « il faut que j'appelle son constructeur ».
Or, le constructeur du Magicienindique qu'il faut d'abord appeler le constructeur de la classe parentePersonnage. Le compilateur va donc voir la classe parente et exécute son code. Il revient ensuite au constructeur du Magicienet exécute son code.

Une fois que tout cela est fait, notre objet merlindevient utilisable et on peut enfin faire subir les pires sévices à notre cible

La portée protected

Il me serait vraiment impossible de vous parler d'héritage sans vous parler de la portée protected.

Actuellement, les portées (ou droits d'accès) que vous connaissez déjà sont :

  • public: les éléments qui suivent sont accessibles depuis l'extérieur de la classe ;

  • private: les éléments qui suivent ne sont pas accessibles depuis l'extérieur de la classe.

Je vous ai en particulier donné la règle fondamentale du C++, l'encapsulation, qui veut que l'on empêche systématiquement au monde extérieur d'accéder aux attributs de nos classes.

La portée protectedest un autre type de droit d'accès que je classerais entre public(le plus permissif) etprivate(le plus restrictif). Il n'a de sens que pour les classes qui se font hériter (les classes mères) mais on peut l'utiliser sur toutes les classes, même quand il n'y a pas d'héritage.

Voici sa signification : les éléments qui suivent protectedne sont pas accessibles depuis l'extérieur de la classe, sauf si c'est une classe fille.

Cela veut dire, par exemple, que si l'on met des éléments en protecteddans la classePersonnage, on y aura accès dans les classes filles Guerrieret Magicien. Avec la portée private, on n'aurait pas pu y accéder !

class Personnage
{
   public:
       Personnage();
       Personnage(std::string nom);
       void recevoirDegats(int degats);
       void coupDePoing(Personnage &cible) const;

   protected: //Privé, mais accessible aux éléments enfants (Guerrier, Magicien)
      int m_vie;
      std::string m_nom;
};

On peut alors directement manipuler la vie et le nom dans tous les éléments enfants dePersonnage, comme GuerrieretMagicien!

Le masquage

Terminons ce chapitre avec une notion qui nous servira dans la suite : le masquage.

Une fonction de la classe mère

Il serait intéressant pour notre petit RPG que nos personnages aient le moyen de se présenter. Comme c'est une action que devraient pouvoir réaliser tous les personnages, quels que soient leur rôle, la fonctionsePresenter()va dans la classePersonnage.

class Personnage
{
    public:
        Personnage();
        Personnage(std::string nom);
        void recevoirDegats(int degats);
        void coupDePoing(Personnage& cible) const;
        
        void sePresenter() const;
 
    protected:
        int m_vie;
        std::string m_nom;
};

Et dans le fichier.cpp:

void Personnage::sePresenter() const
{
    cout << "Bonjour, je m'appelle " << m_nom << "." << endl;
    cout << "J'ai encore " << m_vie << " points de vie." << endl;
}

On peut donc écrire unmain()comme celui-ci :

int main()
{
   Personnage marcel("Marcel");
   marcel.sePresenter();

   return 0;
}

Ce qui nous donne évidemment le résultat suivant :

Bonjour, je m'appelle Marcel.
J'ai encore 100 points de vie.

La fonction est héritée dans les classes filles

Vous le savez déjà, unGuerrierest unPersonnageet, par conséquent, il peut également se présenter.

int main(){

   Guerrier lancelot("Lancelot du Lac");
   lancelot.sePresenter();

   return 0;
}

Avec pour résultat :

Bonjour, je m'appelle Lancelot du Lac.
J'ai encore 100 points de vie.

Jusque là, rien de bien particulier ni de difficile.

Le masquage

Imaginons maintenant que les guerriers aient une manière différente de se présenter. Ils doivent en plus préciser qu'ils sont guerriers. Nous allons donc écrire une version différente de la fonctionsePresenter(), spécialement pour eux :

void Guerrier::sePresenter() const
{
    cout << "Bonjour, je m'appelle " << m_nom << "." << endl;
    cout << "J'ai encore " << m_vie << " points de vie." << endl;
    cout << "Je suis un Guerrier redoutable." << endl;
}

Mais il y aura deux fonctions avec le même nom et les mêmes arguments dans la classe ! C'est interdit !

Vous avez tort et raison. Deux fonctions ne peuvent avoir la même signature (nom et type des arguments). Mais, dans le cadre des classes, c'est différent. La fonction de la classeGuerrierremplace celle héritée de la classePersonnage.

Si l'on exécute le mêmemain()qu'avant, on obtient cette fois le résultat souhaité.

Bonjour, je m'appelle Lancelot du Lac.
J'ai encore 100 points de vie.
Je suis un Guerrier redoutable.

Quand on écrit une fonction qui a le même nom que celle héritée de la classe mère, on parle de masquage. La fonction héritée dePersonnageest masquée, cachée.

C'est bien pratique cela ! Quand on fait un héritage, la classe fille reçoit automatiquement toutes les méthodes de la classe mère. Si une de ces méthodes ne nous plaît pas, on la réécrit dans la classe fille et le compilateur saura quelle version appeler. Si c'est unGuerrier, il utilise la « versionGuerrier» desePresenter()et si c'est unPersonnageou unMagicien, il utilise la version de base (figure suivante).

Principe du masquage
Principe du masquage

Gardez bien ce schéma en mémoire, il nous sera utile au prochain chapitre.

Économisez du code

Ce qu'on a écrit est bien mais on peut faire encore mieux. Si l'on regarde, la fonction sePresenter()de la classe Guerriera deux lignes identiques à ce qu'il y a dans la même fonction de la classePersonnage. On pourrait donc économiser des lignes de code en appelant la fonction masquée.

On aimerait donc écrire quelque chose du genre :

void Guerrier::sePresenter() const
{
    appel_a_la_fonction_masquee();
    //Cela afficherait les informations de base
    
    cout << "Je suis un Guerrier redoutable." << endl;
    //Et ensuite les informations spécifiques
}

Il faudrait donc un moyen d'appeler la fonction de la classe mère.

Le démasquage

On aimerait appeler la fonction dont le nom complet est :Personnage::sePresenter(). Essayons donc :

void Guerrier::sePresenter() const
{
   Personnage::sePresenter();
    cout << "Je suis un Guerrier redoutable." << endl;
}

Et c'est magique, cela donne exactement ce que l'on espérait.

Bonjour, je m'appelle Lancelot du Lac.
J'ai encore 100 points de vie.
Je suis un Guerrier redoutable.

On parle dans ce cas de démasquage, puisqu'on a pu utiliser une fonction qui était masquée.

On a utilisé ici l'opérateur::appelé opérateur de résolution de portée. Il sert à déterminer quelle fonction (ou variable) utiliser quand il y a ambiguïté ou si il y a plusieurs possibilités.

En résumé

  • L'héritage permet de spécialiser une classe.

  • Lorsqu'une classe hérite d'une autre classe, elle récupère toutes ses propriétés et ses méthodes.

  • Faire un héritage a du sens si on peut dire que l'objetA« est un » objetB. Par exemple, uneVoiture« est un »Vehicule.

  • La classe de base est appelée classe mère et la classe qui en hérite est appelée classe fille.

  • Les constructeurs sont appelés dans un ordre bien précis : classe mère, puis classe fille.

  • En plus de publicetprivate, il existe une portée protected. Elle est équivalente à privatemais elle est un peu plus ouverte : les classes filles peuvent elles aussi accéder aux éléments.

  • Si une méthode a le même nom dans la classe fille et la classe mère, c'est la méthode la plus spécialisée, celle de la classe fille, qui est appelée.

Exemple de certificat de réussite
Exemple de certificat de réussite