Constructores, Destructores y la Regla de 0/3/5
Domina el ciclo de vida completo de los objetos C++ — desde la construccion hasta la destruccion — y aprende cuando escribir (o no escribir) funciones miembro especiales.
Ciclo de Vida de Objetos en C++
En C++, tienes control directo sobre cuando y como los objetos se crean y destruyen. El compilador puede generar automaticamente hasta seis funciones miembro especiales: constructor por defecto, constructor de copia, operador de asignacion por copia, constructor de movimiento, operador de asignacion por movimiento y destructor. Entender cuando escribir los tuyos — y cuando dejar que el compilador lo haga — es una de las habilidades mas importantes en C++.
Variedades de Constructores
Los constructores inicializan un objeto. C++ soporta varios tipos: por defecto (sin argumentos), parametrizado, de conversion (un solo argumento, conversion implicita) y delegante (un constructor llama a otro).
#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 Palabra Clave explicit
Los constructores de un solo argumento habilitan conversiones implicitas, que pueden causar errores sutiles. Usa explicit para prevenir conversiones no intencionadas — esto es casi siempre lo que quieres.
#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;
}Listas de Inicializacion de Miembros y Orden de Inicializacion
La lista de inicializacion de miembros es la forma preferida de inicializar miembros. Es requerida para miembros const, miembros referencia y miembros sin constructor por defecto. Los miembros se inicializan en orden de declaracion, no en el orden en que aparecen en la lista de inicializacion.
#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;
}Destructores y la Regla de Cero / Tres / Cinco
Un destructor (~ClassName()) se ejecuta cuando un objeto es destruido — libera recursos y no debe lanzar excepciones. Para la mayoria de las clases, el destructor generado por el compilador es suficiente.
La Regla de Cero: prefiere wrappers RAII (std::string, std::vector, std::unique_ptr) para que nunca necesites escribir ninguna funcion miembro especial. Si debes gestionar un recurso en bruto, la Regla de Tres dice que necesitas un destructor, constructor de copia y operador de asignacion por copia. La Regla de Cinco extiende esto para incluir el constructor de movimiento y el operador de asignacion por movimiento.
#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 y =delete
Puedes solicitar explicitamente la version generada por el compilador de una funcion miembro especial con =default, o suprimirla explicitamente con =delete. Esto hace tu intencion clara y documenta la semantica de copia/movimiento de la clase.
#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;
}Los miembros se inicializan en el orden en que son declarados en la clase, no en el orden en que aparecen en la lista de inicializacion. Si un miembro depende de otro, asegurate de que la dependencia se declare primero. Los compiladores advierten sobre esto con -Wreorder — nunca ignores esa advertencia. Un error clasico: inicializar size_ desde un parametro pero luego usar size_ para asignar data_, con data_ declarada antes de size_.
- Usa
expliciten constructores de un solo argumento para prevenir conversiones implicitas no intencionadas - La lista de inicializacion de miembros es requerida para miembros
consty de referencia, y mas eficiente para todos los miembros - Los miembros se inicializan en el orden de declaracion, no en el orden de la lista de inicializacion
- Apunta a la Regla de Cero — usa tipos RAII para que las funciones miembro especiales generadas por el compilador sean correctas
- Si escribes cualquiera de destructor/ctor-copia/asign-copia, escribe las cinco (Regla de Cinco)
=defaultdocumenta la intencion;=deleteprohibe explicitamente una operacion
Quiz — Pon a prueba tus conocimientos
(20 XP)1. Que previene la palabra clave `explicit` en un constructor?
2. En que orden se inicializan los miembros de una clase?
3. Que es la Regla de Cero?