« Wskaźniki inteligentne (C++11), lekcja »
Rozdział 46. Omówienie funkcjonalności standardowych wskaźników inteligentnych. (lekcja)
Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Zarejestruj się!
Autor: pekfos
Kurs C++

Wskaźniki inteligentne (C++11)

[lekcja] Rozdział 46. Omówienie funkcjonalności standardowych wskaźników inteligentnych.

Wstęp

Wszystkie rzeczy omówione poniżej wymagają C++11 i dołączenia nagłówka <memory>.
Zacznijmy od przykładu (błędnego):
C/C++
int * funkcja( int rozmiar )
{
    int * tab = new int[ rozmiar ];
   
    if( !f1( tab, rozmiar ) )
         return nullptr;
   
    if( !f2( tab, rozmiar ) )
         return nullptr;
   
    return tab;
}
Funkcje f1() i f2() w jakiś sposób przetwarzają dane, zwracają bool dla określenia powodzenia operacji. Mogą zwrócić błąd i wtedy cała funkcja() kończy się z błędem - zwraca pusty wskaźnik, zamiast takiego wskazującego na tablicę z wygenerowanymi danymi. Ta funkcja jest błędna, bo zwrócenie przez nią błędu oznacza wyciek pamięci. Przed każdym zwróceniem nullptr powinno tu być dopisane zwalnianie tab. Wersja poprawniejsza mogłaby wyglądać tak:
C/C++
int * funkcja( int rozmiar )
{
    int * tab = new int[ rozmiar ];
   
    if( f1( tab, rozmiar ) )
    if( f2( tab, rozmiar ) )
         return tab;
   
    delete[] tab;
    return nullptr;
}
Odwróciłem logikę tych warunków, by obsługa błędów była w tylko jednym miejscu, jednak nie zawsze da się tak zrobić. Czasem po prostu trzeba obstawić wszystkie wyjścia. Zwykłe wskaźniki to po prostu liczby opisujące jakiś adres - są za mało inteligentne żeby po sobie posprzątać.

Wskaźnik std::unique_ptr<>

Zacznijmy od odszyfrowania nazwy: unique pointer - unikalny wskaźnik. Wskaźnika nie muszę tłumaczyć. Unikalny znaczy, że jest to jedyne odniesienie do tej pamięci, więc pamięć ma zostać zwolniona w momencie zniszczenia zmiennej std::unique_ptr<>. Aby utworzyć taki wskaźnik dla jakiegoś typu T, typ trzeba wpisać w ostre nawiasy:
C/C++
#include <memory>
std::unique_ptr < int > uptr;
Zwróć uwagę, że nie piszemy gwiazdki nigdzie. Samo użycie std::unique_ptr<> oznacza, że to będzie wskaźnik. Ten jest utworzony jako pusty, zróbmy taki, który zarządza jakąś pamięcią:
C/C++
//std::unique_ptr<int> uptr = new int; // źle!
std::unique_ptr < int > uptr( new int ); // OK
Wskaźniki inteligentne trzeba tworzyć jawnie. Gdyby dało się ot tak skonwertować int* na std::unique_ptr<int>, można by sobie przypadkiem zwolnić pamięć, co mogłoby być katastrofalne w skutkach.
C/C++
std::unique_ptr < int > uptr( new int );
//std::unique_ptr<int> uptr2 = uptr; // błąd
Takich wskaźników nie da się skopiować, bo naruszałoby to założenie, że dana instancja std::unique_ptr<> jest jedynym podmiotem zarządzającym jakimś skrawkiem pamięci. Da się utworzyć 2 wskaźniki inteligentne na podstawie tego samego adresu, ale to czysta głupota - te wskaźniki są inteligentne, ale nie mogą wiedzieć o sobie nawzajem. Zwolnią pamięć spod tego samego adresu w swoim czasie i spowodują tym samym błąd aplikacji.
C/C++
std::unique_ptr < int > uptr( new int );
std::unique_ptr < int > uptr2;

std::cout << "uptr: " <<( uptr != nullptr ) << " uptr2: " <<( uptr2 != nullptr ) << '\n';
uptr2 = std::move( uptr );
std::cout << "uptr: " <<( uptr != nullptr ) << " uptr2: " <<( uptr2 != nullptr ) << '\n';
uptr: 1 uptr2: 0
uptr: 0 uptr2: 1
Nie da się takiego wskaźnika skopiować, ale można go przenieść. Dzięki temu można taki wskaźnik zwrócić z funkcji.
Naprawmy teraz przykład ze wstępu, bez zmieniania niczego w sposobie użycia tej funkcji, ani w sposobie użycia funkcji pomocniczych f1() i f2():
C/C++
int * funkcja( int rozmiar )
{
    std::unique_ptr < int[] > tab( new int[ rozmiar ] );
   
    if( !f1( tab.get(), rozmiar ) )
         return nullptr;
   
    if( !f2( tab.get(), rozmiar ) )
         return nullptr;
   
    return tab.release();
}
Pierwsza rzecz: std::unique_ptr<int[]>. W zwykłych wskaźnikach nie ma rozróżnienia na takie wskazujące na jeden element, czy na tablicę elementów. W pewnym momencie musimy po prostu wiedzieć, którego delete użyć. std::unique_ptr<> też musi to wiedzieć, bo odpowiada za zwolnienie pamięci, więc informujemy go o tym, że wskazuje na tablicę, przez podanie T[] jako typu.
f1() i f2() operowały we wstępie na zwykłych wskaźnikach i tak miało zostać, do wyciągnięcia adresu z std::unique_ptr<> służy metoda get(). Całość miała zwracać zwykły wskaźnik, ale tutaj nie można już użyć get() - wyjście z funkcji spowodowałoby zwolnienie tej tablicy. Odpowiednią metodą w tym wypadku jest release(), która zwraca wskaźnik i ustawia pusty wskaźnik w zmiennej std::unique_ptr<>. Od tego momentu sami odpowiadamy za zwolnienie pamięci.

