Le carnet On est
Séance 2 · encre verte

Constructeur, destructeur & allocation dynamique

Cycle de vie d'un objet, listes d'initialisation, new/delete, constructeur de copie, opérateur d'affectation, fonction amie.

~ 55 min Source : Cours2 Constructeur Destructeur 14 flashcards · 8 QCM

📄 Ressources du cours

📥 Télécharger les slides  ·  🔗 Ouvrir dans un nouvel onglet  ·  📝 Ouvrir le TD/TP

🎥 Vidéos recommandées

  • 🇫🇷 Constructeurs de copie

    MOOC EPFL Introduction à la programmation orientée objet (en C++) · 07:55 · 18k vues — Cette vidéo est reprise d'un MOOC (cours en-ligne) donné sur Coursera : https://www.coursera.org/course/intropoocpp

  • 🇫🇷 Programmer en C++ #04 : Les constructeurs

    coursaline · 07:07 · 13k vues — Lorsqu’un objet d’une classe est créé, ses donnée membres peuvent être initialisées par la fonction constructeur de cette classe. Un constructeur est une fon...

  • 🇬🇧 C++ Copy constructors (beginner-friendly tutorial + practical examples)

    CodeBeauty · 26:39 · 67k vues — This is an OOP tutorial where I'll teach you about copy constructors in C++. After watching this video, you'll learn what is the purpose of copy constructors...

1. Le constructeur

Définition Le constructeur est une fonction membre particulière :
  • elle a le même nom que la classe
  • elle n'a pas de type de retour (même pas void)
  • son rôle est d'initialiser un objet (affecter des valeurs aux données membres)

Une classe peut avoir plusieurs constructeurs (surcharge). Exemple avec une classe Fraction :

class Fraction {
   private:
      int numerateur;
      int denominateur;
   public:
      Fraction();                    // constructeur par défaut
      Fraction(int n);              // 1 paramètre
      Fraction(int n, int d);       // 2 paramètres
      ~Fraction();                   // destructeur
};

Opérateur de résolution de portée ::

Séparer déclaration et implémentation Pour garder une vue d'ensemble lisible, on écrit la définition des méthodes en dehors de la classe avec l'opérateur :: qui relie la méthode à sa classe.
<nom_classe>::<nom_méthode>(args) { ... }

Listes d'initialisation

Deux syntaxes équivalentes Avant les accolades de corps, on peut utiliser une liste d'initialisation avec : et des { }. Plus efficace que d'affecter dans le corps (évite une double initialisation).
// Version classique (affectation dans le corps)
Fraction::Fraction() {
   numerateur = 0;
   denominateur = 1;
}

// Version moderne (liste d'initialisation) — préférée
Fraction::Fraction() : numerateur{0}, denominateur{1} {}

Fraction::Fraction(int n) : numerateur{n}, denominateur{1} {}

Fraction::Fraction(int n, int d) : numerateur{n}, denominateur{d} {}

Appel des constructeurs

Fraction f1;            // constructeur par défaut — ⚠ pas de () vides
Fraction f2(10);        // constructeur 1 paramètre
Fraction f3(5, 6);      // constructeur 2 paramètres

// Initialisation à partir d'un autre objet
Fraction f4(f3);        // constructeur de copie (voir §5)
Fraction f5 = f3;       // idem — pas une affectation !
⚠ Piège classique Fraction f1(); n'est pas un appel du constructeur par défaut — c'est la déclaration d'une fonction qui renvoie une Fraction ! La bonne syntaxe est Fraction f1; tout court.

2. Le destructeur

Définition Le destructeur est une fonction membre :
  • déclarée du même nom que la classe, précédé d'un ~ (tilde)
  • sans type de retour ni paramètre
  • appelée automatiquement à la fin de vie de l'objet
Son rôle principal : libérer les ressources éventuellement mobilisées (mémoire, fichiers, sockets…).
~NomClasse() {
   // libération des ressources
}
  • Le destructeur n'admet pas d'argument → impossible à surcharger
  • Toute classe admet exactement un destructeur
  • S'il n'est pas défini, le compilateur en génère un minimaliste

Exemple — compteur d'instances

long counter(0);   // variable globale

class Rectangle {
   int hauteur, largeur;
public:
   Rectangle() : hauteur{0}, largeur{0} { ++counter; }  // constructeur
   ~Rectangle() { --counter; }                          // destructeur
};

