En C clásico y en C++ antiguo, gestionar memoria significaba llamar a new y asegurarte de llamar a delete exactamente una vez, en el momento correcto, sin importar qué excepciones se lanzasen por el camino. En cuanto el código se complicaba un poco, los memory leaks y los double-free aparecían casi solos.
Los smart pointers resuelven esto aplicando RAII: el objeto se destruye automáticamente cuando el smart pointer sale de ámbito. No hace falta recordar llamar a delete. El compilador lo garantiza.
std::unique_ptr: propiedad exclusiva
std::unique_ptr<T> es el smart pointer para cuando un único dueño posee el objeto. No se puede copiar, solo mover. Cuando el unique_ptr sale de ámbito, llama a delete automáticamente.
#include <memory>
#include <print>
struct Conexion {
Conexion() { std::println("Conexión abierta"); }
~Conexion() { std::println("Conexión cerrada"); }
void enviar(std::string_view msg) { std::println("Enviando: {}", msg); }
};
int main() {
auto conn = std::make_unique<Conexion>();
conn->enviar("hola");
// Al salir de main, ~Conexion() se llama automáticamente
}
Usa siempre std::make_unique en lugar de new directamente. Es más seguro (evita leaks en expresiones complejas) y más legible.
Mover la propiedad
auto a = std::make_unique<int>(42); auto b = std::move(a); // b es el nuevo dueño, a queda como nullptr // a.get() == nullptr // *b == 42
unique_ptr con arrays
auto buffer = std::make_unique<char[]>(1024); // buffer[0..1023] disponible, delete[] automático al salir de ámbito
std::shared_ptr: propiedad compartida
std::shared_ptr<T> mantiene un contador de referencias. Cada copia del shared_ptr incrementa el contador. Cuando el último shared_ptr que apunta al objeto se destruye, el contador llega a cero y el objeto se libera.
#include <memory>
#include <print>
struct Recurso {
int id;
Recurso(int i) : id(i) { std::println("Recurso {} creado", id); }
~Recurso() { std::println("Recurso {} destruido", id); }
};
void usar(std::shared_ptr<Recurso> r) {
std::println("Usando recurso {}, refs: {}", r->id, r.use_count());
}
int main() {
auto r1 = std::make_shared<Recurso>(1);
{
auto r2 = r1; // Copia: ahora hay 2 referencias
usar(r2); // 3 referencias dentro de usar()
} // r2 se destruye: vuelve a 1 referencia
std::println("Al final: {} ref", r1.use_count()); // 1
}
// Al salir de main, r1 se destruye: el Recurso se libera
std::make_shared es especialmente eficiente porque asigna el objeto y el bloque de control del contador en una sola llamada a new.
Cuándo usar shared_ptr y cuándo no
El contador de referencias tiene coste: incremento y decremento atómicos en cada copia. Si el ownership es claro y único, unique_ptr es la opción correcta. shared_ptr es para cuando varios objetos necesitan mantener el mismo recurso vivo.
std::weak_ptr: observar sin poseer
El problema de shared_ptr son los ciclos de referencia. Si A tiene un shared_ptr a B y B tiene un shared_ptr a A, ninguno de los dos llega nunca a cero referencias y ninguno se destruye jamás.
std::weak_ptr<T> rompe ese ciclo: apunta al mismo objeto pero no incrementa el contador de referencias. Para acceder al objeto, hay que llamar a lock(), que devuelve un shared_ptr temporal que es nulo si el objeto ya fue destruido.
#include <memory>
#include <print>
struct Nodo {
int valor;
std::shared_ptr<Nodo> siguiente;
std::weak_ptr<Nodo> anterior; // weak para romper el ciclo
};
int main() {
auto n1 = std::make_shared<Nodo>(Nodo{1, nullptr, {}});
auto n2 = std::make_shared<Nodo>(Nodo{2, nullptr, {}});
n1->siguiente = n2;
n2->anterior = n1; // weak: no incrementa el contador de n1
if (auto prev = n2->anterior.lock()) {
std::println("Anterior de n2: {}", prev->valor); // 1
}
}
// n1 y n2 se destruyen correctamente al salir de main
El patrón del observer con weak_ptr
Otro uso habitual es implementar observadores que no deben mantener vivo al sujeto observado:
class EventBus {
std::vector<std::weak_ptr<Listener>> listeners;
public:
void subscribe(std::shared_ptr<Listener> l) {
listeners.push_back(l);
}
void emitir(const Event& e) {
// Limpiar listeners caducados y notificar al resto
std::erase_if(listeners, [](auto& wp){ return wp.expired(); });
for (auto& wp : listeners) {
if (auto l = wp.lock()) l->on_event(e);
}
}
};
std::move_only_function en C++23
C++23 añade std::move_only_function, que complementa a los smart pointers para callbacks y handlers que solo se deben mover, no copiar. Funciona como std::function pero permite capturar unique_ptr u otros tipos move-only en la lambda:
#include <functional>
#include <memory>
auto recurso = std::make_unique<int>(42);
// std::function NO compila aquí porque unique_ptr no es copiable
// std::move_only_function sí funciona:
std::move_only_function<void()> tarea = [r = std::move(recurso)]() {
// r es el único dueño del int aquí
};
Errores comunes a evitar
Construir un shared_ptr desde un puntero crudo dos veces sobre el mismo objeto es uno de los errores más frecuentes: crea dos contadores independientes y el doble-free está garantizado. Siempre usa make_shared o, si necesitas crear un shared_ptr desde this, hereda de std::enable_shared_from_this:
struct Servicio : std::enable_shared_from_this<Servicio> {
std::shared_ptr<Servicio> get_ptr() {
return shared_from_this(); // Correcto
// return std::shared_ptr<Servicio>(this); // INCORRECTO: doble delete
}
};
Otro error habitual es retener un shared_ptr innecesariamente en capturas de lambda o en miembros de clase cuando solo se necesita observar: ahí corresponde un weak_ptr o incluso un puntero crudo no propietario.
Si vienes de un lenguaje con recolector de basura como C# (que cubrimos en nuestra guía de C# 13), el concepto de ownership explícito es el mayor cambio mental. En C++ el compilador no gestiona la memoria por ti, pero con smart pointers la responsabilidad es suficientemente explícita como para no necesitar un GC.
Para entornos embebidos donde el heap dinámico no está disponible, los smart pointers no son una opción directa. El artículo de esta serie sobre C++ bare metal cubre las alternativas con objetos en stack y placement new.
Imagen: Pexels / Digital Buggu
