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 gestiona recursos como memoria dinámica, has aprendido que necesitas un destructor, un constructor de copia y un constructor de movimiento personalizados. Pero existe un principio rector que te ayuda a decidir qué funciones miembro especiales implementar: la Regla del Tres, el Cinco y el Cero.
La Regla de los Tres establece: si defines cualquiera de estos tres, deberías definir los tres:
- Destructor
- Constructor de copia
- Operador de asignación por copia
La Regla de los Cinco extiende esto para el C++ moderno, añadiendo operaciones de movimiento:
- Destructor
- Constructor de copia
- Operador de asignación por copia
- Constructor de movimiento
- Operador de asignación por 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
};La Regla del Cero es el enfoque más sencillo: si tu clase no gestiona recursos directamente, no definas ninguna de estas funciones. Deja que el compilador las genere, o utiliza punteros inteligentes y contenedores estándar que gestionen los recursos por ti.
class Player {
std::string name; // std::string gestiona su propia memoria
std::vector<int> scores; // std::vector maneja sus recursos
public:
Player(std::string n) : name(n) {}
// ¡No se necesitan funciones de destructor, copia o movimiento!
};Seguir estas reglas evita errores como la doble eliminación, fugas de memoria y punteros colgantes que ocurren cuando se definen algunas operaciones pero faltan otras.
Desafío
FácilVamos a construir una clase TextBuffer que siga la Regla de los Cinco — implementando las cinco funciones miembro especiales para gestionar adecuadamente 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 gestiona recursos.
Crearás dos archivos para organizar tu código:
TextBuffer.h: Define una claseTextBufferque almacena texto en un arreglo de caracteres asignado dinámicamente. Tu clase necesita:- Miembros privados: un puntero
char*llamadodatapara el contenido del texto, y unsize_t lengthpara la longitud de la cadena (sin incluir el terminador nulo) - Un constructor parametrizado que toma una cadena de estilo C (
const char*), asigna memoria, copia el contenido e imprime"TextBuffer created: <text>" - Un destructor que libera la memoria (si no es nula) e imprime
"TextBuffer destroyed" - Un constructor de copia que realiza una copia profunda e imprime
"TextBuffer copied" - Un operador de asignación por copia que maneja la auto-asignación, limpia los datos existentes, realiza una copia profunda e imprime
"TextBuffer copy-assigned". Devuelve*this - Un constructor de movimiento (marcado como
noexcept) que transfiere la propiedad e imprime"TextBuffer moved". Deja el origen en un estado vacío válido - Un operador de asignación por movimiento (marcado como
noexcept) que maneja la auto-asignación, limpia los datos existentes, transfiere la propiedad e imprime"TextBuffer move-assigned". Devuelve*this - Un método
getText()que devuelve el texto almacenado (devuelve una cadena vacía""si data es nulo) - Un método
getLength()que devuelve la longitud
- Miembros privados: un puntero
main.cpp: Demuestra las cinco funciones miembro especiales en acción. Lee una cadena de texto de la entrada, luego:- Crea un
TextBufferllamadooriginalcon el texto de entrada - Crea
copiedusando el constructor de copia deoriginal - Crea
anothercon el texto"Temporary" - Usa la asignación por copia:
another = original - Crea
movedmediante la construcción por movimiento desdeoriginalusandostd::move() - Crea
targetcon el texto"Target" - Usa la asignación por movimiento:
target = std::move(copied) - Imprime
"--- Final State ---" - Imprime
"original: <text> (length: <len>)"para cada buffer: original, copied, moved, another, target
- Crea un
Después de los movimientos, los objetos de origen (original y copied) deben mostrar un texto vacío con longitud 0, mientras que los objetos de destino contienen los datos transferidos. Esto demuestra la Regla de los Cinco en acción — las cinco funciones trabajando juntas para asegurar una gestión de recursos segura.
Incluye <cstring> para funciones de cadena como strlen y strcpy, y <utility> para std::move().
Hoja de referencia
La Regla de los Tres establece que si defines cualquiera de estas tres funciones miembro especiales, deberías definir las tres:
- Destructor
- Constructor de copia
- Operador de asignación por copia
La Regla de los Cinco extiende esto para el C++ moderno añadiendo operaciones de movimiento:
- Destructor
- Constructor de copia
- Operador de asignación por copia
- Constructor de movimiento
- Operador de asignación por 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
};La Regla del Cero establece que si tu clase no gestiona recursos directamente, no definas ninguna función miembro especial. Utiliza punteros inteligentes y contenedores estándar que gestionen los recursos automáticamente:
class Player {
std::string name; // std::string gestiona su propia memoria
std::vector<int> scores; // std::vector maneja sus recursos
public:
Player(std::string n) : name(n) {}
// ¡No se necesitan funciones de destructor, copia o movimiento!
};Seguir estas reglas evita errores como la doble liberación, fugas de memoria y punteros colgantes.
Pruébalo tú mismo
#include <iostream>
#include <string>
#include <utility>
#include "TextBuffer.h"
using namespace std;
int main() {
string input;
getline(cin, input);
// TODO: Crear un TextBuffer llamado 'original' con el texto de entrada
// TODO: Crear 'copied' usando el constructor de copia de 'original'
// TODO: Crear 'another' con el texto "Temporary"
// TODO: Usar la asignación por copia: another = original
// TODO: Crear 'moved' mediante la construcción por movimiento desde 'original' usando std::move()
// TODO: Crear 'target' con el texto "Target"
// TODO: Usar la asignación por movimiento: target = std::move(copied)
// TODO: Imprimir "--- Final State ---"
// TODO: Imprimir el estado de cada búfer en este formato:
// "original: <text> (length: <len>)"
// Imprimir para: original, copied, moved, another, target
return 0;
}
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
1Fundamentos de OOP
Archivos externosConstrucción y compilación en C++Archivos de cabecera y archivos fuenteNamespaces y alcanceIntroducción a OOP en C++Clases vs ObjetosEl puntero 'this'Métodos (Funciones miembro)Atributos (Miembros de datos)Conceptos básicos de Ctors y DtorsResumen - Calculadora simple4Propiedades de clase
Miembros de instancia vs. estáticosGetters y SettersFunciones miembro constPalabra clave mutableMétodos y variables estáticosFunciones y clases amigasResumen - Gestor de cuentas bancarias7Herencia
Herencia básicaNiveles de acceso en la herenciaOrden de llamada de Ctor y DtorSobrescritura de métodosFunciones virtuales y VTableHerencia múltipleHerencia virtualResumen - Jerarquía de empleados2Gestión de memoria
Memoria Stack vs HeapPunteros y referenciasMemoria dinámica (new/delete)Punteros inteligentes en C++RAII en C++Resumen - Gestor de arrays dinámicos5Encapsulamiento
Especificadores de acceso en C++Especificadores de acceso en profundidadOcultamiento de informaciónStruct vs ClassClases anidadas e internasResumen - Sistema de registros de estudiantes8Polimorfismo
Polimorfismo: Compilación vs. Tiempo de ejecuciónSobrecarga de funcionesFunciones virtuales revisadasFunciones virtuales purasClases abstractasDiseño de interfaces en C++Dynamic Casting y RTTIResumen: Calculadora de figuras11Conceptos avanzados de POO
Composición vs. HerenciaMixins mediante CRTPIdioma PimplBorrado de tiposEnum Classes y tipado fuerteManejo de excepciones en POOJerarquías de excepciones personalizadas14Patrones de diseño - Parte 2
Patrón CommandPatrón AdapterPatrón DecoratorPatrón Template MethodPatrón StatePatrón CompositeRAII como patrón3Constructores y Destructores
Constructor por defectoConstructor parametrizadoConstructor de copiaConstructor de movimientoListas de inicialización del constructorConstructores delegadosAnálisis profundo del destructorRegla de tres / cinco / ceroResumen - Clase String6Sobrecarga de operadores
Introducción a la sobrecarga de operadoresSobrecarga de operadores aritméticosSobrecarga de operadores de comparaciónOperadores de flujo (Stream)Sobrecarga del operador de asignaciónSobrecarga de los operadores [] y ()Operadores de conversión de tiposResumen - Clase Matrix9Plantillas
Plantillas de funcionesPlantillas de clasesEspecialización de plantillasPlantillas variádicasConceptos básicos de SFINAE y Type TraitsResumen - Contenedor genérico