Un std::shared_ptr es un smart pointer que permite compartir la propiedad de un mismo objeto entre varias partes del programa.
Ahora vamos a profundizar en uno de los tipos más flexibles, aunque a menudo malinterpretado: el std::shared_ptr.
Mientras que std::unique_ptr posee un recurso en exclusiva, std::shared_ptr permite que varios punteros compartan su propiedad.
Su filosofía es la propiedad compartida (shared ownership). Múltiples punteros pueden apuntar al mismo objeto, y el objeto solo se destruye cuando todos los punteros han desaparecido.
Cómo funciona el recuento de referencias
std::shared_ptr emplea una técnica llamada recuento de referencias (reference counting).
Internamente, un shared_ptr no solo guarda la dirección de memoria del objeto. También mantiene un puntero a un bloque de control compartido. Este bloque contiene un contador (un número entero) que indica cuántos shared_ptr están apuntando a ese objeto ahora mismo.
La lógica es la siguiente:
Cuando creas un shared_ptr, el contador se inicia en 1.
Si copias ese shared_ptr (se lo pasas a otra función, o a otro objeto), el contador aumenta.
Cuando un shared_ptr se destruye (sale del ámbito), el contador disminuye.
Si el contador llega a 0, significa que nadie necesita el objeto. Se libera la memoria automáticamente.
Las operaciones sobre el bloque de control están sincronizadas. Distintos shared_ptr que comparten ese bloque pueden copiarse y destruirse desde varios hilos, aunque esto no hace seguro el acceso simultáneo al objeto gestionado y tiene un pequeño coste.
Creación de un shared_ptr
Para usarlo, necesitamos incluir la librería <memory>.
Aunque podemos construirlo a partir de new, la forma habitual y recomendada es usar std::make_shared.
#include <memory>
#include <iostream>
int main() {
// FORMA CORRECTA: Usando std::make_shared
std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
// FORMA NO RECOMENDADA (pero posible)
// std::shared_ptr<int> ptr2(new int(10));
}
¿Por qué usar std::make_shared?
Hay dos razones técnicas importantes para preferir make_shared:
- Eficiencia: Si usas
new, haces dos asignaciones de memoria (una para el objeto y otra para el bloque de control).make_sharedlas hace en un solo bloque de memoria contiguo. - Seguridad: Evita fugas de memoria sutiles si ocurre una excepción justo en el momento de creación de los argumentos.
Ejemplo de uso y ciclo de vida
Vamos a ver un ejemplo práctico para visualizar cómo cambia el contador de referencias usando el método .use_count().
#include <iostream>
#include <memory>
class Mensaje {
public:
Mensaje() { std::cout << "Mensaje creado\n"; }
~Mensaje() { std::cout << "Mensaje destruido\n"; }
};
int main() {
std::cout << "Inicio del programa\n";
// 1. Creamos el primer puntero
std::shared_ptr<Mensaje> p1 = std::make_shared<Mensaje>();
std::cout << "Contador p1: " << p1.use_count() << "\n"; // Salida: 1
{
// 2. Creamos un segundo puntero COPIANDO p1
// Ambos apuntan a lo mismo.
std::shared_ptr<Mensaje> p2 = p1;
std::cout << "Contador p1: " << p1.use_count() << "\n"; // Salida: 2
std::cout << "Contador p2: " << p2.use_count() << "\n"; // Salida: 2
} // 3. Aquí p2 sale del scope y se destruye. El contador baja.
std::cout << "Contador p1 tras cerrar bloque: " << p1.use_count() << "\n"; // Salida: 1
std::cout << "Fin del programa\n";
} // 4. Aquí p1 sale del scope. Contador = 0. Se llama al destructor.
Salida del programa:
Inicio del programa
Mensaje creado
Contador p1: 1
Contador p1: 2
Contador p2: 2
Contador p1 tras cerrar bloque: 1
Fin del programa
Mensaje destruido
Como vemos, el objeto Mensaje sobrevive al bloque interno porque p1 todavía lo estaba “sujetando”.
¿Cuándo usar shared_ptr?
Muchos programadores novatos en C++ cometen el error de usar shared_ptr para todo porque se parece a los recolectores de basura (Garbage Collectors) de lenguajes como C# o Java. No hagas esto.
El shared_ptr es más pesado que un puntero normal: suele almacenar dos punteros y además tiene la sobrecarga de gestionar el bloque de control.
Úsalo solo cuando:
- Realmente varios objetos necesiten mantener vivo a un tercero y no esté claro quién morirá el último.
- Ejemplos: Estructuras de datos complejas (grafos), recursos cargados en caché (texturas en un videojuego usadas por varios modelos), o paso de datos asíncronos entre hilos.
Si puedes definir un dueño claro para el objeto, usa std::unique_ptr. Si solo necesitas acceder al objeto sin poseerlo, usa un puntero crudo (observador) o una referencia.
El peligro de las referencias circulares
El problema típico de std::shared_ptr son las referencias circulares. Si el Objeto A tiene un shared_ptr al Objeto B, y el Objeto B tiene un shared_ptr al Objeto A… nunca se destruirán.
Ambos tendrán siempre el contador en 1, esperándose mutuamente. Esto genera un Memory Leak.
Para solucionarlo, C++ ofrece un observador que no aumenta el contador de propiedad: std::weak_ptr.