El problema que resuelven los punteros inteligentes
En la página anterior reservaste memoria con new y la liberaste con delete. Eso funciona, pero te carga a ti la responsabilidad: cada new necesita un delete correspondiente, en cada camino del código, incluidos aquellos en los que se lanza una excepción a mitad de camino. Si olvidas uno, hay fuga de memoria; si ejecutas delete dos veces, corrompes el heap.
Los punteros inteligentes solucionan esto atando la vida útil de la memoria del heap a un objeto normal en la pila. Cuando ese objeto sale de ámbito, su destructor ejecuta delete por ti, garantizado, incluso si una excepción desenrolla la pila. Esta idea se llama RAII (Resource Acquisition Is Initialization), y los punteros inteligentes viven en la cabecera <memory>.
Usas *p y p->member exactamente igual que con un puntero crudo. La diferencia es que nunca llamas a delete: lo hace el puntero inteligente.
unique_ptr: un solo dueño, sin compartir
unique_ptr es el puntero inteligente que deberías elegir por defecto. Representa propiedad exclusiva: exactamente un unique_ptr posee el objeto en cada momento, y cuando ese puntero muere, el objeto muere con él. Tiene cero sobrecarga en tiempo de ejecución comparado con un puntero crudo.
Crea uno con make_unique (C++14). Toma los argumentos del constructor y te entrega un puntero listo para usar:
Como solo puede haber un dueño, un unique_ptr no se puede copiar. Intentar copiarlo es un error de compilación, y ese error es el lenguaje protegiéndote de que dos dueños intenten hacer delete del mismo objeto:
auto a = make_unique<int>(10);
auto b = a; // error: call to deleted copy constructor of unique_ptr
Para entregar la propiedad a otro, lo mueves con std::move. Tras el movimiento, el puntero original queda vacío (contiene nullptr):
Este es el modelo que querrás la mayor parte del tiempo: siempre hay exactamente un dueño claro, y el compilador lo hace cumplir.
shared_ptr: propiedad compartida por conteo de referencias
A veces varias partes de tu programa necesitan realmente compartir el mismo objeto, y ninguna sabe cuál terminará la última. Para eso está shared_ptr. Mantiene un conteo de referencias: cada copia incrementa el conteo, cada destrucción lo reduce, y el objeto se libera solo cuando el conteo llega a cero.
Créalos con make_shared:
A diferencia de unique_ptr, copiar un shared_ptr está bien: ese es justo el propósito. La contrapartida es el costo: el conteo de referencias se guarda en el heap y se actualiza de forma atómica (seguro entre hilos), así que shared_ptr es más pesado que unique_ptr. Recurre a él solo cuando la propiedad sea realmente compartida, no solo para evitar pensar en quién posee qué.
make_shared también es más eficiente que shared_ptr<T>(new T(...)): reserva el objeto y el bloque de control en una única reserva en lugar de dos.
weak_ptr y cómo romper ciclos de referencias
shared_ptr tiene una trampa clásica: si dos objetos mantienen shared_ptr el uno al otro, sus conteos de referencias nunca llegan a cero, así que ninguno se libera jamás: una fuga de memoria aunque hayas usado punteros inteligentes.
struct Node {
shared_ptr<Node> next; // si dos nodos se apuntan mutuamente,
}; // se mantienen vivos para siempre
La solución es weak_ptr: un observador no propietario de un shared_ptr. No incrementa el conteo de referencias, así que nunca mantiene un objeto vivo. Para usar el objeto, llamas a .lock(), que te da un shared_ptr si el objeto todavía existe, o uno vacío si ya desapareció.
Usa weak_ptr para "punteros hacia atrás" y cachés: cualquier sitio donde quieras referenciar un objeto sin reclamar su propiedad.
Errores comunes y trampas
Los punteros inteligentes eliminan la mayoría de los bugs de memoria, pero quedan algunas trampas:
No mezcles propiedad inteligente y cruda de la misma memoria. Nunca construyas dos punteros inteligentes a partir del mismo puntero crudo: cada uno intentará hacer delete:
int* raw = new int(5);
unique_ptr<int> a(raw);
unique_ptr<int> b(raw); // desastre: ambos harán delete del mismo int (doble liberación)
Por esto justamente prefieres make_unique/make_shared: no hay un puntero crudo suelto que se pueda usar mal.
Un unique_ptr solo se puede mover, así que pásalo por valor para transferir la propiedad. Si una función debe usar pero no poseer el objeto, toma una referencia simple o un T* crudo: un puntero crudo que solo observa está perfectamente bien:
void consume(unique_ptr<int> p); // toma la propiedad (mover hacia dentro)
void observe(int* p); // solo mira, no posee nada
No recurras a shared_ptr por defecto. Resulta tentador porque se copia sin restricciones, pero el conteo atómico de referencias cuesta rendimiento real, y la propiedad compartida hace más difícil razonar sobre las vidas útiles. Usa unique_ptr por defecto; pasa a shared_ptr solo cuando realmente necesites varios dueños.
unique_ptr para arreglos necesita la forma de arreglo. make_unique<int[]>(n) te da un unique_ptr<int[]> que llama a delete[] correctamente. En la práctica, prefiere std::vector para arreglos dinámicos: gestiona la memoria por ti y además te da seguimiento del tamaño.
Siguiente: Strings
Ahora tienes la gestión de memoria bajo control: los punteros inteligentes te dan reserva en el heap sin fugas. Una de las cosas más comunes que reservarás y pasarás de un lado a otro es texto, y C++ te ofrece una herramienta mucho más segura que los búferes crudos char*. La siguiente página cubre std::string: cómo crece por sí misma, las operaciones que usarás a diario y por qué te libera por completo del trabajo manual con memoria.
Preguntas frecuentes
¿Qué son los punteros inteligentes en C++?
Los punteros inteligentes son objetos de <memory> (unique_ptr, shared_ptr, weak_ptr) que envuelven un puntero crudo y hacen delete de la memoria automáticamente cuando salen de ámbito. Te dan reserva en el heap sin el delete manual ni las fugas que provoca olvidarlo.
¿Cuál es la diferencia entre unique_ptr y shared_ptr?
unique_ptr es el único dueño de su objeto: no se puede copiar, solo mover, y libera la memoria en el momento en que muere. shared_ptr permite propiedad compartida mediante un conteo de referencias: muchos shared_ptr pueden apuntar al mismo objeto, y este se libera solo cuando se destruye el último. Prefiere unique_ptr salvo que realmente necesites propiedad compartida.
¿Debería usar make_unique o new en el C++ moderno?
Usa make_unique y make_shared. Reservan y envuelven el objeto en un solo paso, así que no hay un new crudo cuyo resultado pueda perderse antes de llegar a un puntero inteligente. Como regla general, una base de código moderna en C++ no debería tener casi ningún new o delete suelto.