Mis à jour le 04/12/2018
  • 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 !

Associez les classes et les pointeurs

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

Dans les chapitres précédents, j'ai volontairement évité d'utiliser les pointeurs avec les classes. En effet, les pointeurs en C++ sont un vaste et sensible sujet. Comme vous l'avez probablement remarqué par le passé, bien gérer les pointeurs est essentiel car, à la moindre erreur, votre programme risque de :

  • consommer trop de mémoire parce que vous oubliez de libérer certains éléments ;

  • planter si votre pointeur pointe vers n'importe où dans la mémoire.

Comment associe-t-on classes et pointeurs ? Quelles sont les règles à connaître, les bonnes habitudes à prendre ?
Voilà un sujet qui méritait au moins un chapitre à lui tout seul !

Pointeur d'une classe vers une autre classe

Reprenons notre classePersonnage.
Dans les précédents chapitres, nous lui avons ajouté une Armeque 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 différentes d'associer des classes entre elles. Celle-ci fonctionne bien dans notre cas mais l'Armeest vraiment « liée » auPersonnage, elle ne peut pas en sortir.

Schématiquement, cela donnerait quelque chose de comparable à la figure suivante :

Une classe contenant une autre classe

Vous le voyez, l'Armeest vraiment dans lePersonnage.

Il y a une autre technique, plus souple, qui offre plus de possibilités mais qui est plus complexe : ne pas intégrer l'Armeau Personnageet 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
    //…
};

Notre Armeétant un pointeur, on ne peut plus dire qu'elle appartient auPersonnage

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

On considère que l'Armeest maintenant externe auPersonnage.
Les avantages de cette technique sont les suivants :

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

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

  • Si le Personnagen'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.

Gestion de 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 Personnageet je suppose que vous avez mis l'attribut m_armeen 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
    //…
};

Notre 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

À votre avis, où se fait l'allocation de mémoire pour notreArme?
Il n'y a pas 36 endroits pour cela : c'est dans le constructeur. C'est en effet le rôle du constructeur de 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 Personnagecommence 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 Armetandis que new Arme(nomArme, degatsArme)appelle le constructeur surchargé. Le newrenvoie l'adresse de l'objet créé, adresse qui est stockée dans notre pointeur m_arme.

Par sécurité, on initialise d'abord le pointeur à 0 dans la liste d'initialisation puis on fait l'allocation avec le newentre les accolades du constructeur.

Désallocation de mémoire pour l'objet

Notre Armeétant un pointeur, lorsque l'objet de type Personnageest supprimé, l'Arme ne disparaît pas toute seule ! Si on se contente d'un newdans le constructeur, et qu'on ne met rien dans le destructeur, lorsque l'objet de type Personnageest détruit nous avons un problème (figure suivante).

Seul Personnage est supprimé
Seul Personnage est supprimé

L'objet de type Personnagedisparaît bel et bien mais l'objet de type Armesubsiste en mémoire et il n'y a plus aucun pointeur pour se « rappeler » son adresse. En clair, l'Armeva traîner en mémoire et on ne pourra plus jamais la supprimer. C'est ce qu'on appelle une fuite de mémoire.

Pour résoudre ce problème, il faut faire un deletede l'Armedans le destructeur du personnage afin que l'Armesoit supprimée avant le personnage. Le code est tout simple :

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

Cette fois le destructeur est réellement indispensable. Maintenant, lorsque quelqu'un demande à détruire lePersonnage, il se passe ceci :

  1. Appel du destructeur… et donc, dans notre cas, suppression de l'Arme(avec ledelete) ;

  2. Puis suppression duPersonnage.

Au final, les deux objets sont bel et bien supprimés et la mémoire reste propre :

Tous les objets sont proprement supprimés
Tous les objets sont proprement supprimés

N'oubliez pas que m_armeest maintenant un pointeur !

Cela implique de changer toutes les méthodes qui l'utilisent. Par exemple :

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

… devient :

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

Notez la différence : le point a été remplacé par la flèche carm_armeest un pointeur.

Le pointeur  this

Ce chapitre étant difficile, je vous propose un passage un peu plus cool. 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 mais je pense que cela passera mieux avec un schéma maison :

Le pointeur this
Le pointeur this

