• 10 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 10/02/2022

Associez les classes et les pointeurs

Faites un pointeur d'une classe vers une autre classe

Reprenons notre classe Personnage  . Dans les précédents chapitres, nous lui avons ajouté une Arme que nous avons directement intégrée à ses attributs :

class Personnage
{
    public:

    //Quelques méthodes…

    private:

    Arme m_arme; // L'Arme est "contenue" dans le Personnage
    //…
};

Il y a plusieurs façons d'associer des classes entre elles. Celle-ci fonctionne bien dans notre cas, mais Arme est liée au Personnage  , elle ne peut pas en sortir :

La classe Personnage contient une autre classe : Arme.
Arme est vraiment dans Personnage.

Il y a une autre technique, plus souple mais plus complexe : ne pas intégrer Arme à Personnage et utiliser un pointeur à la place. Au niveau de la déclaration de la classe, le changement correspond à… une étoile en plus :

class Personnage
{
    public:

    //Quelques méthodes…

    private:

    Arme *m_arme;
    //L'Arme est un pointeur, l'objet n'est plus contenu dans le Personnage
    //…
};

 Arme étant un pointeur, on ne peut plus dire qu'elle appartient à Personnage  .

Des classes liées par un pointeur
Des classes liées par un pointeur

On considère que Arme est maintenant externe au Personnage  . Les avantages de cette technique sont les suivants :

  • Le Personnage peut changer d' Arme en faisant tout simplement pointer m_arme vers un autre objet. Par exemple, si le Personnage possède un inventaire (dans un sac à dos), il peut changer son Arme à tout moment en modifiant le pointeur.

  • Le Personnage peut donner son Arme à un autrePersonnage  , il suffit de changer les pointeurs de chacun des personnages.

  • Si le Personnage n'a plus d' Arme  , il suffit de mettre le pointeur m_arme à 0.

Mais des défauts, il y en a aussi. Gérer une classe qui contient des pointeurs, ce n'est pas de la tarte, vous pouvez me croire, et d'ailleurs vous allez le constater.

Alors, faut-il utiliser un pointeur ou pas pour Arme  ?

Retenez donc qu'il n'y a pas de meilleure méthode. Ce sera à vous de choisir, en fonction de votre cas, si vous intégrez directement un objet dans une classe ou si vous utilisez un pointeur.

Gérez l'allocation dynamique

On va voir ici comment on travaille quand une classe contient des pointeurs vers des objets.

On travaille là encore sur la classe Personnage  , et je suppose que vous avez mis l'attribut m_arme en pointeur comme je l'ai montré un peu plus haut :

class Personnage
{
    public:
 
    //Quelques méthodes…
 
    private:
 
    Arme *m_arme;
    //L'Arme est un pointeur, l'objet n'est plus contenu dans le Personnage
    //…
};

Arme étant un pointeur, il va falloir le créer par le biais d'une allocation dynamique avec new  . Sinon, l'objet ne se créera pas tout seul.

Allocation de mémoire pour l'objet

Où se fait l'allocation de mémoire pour notre Arme  ?

Dans le constructeur. C'est son rôle : faire en sorte que l'objet soit bien construit, donc notamment que tous les pointeurs pointent vers quelque chose.

Dans notre cas, nous sommes obligés de faire une allocation dynamique, donc d'utiliser new  . Voici ce que cela donne dans le constructeur par défaut :

Personnage::Personnage() : m_arme(0), m_vie(100), m_mana(100)
{
    m_arme = new Arme();
}

Si vous vous souvenez bien, on avait aussi fait un second constructeur pour ceux qui voulaient que le Personnage commence avec une arme plus puissante dès le début. Il faut là aussi y faire une allocation dynamique :

Personnage::Personnage(string nomArme, int degatsArme) : m_arme(0), m_vie(100), m_mana(100)
{
    m_arme = new Arme(nomArme, degatsArme);
}

Voici sans plus attendre les explications :

  • new Arme()  appelle le constructeur par défaut de la classe  Arme  .

  • new Arme(nomArme, degatsArme)  appelle le constructeur surchargé.

  •  new renvoie l'adresse de l'objet créé, adresse qui est stockée dans notre pointeur m_arme  .