int main() {
   Rectangle r1;          // counter = 1
   {
      Rectangle r2;       // counter = 2
   }                      // r2 sort du scope : ~Rectangle() appelé, counter = 1
}                         // r1 détruit, counter = 0
📌 Cycle de vie
Constructeur appelé à la création de l'objet. Destructeur appelé quand l'objet sort de portée (variable locale en pile), ou quand on fait delete (allocation dynamique).

3. Allocation dynamique — new / delete

Pile vs Tas

Zone mémoireUsageTaille
CodeCode binaire des fonctionsFixe
Données statiquesVariables globales et statiquesFixe
Pile (stack)Appels de fonctions, variables localesVariable mais limitée
Tas (heap)Données allouées dynamiquement (new)Variable, grande

Allocation sur la pile vs sur le tas

Sur la pile — automatique

void test() {
   Fraction f3(5, 6);
   // f3 vit jusqu'à la fin du scope
}  // ~Fraction() appelé automatiquement

Sur le tas — manuel

void test() {
   Fraction* pf;
   pf = new Fraction(5, 6);
   // ...
   delete pf;  // IMPORTANT
}
⚠ Fuite mémoire Tout objet créé avec new doit être détruit avec delete. Sinon : memory leak. C++ n'a pas de garbage collector.

Tableau dynamique — new[] / delete[]

void test() {
   int n;
   cout << "Nombre de fractions ?";
   cin >> n;

   Fraction* pf = new Fraction[n];   // n constructeurs par défaut appelés
   for (int i = 0; i < n; i++) {
      pf[i] = Fraction(i, i+1);
   }

   delete[] pf;  // les n destructeurs sont appelés
}
🔑 Le constructeur par défaut DOIT exister
Pour new T[n], le compilateur appelle n fois le constructeur par défaut. Sans constructeur sans arguments, ça ne compile pas.
Règle d'or
  • new   ↔   delete
  • new[] ↔  delete[]
Jamais mélanger — undefined behavior garanti.

4. Rappels — pointeurs et tableaux

  • Un tableau est une zone mémoire contenant N éléments consécutifs de même type
  • Le nom d'un tableau est un pointeur constant sur le premier élément (indice 0)
  • Un tableau n'est pas modifiable (lvalue non modifiable) — on ne peut pas faire tab = …;
Fraction tab[20];
Fraction* pf;
pf = &tab[0];     // équivaut à : pf = tab;
pf++;              // pf pointe sur le 2e élément
tab++;             // ERREUR — tab est un pointeur constant

Bonus de typage

Fraction tab[3];
Fraction* p;
Fraction** pp;
Fraction (*pf)[3];   // pointeur sur tableau de 3 Fractions

p  = &tab;   // WARNING : types incompatibles
pp = &tab;   // WARNING : types incompatibles
pf = &tab;   // OK — types identiques

Résumé des déclarations

DéclarationSignification
Fraction t1[3];Tableau de 3 Fractions
Fraction* p1;Pointeur de Fraction
Fraction t2[3][4];3 tableaux de 4 Fractions → 12 Fractions
Fraction** p2;Pointeur de pointeur de Fraction
Fraction* t3[3];Tableau de 3 pointeurs de Fraction
Fraction (*p3)[3];Pointeur sur tableau de 3 Fractions

5. Constructeur de copie & opérateur d'affectation

Deux opérations à distinguer
  • Construction de copie : on crée un nouvel objet à partir d'un existant
  • Affectation : on modifie un objet existant à partir d'un autre
Fraction f1(5, 6);

Fraction f2(f1);    // constructeur de copie
Fraction f2 = f1;   // idem — initialisation

Fraction f3;
f3 = f1;            // opérateur d'affectation (objet déjà construit)

Comportement par défaut

Pour les classes simples, la copie/affectation par défaut est une copie membre à membre. Ça pose problème dès qu'il y a de l'allocation dynamique :

Exemple problématique — classe Vector

// vector.h
class Vector {
   private:
      double* elem;
      unsigned int sz;
   public:
      Vector(unsigned int s);    // constructeur
      ~Vector() { delete[] elem; } // destructeur
};

// vector.cpp
Vector::Vector(unsigned int s) : elem{new double[s]}, sz{s} { }
⚠ Copie superficielle (shallow copy) Si on fait Vector v2 = v1; avec la copie par défaut :
Les deux objets partagent la même adresse mémoire pour elem. Quand v1 ou v2 est détruit, l'autre devient invalide. Pire : delete[] appelé deux fois ⇒ crash.

Constructeur de copie — solution

class Vector {
public:
   Vector(const Vector& a);                // constructeur de copie
   Vector& operator=(const Vector& a);     // opérateur d'affectation
};

