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 la memoria dinámica, has aprendido que necesitas un destructor personalizado, un constructor de copia y un constructor de movimiento. Pero existe un principio rector que te ayuda a decidir qué funciones miembro especiales implementar: la Regla de Tres, Cinco y 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 de copia
La Regla de los cinco amplía esto para el 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 de copia
Buffer(Buffer&& other) noexcept; // 4. Constructor de movimiento
Buffer& operator=(Buffer&& other) noexcept; // 5. Asignación de 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 destructor, copy ni move!
};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.
Desafío
FácilConstruyamos una clase TextBuffer que siga la Regla de Cinco: implementando las cinco funciones miembro especiales para gestionar correctamente 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 recibe una cadena 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 gestiona la autoasignació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 gestiona la autoasignació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 y después:- Crea un
TextBufferllamadooriginalcon el texto de entrada - Crea
copiedusando el constructor de copia a partir deoriginal - Crea
anothercon el texto"Temporary" - Usa la asignación por copia:
another = original - Crea
movedmediante construcción por movimiento a partir deoriginalusandostd::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 búfer: original, copied, moved, another, target
- Crea un
Después de los movimientos, los objetos de origen (original y copied) deberían mostrar un 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 de 'original'
// TODO: Crea 'another' con el texto "Temporary"
// TODO: Usa la asignación por copia: another = original
// TODO: Crea 'moved' mediante construcción por movimiento desde '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 buffer en este formato:
// "original: <text> (length: <len>)"
// Imprime 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éricoPractica por tu cuenta: Compilador de C++ online