Désallocation de mémoire pour l'objet

Notre Arme étant un pointeur, lorsque l'objet de type Personnage est supprimé, l'Arme ne disparaît pas toute seule ! Si on se contente d'un new dans le constructeur, et qu'on ne met rien dans le destructeur, lorsque l'objet de type Personnage est détruit nous avons un problème :

Seul Personnage est supprimé

Pour résoudre ce problème, il faut faire un delete de Arme dans le destructeur du personnage afin que l' Arme soit supprimée avant le personnage :

Personnage::~Personnage()
{
    delete m_arme;
}

Maintenant, lorsque quelqu'un demande à détruire le Personnage  , il se passe ceci :

  1. Appel du destructeur, et donc, dans notre cas, suppression de l' Arme  (avec le delete  ) .

  2. Puis suppression du Personnage  .

Les deux objets sont bien supprimés, et la mémoire reste propre :

Tous les objets sont proprement supprimés

Par exemple :

void Personnage::attaquer(Personnage &cible)
{
    cible.recevoirDegats(m_arme.getDegats());
}

devient :

void Personnage::attaquer(Personnage &cible)
{
    cible.recevoirDegats(m_arme->getDegats());
}

​​Dans le screencast suivant, nous allons voir comment modifier notre attribut  m_arme  en un pointeur d’  Arme  . Il y a plusieurs étapes à ne pas manquer, voyons cela !

Utilisez le pointeur  this

Puisqu'on parle de POO et de pointeurs, je me dois de vous parler du pointeur this  .

Dans toutes les classes, on dispose d'un pointeur ayant pour nom this  , qui pointe vers l'objet actuel. Je reconnais que ce n'est pas simple à imaginer… Voici un schéma :

Le pointeur this

Chaque objet (ici de type Personnage  ) possède un pointeur this qui pointe vers l'objet lui-même !

Mais… à quoi peut bien servir this  ?

Par exemple : si vous êtes dans une méthode de votre classe, cette méthode doit renvoyer un pointeur vers l'objet auquel elle appartient. Sans le this  , on ne pourrait pas l'écrire.

Voilà ce que cela pourrait donner :

Personnage* Personnage::getAdresse() const
{
    return this;
}

Nous l'avons en fait déjà rencontré une fois, lors de la surcharge de l'opérateur+=  . Souvenez-vous, notre opérateur ressemblait à ceci :

Duree& Duree::operator+=(const Duree &duree2)
{
    //Des calculs compliqués…

    return *this;
}

this étant un pointeur sur un objet, *this est l'objet lui-même ! Notre opérateur renvoie donc l'objet lui-même.

Rappelez-vous du constructeur de copie

Comme nous l'avions vu brièvement, le constructeur de copie est une surcharge particulière du constructeur. Le constructeur de copie devient généralement indispensable dans une classe qui contient des pointeurs. Cela tombe bien vu que c'est précisément notre cas ici.

Comprenez l'intérêt du constructeur de copie

Reprenons concrètement ce qui se passe lorsqu'on crée un objet en lui affectant… un autre objet ! Par exemple :

int main()
{
    Personnage goliath("Epee aiguisee", 20);
    
    Personnage david(goliath);
    //On crée david à partir de goliath. david sera une "copie" de goliath.

    return 0;
}

Le rôle du constructeur de copie est de copier la valeur de tous les attributs du premier objet dans le second. Donc david récupère la vie de goliath  , le mana de goliath  , etc.

Dans quels cas le constructeur de copie est-il appelé ?

Le constructeur de copie est appelé lorsqu'on crée un nouvel objet en lui affectant la valeur d'un autre :

Personnage david(goliath); //Appel du constructeur de copie (cas 1)

Ceci est strictement équivalent à :

Personnage david = goliath; //Appel du constructeur de copie (cas 2)

Dans ce second cas, c'est aussi au constructeur de copie qu'on fait appel. Mais ce n'est pas tout ! Lorsque vous envoyez un objet à une fonction sans utiliser de pointeur ni de référence, l'objet est là aussi copié !

Imaginons la fonction :

void maFonction(Personnage unPersonnage)
{

}

Si vous appelez cette fonction qui n'utilise pas de pointeur ni de référence, alors l'objet sera copié en utilisant, au moment de l'appel de la fonction, un constructeur de copie :

