cpp-que-son-smart-pointers

Qué son y cómo usar smart pointers en C++

  • 5 min

Un Smart Pointer es un objeto que gestiona automáticamente un puntero y libera el recurso cuando ya no hace falta.

Si llevas un tiempo programando en C++, sabrás que uno de los mayores dolores de cabeza del lenguaje es la gestión manual de la memoria.

En C++ clásico (“C con clases”), éramos responsables de reservar memoria con new y liberarla con delete. Si se te olvidaba el delete, tenías un memory leak. Si lo hacías dos veces, corrupción de memoria. Si accedías después de borrar, segmentation fault. Un campo de minas.

Para solucionar esto, C++11 introdujo los Smart Pointers (punteros inteligentes).

Un Smart Pointer es una clase que envuelve a un puntero “crudo” (raw pointer) y se encarga de gestionar su ciclo de vida automáticamente. Básicamente, se aseguran de que la memoria se libere cuando ya no es necesaria, sin que tengas que acordarte de hacerlo tú.

El uso de Smart Pointers es la base del C++ Moderno. Hoy en día, el uso de new y delete manual se considera una mala práctica en la inmensa mayoría de los casos.

El problema de los punteros crudos

Para entender por qué usamos smart pointers, primero recordemos el problema con este código clásico:

void procesarDatos() {
    MiObjeto* obj = new MiObjeto(); // Reservamos memoria
    obj->hacerAlgo();
    
    // ... código complejo ...
    
    if (hayError) {
        return; // ¡OPS! Nos vamos sin hacer delete. Memory Leak.
    }

    delete obj; // Si llegamos aquí, todo bien. Si no... mal asunto.
}
Copied!

Si la función retorna antes de tiempo (por un if o porque se lanza una excepción), la línea delete obj nunca se ejecuta. Esa memoria se queda ocupada para siempre (hasta que cierres el programa).

Los Smart Pointers solucionan esto aplicando un concepto fundamental de C++: RAII.

RAII: vincular recursos y objetos

El funcionamiento de los Smart Pointers se basa en el idioma RAII (Resource Acquisition Is Initialization).

La idea es sencilla: atar el ciclo de vida de un recurso (memoria, archivos, sockets) al ciclo de vida de un objeto.

Cuando creamos un Smart Pointer (una variable en el Stack), este adquiere el recurso (la memoria en el Heap). Cuando el Smart Pointer sale de su ámbito (scope), su destructor se ejecuta automáticamente y libera la memoria.

Veamos el mismo ejemplo anterior, pero con un Smart Pointer:

#include <memory> // Necesario para usar smart pointers

void procesarDatos() {
    std::unique_ptr<MiObjeto> obj = std::make_unique<MiObjeto>();
    
    obj->hacerAlgo(); // Se usa igual que un puntero normal (->)
    
    if (hayError) {
        return; // El destructor de 'obj' salta aquí. Memoria liberada.
    }

} // El destructor de 'obj' salta aquí también. Memoria liberada.
Copied!

No importa cómo salgamos de la función. La memoria siempre se libera, que era justo lo que queríamos.

Tipos de smart pointers

En la librería estándar <memory>, tenemos tres tipos principales de punteros inteligentes, cada uno diseñado para una estrategia de propiedad (ownership) diferente.

std::unique_ptr (Propietario único)

Es el más común y eficiente. Representa una propiedad exclusiva.

  • Solo un unique_ptr puede poseer el objeto al mismo tiempo.
  • No se puede copiar (no tendría sentido tener dos dueños únicos).
  • Sí se puede “mover” (transferir la propiedad a otro).
  • Tiene un coste de rendimiento nulo (es igual de rápido que un puntero crudo).

Úsalo por defecto. Si no sabes cuál usar, usa std::unique_ptr.

std::shared_ptr (Propiedad compartida)

Representa una propiedad compartida mediante un contador de referencias.

  • Varios shared_ptr pueden apuntar al mismo objeto.
  • Lleva una cuenta interna (reference count) de cuántos punteros lo están usando.
  • La memoria solo se libera cuando el último shared_ptr es destruido (el contador llega a 0).

weak_ptr (Observador débil)

Es un compañero del shared_ptr.

  • Apunta a un objeto gestionado por un shared_ptr pero no aumenta el contador de referencias.
  • Sirve para romper referencias circulares y para observar si un objeto sigue vivo sin adueñarse de él.

¿Qué pasó con auto_ptr?

Si vienes de versiones muy antiguas de C++ (anteriores a C++11), quizás recuerdes std::auto_ptr.

std::auto_ptr está obsoleto y fue eliminado en C++17. Tenía graves problemas de diseño al copiarlo, así que no lo uses en código nuevo. Su reemplazo directo es std::unique_ptr.

Comparativa de sintaxis

Aunque profundizaremos en cada uno en sus propios artículos, aquí tienes una tabla rápida de cómo se declaran y usan comparados con los punteros crudos.

AcciónPuntero Crudo (Legacy)Smart Pointer (Moderno)
CabeceraNinguna#include <memory>
Creaciónnew Tipo()std::make_unique<Tipo>()
Accesoptr->metodo()ptr->metodo() (Igual)
Desreferencia*ptr*ptr (Igual)
Liberacióndelete ptrAutomática

¿Cuál debo usar?

Una idea práctica en C++ moderno es:

  1. Usa objetos locales (en el stack) siempre que puedas.
  2. Si necesitas memoria dinámica, usa std::unique_ptr.
  3. Si realmente necesitas compartir la propiedad entre varios dueños (no solo pasar el puntero para que lo usen, sino compartir la responsabilidad de borrarlo), usa std::shared_ptr.
  4. Evita usar new y delete explícitamente, salvo en código de bajo nivel donde realmente necesites gestionar el recurso de forma manual.