Wstęp
Wszystkie rzeczy omówione poniżej wymagają C++11 i dołączenia nagłówka
<memory>.
Zacznijmy od przykładu (błędnego):
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:
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:
#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ą:
std::unique_ptr < int > uptr( new int );
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.
std::unique_ptr < int > uptr( new int );
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.
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():
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".
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ą:
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.
struct Lista
{
int liczba;
std::shared_ptr < Lista > nastepny;
Lista * poprzedni;
};
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<>.
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().
#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