Problem, który rozwiązują inteligentne wskaźniki
Na poprzedniej stronie pamięć była alokowana przez new i zwalniana przez delete. To działa, ale cały ciężar spada na ciebie: każde new potrzebuje pasującego delete na każdej ścieżce kodu, łącznie z tymi, na których w połowie rzucany jest wyjątek. Pomiń jedno, a pamięć wycieknie; wykonaj delete dwa razy, a uszkodzisz stertę.
Inteligentne wskaźniki naprawiają to, wiążąc czas życia pamięci na stercie ze zwykłym obiektem na stosie. Gdy ten obiekt wychodzi z zasięgu, jego destruktor wykonuje za ciebie delete, gwarantowanie, nawet gdy wyjątek zwija stos. Ta idea nazywa się RAII (Resource Acquisition Is Initialization), a inteligentne wskaźniki mieszkają w nagłówku <memory>.
*p i p->member używasz dokładnie tak jak przy surowym wskaźniku. Różnica polega na tym, że nigdy nie wywołujesz delete: robi to inteligentny wskaźnik.
unique_ptr: jeden właściciel, bez współdzielenia
unique_ptr to inteligentny wskaźnik, po który warto sięgać domyślnie. Reprezentuje własność wyłączną: w danej chwili obiekt ma dokładnie jednego właściciela unique_ptr, a gdy ten wskaźnik umiera, obiekt umiera razem z nim. W porównaniu z surowym wskaźnikiem nie ma żadnego narzutu w czasie działania.
Tworzysz go przez make_unique (C++14). Przyjmuje argumenty konstruktora i daje gotowy do użycia wskaźnik:
Ponieważ właściciel może być tylko jeden, unique_ptr nie da się skopiować. Próba kopiowania to błąd kompilacji, a ten błąd to język chroniący cię przed dwoma właścicielami, którzy obaj próbują wykonać delete na tym samym obiekcie:
auto a = make_unique<int>(10);
auto b = a; // error: call to deleted copy constructor of unique_ptr
Aby przekazać własność komuś innemu, przenosisz ją przez std::move. Po przeniesieniu oryginalny wskaźnik jest pusty (przechowuje nullptr):
Tego modelu chcesz przez większość czasu: zawsze jest dokładnie jeden jasny właściciel, a kompilator to wymusza.
shared_ptr: współdzielona własność przez zliczanie referencji
Czasem kilka części programu naprawdę musi współdzielić ten sam obiekt i żadna nie wie, która skończy ostatnia. Do tego służy shared_ptr. Prowadzi licznik referencji: każda kopia go zwiększa, każde zniszczenie zmniejsza, a obiekt jest zwalniany dopiero, gdy licznik spadnie do zera.
Tworzysz je przez make_shared:
W odróżnieniu od unique_ptr kopiowanie shared_ptr jest w porządku, i o to właśnie chodzi. Ceną jest koszt: licznik referencji jest trzymany na stercie i aktualizowany atomowo (bezpiecznie dla wątków), więc shared_ptr jest cięższy niż unique_ptr. Sięgaj po niego tylko wtedy, gdy własność jest naprawdę współdzielona, a nie po to, żeby nie myśleć o tym, kto jest właścicielem czego.
make_shared jest też wydajniejsze niż shared_ptr<T>(new T(...)): alokuje obiekt i blok kontrolny w jednej alokacji zamiast w dwóch.
weak_ptr i przerywanie cykli referencji
shared_ptr ma jedną klasyczną pułapkę: jeśli dwa obiekty trzymają shared_ptr do siebie nawzajem, ich liczniki referencji nigdy nie spadną do zera, więc żaden nie zostanie zwolniony. To wyciek pamięci mimo użycia inteligentnych wskaźników.
struct Node {
shared_ptr<Node> next; // jeśli dwa węzły wskazują na siebie nawzajem,
}; // utrzymują się przy życiu na zawsze
Rozwiązaniem jest weak_ptr: obserwator shared_ptr, który nie jest właścicielem. Nie zwiększa licznika referencji, więc nigdy nie utrzymuje obiektu przy życiu. Aby użyć obiektu, wywołujesz .lock(), które daje shared_ptr, jeśli obiekt nadal istnieje, albo pusty wskaźnik, jeśli już go nie ma.
Używaj weak_ptr do „wskaźników wstecz” i pamięci podręcznych: wszędzie tam, gdzie chcesz odwoływać się do obiektu bez przejmowania własności.
Częste błędy i pułapki
Inteligentne wskaźniki usuwają większość błędów pamięci, ale kilka pułapek zostaje:
Nie mieszaj własności inteligentnej i surowej tej samej pamięci. Nigdy nie buduj dwóch inteligentnych wskaźników z tego samego surowego wskaźnika, bo każdy spróbuje wykonać na nim delete:
int* raw = new int(5);
unique_ptr<int> a(raw);
unique_ptr<int> b(raw); // katastrofa: oba usuną ten sam int (podwójne zwolnienie)
Właśnie dlatego wybiera się make_unique/make_shared: nie ma luźnego surowego wskaźnika, którego można by źle użyć.
unique_ptr da się tylko przenosić, więc przekazuj go przez wartość, gdy chcesz przekazać własność. Jeśli funkcja ma obiektu używać, ale nie posiadać, przyjmij zwykłą referencję albo surowe T*: surowy wskaźnik, który tylko obserwuje, jest całkowicie w porządku:
void consume(unique_ptr<int> p); // przejmuje własność (przenieś do niej)
void observe(int* p); // tylko patrzy, niczego nie posiada
Nie sięgaj domyślnie po shared_ptr. Kusi, bo swobodnie się kopiuje, ale atomowe zliczanie referencji kosztuje realną wydajność, a współdzielona własność utrudnia rozumowanie o czasie życia obiektów. Domyślnie wybieraj unique_ptr; na shared_ptr przechodź tylko wtedy, gdy faktycznie potrzebujesz wielu właścicieli.
unique_ptr dla tablic wymaga formy tablicowej. make_unique<int[]>(n) daje unique_ptr<int[]>, który poprawnie wywołuje delete[]. W praktyce do tablic dynamicznych wybieraj std::vector: sam zarządza pamięcią, a do tego śledzi rozmiar.
Dalej: napisy
Zarządzanie pamięcią masz teraz pod kontrolą: inteligentne wskaźniki dają alokację na stercie bez wycieków. Jedną z najczęściej alokowanych i przekazywanych rzeczy jest tekst, a C++ daje do niego znacznie bezpieczniejsze narzędzie niż surowe bufory char*. Następna strona omawia std::string: jak sam się powiększa, jakich operacji będziesz używać na co dzień i dlaczego całkowicie zwalnia cię z ręcznej pracy z pamięcią.
Najczęściej zadawane pytania
Czym są inteligentne wskaźniki w C++?
Inteligentne wskaźniki to obiekty z <memory> (unique_ptr, shared_ptr, weak_ptr), które opakowują surowy wskaźnik i automatycznie wykonują delete na pamięci, gdy wychodzą z zasięgu. Dają alokację na stercie bez ręcznego delete i bez wycieków, które biorą się z zapominania o nim.
Czym się różni unique_ptr od shared_ptr?
unique_ptr jest jedynym właścicielem swojego obiektu: nie da się go skopiować, tylko przenieść, i zwalnia pamięć w chwili, gdy sam umiera. shared_ptr pozwala na współdzieloną własność przez zliczanie referencji: wiele shared_ptr może wskazywać na ten sam obiekt, a obiekt jest zwalniany dopiero wtedy, gdy zniszczony zostanie ostatni z nich. Wybieraj unique_ptr, chyba że naprawdę potrzebujesz współdzielonej własności.
Czy w nowoczesnym C++ używać make_unique czy new?
Używaj make_unique i make_shared. Alokują i opakowują obiekt w jednym kroku, więc nie ma surowego new, którego wynik mógłby wyciec, zanim trafi do inteligentnego wskaźnika. Praktyczna zasada: w nowoczesnym kodzie C++ nie powinno być prawie wcale gołych new ani delete.