Ir al contenido

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).

constructors.cpp
#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.

explicit.cpp
#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.

init_list.cpp
#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.

rule_of_five.cpp
#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.

default_delete.cpp
#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;
}
Error común

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_.

Puntos clave
  • Usa explicit en constructores de un solo argumento para prevenir conversiones implicitas no intencionadas
  • La lista de inicializacion de miembros es requerida para miembros const y 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)
  • =default documenta la intencion; =delete prohibe 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?