C++, Programowanie

Inteligentne wskaźniki C++

inteligentne wskaźniki c++

Dzisiejszy wpis dotyczyć będzie inteligentnych wskaźników obecnych w C++ od standardu C++ 11. Przedstawię wam różnice pomiędzy poszczególnymi klasami inteligentnych wskaźników oraz ich przewagę nad surowymi wskaźnikami.

Tworzenie inteligentnego wskaźnika

Inteligentne wskaźniki to obiekty klas wprowadzonych w standardzie C++ 11 udoskonalające działanie wskaźników. Sposób użycia wskaźników inteligentnych praktycznie nie różni się od metody korzystania ze znanych nam raw pointerów. Zasadnicza różnica występuje przy tworzeniu inteligentnego wskaźnika. Zobaczmy na czym to polega (nie zapomnij dodać dyrektywy #include <memory>):

Jaki widzimy tworzenie instancji klasy inteligentnego wskaźnika na pierwszy rzut oka wydaje się nieco bardziej skomplikowane, ale co do zasady jest to bardzo proste. Posługujemy się wyrażeniem o takiej składni:

Pamiętaj, aby tworząc nowy obiekt inteligentnego wskaźnika zawsze starać się przydzielać mu dynamicznie alokowaną pamięć. Taką pamięć alokuje się na stercie i w odpowiednim momencie zwalnia. Gdybyśmy chcieli przydzielić inteligentnemu wskaźnikowi adres pamięci spoza sterty może dojść do próby zwolnienia tej pamięci co może okazać się co najmniej niebezpieczne dla działania programu. Nie rób tak.

Ponadto wskaźnik na pamięć na stercie musi być przekonwertowany jawnie.

 

Inteligentne wskaźniki C++ – zwalnianie pamięci

Jedną z najczęściej wymienianych zalet korzystania z inteligentnych wskaźników jest fakt, że są one de facto obiektami pewnych klas. W takim razie można zdefiniować konstruktor i destruktor wewnątrz definicji klasy. Działanie konstruktora mogłeś zauważyć już wcześniej. Operator new type_name zwraca niezerowy wskaźnik na właśnie zaalokowaną pamięć i właśnie ten wskaźnik jest argumentem konstruktora.

Natomiast w definicji destruktora określono zwalnianie wskazywanej przez wskaźnik pamięci na stercie. Jest to o tyle przydatne, że nie musimy pamiętać o zwalnianiu pamięci, ponieważ destruktor zrobi to za nas. Spróbujmy to zrozumieć na pewnym przykładzie:

 

W pierwszej z funkcji foo() tworzymy zmienną lokalną istniejącą tylko w bloku tej funkcji. Po słowie kluczowym return automatycznie zwolni się pamięć przydzielona na samą zmienną wskaźnikową ptr. Natomiast nowo zaalokowana pamięć pozostanie na stercie, co jak pewnie się domyślasz będzie błędem.

W kolejnej funkcji również korzystamy z raw pointera, ale przed jednym z returnów umieściliśmy operator delete, który zaalokowaną pamięć zwolni. Niby cacy, ale jeżeli funkcja ma więcej niż jedną ścieżkę kończącą to musimy pamiętać o każdorazowym skorzystaniu z delete. W foo2() funkcja nie ma ochoty na liczbę 5 i kończy swoje działanie. Jednak zapomnieliśmy o delete, co oczywiście jest błędem -> kolejny wyciek pamięci.

W ostatniej funkcji poszliśmy po rozum do głowy i skorzystaliśmy z inteligentnego wskaźnika. Nieważne co się stanie w obrębie tej funkcji. Zawsze wraz z końcem żywota inteligentnego wskaźnika, wywołany zostanie jego destruktor zwalniający zaalokowaną pamięć. Żyć nie umierać 😊.

 

Pojęcie własności inteligentnych wskaźników

Pewnie zauważyłeś, że w jednym ze skrawków kodów wcześnie użyłem klasy unique_ptr, a później użyłem wskaźnika shared_ptr. Nie było takiej potrzeby, po prostu chciałem, abyś zobaczył, że C++ 11 dostarczył więcej niż jeden szablon klasy inteligentnych wskaźników. Teraz poznamy zasadniczą różnicę pomiędzy unique_ptr() a shared_ptr().

Inteligentne wskaźniki klasy unique_ptr są (Mr. Obvious) unikalne. Korzystając z unikalnego wskaźnika gwarantujemy, że on i tylko on będzie wskazywał na dany obszar w pamięci. Jest on właścicielem tego miejsca. Zatem skorzystamy z niego w momencie, gdy będziemy chcieli mieć pewność, że pamięć nie zostanie swobodnie przekazana do innego wskaźnika. Jeżeli już będziemy chcieli to zrobić to skorzystamy z jawnych metod (np. funkcja move() lub metoda swap()).

Mechanizm unikalnego wskaźnika jest o tyle bezpieczny, że nigdy nie dojdzie do sytuacji, w której destruktory dwóch wskaźników będą próbowały zwolnić tą samą pamięć.

 

Shared_ptr czyli wspólnego wskaźnika użyjemy w przypadku, gdy będziemy chcieli umożliwić wskazywanie tego samego miejsca w pamięci przez kilka wskaźników. Inteligentne wskaźniki shared_ptr zostały obmyślone w taki sposób, aby w przypadku korzystania z nich również nie doszło do próby zwolnienia tego samego obszaru pamięci przez kilka destruktorów. Zaimplementowano w tym celu rodzaj licznika zliczającego liczbę wskaźników wskazujących na tą samą pamięć. Dzięki temu do zwolnienia pamięci dojdzie wówczas, gdy żywot zakończy ostatni z wskaźników.

W powyższym przykładzie mamy trzy wskaźniki zarządzające tym samym skrawkiem pamięci z tym, że dwa z nich kończą swój żywot wcześniej. Gdyby wraz z ich zniszczeniem zwolniono też pamięć to nie bylibyśmy w stanie odwołać  się do tej pamięci po tym fakcie.

 

Podsumowanie

Temat inteligentnych wskaźników w C++ jest o wiele szerszy niż w niniejszym wpisie. Ponadto budzi on wiele emocji wśród programistów tego języka. Słusznie, bo jest to pretekst do merytorycznych dyskusji na ten temat. Nie chciałem specjalnie wydłużać tego wpisu, ale jeszcze kiedyś wrócę do tego tematu. Do opisania jest kolejny inteligentny wskaźnik weak_ptr oraz sposób użycia wskaźników przy pracy z tablicami. Jak zawsze odsyłam do dokumentacji C++.

 

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *