Menu

Inteligentne wskaźniki w C++: unique_ptr i shared_ptr

Inteligentne wskaźniki są właścicielami pamięci na stercie i zwalniają ją automatycznie. Poznaj unique_ptr, shared_ptr, make_unique i make_shared oraz dowiedz się, dlaczego prawie nigdy nie musisz już pisać new/delete.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