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
Rozszerzenie materiału
Poniżej znajdziesz rozszerzenie materiału opracowane przez Grzegorza Bazior.
std::make_shared jako lepsza alternatywa tworzenia inteligentnych wskaźników
Tworzenie inteligentnych wskaźników typu shared_ptr przy użyciu operatora new powoduje dwie alokacje pamięci - osobno dla obiektu oraz dla bloku kontrolnego. W wielu przypadkach można to zoptymalizować do jednej alokacji przy użyciu std::make_shared.
Funkcja
std::make_shared tworzy zarówno obiekt, jak i blok kontrolny w jednej operacji:
std::shared_ptr < int > sptr = std::make_shared < int >();
std::shared_ptr < int > sptr2 = std::make_shared < int >( 44 ); auto sptr3 = std::make_shared < int >( 45 );
Zalety
std::make_shared:
std::shared_ptr < VeryLongTypeName > sptr { new VeryLongTypeName };
auto sptr = std::make_shared < VeryLongTypeName >();
a także dzięki jednokrotnemu podawaniu typu unikamy przypadkowych błędów przy jego zmianie.
std::make_unique jako lepsza alternatywa tworzenia inteligentnych wskaźników
std::make_unique jest zalecanym sposobem tworzenia obiektów typu
std::unique_ptr.
W przeciwieństwie do
std::shared_ptr, nie zmienia on modelu alokacji pamięci, ale zapewnia inne korzyści:
std::unique_ptr < VeryLongTypeName > sptr { new VeryLongTypeName };
auto sptr = std::make_unique < VeryLongTypeName >();
Unikamy także błędów związanych z niezgodnością typów:
std::shared_ptr < BaseClass > sptr { new DerivedClass };
W przypadku tablic należy używać odpowiedniej wersji:
auto sptr = std::make_unique < int[ ] >( 10 );
Niepoprawne użycie prowadzi do niezdefiniowanego zachowania:
std::unique_ptr < int > sptr { new int[ 10 ] { } }; std::unique_ptr < int[ ] > sptr { new int { 44 } };
Bonus - zwalnianie innych zasobów przy pomocy inteligentnych wskaźników
Inteligentne wskaźniki mogą zarządzać nie tylko pamięcią, ale także innymi zasobami (np. plikami), dzięki możliwości podania własnego deletera:
#include <iostream>
#include <memory>
#include <cstdio>
int main()
{
auto fileCloser =[ ]( FILE * file )
{
fclose( file );
std::cout << "File closed!\n";
};
std::unique_ptr < FILE, decltype( fileCloser ) > file(
fopen( "test.txt", "w" ),
fileCloser
);
if( file )
{
fprintf( file.get(), "Hello World!\n" );
}
else
{
std::cerr << "Error opening file!\n";
}
}
Typ deletera jest częścią typu
std::unique_ptr.
W przypadku
std::shared_ptr deleter nie jest częścią typu, lecz przechowywany jest w bloku kontrolnym:
#include <iostream>
#include <memory>
#include <cstdio>
int main()
{
std::shared_ptr < FILE > file(
fopen( "test.txt", "w" ),
::fclose
);
if( file )
{
fprintf( file.get(), "Hello World!\n" );
}
else
{
std::cerr << "Error opening file!\n";
}
}