Constructeur, destructeur & allocation dynamique
Cycle de vie d'un objet, listes d'initialisation, new/delete, constructeur de copie, opérateur d'affectation, fonction amie.
📄 Ressources du cours
🎥 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
- 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 ::
:: qui relie la méthode à sa classe.
<nom_classe>::<nom_méthode>(args) { ... }
Listes d'initialisation
: 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 !
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é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
~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
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émoire | Usage | Taille |
|---|---|---|
| Code | Code binaire des fonctions | Fixe |
| Données statiques | Variables globales et statiques | Fixe |
| Pile (stack) | Appels de fonctions, variables locales | Variable 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
}
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
}
Pour
new T[n], le compilateur appelle n fois le constructeur par défaut. Sans constructeur sans arguments, ça ne compile pas.
new↔deletenew[]↔delete[]
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éclaration | Signification |
|---|---|
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
- 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} { }
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
}
thisChaque 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)
- Destructeur (libérer)
- Constructeur de copie (deep copy)
- Opérateur d'affectation (libérer puis deep copy)
delete, fuites, données partagées par erreur.
6. Fonctions amies
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é
- 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
:: dans Point::setX(int) ?class { }.: x{a}, y{b} {} à l'affectation dans le corps ?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.~NomClasse · ② Pas de paramètre, pas de retour · ③ Appelé automatiquement à la fin de vie. Rôle : libérer les ressources.new, gérés manuellement, grande taille.delete après new ?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.Vector v2 = v1). Affectation = modification d'un objet déjà construit (v2 = v1 après Vector v2;).delete au final.this ?*this = l'objet déférencé. Utile pour return *this; (chaînage).friend ?operator<< pour afficher un objet.✎ Quiz éclair
Fraction f(); ?Fraction f; sans parenthèses.Fraction* p = new Fraction[10];, comment libérer ?new[] correspond delete[]. Mélanger avec delete est undefined behavior.Vector v2 = v1; où Vector contient un pointeur, quel danger sans constructeur de copie défini ?Fraction* p = new Fraction[5]; ?new T[n] appelle n fois le constructeur par défaut. Ce constructeur doit donc exister.operator= renvoie-t-il *this ?*this (par référence) permet d'enchaîner les affectations : a = (b = c);.Score : 0 / 8 ·