Gestión de memoria con punteros inteligentes
En C++, la gestión segura y eficiente de memoria dinámica es un aspecto crítico del desarrollo robusto. Tras comprender los conceptos de RAII y propiedad, resulta natural introducir los punteros inteligentes, que son herramientas diseñadas para aplicar estos principios a la gestión de memoria.
¿Qué son los punteros inteligentes?
Un puntero inteligente es una clase plantilla de la biblioteca estándar (<memory>) que encapsula un puntero crudo y gestiona automáticamente la vida útil del objeto al que apunta, aplicando RAII.
Al usarlos:
- La memoria se adquiere al crear el puntero.
- La memoria se libera automáticamente al destruirse el puntero, sin necesidad de
delete. - La propiedad del recurso es explícita, reduciendo fugas de memoria y accesos inválidos.
Los más utilizados en C++ moderno son std::unique_ptr, std::shared_ptr y std::weak_ptr.
std::unique_ptr
Representa propiedad exclusiva. Solo un unique_ptr puede poseer un recurso.
- No se puede copiar, solo mover con
std::move. - Libera el recurso automáticamente al destruirse.
- Ideal para recursos que no deben compartirse.
#include <memory>
#include <iostream>
int main() {
// Creación segura con make_unique
std::unique_ptr<int> ptr = std::make_unique<int>(42);
std::cout << *ptr << "\n";
// Transferencia de propiedad
std::unique_ptr<int> ptr2 = std::move(ptr);
if (!ptr) {
std::cout << "ptr ya no posee el recurso.\n";
}
return 0; // ptr2 libera automáticamente la memoria
}
std::make_unique es la forma recomendada de crear unique_ptr:
- Evita errores en expresiones complejas.
- Hace el código más claro y seguro.
- Introducido en C++14.
std::shared_ptr
Representa propiedad compartida mediante conteo de referencias.
- Varias copias comparten el mismo recurso.
- El recurso se libera cuando el contador de referencias llega a cero.
- Útil en grafos, estructuras compartidas o cachés.
- Creamos punteros de este tipo con
std::make_shared.
#include <memory>
#include <iostream>
int main() {
std::shared_ptr<int> sp1 = std::make_shared<int>(100);
{
std::shared_ptr<int> sp2 = sp1; // Copia: ambos comparten propiedad
std::cout << "Valor: " << *sp2 << "\n";
std::cout << "Conteo: " << sp1.use_count() << "\n";
} // sp2 se destruye: el conteo baja
std::cout << "Conteo tras salir del bloque: " << sp1.use_count() << "\n";
return 0; // El recurso se libera cuando desaparece el último shared_ptr
}
Ciclos de referencias
Un problema común con shared_ptr son los ciclos de referencias:
- Ocurren cuando dos (o más) objetos se apuntan entre sí mediante
shared_ptr. - Ninguno de los contadores llega a cero, por lo que el recurso nunca se libera: fuga de memoria.
Ejemplo de ciclo:
struct Nodo {
std::shared_ptr<Nodo> siguiente;
};
Si nodo1.siguiente = nodo2 y nodo2.siguiente = nodo1, ambos quedan bloqueados en memoria.
std::weak_ptr
Es un puntero observador sin propiedad.
- No incrementa el contador de referencias.
- Se usa junto a
shared_ptrpara romper ciclos de referencias. - Permite comprobar si el recurso aún existe mediante
.lock().
#include <memory>
#include <iostream>
int main() {
std::shared_ptr<int> sp = std::make_shared<int>(200);
std::weak_ptr<int> wp = sp; // Observador
if (auto spt = wp.lock()) { // Convierte temporalmente a shared_ptr
std::cout << *spt << "\n";
} else {
std::cout << "El recurso ya fue liberado.\n";
}
return 0;
}
Como std::weak_ptr no es dueño del recurso, no puede acceder directamente a él. Antes debe comprobar si el recurso sigue existiendo.
lock() sirve para eso:
- Comprueba si el
shared_ptrasociado aún tiene propiedad del recurso (es decir, su contador de referencias es > 0). - Si existe, crea y devuelve un
std::shared_ptrtemporal, aumentando el contador de referencias mientras se usa. - Si ya no existe, devuelve un
std::shared_ptrvacío (equivalente anullptr).