¿Qué son los punteros inteligentes y cuándo debo usar uno?

¿Qué son los punteros inteligentes y cuándo debo usar uno?

En este tutorial, aprenderá punteros inteligentes y por qué y cómo utilizar el puntero inteligente en programas C++. Veremos primero qué son los punteros inteligentes y cuándo debemos usarlos. El requisito previo principal de este tutorial es que debe tener conocimientos básicos sobre punteros. Antes de comprender la aplicación de los punteros inteligentes, comprendamos el problema con los punteros normales.

¿Cuáles son los problemas con los punteros normales o sin procesar?

Creo que sabe que la memoria asignada por new no se destruirá automáticamente, debe hacerlo manualmente llamando a delete . Te ofrece las ventajas de conservarlos todo el tiempo que quieras.

El problema con los punteros de C++ 'en bruto' es que el programador tiene que destruir explícitamente el objeto cuando ya no es útil. Si olvidó liberar la memoria asignada o se produce una excepción antes de eliminar la memoria, se producirán pérdidas de memoria. Como todos saben, se produce una fuga de memoria cuando los programadores se olvidan de desasignar la memoria asignada.

Vea el siguiente programa en C++,

#include <iostream>
using namespace std;


void fun()
{
    // Using a raw pointer -- not recommended.
    int* ptr = new int;

    /*
    Use ptr...
    */
}

int main()
{
    // Infinite Loop
    while (1)
    {
        fun();
    }

    return 0;
}

La función mencionada anteriormente fun() está creando un puntero sin procesar local que apunta a la memoria asignada para el número entero. Cuando la función fun() finaliza, el puntero local ptr será destruido ya que es una variable de pila. Pero, la memoria a la que apunta no se desasignará porque olvidamos usar delete ptr; al final de la diversión(). Entonces, la memoria se pierde porque la memoria asignada se vuelve inalcanzable y no se puede desasignar.

Pero ahora dirás que es un error del programador. Nunca olvidaré agregar eliminar. Siempre escribo código limpio y a prueba de errores, ¿por qué debería usar punteros inteligentes? Y me preguntaste, "Oye, revisa mi código", aquí estoy asignando la memoria y desasignándola correctamente después de sus usos. Ahora dime "¿Por qué debo usar un puntero inteligente y cuál es la necesidad de un puntero inteligente"?

#include <iostream>
using namespace std;


void fun()
{
    // Using a raw pointer -- not recommended.
    int* ptr = new int;

    /*
    Use ptr...
    .
    .
    .
    */
    delete ptr;
}

int main()
{
    // Infinite Loop
    while (1)
    {
        fun();
    }

    return 0;
}

Después de mirar su código, estoy de acuerdo con sus palabras en que está asignando y liberando la memoria correctamente. Además, su código funcionará perfectamente en escenarios normales.

Pero piense en algunos escenarios prácticos. Puede existir la posibilidad de que se produzca alguna excepción debido a alguna operación no válida entre la asignación y desasignación de memoria. Esta excepción podría deberse al acceso a una ubicación de memoria no válida, la división por cero o... etc.

Entonces, si ocurre una excepción u otro programador integra una declaración de devolución prematura para corregir otro error entre la asignación y la desasignación de memoria. En todos los casos, nunca llegarás al punto en el que se libera la memoria. Una solución simple a todos los problemas anteriores son los punteros inteligentes.

Es la razón por la que muchos programadores odian los punteros en bruto. Hay muchos problemas relacionados con los punteros normales, como una pérdida de memoria, un puntero colgante, etc.

¿Qué es un puntero inteligente?

Un puntero inteligente es una clase modelada RAII diseñada para manejar la memoria asignada dinámicamente. Los punteros inteligentes garantizan que la memoria asignada se liberará cuando el objeto del puntero inteligente salga del alcance. De esta forma, el programador se libera de la gestión manual de la memoria asignada dinámicamente.

En la programación C++ moderna (since C++11) , la biblioteca estándar incluye punteros inteligentes. C++11 tiene tres tipos de punteros inteligentes std::unique_ptrstd::shared_ptr y std::weak_ptr . Estos punteros inteligentes se definen en el espacio de nombres estándar en el <memory> archivo de cabecera. Entonces debes incluir <memory> archivos de encabezado antes de usar estos punteros inteligentes.

Veremos estos punteros inteligentes uno por uno, pero antes de usarlos, comprendamos el funcionamiento de los punteros inteligentes e implementemos nuestros propios punteros inteligentes.

Implementación de puntero inteligente:

Los punteros inteligentes son solo clases que envuelven el puntero sin formato y sobrecargan el -> y * operador. Estos operadores sobrecargados les permiten ofrecer la misma sintaxis que un puntero sin formato. Significa que los objetos de la clase de puntero inteligente parecen punteros normales.

Considere el siguiente SmartPointer simple clase. En el que hemos sobrecargado el -> y * operadores y el destructor de clases contiene la llamada a eliminar.

