Gestión de memoria dinámica y smart pointers en C++ moderno
En C++ clásico, la asignación y liberación de memoria dinámica se realizaba manualmente con new
y delete
, lo que conllevaba riesgos de fugas de memoria, doble liberación o accesos inválidos. Con la llegada de C++11 y posteriores, los smart pointers (punteros inteligentes) ofrecen una forma más segura y expresiva de gestionar la memoria dinámica.
1. Por qué usar smart pointers
- Seguridad: Liberan automáticamente la memoria cuando dejan de usarse, evitando fugas.
- Exception safety: En caso de excepción, no olvidamos
delete
. - Claridad de intención: El tipo de smart pointer comunica si la referencia es única, compartida o débil.
2. std::unique_ptr
: propiedad exclusiva
unique_ptr
es un puntero inteligente que posee un recurso de forma única. No se puede copiar, solo mover.
#include <memory> #include <iostream> struct Widget { Widget() { std::cout << "Widget creado\n"; } ~Widget() { std::cout << "Widget destruido\n"; } void saludar() { std::cout << "¡Hola desde Widget!\n"; } }; int main() { // Crear un unique_ptr a Widget std::unique_ptr<Widget> p1 = std::make_unique<Widget>(); p1->saludar(); // Transferir propiedad con std::move std::unique_ptr<Widget> p2 = std::move(p1); if (!p1) std::cout << "p1 ya no posee el Widget\n"; // Al salir de scope, p2 libera automáticamente el Widget }
Ventajas:
- Ningún coste de contaje de referencias.
- El recurso siempre tiene un único dueño.
3. std::shared_ptr
: propiedad compartida
shared_ptr
implementa un contaje de referencias: múltiples punteros comparten el mismo recurso. Se libera cuando la última referencia se destruye.
#include <memory> #include <iostream> struct Nodo { int valor; std::shared_ptr<Nodo> siguiente; Nodo(int v) : valor(v) { std::cout << "Nodo " << v << " creado\n"; } ~Nodo() { std::cout << "Nodo " << valor << " destruido\n"; } }; int main() { auto n1 = std::make_shared<Nodo>(1); { auto n2 = std::make_shared<Nodo>(2); n1->siguiente = n2; // n1 comparte n2 std::cout << "Use count n2: " << n2.use_count() << "\n"; // normalmente 2 } // n2 sale de scope, pero el Nodo 2 sigue vivo porque n1->siguiente lo mantiene std::cout << "Use count n1: " << n1.use_count() << "\n"; // 1 }
Precaución: Las referencias cíclicas (A apunta a B y B a A) nunca se liberan automáticamente.
4. std::weak_ptr
: romper ciclos
weak_ptr
observa un objeto gestionado por shared_ptr
sin incrementar el contaje. Útil para romper ciclos de referencia.
#include <memory> #include <iostream> struct A; struct B; struct A { std::shared_ptr<B> bptr; ~A() { std::cout << "A destruido\n"; } }; struct B { std::weak_ptr<A> aptr; // observa A sin poseerlo ~B() { std::cout << "B destruido\n"; } }; int main() { auto a = std::make_shared<A>(); auto b = std::make_shared<B>(); a->bptr = b; b->aptr = a; // no incrementa use_count de A // Al salir de scope ambos objetos son liberados correctamente }
5. Buenas prácticas
- Prefiere
std::make_unique
/std::make_shared
: Evitas posibles fugas si la construcción lanza excepción. - Usa
unique_ptr
por defecto: Solo recurre ashared_ptr
cuando realmente haya múltiples dueños. - Rompe ciclos con
weak_ptr
: Siempre que crees estructuras con referencias mutuas. - No mezcles punteros crudos: Si gestionas un recurso con smart pointers, no uses
delete
directamente ni extrae el puntero bruto salvo para interoperabilidad temporaria.
6. Conclusión
Los smart pointers de la STL son fundamentales para escribir código C++ moderno, seguro y libre de fugas de memoria. Aprender a elegir entre unique_ptr
, shared_ptr
y weak_ptr
, y combinarlos adecuadamente, mejora tanto la calidad como la robustez de tus proyectos.
Siguiente paso recomendado: explora cómo integrar smart pointers con containers de la STL (por ejemplo, std::vector<std::unique_ptr<T>>
) para gestionar colecciones de objetos dinámicos de forma segura.