Chaque objet (ici de typePersonnage) possède un pointeur thisqui pointe vers… l'objet lui-même !

Mais… à quoi peut bien servir this?

Répondre à cette question me sera délicat.
En revanche je peux vous donner un exemple : vous êtes dans une méthode de votre classe et cette méthode doit renvoyer un pointeur vers l'objet auquel elle appartient. Sans lethis, 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,*thisest l'objet lui-même ! Notre opérateur renvoie donc l'objet lui-même. La raison pour laquelle on doit renvoyer l'objet est compliquée mais c'est la forme correcte des opérateurs. Je vous propose donc simplement d'apprendre cette syntaxe par cœur.

À part pour la surcharge des opérateurs, vous n'avez certainement pas à utiliserthisdans l'immédiat mais il arrivera un jour où, pour résoudre un problème particulier, vous aurez besoin d'un tel pointeur. Ce jour-là, souvenez-vous qu'un objet peut « retrouver » son adresse à l'aide du pointeurthis.

Comme c'est l'endroit le plus adapté pour en parler dans ce cours, j'en profite. Cela ne va pas changer votre vie tout de suite mais il se peut que, bien plus tard, dans plusieurs chapitres, je vous dise tel un vieillard sur sa canne « Souvenez-vous, souvenez-vous du pointeurthis! ». Alors ne l'oubliez pas !

Retour sur le constructeur de copie

Comme nous l'avons 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 et cela tombe bien vu que c'est précisément notre cas ici.

Le problème

Pour bien comprendre 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("Epée aiguisée", 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 davidrécupère la vie de goliath, le mana de goliath, etc.

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

On vient de le voir, 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 classePersonnage, un des attributs est un pointeur ! Que fait l'ordinateur ? Il copie la valeur du pointeur, donc l'adresse de l'Arme. Au final, les 2 objets ont un pointeur qui pointe vers le même objet de typeArme!
Ah les fourbes !

L'ordinateur a copié le pointeur, les deux Personnages pointent vers la même Arme
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 autreArme… Qu'à cela ne tienne, nous allons le lui expliquer.

Création du constructeur de copie

Le constructeur de copie, comme je vous l'ai dit un peu plus haut, est une surcharge particulière du constructeur, qui prend pour paramètre… une référence constante vers un objet du même type !
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);

Leconstindique 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 dupersonnageACopierdans lePersonnageactuel. 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)
{

}

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 typeArmeet non l'objet en entier !

Pour résoudre le problème, il faut copier l'objet de typeArmeen faisant une allocation dynamique, donc unnew. Attention, accrochez-vous parce que ce n'est pas simple.

Si on fait :

m_arme = new Arme();

… on crée bien une nouvelleArmemais on utilise le constructeur par défaut, donc cela crée l'Armede base. Or on veut avoir exactement la mêmeArmeque celle dupersonnageACopier(eh bien oui, c'est un constructeur de copie).

La bonne nouvelle, c'est que 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. Et cela tombe bien, la classeArmen'utilise pas de pointeur, on peut donc se contenter du constructeur qui a été généré.

Il faut donc appeler le constructeur de copie deArme, 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 quem_armeest 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 au final :

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

Cette ligne alloue dynamiquement une nouvelle arme, en se basant sur l'arme dupersonnageACopier. Pas simple, je le reconnais, mais relisez plusieurs fois les étapes de mon raisonnement et vous allez comprendre.
Pour bien suivre tout ce que j'ai dit, il faut vraiment que vous soyez au point sur tout : les pointeurs, les références, et les… constructeurs de copie.

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 (figure suivante).

Chaque personnage a maintenant son arme

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;

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 :

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

  • 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 (thiset&personnageACopier) soient différentes.

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

  • Il faut penser à supprimer l'ancienne Armeavant de créer la nouvelle. C'est ce qui est fait au niveau de l'instruction delete, surlignée dans le code. 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 classePersonnage. 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.

Il y a une chose importante à retenir au sujet de cet opérateur : il va toujours de pair avec le constructeur de copie.

C'est une règle très importante à respecter. Vous risquez de graves problèmes de pointeurs si vous ne la respectez pas.

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 tout cela par la suite 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 pointeurthisest un pointeur qui existe dans tous les objets. Il pointe vers… l'objet lui-même.

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