Smart pointers en C++ moderno

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

  1. Prefiere std::make_unique / std::make_shared: Evitas posibles fugas si la construcción lanza excepción.
  2. Usa unique_ptr por defecto: Solo recurre a shared_ptr cuando realmente haya múltiples dueños.
  3. Rompe ciclos con weak_ptr: Siempre que crees estructuras con referencias mutuas.
  4. 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.

¿Cómo se usan los punteros en C++?

Los punteros son una característica fundamental en C++, pero pueden resultar confusos para los principiantes. En este artículo, explicaremos las particularidades de los punteros en C++ de manera clara y concisa.

¿Qué es un puntero?

Un puntero no es más que una variable que almacena la dirección de memoria de otra variable. Esto significa que, en lugar de almacenar el valor de una variable directamente, un puntero almacena la dirección de memoria donde se encuentra esa variable.

Cómo se usan los punteros en C++

Para declarar un puntero en C++, se utiliza el símbolo de asterisco (*). Por ejemplo, la declaración «int *p» declara un puntero a un entero llamado «p». Una vez que se ha declarado un puntero, se puede asignar a él la dirección de memoria de una variable utilizando el operador de dirección «&». Por ejemplo, «p = &x» asigna a «p» la dirección de memoria de la variable «x».

Diagrama punteros

En el diagrama de ejemplo anterior hemos creado dos punteros que apuntan a variables de tipo entero. Estos punteros p1 y p2 solamente señalan a la dirección de memoria que contiene a las variables 1 y 2. En este caso los punteros contienen los valores de memoria 0x01 y 0x05.

Particularidades

Una de las particularidades de los punteros en C++ es que se pueden utilizar para acceder al valor de la variable a la que apuntan mediante el operador de indirección «*». Por ejemplo, «cout << *p» imprimirá el valor de la variable a la que apunta el puntero «p».

Un uso habitual de los punteros en C++ es para crear y manipular estructuras de datos dinámicas, como matrices y listas enlazadas. Esto se logra utilizando los operadores «new» y «delete» para asignar y liberar memoria dinámicamente.

Peligros

Es importante tener en cuenta que los punteros en C++ pueden ser peligrosos si se usan incorrectamente. Es fácil cometer errores como acceder a una dirección de memoria no válida o liberar la memoria dos veces. Para evitar estos errores, se recomienda utilizar técnicas de programación defensiva, como inicializar los punteros a null y comprobar que no son null antes de utilizarlos.

Conclusión

En resumen, los punteros son una característica importante en C++, que permiten acceder a la memoria de manera eficiente y crear estructuras de datos dinámicas. Sin embargo, es fundamental tener cuidado al utilizarlos para evitar errores peligrosos.