Publicidad
Patrocinador Verificado
Infraestructura Cloud, GPU Clusters y APIs de IA para Desarrolladores
Despliega pipelines de inferencia y trading con latencia ultra baja. Explora planes.
Conocer Más →

C++ Moderno y Gestión de Memoria: Smart Pointers (unique_ptr, shared_ptr)

Por Elena Rostova • Avanzado • 13 min de lectura • Actualizado 2026-09-11

Lo Que Dominarás en Este Tutorial

  • Understand unique ownership with std::unique_ptr and zero-cost abstraction.

1. The Power of std::unique_ptr and Move Semantics

std::unique_ptr automatically deallocates memory when it goes out of scope.

CPP
#include <iostream>
#include <memory>

void runWorker() {
    auto conn = std::make_unique<int>(42);
    // Destructor called automatically when out of scope
}
Nota: Always prefer std::make_unique over new.
Publicidad
Patrocinador Verificado
Trading Cuantitativo y 30 Modelos de Negocio con IA
Genera ingresos predecibles con retainers mensuales y bots automatizados.
Ver Blueprints →

Evaluación Rápida: Pon a Prueba tus Conocimientos

1. What is the runtime performance overhead of std::unique_ptr compared to a raw pointer?

Preguntas Frecuentes

When should I use std::shared_ptr instead of std::unique_ptr?
Only when an asset truly requires multiple owners across different lifetimes or asynchronous threads.

2. Paso a paso: refactoriza una clase con punteros crudos a punteros inteligentes

La teoría se vuelve hábito la primera vez que refactorizas una clase que posee a mano un objeto en el heap. Esta es la forma clásica, anterior al C++ moderno: un Car que crea un Engine con new y lo borra en el destructor:

CPP
class Engine {
public:
    void start() {}
};

class Car {
    Engine* engine;                       // owning raw pointer
public:
    Car() : engine(new Engine) {}
    ~Car() { delete engine; }             // manual cleanup

    // Compiler-generated copy operations copy the POINTER,
    // so two Cars delete the same Engine. Double free.
};

El problema no es el delete; es que la propiedad es invisible. Nada en el tipo dice que Car es dueño del Engine, así que el compilador genera sin problema operaciones de copia que duplican esa obligación. La refactorización convierte la propiedad en el propio tipo:

CPP
#include <memory>
#include <utility>

class Engine {
public:
    void start() {}
};

class Car {
    std::unique_ptr<Engine> engine;       // ownership is now visible
public:
    Car() : engine(std::make_unique<Engine>()) {}

    // unique_ptr is move-only, so Car is move-only too:
    // copies are deleted by the compiler, moves transfer ownership.
    Car(Car&&) noexcept = default;
    Car& operator=(Car&&) noexcept = default;

    Engine& getEngine() { return *engine; }  // observer access, no ownership
};

Tres reglas completan la refactorización dondequiera que la apliques. Las funciones factoría devuelven std::unique_ptr, para que la transferencia de propiedad quede escrita en la firma. Las funciones que solo observan un objeto reciben una referencia o un puntero crudo obtenido con .get(): observar es gratis y no reclama nada. Y una clase que guarda un unique_ptr casi nunca vuelve a necesitar un destructor escrito a mano: la Regla del Cero sustituye a la Regla de los Cinco.

3. Un ejemplo completo

Una caché de texturas para un juego es el problema canónico de propiedad compartida: muchos sprites comparten una misma textura cargada, la textura debe descargarse cuando el último usuario la suelta y la propia caché no debe mantenerlo todo vivo para siempre. Esa combinación —shared_ptr para quienes usan la textura y weak_ptr para la caché— es todo el diseño:

CPP
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>

struct Texture {
    std::string path;
    explicit Texture(std::string p) : path(std::move(p)) {
        std::cout << "Loaded " << path << "\n";
    }
    ~Texture() { std::cout << "Unloaded " << path << "\n"; }
};

class TextureCache {
public:
    std::shared_ptr<Texture> load(const std::string& path) {
        if (auto it = cache_.find(path); it != cache_.end()) {
            if (auto tex = it->second.lock()) {   // still alive? reuse it
                return tex;
            }
        }
        auto tex = std::make_shared<Texture>(path);
        cache_[path] = tex;                        // cache observes via weak_ptr
        return tex;
    }

private:
    std::unordered_map<std::string, std::weak_ptr<Texture>> cache_;
};

