Costruttori, Distruttori e Regola dello 0/3/5
Padroneggia l'intero ciclo di vita degli oggetti C++ — dalla costruzione alla distruzione — e impara quando scrivere (o non scrivere) le funzioni membro speciali.
Il Ciclo di Vita degli Oggetti in C++
In C++ hai il controllo diretto su quando e come gli oggetti vengono creati e distrutti. Il compilatore può generare automaticamente fino a sei funzioni membro speciali: costruttore predefinito, costruttore di copia, operatore di assegnamento per copia, costruttore di spostamento, operatore di assegnamento per spostamento e distruttore. Capire quando scriverle tu — e quando lasciar fare al compilatore — è una delle abilità più importanti in C++.
Tipi di Costruttori
I costruttori inizializzano un oggetto. Il C++ ne supporta diversi tipi: predefinito (senza argomenti), parametrizzato, di conversione (argomento singolo, conversione implicita) e delegante (un costruttore ne chiama un altro).
#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;
}La Parola Chiave explicit
I costruttori a un solo argomento abilitano le conversioni implicite, che possono causare bug sottili. Usa explicit per impedire conversioni indesiderate — è quasi sempre ciò che vuoi.
#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;
}Liste di Inizializzazione dei Membri e Ordine di Inizializzazione
La lista di inizializzazione dei membri è il modo preferito per inizializzare i membri. È obbligatoria per i membri const, per i membri riferimento e per i membri privi di costruttore predefinito. I membri vengono inizializzati nell'ordine di dichiarazione, non nell'ordine in cui compaiono nella lista di inizializzazione.
#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;
}Distruttori e Regola dello Zero / del Tre / del Cinque
Un distruttore (~ClassName()) viene eseguito quando un oggetto viene distrutto — rilascia le risorse e non deve lanciare eccezioni. Per la maggior parte delle classi, il distruttore generato dal compilatore è sufficiente.
La Regola dello Zero: preferisci wrapper RAII (std::string, std::vector, std::unique_ptr) così da non dover mai scrivere alcuna funzione membro speciale. Se devi gestire una risorsa grezza, la Regola del Tre dice che ti servono un distruttore, un costruttore di copia e un operatore di assegnamento per copia. La Regola del Cinque estende questo elenco includendo il costruttore di spostamento e l'operatore di assegnamento per spostamento.
#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 e =delete
Puoi richiedere esplicitamente la versione generata dal compilatore di un membro speciale con =default, oppure sopprimerla esplicitamente con =delete. Questo rende chiare le tue intenzioni e documenta la semantica di copia e spostamento della 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;
}I membri vengono inizializzati nell'ordine in cui sono dichiarati nella classe, non nell'ordine in cui compaiono nella lista di inizializzazione. Se un membro dipende da un altro, assicurati che la dipendenza sia dichiarata per prima. I compilatori avvisano di questo con -Wreorder — non ignorare mai quel warning. Un bug classico: inizializzare size_ da un parametro e poi usare size_ per allocare data_, con data_ dichiarato prima di size_.
- Usa
explicitsui costruttori a un solo argomento per impedire conversioni implicite indesiderate - La lista di inizializzazione dei membri è obbligatoria per i membri
conste riferimento, ed è più efficiente per tutti i membri - I membri si inizializzano nell'ordine di dichiarazione, non in quello della lista di inizializzazione
- Punta alla Regola dello Zero — usa tipi RAII così che i membri speciali generati dal compilatore siano corretti
- Se scrivi uno tra distruttore, costruttore di copia e assegnamento per copia, scrivili tutti e cinque (Regola del Cinque)
=defaultdocumenta l'intenzione;=deletevieta esplicitamente un'operazione
Quiz — Metti alla prova le tue conoscenze
(20 XP)1. Cosa impedisce la parola chiave `explicit` su un costruttore?
2. In quale ordine vengono inizializzati i membri di una classe?
3. Che cos'è la Regola dello Zero?