Cosa sono i puntatori intelligenti e quando dovrei usarne uno?

Cosa sono i puntatori intelligenti e quando dovrei usarne uno?

In questo tutorial imparerai i puntatori intelligenti e perché e come utilizzare il puntatore intelligente nei programmi C++. Vedremo prima cosa sono i puntatori intelligenti e quando usarli. Il prerequisito principale di questo tutorial è che dovresti avere una conoscenza di base sui puntatori. Prima di comprendere l'applicazione dei puntatori intelligenti, comprendiamo il problema con i puntatori normali.

Quali sono i problemi con i puntatori normali o non elaborati?

Credo tu sappia che la memoria allocata da new non verrà distrutta automaticamente, devi farlo manualmente chiamando il cancella . Ti offre i vantaggi di tenerli per tutto il tempo che desideri.

Il problema con i puntatori C++ "grezzi" è che il programmatore deve distruggere esplicitamente l'oggetto quando non è più utile. Se hai dimenticato di rilasciare la memoria allocata o si verifica un'eccezione prima di eliminare la memoria, si verificheranno perdite di memoria. Come tutti sapete, si verifica una perdita di memoria quando i programmatori dimenticano di deallocare la memoria allocata.

Vedi il seguente programma 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 funzione sopra menzionata fun() sta creando un puntatore non elaborato locale che punta alla memoria allocata per l'intero. Quando la funzione fun() termina, il puntatore locale ptr verrà distrutto in quanto è una variabile di stack. Ma la memoria a cui punta non verrà deallocata perché ci siamo dimenticati di usare delete ptr; alla fine del divertimento(). Quindi la memoria viene trapelata perché la memoria allocata diventa irraggiungibile e non può essere deallocata.

Ma ora dirai che è un errore del programmatore che non dimenticherò mai di aggiungere elimina. Scrivo sempre codice pulito e a prova di errore, perché dovrei usare i puntatori intelligenti? E tu mi hai chiesto "Ehi, controlla il mio codice", qui sto allocando la memoria e deallocandola correttamente dopo i suoi usi. Ora dimmi "Perché dovrei usare un puntatore intelligente e qual è la necessità di un puntatore intelligente"?

#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;
}

Dopo aver esaminato il tuo codice, sono d'accordo con le tue parole che stai allocando e rilasciando la memoria correttamente. Inoltre, il tuo codice funzionerà perfettamente in scenari normali.

Ma pensa ad alcuni scenari pratici. Potrebbe esserci la possibilità che si verifichi qualche eccezione a causa di un'operazione non valida tra l'allocazione della memoria e la deallocazione. Questa eccezione potrebbe essere dovuta all'accesso a una posizione di memoria non valida, alla divisione per zero o ..ecc

Quindi, se si verifica un'eccezione o un altro programmatore integra un'istruzione di ritorno prematura per correggere un altro bug tra l'allocazione della memoria e la deallocazione. In tutti i casi, non raggiungerai mai il punto in cui la memoria viene rilasciata. Una soluzione semplice a tutti i problemi di cui sopra sono i puntatori intelligenti.

È il motivo per cui molti programmatori odiano i puntatori grezzi. Molti problemi sono coinvolti con i normali puntatori come una perdita di memoria, un puntatore penzolante, ecc.

Cos'è un puntatore intelligente?

Un puntatore intelligente è una classe modellata RAII progettata per gestire la memoria allocata dinamicamente. I puntatori intelligenti assicurano che la memoria allocata venga rilasciata quando l'oggetto puntatore intelligente esce dall'ambito. In questo modo il programmatore è libero dalla gestione manuale della memoria allocata dinamicamente.

Nella moderna programmazione C++ (since C++11) , la libreria standard include puntatori intelligenti. C++11 ha tre tipi di puntatori intelligenti std::unique_ptrstd::shared_ptr e std::weak_ptr . Questi puntatori intelligenti sono definiti nello spazio dei nomi std in <memory> file di intestazione. Quindi devi includere <memory> file di intestazione prima di utilizzare questi puntatori intelligenti.

Vedremo questi puntatori intelligenti uno per uno, ma prima di utilizzarli comprendiamo il funzionamento dei puntatori intelligenti e implementiamo i nostri puntatori intelligenti.

Implementazione del puntatore intelligente:

I puntatori intelligenti sono solo classi che avvolgono il puntatore non elaborato e sovraccaricano il -> e * operatore. Questi operatori sovraccaricati consentono loro di offrire la stessa sintassi di un puntatore non elaborato. Significa che gli oggetti della classe puntatore intelligente sembrano normali puntatori.

Considera il seguente semplice SmartPointer classe. In cui abbiamo sovraccaricato il -> e * operatori e il distruttore di classi contiene la chiamata da eliminare.

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;
};

