« Wskaźniki inteligentne (C++11), lekcja »
Rozdział 45. 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ł 45. 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
Poprzedni dokumentNastępny dokument
Zarządzanie pamięcią new, deletePrzeczytałem kurs C++ - co dalej?