class SmartPointer
{
public:
    // Constructor
    explicit SmartPointer(int* ptr) : m_ptr(ptr) {}

    // Destructor
    ~SmartPointer()
    {
        delete m_ptr;
    }

    // Overloading dereferencing operator
    int& operator* ()
    {
        return *m_ptr;
    }

    // Overloading arrow operator
    int* operator->()
    {
        return m_ptr;
    }

private:
    int* m_ptr;
};

Puede usar la clase SmartPointer como objetos asignados en la pila. Debido a que el puntero inteligente se declara en la pila, se destruye automáticamente cuando quedan fuera del alcance . Y el compilador se encarga de llamar automáticamente al destructor. El destructor de puntero inteligente contiene un operador de eliminación que liberará la memoria asignada.

Considere el siguiente programa C++ donde estoy usando la clase SmartPointer. Puede ver que esta clase maneja automáticamente la memoria dinámica y no necesita preocuparse por la desasignación de memoria.

#include <iostream>
using namespace std;

class SmartPointer
{
public:
    // Constructor
    explicit SmartPointer(int* ptr) : m_ptr(ptr) {}

    // Destructor
    ~SmartPointer()
    {
        cout<<"Release the allocated memory\n";
        delete m_ptr;
    }

    // Overloading dereferencing operator
    int& operator* ()
    {
        return *m_ptr;
    }

    // Overloading arrow operator
    int* operator->()
    {
        return m_ptr;
    }

private:
    int* m_ptr;
};


int main()
{
    SmartPointer ptr(new int(27));

    //print the value
    cout<< *ptr <<endl;

    //Assign a value
    *ptr = 10;

    //print the value
    cout<< *ptr <<endl;

    return 0;
}

Output:

El SmartPointer mencionado anteriormente La clase solo funciona para números enteros. Pero puede hacerlo genérico usando las plantillas de C++. Considere el siguiente ejemplo.

#include <iostream>
using namespace std;

//Generic smart pointer class
template <class T>
class SmartPointer
{
public:
    // Constructor
    explicit SmartPointer(T* ptr) : m_ptr(ptr) {}

    // Destructor
    ~SmartPointer()
    {
        cout<<"Release the allocated memory\n";
        delete m_ptr;
    }

    // Overloading dereferencing operator
    T& operator* ()
    {
        return *m_ptr;
    }

    // Overloading arrow operator
    T* operator->()
    {
        return m_ptr;
    }

private:
    T* m_ptr;
};

class Display
{
public:
    void printMessage()
    {
        cout<<"Smart pointers for smart people\n\n\n";
    }
};


int main()
{
    //With integer
    SmartPointer<int> ptr(new int(27));

    //print the value
    cout<< *ptr <<endl;

    //Assign a value
    *ptr = 10;

    //print the value
    cout<< *ptr <<endl;


    //With custom class
    SmartPointer<Display> ptr1(new Display());
    ptr1->printMessage();

    return 0;
}

Output:

Remark: El código de implementación de puntero inteligente anterior solo está hecho para comprender el concepto de punteros inteligentes. Esta implementación no es adecuada para muchos casos prácticos. Además, de ninguna manera es una interfaz completa de un puntero inteligente realista.

Tipos de punteros inteligentes:

La siguiente sección resume los diferentes tipos de punteros inteligentes que están disponibles en C++11 y describe cuándo usarlos.

punto_único:

Se define en el encabezado en la biblioteca estándar de C++. Básicamente, un puntero único es un objeto que posee otro objeto y administra ese otro objeto a través de un puntero. El puntero único tiene la propiedad exclusiva del objeto al que apunta.

Entendamos unique_ptr con un ejemplo, supongamos U es un objeto del puntero único que almacena un puntero a un segundo objeto P . El objeto U se deshará de P cuando U es en sí mismo destruido. En este contexto, U se dice que posee P .

Además, debe recordar que unique_ptr no comparte su puntero con ningún otro unique_ptr. Esto solo se puede mover. Esto significa que la propiedad del recurso de memoria se transfiere a otro unique_ptr y el unique_ptr original ya no lo posee.

El siguiente ejemplo muestra cómo crear instancias de unique_ptr y cómo mover la propiedad a otro puntero único.

#include <iostream>
#include <memory>
using namespace std;


class Test
{
public:
    void print()
    {
        cout << "Test::print()" << endl;
    }
};

int main()
{
    /*
    Create an unique pointer
    object that store the pointer to
    the Test object
    */
    unique_ptr<Test> ptr1(new Test);

    //Calling print function using the
    //unique pointer
    ptr1->print();

    //returns a pointer to the managed object
    cout << "ptr1.get() = "<< ptr1.get() << endl;

    /*
    transfers ptr1 ownership to ptr2 using the move.
    Now ptr1 don't have any ownership
    and ptr1 is now in a 'empty' state, equal to `nullptr`
    */
    unique_ptr<Test> ptr2 = move(ptr1);
    ptr2->print();

    //Prints return of pointer to the managed object
    cout << "ptr1.get() = "<< ptr1.get() << endl;
    cout << "ptr2.get() = "<< ptr2.get() << endl;

    return 0;
}