// Implémentation du constructeur de copie
Vector::Vector(const Vector& a)
   : elem{new double[a.sz]}, sz{a.sz}     // allocation indépendante
{
   for (unsigned int i = 0; i < sz; ++i)
      elem[i] = a.elem[i];                  // copie terme à terme
}

Opérateur d'affectation — solution

Vector& Vector::operator=(const Vector& a) {
   sz = a.sz;
   delete[] elem;                      // libérer l'ancien contenu
   elem = new double[sz];               // nouvelle zone
   for (unsigned int i = 0; i < sz; ++i)
      elem[i] = a.elem[i];               // copie terme à terme
   return *this;                       // permet le chaînage v1 = v2 = v3
}
🔑 Le pointeur this
Chaque objet C++ a accès à sa propre adresse via un pointeur appelé this. Seules les fonctions membres ont ce pointeur. *this est l'objet courant déférencé.

Règle de trois (rule of three)

Si tu dois écrire l'un, écris les trois Pour toute classe qui gère une ressource (mémoire dynamique, fichier…) :
  1. Destructeur (libérer)
  2. Constructeur de copie (deep copy)
  3. Opérateur d'affectation (libérer puis deep copy)
Oublier l'un des trois mène à des bugs subtils : double delete, fuites, données partagées par erreur.

6. Fonctions amies

Pourquoi ? En théorie, une fonction non-membre n'a pas accès aux attributs/méthodes privés ou protégés. Mais en la déclarant friend, elle gagne ce privilège.

Cas typique — surcharger << pour afficher

// fraction.h
class Fraction {
   private:
      int numerateur;
      int denominateur;
   public:
      friend ostream& operator<<(ostream& out, const Fraction& f);
};

// fraction.cpp
ostream& operator<<(ostream& out, const Fraction& f) {
   out << "Fraction : " << f.numerateur << "/" << f.denominateur;
   return out;
}

// main.cpp
Fraction f(5, 6);
cout << f << endl;   // "Fraction : 5/6"

Sans friend, l'operator<< ne pourrait pas accéder aux private de Fraction.

Règles de l'amitié

L'amitié n'est ni héritée, ni transitive, ni réciproque
  • Je ne fais pas confiance aux enfants de mes amis — les sous-classes d'un ami ne sont pas amies
  • Je ne fais pas confiance aux amis de mes amis — pas de transitivité
  • L'amitié n'est pas réciproque — si A est ami de B, B n'est pas automatiquement ami de A

Réviser le chapitre

Pour vérifier ta compréhension

Quand est appelé le constructeur de copie ?

Dans 3 cas : (1) construction d'un objet à partir d'un autre du même type (Point p2(p1); ou Point p2 = p1;), (2) passage par valeur d'un objet en paramètre de fonction, (3) retour par valeur d'un objet depuis une fonction.

Quelle est la signature canonique d'un constructeur de copie ?

NomClasse(const NomClasse& autre). Référence obligatoire (sinon récursion infinie) et const par convention pour pouvoir copier des objets temporaires.

Quel est le rôle du destructeur ?

Libérer les ressources détenues par l'objet (mémoire allouée avec new, fichiers ouverts, connexions…). Il est appelé automatiquement quand l'objet sort de portée (fin de bloc, fin de programme, ou delete pour un pointeur). Signature : ~NomClasse(), jamais de paramètre, jamais d'arguments.

Qu'est-ce que la rule of three ?

Si une classe doit définir manuellement l'un des trois éléments — destructeur, constructeur de copie, opérateur d'affectation — elle doit probablement les définir tous les trois. C'est typiquement le cas quand la classe gère une ressource (mémoire allouée, fichier…).

Comment éviter une double libération lors d'une copie ?

Implémenter un constructeur de copie qui fait une copie profonde (allouer de nouvelles ressources et copier le contenu) plutôt qu'une copie superficielle (copie du pointeur). Sans cela, deux objets partagent la même mémoire et leurs destructeurs essaient de la libérer deux fois — undefined behavior.

🃏 Flashcards