Wskaźnik std::shared_ptr<>

Wskaźnik współdzielony. Różnica względem wskaźnika std::unique_ptr<> polega na tym, że można go kopiować. Kopie wskaźników std::shared_ptr<> utrzymują między sobą licznik referencji, zwiększany w momencie utworzenia kopii, zmniejszany w momencie zniszczenia kopii. Pamięć zostaje zwolniona według reguły "kto ostatni, ten sprząta".
C/C++
std::shared_ptr < int > sptr( new int );
std::shared_ptr < int > sptr2;

std::cout << "sptr: " << sptr.use_count() << " sptr2: " << sptr2.use_count() << '\n';
sptr2 = sptr;
std::cout << "sptr: " << sptr.use_count() << " sptr2: " << sptr2.use_count() << '\n';

{
    std::shared_ptr < int > sptr3 = sptr2;
    std::cout << "sptr: " << sptr.use_count() << " sptr2: " << sptr2.use_count() << '\n';
}

std::cout << "sptr: " << sptr.use_count() << " sptr2: " << sptr2.use_count() << '\n';
sptr: 1 sptr2: 0
sptr: 2 sptr2: 2
sptr: 3 sptr2: 3
sptr: 2 sptr2: 2

Wskaźnik std::weak_ptr<>

std::shared_ptr<> pilnuje zaalokowanej pamięci nawet gdy kopie wskaźnika pojawiają się i znikają, pamięć zostanie zwolniona gdy nic już się do niej nie odnosi (a przynajmniej nie std::shared_ptr<>). Jednak wciąż, pamięć zarządzana przez std::shared_ptr<> może wyciec, jeśli się nie uważa na to, co się robi. Aby zarządzana pamięć została zwolniona, ostatni wskaźnik std::shared_ptr<> musi zostać zniszczony, co jest proste do zagwarantowania w przypadku zmiennych automatycznych, ale w przypadku dynamicznej alokacji już nie bardzo. Oczywiście to byłoby głupie mieszać bezpieczne z niebezpiecznym i oczekiwać, że całość będzie bezpieczna, więc rozważmy błędnie działający przypadek, w którym wszystkie wskaźniki to "bezpieczne" std::shared_ptr<> - nie ma żadnej alokacji zostawionej samej sobie, bez wskaźnika inteligentnego. Weźmy taką listę dwukierunkową:
C/C++
struct Lista
{
    int liczba;
    std::shared_ptr < Lista > nastepny, poprzedni;
};
Węzeł wskazuje na następny i poprzedni, a więc podtrzymuje je przy życiu. Sam również jest podtrzymywany przy życiu przez te węzły. Jeśli w liście jest więcej niż jeden element, powstaje cykl referencji. Gdyby to była lista jednokierunkowa, można by po prostu zapomnieć o niej i wtedy wyzeruje się licznik referencji wskaźnika na początek listy, początek listy zostanie usunięty i rekurencyjnie cała reszta też. Tutaj, zapomnienie o pierwszym elemencie nic nie da, bo drugi element o nim pamięta, przez wskaźnik na poprzednika.
C/C++
struct Lista
{
    int liczba;
    std::shared_ptr < Lista > nastepny;
    Lista * poprzedni; // No to może zwykły wskaźnik..?
};
Takie rozwiązanie naprawi wyciek pamięci, ale nie jest idealne. Zwykły wskaźnik, o ile realizuje słabą referencję (nie podtrzymuje obiektu przy życiu), nie zapewnia żadnej informacji o tym, czy obiekt został już usunięty, czy jeszcze nie. Nie zapominajmy o tym, że usuwanie dzieje się automatycznie. W takiej liście dałoby się jeszcze nad tym zapanować. Do tego, ze zwykłego wskaźnika nie odzyskamy potem wskaźnika std::shared_ptr<> który zarządza daną pamięcią, więc przydałoby się coś lepszego: std::weak_ptr<>.
C/C++
struct Lista
{
    int liczba;
    std::shared_ptr < Lista > nastepny;
    std::weak_ptr < Lista > poprzedni;
};
Wskaźnik std::weak_ptr<> jest związany z pamięcią zarządzaną przez std::shared_ptr<>, ale nie będzie przeszkadzać w usuwaniu obiektu. Można na nim sprawdzić, czy zarządzana pamięć została już zwolniona oraz uzyskać std::shared_ptr<> na tą pamięć, do czego służy metoda lock().
C/C++
#include <iostream>
#include <memory>


int main()
{
    std::weak_ptr < int > weak;
    std::cout << "expired: " << weak.expired() << "; address: " << weak.lock() << '\n';
   
    {
        std::shared_ptr < int > strong( new int );
        weak = strong;
        std::cout << "expired: " << weak.expired() << "; address: " << weak.lock() << '\n';
    }
   
    std::cout << "expired: " << weak.expired() << "; address: " << weak.lock() << '\n';
}
expired: 1; address: 0
expired: 0; address: 0x77ad90
expired: 1; address: 0
Poprzedni dokumentNastępny dokument
StrukturyRekurencja