È possibile utilizzare la classe SmartPointer come oggetti allocati nello stack. Poiché il puntatore intelligente dichiarato nello stack viene automaticamente eliminato quando esce dall'ambito . E il compilatore si occupa di chiamare automaticamente il distruttore. Il distruttore puntatore intelligente contiene un operatore di eliminazione che rilascerà la memoria allocata.

Considera il seguente programma C++ in cui sto usando la classe SmartPointer. Puoi vedere che la memoria dinamica viene gestita automaticamente da questa classe e non devi preoccuparti della deallocazione della 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:

Il summenzionato SmartPointer class funziona solo per numeri interi. Ma puoi renderlo generico usando i modelli C++. Considera l'esempio seguente.

#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: Il codice di implementazione del puntatore intelligente sopra è stato creato solo per comprendere il concetto di puntatori intelligenti. Questa implementazione non è adatta per molti casi pratici. Inoltre, non è affatto un'interfaccia completa di un puntatore intelligente realistico.

Tipi di puntatori intelligenti:

La sezione seguente riassume i diversi tipi di puntatori intelligenti disponibili in C++11 e descrive quando usarli.

ptr_unico:

È definito nell'intestazione nella libreria standard C++. Fondamentalmente, un puntatore univoco è un oggetto che possiede un altro oggetto e gestisce quell'altro oggetto tramite un puntatore. Il puntatore univoco ha la proprietà esclusiva dell'oggetto a cui punta.

Comprendiamo unique_ptr con un esempio, supponiamo U è un oggetto del puntatore univoco che memorizza un puntatore a un secondo oggetto P . L'oggetto U eliminerà P quando U è esso stesso distrutto. In questo contesto, U si dice che possieda P .

Inoltre, devi ricordare che unique_ptr non condivide il suo puntatore con nessun altro unique_ptr. Questo può essere solo spostato. Ciò significa che la proprietà della risorsa di memoria viene trasferita a un altro unique_ptr e l'originale unique_ptr non ne è più il proprietario.

L'esempio seguente mostra come creare istanze unique_ptr e come spostare la proprietà su un altro puntatore univoco.

#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;
}

Risultato:

Remark: I suoi usi includono la sicurezza delle eccezioni per la memoria allocata dinamicamente, il passaggio della proprietà della memoria allocata dinamicamente a una funzione e la restituzione della memoria allocata dinamicamente da una funzione.

ptr_condiviso:

shared_ptr è un tipo di puntatore intelligente progettato per scenari in cui la durata dell'oggetto in memoria è gestita da più proprietari. Significa il shared_ptr implementa la semantica della proprietà condivisa.

Come unique_ptr, anche shared_ptr è definito nell'intestazione nella libreria standard C++. Poiché segue il concetto di proprietà condivisa, dopo aver inizializzato un shared_ptr puoi copiarlo, assegnarlo o passarlo per valore negli argomenti della funzione. Tutte le istanze puntano allo stesso oggetto allocato.

shared_ptr è un puntatore contato di riferimento. Un contatore di riferimento viene aumentato ogni volta che viene aggiunto un nuovo shared_ptr e diminuisce ogni volta che un shared_ptr esce dall'ambito o viene reimpostato. Quando il conteggio dei riferimenti raggiunge lo zero, l'oggetto appuntito viene eliminato. Significa che l'ultimo proprietario rimasto del puntatore è responsabile della distruzione dell'oggetto.

Remark: Un shared_ptr si dice vuoto se non possiede un puntatore.

L'esempio seguente mostra come creare istanze shared_ptr e come condividere la proprietà con un altro puntatore 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

punto_debole

Un weak_ptr è un puntatore intelligente che memorizza un riferimento debole a un oggetto che è già gestito da un shared_ptr . Il debole_ptr non assume la proprietà di un oggetto ma agisce come un osservatore (debole_ptr sono per l'osservazione condivisa). Ciò significa di per sé che non partecipa al conteggio dei riferimenti per eliminare un oggetto o prolungarne la durata. Utilizziamo principalmente il debole_ptr per interrompere i cicli di riferimento formati da oggetti gestiti da std::shared_ptr.

Un debole_ptr può essere convertito in un shared_ptr usando il blocco della funzione membro per accedere all'oggetto. Significa che puoi usare un debole_ptr per provare ad ottenere una nuova copia di shared_ptr con cui è stato inizializzato. Se la memoria è già stata cancellata, l'operatore bool di deboli_ptr restituisce false.

Articoli consigliati per te:

  • Corsi ed esercitazioni di programmazione C++.
  • Come creare e utilizzare puntatori univoci in C++.
  • nuovo operatore in C++ per la memoria dinamica
  • maloc() vs nuovo.
  • Introduzione di riferimento in C++.
  • Puntatore in C/C++.
  • Domande del colloquio C++ con risposte.
  • Elenco di alcuni dei migliori libri C++, devi assolutamente vedere.
  • Domande del colloquio sull'allocazione dinamica della memoria.

Riferimenti:
Gestione dinamica della memoria.