Menu
CoddyTech

Regla de tres / cinco / cero

Parte de la sección Programación Orientada a Objetos del Journey de C++ de Coddy. Lección 25 de 104.

Cuando tu clase administra recursos como memoria dinámica, has aprendido que necesitas un destructor personalizado, un constructor de copia y un constructor de movimiento. Pero hay un principio rector que te ayuda a decidir qué funciones miembro especiales implementar: la Regla de Tres, Cinco y Cero.

La Regla de Tres establece: si defines cualquiera de estos tres elementos, deberías definir los tres:

  • Destructor
  • Constructor de copia
  • Operador de asignación de copia

La Regla de cinco amplía esto para C++ moderno, añadiendo operaciones de movimiento:

  • Destructor
  • Constructor de copia
  • Operador de asignación de copia
  • Constructor de movimiento
  • Operador de asignación de movimiento
class Buffer {
    int* data;
    size_t size;
public:
    Buffer(size_t s) : size(s), data(new int[s]) {}
    
    ~Buffer() { delete[] data; }                          // 1. Destructor
    Buffer(const Buffer& other);                          // 2. Constructor de copia
    Buffer& operator=(const Buffer& other);               // 3. Asignación por copia
    Buffer(Buffer&& other) noexcept;                      // 4. Constructor de movimiento
    Buffer& operator=(Buffer&& other) noexcept;           // 5. Asignación por movimiento
};

Ambos operadores de asignación siguen un patrón: protegerse contra la autoasignación, liberar el recurso antiguo, tomar el nuevo y devolver *this:

Buffer& Buffer::operator=(const Buffer& other) {   // asignación por copia
    if (this != &other) {
        delete[] data;
        size = other.size;
        data = new int[size];
        std::copy(other.data, other.data + size, data);
    }
    return *this;
}

Buffer& Buffer::operator=(Buffer&& other) noexcept {   // asignación por movimiento
    if (this != &other) {
        delete[] data;
        data = other.data;
        size = other.size;
        other.data = nullptr;
        other.size = 0;
    }
    return *this;
}

La Regla del Cero es el enfoque más sencillo: si tu clase no administra recursos directamente, no definas ninguna de estas funciones. Deja que el compilador las genere o usa punteros inteligentes y contenedores estándar que se encarguen de los recursos por ti.

class Player {
    std::string name;              // std::string gestiona su propia memoria
    std::vector<int> scores;       // std::vector gestiona sus recursos
public:
    Player(std::string n) : name(n) {}
    // ¡No se necesitan funciones de destrucción, copia ni movimiento!
};

Seguir estas reglas evita errores como la doble eliminación, las fugas de memoria y los punteros colgantes que ocurren cuando algunas operaciones están definidas, pero faltan otras.

challenge icon

Desafío

Fácil

Vamos a crear una clase TextBuffer que siga la Regla de Cinco: implementaremos las cinco funciones miembro especiales para gestionar correctamente los datos de caracteres asignados dinámicamente. Esto demostrará cómo las operaciones de copia y movimiento trabajan juntas para crear una clase robusta que gestione recursos.

Crearás dos archivos para organizar tu código:

  • TextBuffer.h: Define una clase TextBuffer que almacene texto en una matriz de caracteres asignada dinámicamente. Tu clase necesita:
    • Miembros privados: un puntero char* llamado data para el contenido de texto y un size_t length para la longitud de la cadena (sin incluir el terminador nulo)
    • Un constructor parametrizado que acepte una cadena C (const char*), asigne memoria, copie el contenido e imprima "TextBuffer created: <text>"
    • Un destructor que libere la memoria (si no es nula) e imprima "TextBuffer destroyed"
    • Un constructor de copia que realice una copia profunda e imprima "TextBuffer copied"
    • Un operador de asignación por copia que gestione la autoasignación, libere los datos existentes, realice una copia profunda e imprima "TextBuffer copy-assigned". Devuelve *this
    • Un constructor de movimiento (marcado como noexcept) que transfiera la propiedad e imprima "TextBuffer moved". Deja el origen en un estado vacío válido
    • Un operador de asignación por movimiento (marcado como noexcept) que gestione la autoasignación, libere los datos existentes, transfiera la propiedad e imprima "TextBuffer move-assigned". Devuelve *this
    • Un método getText() que devuelva el texto almacenado (devuelve la cadena vacía "" si data es nulo)
    • Un método getLength() que devuelva la longitud
  • main.cpp: Demuestra las cinco funciones miembro especiales en acción. Lee una cadena de texto de la entrada y luego:
    • Crea un TextBuffer llamado original con el texto de entrada
    • Crea copied usando el constructor de copia a partir de original
    • Crea another con el texto "Temporary"
    • Usa la asignación por copia: another = original
    • Crea moved mediante la construcción por movimiento desde original, usando std::move()
    • Crea target con el texto "Target"
    • Usa la asignación por movimiento: target = std::move(copied)
    • Imprime "--- Final State ---"
    • Imprime "original: <text> (length: <len>)" para cada búfer: original, copied, moved, another, target

Después de los movimientos, los objetos de origen (original y copied) deberían mostrar texto vacío con longitud 0, mientras que los objetos de destino contienen los datos transferidos. Esto demuestra la Regla de Cinco en acción: las cinco funciones trabajan juntas para garantizar una gestión segura de los recursos.

Incluye <cstring> para funciones de cadenas como strlen y strcpy, y <utility> para std::move().

Pruébalo tú mismo

#include <iostream>
#include <string>
#include <utility>
#include "TextBuffer.h"

using namespace std;

int main() {
    string input;
    getline(cin, input);

    // TODO: Crea un TextBuffer llamado 'original' con el texto de entrada

    // TODO: Crea 'copied' usando el constructor de copia a partir de 'original'

    // TODO: Crea 'another' con el texto "Temporary"

    // TODO: Usa la asignación por copia: another = original

    // TODO: Crea 'moved' mediante el constructor de movimiento a partir de 'original' usando std::move()

    // TODO: Crea 'target' con el texto "Target"

    // TODO: Usa la asignación por movimiento: target = std::move(copied)

    // TODO: Imprime "--- Final State ---"

    // TODO: Imprime el estado de cada búfer con este formato:
    // "original: <text> (length: <len>)"
    // Imprime el estado de: original, copied, moved, another, target

    return 0;
}
quiz iconPonte a prueba

Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.

Todas las lecciones de Programación Orientada a Objetos

Practica por tu cuenta: Compilador de C++ online