3 caractéristiques d'un constructeur ?
① Même nom que la classe · ② Pas de type de retour (même pas void) · ③ Rôle : initialiser les données membres.
À quoi sert :: dans Point::setX(int) ?
Opérateur de résolution de portée — relie la définition de la méthode à la classe quand on écrit l'implémentation en dehors du class { }.
Pourquoi préférer : x{a}, y{b} {} à l'affectation dans le corps ?
La liste d'initialisation initialise directement les membres. L'affectation dans le corps les construit d'abord puis les écrase — deux fois plus de travail.
Fraction f1; vs Fraction f1(); ?
Fraction f1; = appel du constructeur par défaut. Fraction f1(); = déclaration d'une fonction nommée f1 qui retourne une Fraction ! Piège classique.
3 caractéristiques d'un destructeur ?
① Nom = ~NomClasse · ② Pas de paramètre, pas de retour · ③ Appelé automatiquement à la fin de vie. Rôle : libérer les ressources.
Peut-on surcharger le destructeur ?
Non — il n'admet pas d'argument. Une classe a exactement un destructeur. Si on ne l'écrit pas, le compilateur en génère un minimaliste.
Différence pile / tas ?
Pile : variables locales, gérée automatiquement, taille limitée. Tas : objets dynamiques avec new, gérés manuellement, grande taille.
Que se passe-t-il si on oublie delete après new ?
Memory leak (fuite mémoire). Le programme garde la mémoire allouée même si plus utile. À long terme : épuise la RAM.
Quand utiliser delete[] vs delete ?
delete[] pour un tableau alloué avec new T[n]. delete pour un objet unique alloué avec new T(…). Jamais mélanger ⇒ undefined behavior.
Différence copie et affectation ?
Copie = création d'un nouvel objet (Vector v2 = v1). Affectation = modification d'un objet déjà construit (v2 = v1 après Vector v2;).
Pourquoi écrire son propre constructeur de copie ?
Quand la classe gère une ressource dynamique (pointeur, fichier). La copie par défaut fait une shallow copy — deux objets partagent la même mémoire → double delete au final.
Énonce la « règle de trois »
Si tu dois écrire l'un de ces trois, écris les trois : destructeur, constructeur de copie, opérateur d'affectation. Typique pour les classes gérant des ressources.
Que représente this ?
Un pointeur sur l'objet courant, disponible dans toute fonction membre. *this = l'objet déférencé. Utile pour return *this; (chaînage).
Quand utiliser friend ?
Pour permettre à une fonction non-membre d'accéder aux private/protected. Cas typique : surcharger operator<< pour afficher un objet.

✎ Quiz éclair

1.Quel type retourne un constructeur ?
  • void
  • Le type de la classe
  • Aucun (pas même void)
  • int (code de retour)
Un constructeur n'a pas de type de retour, c'est une spécificité du langage.
2.Que fait exactement Fraction f(); ?
  • Appelle le constructeur par défaut
  • Déclare une fonction nommée f qui retourne une Fraction
  • Crée un pointeur sur Fraction
  • Provoque une erreur de compilation
Piège classique. Pour le constructeur par défaut : Fraction f; sans parenthèses.
3.Pour appeler le destructeur d'une variable locale, on doit :
  • Ne rien faire — il est appelé automatiquement
  • Faire ~MaClasse(obj)
  • Faire delete obj
  • Faire obj.~MaClasse()
Les destructeurs des variables locales sont appelés automatiquement à la fin du scope.
4.Avec Fraction* p = new Fraction[10];, comment libérer ?
  • delete p;
  • free(p);
  • Rien (libéré automatiquement)
  • delete[] p;
À new[] correspond delete[]. Mélanger avec delete est undefined behavior.
5.Pour Vector v2 = v1; où Vector contient un pointeur, quel danger sans constructeur de copie défini ?
  • Erreur de compilation
  • Aucun, deep copy par défaut
  • Shallow copy : v1 et v2 partagent le même tableau → double delete
  • Memory leak garanti
La copie par défaut copie membre à membre, donc le pointeur (et pas ce qu'il pointe). Deux objets partagent la même zone — quand l'un meurt, l'autre est invalide.
6.Combien de fois le constructeur est-il appelé pour Fraction* p = new Fraction[5]; ?
  • 0
  • 1
  • 5 (constructeur par défaut)
  • 6 (1 pour le tableau + 5 pour les éléments)
new T[n] appelle n fois le constructeur par défaut. Ce constructeur doit donc exister.
7.Quelle signature pour un constructeur de copie standard ?
  • MaClasse(const MaClasse& a);
  • MaClasse(MaClasse a);
  • MaClasse* MaClasse(MaClasse* a);
  • void copy(const MaClasse& a);
Référence constante pour éviter une copie infinie et garantir qu'on ne modifie pas la source.
8.Pourquoi operator= renvoie-t-il *this ?
  • Pour appeler le destructeur
  • Pour permettre le chaînage a = b = c;
  • C'est obligatoire pour compiler
  • Pour éviter les fuites mémoire
Renvoyer *this (par référence) permet d'enchaîner les affectations : a = (b = c);.

Score : 0 / 8 ·