int main() {
    TextureCache cache;
    {
        auto hero = cache.load("hero.png");        // Loaded hero.png
        auto heroAgain = cache.load("hero.png");   // reused, no reload
        std::cout << "Same object: " << (hero == heroAgain) << "\n";
    }   // last shared_ptr dies -> Unloaded hero.png

    auto bg = cache.load("bg.png");   // weak_ptr had expired -> fresh load
    return 0;
}

Ejecútalo y la salida enseña la semántica: hero.png se carga una sola vez aunque load() se llamó dos veces, se descarga justo cuando se cierra el ámbito interno y la caché no prolonga la vida de ninguna textura ni una sola instrucción, porque un weak_ptr observa sin poseer. Cambia la caché a shared_ptr y cada textura vivirá hasta que muera la caché; esa diferencia de una palabra es el error de diseño con shared_ptr más común en el mundo real.

Paso 3: entrega la propiedad desde las funciones factoría y gestiona recursos que no son memoria

Las factorías completan el patrón: una función que crea un objeto lo devuelve como unique_ptr, de modo que quien llama recibe la propiedad sin ver nunca un new crudo. El mismo mecanismo gestiona recursos que no son memoria en absoluto. Un handle de archivo en C no tiene destructor, así que le aportas uno —un deleter personalizado— y RAII se encarga del resto:

CPP
#include <cstdio>
#include <memory>

struct FileCloser {
    void operator()(std::FILE* f) const {
        if (f) std::fclose(f);
    }
};

// A unique_ptr whose "delete" is fclose
using UniqueFile = std::unique_ptr<std::FILE, FileCloser>;

UniqueFile openLogFile(const char* path) {
    return UniqueFile(std::fopen(path, "a"));   // ownership moves to caller
}

void appendLine(const char* path, const char* line) {
    auto file = openLogFile(path);              // closes on every exit path,
    if (!file) return;                          // including exceptions
    std::fputs(line, file.get());               // .get(): observe, don't own
}

Esta es la razón de fondo por la que la sección en profundidad te pide diseñar primero la propiedad: cuando la propiedad es un tipo, los archivos, los sockets y los handles de bases de datos obtienen la misma garantía de limpieza en una línea que los objetos del heap, sin bloques finally y sin cierres olvidados en el camino de error.

4. Solución de errores

Profundizando: punteros inteligentes en la práctica

Conocer la sintaxis de std::unique_ptr y std::shared_ptr es solo la mitad de la habilidad; la otra mitad es desarrollar instinto para diseñar la propiedad. En código de producción, los punteros inteligentes brillan cuando la propiedad se decide en la fase de diseño: las funciones fábrica devuelven std::unique_ptr para entregar la propiedad sin ambigüedad, los gestores de larga vida guardan std::shared_ptr solo para recursos genuinamente compartidos, y todo lo demás se pasa como un puntero crudo o una referencia que simplemente observa. Los equipos que adoptan esta disciplina comprueban que clases enteras de errores —fugas, dobles liberaciones, uso tras la liberación— dejan de aparecer en las revisiones de código, porque las propias firmas de los tipos documentan quién es responsable de cada objeto, y el compilador hace cumplir lo que antes la documentación solo prometía. El resultado es un código más corto y más seguro a la vez, porque asignar y liberar dejan de ser decisiones separadas y repartidas por todo un archivo.

Errores comunes

Preguntas frecuentes

¿Debería usar new y delete directamente alguna vez en C++ moderno?

Casi nunca en código de aplicación. std::make_unique y std::make_shared construyen el puntero inteligente sin un new desnudo en tu código, lo que mantiene la propiedad explícita y segura ante excepciones desde la primera línea.

¿Para qué sirve realmente std::weak_ptr?

Un weak_ptr observa un objeto gestionado por un shared_ptr sin prolongar su vida. Es la solución estándar para los ciclos de propiedad, y la herramienta adecuada para cachés y registros que deben comprobar si un objeto sigue existiendo sin mantenerlo vivo.

¿Los punteros inteligentes reemplazan a RAII, o son RAII?

Son RAII aplicado a la memoria: el destructor del objeto puntero libera el recurso automáticamente al salir del ámbito, exactamente igual que un guarda de bloqueo libera un mutex o un envoltorio de archivo cierra su descriptor.

Recibe 1 plan de agente IA cada semana

Únete al boletín gratuito de PVT: un agente de IA o automatización práctica que puedes construir y vender, cada semana. Sin spam.