Salida:

Remark: Sus usos incluyen seguridad de excepción para la memoria asignada dinámicamente, pasar la propiedad de la memoria asignada dinámicamente a una función y devolver la memoria asignada dinámicamente desde una función.

punto_compartido:

shared_ptr es un tipo de puntero inteligente que está diseñado para escenarios en los que más de un propietario administra la duración del objeto en la memoria. Significa el shared_ptr implementa semántica de propiedad compartida.

Al igual que unique_ptr, shared_ptr también se define en el encabezado en la biblioteca estándar de C++. Debido a que sigue el concepto de propiedad compartida, después de inicializar un shared_ptr, puede copiarlo, asignarlo o pasarlo por valor en los argumentos de la función. Todas las instancias apuntan al mismo objeto asignado.

shared_ptr es un puntero contado de referencia. Un contador de referencia aumenta cada vez que se agrega un nuevo shared_ptr y disminuye cada vez que un shared_ptr queda fuera del alcance o se restablece. Cuando el recuento de referencias llega a cero, el objeto puntiagudo se elimina. Significa que el último propietario restante del puntero es responsable de destruir el objeto.

Remark: Se dice que shared_ptr está vacío si no posee un puntero.

El siguiente ejemplo muestra cómo crear instancias shared_ptr y cómo compartir la propiedad con otro puntero shared_ptr.

#include <iostream>
#include <memory>
using namespace std;

class Test
{
public:
    void print()
    {
        cout << "Test::print()" << endl;
    }
};

int main()
{
    /*
    Create an shared ptr
    object that store the pointer to
    the Test object
    */
    shared_ptr<Test> ptr1(new Test);

    //returns a pointer to the managed object
    cout << "ptr1.get() = "<< ptr1.get() << endl;
    //print the reference count
    cout << "ptr1.use_count() = " << ptr1.use_count() << endl;


    cout <<"\nCreate another shared pointer "
         "and Initialize with copy constructor.\n";
    /*
     Second shared_ptr object will also point to same pointer internally
     It will make the reference count to 2.
    */
    shared_ptr<Test> ptr2(ptr1);

    cout << "Prints return of pointer to the managed object\n";
    cout << "ptr1.get() = "<< ptr1.get() << endl;
    cout << "ptr2.get() = "<< ptr2.get() << endl;


    cout <<"\nprint the reference count after creating another shared object\n";
    cout << "ptr1.use_count() = " << ptr1.use_count() << endl;
    cout << "ptr2.use_count() = " << ptr2.use_count() << endl;

    // Relinquishes ownership of ptr1 on the object
    // and pointer becomes NULL
    cout <<"\nprint the reference count after reseting the first object\n";
    ptr1.reset();
    cout << "ptr1.get() = "<< ptr1.get() << endl;
    cout << "ptr2.use_count() = " << ptr2.use_count() << endl;
    cout << "ptr2.get() = "<< ptr2.get() << endl;

    return 0;
}

Output:

ptr1.get() = 0xf81700
ptr1.use_count() = 1

Create another shared pointer and Initialize with copy constructor.
Prints return of pointer to the managed object
ptr1.get() = 0xf81700
ptr2.get() = 0xf81700

print the reference count after creating another shared object
ptr1.use_count() = 2
ptr2.use_count() = 2

print the reference count after reseting the first object
ptr1.get() = 0
ptr2.use_count() = 1
ptr2.get() = 0xf81700

débil_ptr

Un weak_ptr es un puntero inteligente que almacena una referencia débil a un objeto que ya está administrado por un shared_ptr . El débil_ptr no se apropia de un objeto, pero actúa como un observador (débil_ptrs son para la observación compartida). Esto significa que en sí mismo no participa en el conteo de referencias para eliminar un objeto o extender su vida útil. Principalmente usamos el débil_ptr para romper los ciclos de referencia formados por objetos administrados por std::shared_ptr.

Un punto débil se puede convertir en un punto compartido mediante el bloqueo de función miembro para acceder al objeto. Significa que puede usar un débil_ptr para intentar obtener una nueva copia del shared_ptr con el que se inicializó. Si la memoria ya se eliminó, el operador booleano de débil_ptr devuelve falso.

Artículos recomendados para ti:

  • Cursos y tutoriales de programación en C++.
  • Cómo crear y usar punteros únicos en C++.
  • nuevo operador en C++ para memoria dinámica
  • malloc() frente a nuevo.
  • Introducción de referencia en C++.
  • Puntero en C/C++.
  • Preguntas de la entrevista de C++ con respuestas.
  • Lista de algunos de los mejores libros de C++ que debe ver.
  • Preguntas de la entrevista sobre la asignación de memoria dinámica.

Referencias:
Gestión de memoria dinámica.