maFonction(Goliath); //Appel du constructeur de copie (cas 3)

Bien entendu, il est généralement préférable d'utiliser une référence, car l'objet n'a pas besoin d'être copié. Cela va donc bien plus vite et nécessite moins de mémoire. Toutefois, il arrivera des cas où vous aurez besoin de créer une fonction qui, comme ici, fait une copie de l'objet.

Le problème ? Eh bien justement, il se trouve que, dans notre classe Personnage  , un des attributs est un pointeur ! Que fait l'ordinateur ? Il copie la valeur du pointeur, donc l'adresse de l' Arme  . Les 2 objets ont donc un pointeur qui pointe vers le même objet de type Arme  ! Ah les fourbes !

L'ordinateur a copié le pointeur, les deux Personnages pointent vers la même Arme

Le constructeur de copie généré automatiquement par le compilateur n'est pas assez intelligent pour comprendre qu'il faut allouer de la mémoire pour une autre Arme  … Qu'à cela ne tienne, nous allons le lui expliquer.

La création du constructeur de copie

Si vous ne trouvez pas cela clair, peut-être qu'un exemple vous aidera :

class Personnage
{
    public:

    Personnage();
    Personnage(Personnage const& personnageACopier);
    //Le prototype du constructeur de copie
    Personnage(std::string nomArme, int degatsArme);
    ~Personnage();
    
    /*
    … plein d'autres méthodes qui ne nous intéressent pas ici
    */

    private:

    int m_vie;
    int m_mana;
    Arme *m_arme;
};

En résumé, le prototype d'un constructeur de copie est :

Objet(Objet const& objetACopier);

Le const indique simplement que l'on n'a pas le droit de modifier les valeurs de l' objetACopier  (c'est logique, on a seulement besoin de "lire" ses valeurs pour le copier).

Écrivons l'implémentation de ce constructeur. Il faut copier tous les attributs du personnageACopier dans le Personnage actuel. Commençons par les attributs "simples", c'est-à-dire ceux qui ne sont pas des pointeurs :

Personnage::Personnage(Personnage const& personnageACopier) 
   : m_vie(personnageACopier.m_vie), m_mana(personnageACopier.m_mana), m_arme(0)
{

}

Comment cela se fait qu'on puisse accéder aux attributs m_vie et m_mana du personnageACopier  ? En effet, m_vie et m_mana sont privés, donc on ne peut pas y accéder depuis l'extérieur de la classe… Sauf qu'il y a une exception ici : on est dans une méthode de la classe Personnage  , et on a donc le droit d'accéder à tous les éléments (même privés) d'un autre Personnage  .

C'est un peu tordu, je l'avoue, mais dans le cas présent, cela nous simplifie grandement la vie.

Il reste maintenant à "copier" m_arme  . Si on écrit :

m_arme = personnageACopier.m_arme;

… on fait exactement la même erreur que le compilateur, c'est-à-dire qu'on ne copie que l'adresse de l'objet de type Arme et non l'objet en entier ! Pour résoudre le problème, il faut copier l'objet de type Arme en faisant une allocation dynamique, donc un new  . 

Si on fait :

m_arme = new Arme();

… on crée bien une nouvelle Arme mais on utilise le constructeur par défaut, donc cela crée l' Arme de base. Or on veut avoir exactement la même Arme que celle du personnageACopier  (eh oui, c'est un constructeur de copie).

Bonne nouvelle : le constructeur de copie est automatiquement généré par le compilateur. Tant que la classe n'utilise pas de pointeurs vers des attributs, il n'y a pas de danger. De plus, la classe Arme n'utilise pas de pointeur, on peut donc se contenter du constructeur qui a été généré.

Il faut alors appeler le constructeur de copie de Arme  , en passant en paramètre l'objet à copier. Vous pourriez penser qu'il faut faire ceci :

m_arme = new Arme(personnageACopier.m_arme);

Presque ! Sauf que m_arme est un pointeur, et le prototype du constructeur de copie est :

Arme(Arme const& arme);

… ce qui veut dire qu'il faut envoyer l'objet lui-même et pas son adresse. Vous vous souvenez de la manière d'obtenir l'objet (ou la variable) à partir de son adresse ? On utilise l'étoile * ! Cela donne :

