Constructeurs, destructeurs et règle de 0/3/5
Maîtrisez le cycle de vie complet des objets C++ — de la construction à la destruction — et apprenez quand écrire (ou ne pas écrire) les fonctions membres spéciales.
Cycle de vie des objets en C++
En C++, vous avez un contrôle direct sur quand et comment les objets sont créés et détruits. Le compilateur peut générer automatiquement jusqu'à six fonctions membres spéciales : constructeur par défaut, constructeur de copie, opérateur d'assignation par copie, constructeur de déplacement, opérateur d'assignation par déplacement, et destructeur. Comprendre quand écrire les vôtres — et quand laisser le compilateur le faire — est l'une des compétences les plus importantes en C++.
Variétés de constructeurs
Les constructeurs initialisent un objet. Le C++ supporte plusieurs sortes : par défaut (sans arguments), paramétré, convertissant (argument unique, conversion implicite), et délégant (un constructeur en appelle un autre).
#include <iostream>
#include <string>
class Temperature {
double celsius_;
std::string label_;
public:
// Default constructor
Temperature() : celsius_{0.0}, label_{"unknown"} {}
// Parameterized constructor
Temperature(double c, const std::string& label)
: celsius_{c}, label_{label} {}
// Converting constructor — allows implicit conversion from double
Temperature(double c) : celsius_{c}, label_{"sensor"} {}
// Delegating constructor — reuses another constructor
Temperature(const std::string& label)
: Temperature{0.0, label} {} // delegates to parameterized
void print() const {
std::cout << label_ << ": " << celsius_ << "°C\n";
}
};
int main() {
Temperature t1; // default
Temperature t2{36.6, "body"}; // parameterized
Temperature t3 = 100.0; // converting (implicit)
Temperature t4{"outdoor"}; // delegating
t1.print(); // unknown: 0°C
t2.print(); // body: 36.6°C
t3.print(); // sensor: 100°C
t4.print(); // outdoor: 0°C
return 0;
}Le mot-clé explicit
Les constructeurs à argument unique permettent des conversions implicites, ce qui peut causer des bogues subtils. Utilisez explicit pour empêcher les conversions involontaires — c'est presque toujours ce que vous voulez.
#include <iostream>
#include <string>
class UserId {
int id_;
public:
explicit UserId(int id) : id_{id} {}
int get() const { return id_; }
};
void process(UserId uid) {
std::cout << "Processing user #" << uid.get() << '\n';
}
int main() {
UserId u1{42}; // OK: direct initialization
// UserId u2 = 42; // ERROR: implicit conversion blocked by explicit
// process(42); // ERROR: would require implicit conversion
process(UserId{42}); // OK: explicit construction
return 0;
}Listes d'initialisation des membres et ordre d'initialisation
La liste d'initialisation des membres est la méthode préférée pour initialiser les membres. Elle est requise pour les membres const, les membres référence et les membres sans constructeur par défaut. Les membres sont initialisés dans l'ordre de déclaration, pas dans l'ordre où ils apparaissent dans la liste d'initialisation.
#include <iostream>
#include <string>
class Connection {
const int max_retries_; // const: MUST use init list
std::string& log_target_; // reference: MUST use init list
std::string host_;
int port_;
public:
Connection(std::string& log, const std::string& host, int port)
: max_retries_{3} // const member
, log_target_{log} // reference member
, host_{host} // initialized in declaration order
, port_{port}
{
// Constructor body runs AFTER all members are initialized
log_target_ += "[Connection to " + host_ + " created]\n";
}
void print() const {
std::cout << host_ << ':' << port_
<< " (max retries: " << max_retries_ << ")\n";
}
};
int main() {
std::string log;
Connection conn{log, "db.example.com", 5432};
conn.print();
std::cout << log;
return 0;
}Destructeurs et règle de zéro / trois / cinq
Un destructeur (~ClassName()) s'exécute quand un objet est détruit — il libère les ressources et ne doit pas lancer d'exceptions. Pour la plupart des classes, le destructeur généré par le compilateur est suffisant.
La règle de zéro : préférez les wrappers RAII (std::string, std::vector, std::unique_ptr) pour ne jamais avoir besoin d'écrire de fonctions membres spéciales. Si vous devez gérer une ressource brute, la règle de trois dit que vous avez besoin d'un destructeur, d'un constructeur de copie et d'un opérateur d'assignation par copie. La règle de cinq étend cela pour inclure le constructeur de déplacement et l'opérateur d'assignation par déplacement.
#include <iostream>
#include <cstring>
// === Rule of Zero: the GOAL ===
// No special members needed — std::string handles everything
class Person {
std::string name_;
int age_;
public:
Person(const std::string& name, int age)
: name_{name}, age_{age} {}
// Compiler generates correct copy/move/destructor automatically!
};
// === Rule of Five: when managing a raw resource ===
class CharBuffer {
char* data_;
std::size_t size_;
public:
explicit CharBuffer(const char* str)
: size_{std::strlen(str)}
, data_{new char[std::strlen(str) + 1]}
{
std::strcpy(data_, str);
}
// Destructor
~CharBuffer() { delete[] data_; }
// Copy constructor (deep copy)
CharBuffer(const CharBuffer& other)
: size_{other.size_}
, data_{new char[other.size_ + 1]}
{
std::strcpy(data_, other.data_);
}
// Copy assignment (copy-and-swap idiom)
CharBuffer& operator=(CharBuffer other) { // note: by value
swap(*this, other);
return *this;
}
// Move constructor
CharBuffer(CharBuffer&& other) noexcept
: data_{other.data_}, size_{other.size_}
{
other.data_ = nullptr;
other.size_ = 0;
}
// Move assignment
CharBuffer& operator=(CharBuffer&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
friend void swap(CharBuffer& a, CharBuffer& b) noexcept {
using std::swap;
swap(a.data_, b.data_);
swap(a.size_, b.size_);
}
void print() const {
std::cout << (data_ ? data_ : "(null)") << '\n';
}
};
int main() {
CharBuffer a{"Hello"};
CharBuffer b = a; // copy constructor
CharBuffer c{"World"};
c = a; // copy assignment
CharBuffer d = std::move(a); // move constructor
d.print(); // Hello
a.print(); // (null) — moved-from state
return 0;
}=default et =delete
Vous pouvez explicitement demander la version générée par le compilateur d'un membre spécial avec =default, ou la supprimer explicitement avec =delete. Cela rend votre intention claire et documente la sémantique de copie/déplacement de la classe.
#include <memory>
#include <string>
// Non-copyable, movable resource handle
class FileHandle {
std::string path_;
int fd_;
public:
FileHandle(const std::string& path, int fd)
: path_{path}, fd_{fd} {}
// Explicitly default the destructor
~FileHandle() = default;
// Delete copy operations — file handles should not be duplicated
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// Default move operations
FileHandle(FileHandle&&) = default;
FileHandle& operator=(FileHandle&&) = default;
};
int main() {
FileHandle fh{"data.txt", 3};
// FileHandle copy = fh; // ERROR: copy deleted
FileHandle moved = std::move(fh); // OK: move is allowed
return 0;
}Les membres sont initialisés dans l'ordre où ils sont déclarés dans la classe, pas dans l'ordre où ils apparaissent dans la liste d'initialisation. Si un membre dépend d'un autre, assurez-vous que la dépendance est déclarée en premier. Les compilateurs avertissent à ce sujet avec -Wreorder — n'ignorez jamais cet avertissement. Un bogue classique : initialiser size_ à partir d'un paramètre puis utiliser size_ pour allouer data_, avec data_ déclaré avant size_.
- Utilisez
explicitsur les constructeurs à argument unique pour empêcher les conversions implicites involontaires - La liste d'initialisation des membres est requise pour les membres
constet référence, et plus efficace pour tous les membres - Les membres s'initialisent dans l'ordre de déclaration, pas dans l'ordre de la liste d'initialisation
- Visez la règle de zéro — utilisez des types RAII pour que les membres spéciaux générés par le compilateur soient corrects
- Si vous écrivez l'un parmi destructeur/constructeur de copie/assignation de copie, écrivez les cinq (règle de cinq)
=defaultdocumente l'intention ;=deleteinterdit explicitement une opération
Quiz — Testez vos connaissances
(20 XP)1. Qu'empêche le mot-clé `explicit` sur un constructeur ?
2. Dans quel ordre les membres de classe sont-ils initialisés ?
3. Qu'est-ce que la règle de zéro ?