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. Pointeurs et références dans la classe
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

Pointeurs et références dans la classe

Introduction

Ce chapitre, qui intègre pointeurs et références dans la classe, nous permet de compléter l’étude des fonctions spéciales commencée au chapitre Classes. Il s’agit des six fonctions pour lesquelles la norme C++ prévoit un fonctionnement par défaut. Trois concernent les constructeurs : constructeur par défaut, constructeur de copie, constructeur de déplacement. Deux se rapportent à l’affectation = : affectation de copie, affectation de déplacement. Et une concerne la libération de la mémoire allouée dynamiquement lors de la création d’une classe, il s’agit du destructeur à l’inverse du constructeur.

Par ailleurs, nous aborderons aussi quelques questions soit d’écriture, par exemple avec une référence en propriété, soit de conception comme pour obtenir un singleton, soit encore comment singulariser une classe générique en pointeur.

Ce qui concerne les mises en relation de classes entre elles grâce à des pointeurs se trouve traité au chapitre Pointeurs, utilisations classiques dans la section Relier des objets. Toujours sur la façon d’allier des classes entre elles, des approfondissements sont également proposés au chapitre Associations entre classes.

Le destructeur

Dans une classe en vis-à-vis des constructeurs figure aussi un destructeur. Son rôle consiste le plus souvent à nettoyer la mémoire lors de la disparition d’un objet.

1. Les éléments non dynamiques s’autodétruisent (rappel)

Une variable non dynamique, c’est-à-dire allouée automatiquement par la machine et non par le développeur, est visible dans le bloc où elle est déclarée et dans tous ses sous-blocs. Dans le chapitre Précisions sur les variables, attributs, nous avons vu que sa durée de vie correspond à celle du bloc, où elle est déclarée. À l’issue de l’exécution de son bloc, l’espace mémoire qui lui était réservé est libéré automatiquement sans intervention du développeur.

Par exemple, soit une classe Ligne qui embarque un tableau statique de structure point :

#include <iostream> 
 
struct Point { 
    int x = rand() % 100; 
    int y = rand() % 100; 
}; 
 
class Ligne 
{ 
private: 
    Point points[10]; 
public : 
    Ligne() = default; 
    void affiche()  
    {  
        for (Point p : points) 
            std::cout << '(' << p.x << ',' << p.y << ')'; 
    } 
}; 

Une fonction quelconque déclare un objet Ligne dans son bloc et affiche son contenu.