m_arme = new Arme(*(personnageACopier.m_arme));

Cette ligne alloue dynamiquement une nouvelle arme, en se basant sur l'arme du personnageACopier  .

Le constructeur de copie une fois terminé

Le constructeur de copie correct ressemblera donc au final à ceci :

Personnage::Personnage(Personnage const& personnageACopier) 
   : m_vie(personnageACopier.m_vie), m_mana(personnageACopier.m_mana), m_arme(0)
{
    m_arme = new Arme(*(personnageACopier.m_arme));
}

Ainsi, nos deux personnages ont chacun une arme identique mais dupliquée, afin d'éviter les problèmes que je vous ai expliqués plus haut :

Chaque personnage a maintenant son arme

Rien de mieux qu’un petit screencast pour éclaircir les idées :

L'opérateur d'affectation

Nous avons déjà parlé de la surcharge des opérateurs, mais il y en a un que je ne vous ai pas présenté : il s'agit de l'opérateur d'affectation ( operator=  ).

La méthode operator= sera appelée dès qu'on essaie d'affecter une valeur à l'objet. C'est le cas, par exemple, si nous affectons à notre objet la valeur d'un autre objet :

david = goliath;
Personnage david = goliath; //Constructeur de copie
david = goliath; //operator=

Cette méthode effectue le même travail que le constructeur de copie. Écrire son implémentation est donc relativement simple, une fois qu'on a compris le principe, bien sûr.

Personnage& Personnage::operator=(Personnage const& personnageACopier) 
{
    if(this != &personnageACopier)
    //On vérifie que l'objet n'est pas le même que celui reçu en argument
    {
        m_vie = personnageACopier.m_vie; //On copie tous les champs
        m_mana = personnageACopier.m_mana;
	delete m_arme;
        m_arme = new Arme(*(personnageACopier.m_arme));
    }
    return *this; //On renvoie l'objet lui-même
}

Il y a tout de même quatre différences :

  1. Comme ce n'est pas un constructeur, on ne peut pas utiliser la liste d'initialisation, et donc tout se passe entre les accolades.

  2. Il faut penser à vérifier que l'on n'est pas en train de faire david=david  , que l'on travaille donc avec deux objets distincts. Il faut donc vérifier que leurs adresses mémoires ( this et &personnageACopier  ) soient différentes.

  3. Il faut renvoyer*this comme pour les opérateurs +=  , -=, etc. C'est une règle à respecter.

  4. Il faut penser à supprimer l'ancienne Arme avant de créer la nouvelle. C'est ce qui est fait au niveau de l'instruction delete  . Ceci n'était pas nécessaire dans le constructeur de copie puisque le personnage ne possédait pas d'arme avant.

Cet opérateur est toujours similaire à celui que je vous donne pour la classe Personnage  . Les seuls éléments qui changent d'une classe à l'autre sont les lignes figurant dans le if  . Je vous ai en quelque sorte donné la recette universelle.

Comme vous commencez à vous en rendre compte, la POO n'est pas simple, surtout quand on commence à manipuler des objets avec des pointeurs. Heureusement, vous aurez l'occasion de pratiquer, et vous allez petit à petit prendre l'habitude d'éviter les pièges des pointeurs.

En résumé

  • Pour associer des classes entre elles, on peut utiliser les pointeurs : une classe peut contenir un pointeur vers une autre classe.

  • Lorsque les classes sont associées par un pointeur, il faut veiller à bien libérer la mémoire afin que tous les éléments soient supprimés.

  • Il existe une surcharge particulière du constructeur appelée "constructeur de copie". C'est un constructeur appelé lorsqu'un objet doit être copié. Il est important de le définir lorsqu'un objet utilise des pointeurs vers d'autres objets.

  • Le pointeur this est un pointeur qui existe dans tous les objets. Il pointe vers l'objet lui-même.

Nous allons pouvoir faire une pause concernant les pointeurs, mais il n’est certainement pas l’heure de faire une sieste. Nous allons maintenant voir un des concepts les plus importants de la programmation orientée objet, je vous garde la surprise. Accrochez-vous bien !

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