void fonct() 
{ 
    Ligne ligne;    // local à la fonction 
    ligne.affiche();...

Constructeur de copie

1. Principe

Un constructeur de copie peut initialiser un objet avec les valeurs d’un autre objet de même type déjà présent dans le programme.

Soit une classe M. Le constructeur de copie prend en paramètre une référence d’objet déjà existant de la classe M soit de type M& soit de type const M& et répond aux trois possibilités suivantes :

M(const M& )                   (1) 
M(const M& ) = default;        (2) 
M(const M& ) = delete;         (3) 

(1) Déclaration typique d’un constructeur de copie dont le contenu et le fonctionnement sont à définir par le développeur. La référence marquée const du paramètre assure que l’objet à copier ne sera pas modifié.

(2) L’affectation du mot-clé default force le compilateur à générer lui-même un constructeur de copie par défaut, ce constructeur effectue une copie membre à membre de l’objet M passé en paramètre vers le nouvel objet créé.

(3) L’affectation du mot-clé delete au constructeur a pour effet d’interdire la copie d’objet, elle annule le constructeur de copie.

2. Copier un objet à la déclaration

Si aucun constructeur de copie n’est défini, la copie par défaut effectue une copie membre à membre. Ce constructeur de copie implicite s’utilise comme un constructeur ordinaire, avec l’objet à copier entre parenthèses, ou avec l’opérateur d’affectation comme ci-dessous :

#include <iostream> 
 
class M 
{ ...

Constructeur de déplacement

Dans son principe, le constructeur de déplacement ne duplique pas le bloc mémoire d’un autre objet comme le constructeur de copie, mais il déplace le bloc en mémoire d’un objet source vers un objet de destination. Il permet par exemple d’affecter directement un objet source retourné par un constructeur à un objet de destination sans avoir en mémoire à effectuer de copie intermédiaire.

D’une façon générale, d’après les termes donnés dans la documentation C++ de Microsoft, le principe du déplacement permet de "déplacer directement des objets sans qu’il soit nécessaire d’exécuter des opérations d’allocation de mémoire et de copie coûteuses". Il peut être utilisé par exemple dans la gestion des conteneurs afin d’optimiser les insertions d’éléments.

Le constructeur de déplacement repose sur l’utilisation de références rvalue et de l’opérateur &&. Associé à des conversions rvalue comme forwatd<>, il facilite d’une façon générale l’écriture des constructeurs lorsque, dans une classe, des éléments se manipulent avec des références.

1. Principe

Dans son principe, le constructeur de déplacement déplace un objet source vers un objet de destination. À la différence de la copie, l’objet source doit être invalidé. Si l’objet source est un objet persistant (lvalue), le constructeur de déplacement de la classe annule ses valeurs. Cette opération touche tout particulièrement des éléments dynamiques contenus dans la classe. Si l’objet source est non persistant (rvalue), il devient...

Surcharge des opérateurs et données dynamiques

Cette section termine les questions de copie et de déplacement d’objets. De façon plus générale, elle complète avec des données dynamiques la surcharge des opérateurs présentée au chapitre Classes. En effet, avec les pointeurs se pose la question de gérer des données dynamiques. Avec elles, il faut garder à l’esprit que ce sont des adresses mémoire qui transitent. Il est alors indispensable de veiller à ce que les allocations mémoire soient toujours correctement effectuées et faire très attention au fait que des pointeurs d’objets différents peuvent pointer sur les mêmes données.

1. Affectation de copie (operator=)

Avec le constructeur de copie, il est possible d’initialiser un objet avec un autre objet à sa déclaration en écrivant par exemple :

Tab t1{10}; 
Tab t2 = t1; 

Si le constructeur de copie a été implémenté dans la classe, c’est lui qui opère l’affectation ; s’il n’est pas implémenté le constructeur de copie par défaut opère une copie membre des propriétés des deux objets.

Le principe est le même pour ce qui concerne une affectation réalisée en dehors d’une déclaration. Par exemple :

Tab t1{10}; 
Tab t2{5}; 
   (...) 
 t2 = t1; 

Si la classe implémente une surcharge de l’affectation, cette implémentation réalise l’opération, sinon c’est l’affectation par défaut qui opère une copie membre à membre des propriétés des objets.

Lorsqu’il n’y a pas de données dynamiques, une copie membre à membre...

Questions diverses

1. Spécialiser une classe générique en pointeur

Soit une classe générique de type T :

template<class T> class test { /*...*/} 

La spécialisation partielle permet par exemple de réécrire la classe test en cas de type pointeur sur T, ce qui donne la syntaxe :

template<class T> class test<T*> { /*...*/} 

La spécialisation est partielle parce que le type générique T reste nécessaire.

Voici une classe générique de type T :

#include <iostream> 
template<class T> class test 
{ 
public: 
    T val; 
    test(T n) : val(n) {} 
    void affiche(); 
}; 
template<class T> 
void test<T>::affiche() 
{ 
    std::cout << "cas general : " << val << '\n'; 
} 

Lorsque T est un pointeur, la classe se comporte différemment. Elle produit un tableau de T et nous obtenons la spécialisation suivante :

template<class T> class test<T*> 
{ 
public: 
    T* dat; 
    int nb; 
    test(int n); 
    void affiche(); 
}; 
template<class T> 
test<T*>::test(int n) : nb{ n <= 0 ? 0 : n } 
{ 
    dat = nb == 0 ? nullptr : new T[nb]{ 0 }; 
} 
template<class T> 
void test<T*>::affiche() 
{ 
    std::cout << "cas pointeur, un tableau : \n"; 
    for (int i = 0; i < nb; i++) 
        std::cout << dat[i] << "-"; ...