Jednym z najwiÄ™kszych wyzwaÅ„ podczas tworzenia dużych aplikacji jest efektywne zarzÄ…dzanie pamiÄ™ciÄ…. W jÄ™zykach poziomu C i C++ nie ma żadnych mechanizmów które zabezpieczajÄ… przed tzw. wyciekiem pamiÄ™ci – zjawisku w którym tworzony jest obiekt, ale gubiony jest do niego wskaźnik w skutek czego pozostaje on w pamiÄ™ci nieużywany i niemożliwy do usuniÄ™cia aż do zakoÅ„czenia programu.
Jak wiele języków (środowisk) w C++ pamięć dzieli się na stos oraz stertę.
Stos to „pamięć podrÄ™czna”, miejsce gdzie trzymane sÄ… lokalne zmienne (w tym obiekty, ale lokalne!). Pamięć ta jest rezerwowana w momencie wywoÅ‚ania funkcji (przesuniÄ™cie wskaźnika stosu do przodu) oraz zwalniana gdy siÄ™ z tej funkcji wychodzi (wskaźnik jest cofany), kompilator sam na podstawie symboli znajdujÄ…cych siÄ™ w tej funkcji/metodzie wywoÅ‚uje odpowiednie destruktory. Pamięć ta ma jednÄ… wadÄ™ — jest ograniczona i nie powinno siÄ™ w niej trzymać dużych danych.
class Bar{ public: ~Bar(){std::cout<<\"destruct Bar\\n\";} }; class Foo{ public: Bar bar; ~Foo(){std::cout<<\"destruct Foo\\n\";} int getVal(){ return 1; } }; int foo(){ Foo obj; return obj.getVal(); }
Zarówno obiekty `obj\' jak i jego pole `bar\' zostaną usunięte po zakończeniu funkcji `foo()\'.
Sterta to „wszystko poza stosem”. Jest to pamięć rezerwowana w C++ poprzez operator new, który zwraca wskaźnik do tej pamiÄ™ci (i przy okazji wywoÅ‚uje odpowiedni konstruktor). Pamięć tak zarezerwowana musi zostać rÄ™cznie zwolniona operatorem delete, w przeciwnym razie zostanie zwolniona dopiero w momencie wyÅ‚Ä…czenia programu. JeÅ›li utracimy wskaźnik do takiego obiektu nie bÄ™dziemy w stanie go ani wykorzystać, ani usunąć. Po zakoÅ„czeniu funkcji lub destrukcji obiektu który takÄ… pamięć zadeklarowaÅ‚ usuwany jest wÅ‚aÅ›nie jedynie wksaźnik – zarezerwowana pamięć pozostaje.
class Bar{ public: ~Bar(){std::cout<<\"destruct Bar\\n\";} }; class Foo{ public: Bar *bar; Foo(){ bar = new Bar();} ~Foo(){std::cout<<\"destruct Foo\\n\";} int getVal(){ return 1; } }; int foo(){ Foo *obj = new Foo(); return obj->getVal(); }
W tym przypadku żaden destruktor nie będzie wywołany, żadna pamięć nie zostanie zwolniona, po każdym wywołaniu `foo()\' w pamięci pozostaną oba obiekty bez żadnej referencji do nich.
NajprostszÄ… metodÄ… jest oczywiÅ›cie trzymanie wszystkich wskaźników w polach klasy i usuwanie pamiÄ™ci na którÄ… wskazujÄ… w destruktorach. DziaÅ‚a to jednak tylko gdy dane organizujÄ… siÄ™ w drzewa: każdy obiekt ma swój wÅ‚asny zestaw „dzieci” i tylko jednego rodzica.
W grach na planszy może być jednocześnie kilkaset elementów używających tych samych zasobów tj. grafiki, dźwięki, nie ma sensu by każdy z tych obiektów ładował własny taki zestaw, wtedy najlepiej stworzyć menedżer zasobów, który będzie je ładował na początku rozgrywki (ktoś paimęta ekran ładowania z Quake 3 Arena?) i później ewentualnie dodawał kolejne potrzebne (do gry dołącza ktoś używający innego modelu postaci), a same obiekty posiadają wtedy tylko wskaźniki do tych zasobów, ale same ich nie zwalniają.
Czasem jednak dane są zbyt ciężkie żeby pozwolić sobie na ciągłe trzymanie ich w pamięci, powinny być załadowane wtedy i tylko wtedy gdy faktycznie są wykorzystywane. Trzeba wtedy zadbać by przy destrukcji użytkowników takiego zasobu zadbać o to by zasób nie był usunięty przedwcześnie (gdy są jeszcze inni użytkownicy) lub w ogóle (ostatni użytkownik zniszczony, a zasób pozostaje). Tutaj z pomocą przychodzą inteligentne wskaźniki.
C++11 włącza w standard szablonowe klasy inteligentnych wskaźników (wcześniej można było samemu je zaimplementować lub skorzystać z biblioteki Boost). Są to klasy zachowujące się jak wskaźniki które pozwalają wygodniej zarządzać pamięcią. Mamy do dyspozycji 3 klasy:
Unikalne wskaźniki przydają sie gdy mamy tylko jeden wskaźnik do danego obiektu i nie więcej.
class Foo{ public: std::unique_ptrbar; Foo() : bar (std::unique_ptr (new Bar())) {} //poprzez inicjalizacjÄ™ //Foo() { bar = std::move(std::unique_ptr (new Bar()));} //poprzez przypisanie ~Foo() {std::cout<<\"destruct Foo\\n\";} int getVal(){ return 1; } };
Obiekt `bar\' zostanie usunięty, zajmie się tym destruktor klasy wskaźnikowej std::unique_ptr. Obiekt na takim wskaźniku tworzy się od razu przy zadeklarowaniu wskaźnika:
std::unique_ptralbo poprzez przeniesienie innego (unikalnego) wskaźnika:foo(new Foo());
foo = std::move(std::unique_ptrWtedy wskaźnik podany za argument std::move zostanie zwolniony (wskaże nullptr), a jedynym właścicielem będzie ten nowy wskaźnik (`foo\').(new Foo()));
Wiele wskaźników współdzielonych może wskazywać na ten sam obiekt.
class Bar{ public: ~Bar(){std::cout<<\"destruct Bar\\n\";} }; class Foo{ int n; public: std::shared_ptrbar; Foo(int n) : n(n){} ~Foo(){std::cout<<\"destruct Foo \"< bar(new Bar()); obj1->bar = bar; obj2->bar = bar; std::cout<<\"release local pointer\\n\"; bar.reset(); std::cout<<\"destruct objects\\n\"; delete obj1; delete obj2; std::cout<<\"end\\n\"; }
Wynikiem wywołania funkcji `foo()\' będzie wypisanie kolejno:
release local pointer destruct objects destruct Foo 1 destruct Foo 2 destruct Bar endCo świadczy o tym, że obiekt `bar\' został zniszczony dopiero po zniszczeniu ostatniej referencji, czyli ostatniego wskaźnika inteligentnego, a jednocześnie przed końcem funkcji w której był stworzony. Metoda reset() tych wskaźników jest równoznaczna z utratą referencji (zniszczenia wskaźnika, ale do tego wskaźnika można przypisać kolejny).
Słabe wskaźniki stosuje się gdy używamy wskaźników współdzielonych, ale chcemy użyć referencji do takiego obiektu gdzie będzie on przydatny, ale nie wymagany do działania (dobrze jak jest, ale jak go nie ma to nie zakłóci to działania), przykładowo niektóre przypadki słuchaczy: co z tego że nasłuchują, skoro nic z tym nie robią, nie są nigdzie indziej wykorzystywani? Bazując na poprzednich definicjach klas:
void foo(){ Foo *obj1 = new Foo(1); Foo *obj2 = new Foo(2); std::shared_ptrbar(new Bar()); std::weak_ptr weak(bar); obj1->bar = bar; obj2->bar = bar; std::cout<<(!weak.expired()?\"weak not null\\n\":\"weak null\\n\"); std::cout<<\"release local\\n\"; bar.reset(); std::cout<<(!weak.expired()?\"weak not null\\n\":\"weak null\\n\"); std::cout<<\"destruct objects\\n\"; delete obj1; std::cout<<(!weak.expired()?\"weak not null\\n\":\"weak null\\n\"); delete obj2; std::cout<<(!weak.expired()?\"weak not null\\n\":\"weak null\\n\"); std::cout<<\"end\\n\"; }
Da wynik:
weak not null release local weak not null destruct objects destruct Foo 1 weak not null destruct Foo 2 destruct Bar weak null endOznacza to że słaby wskaźnik utrzymuje referencję do obiektu, ale gdy ten zostanie zniszczony, będzie o tym wiedział.
Słabe wskaźniki są niebezpieczne podczas pracy wielowątkowej, kiedy chce się na nich działać, bezpiecznie jest utworzyć z nich tymczasowo wskaźnik współdzielony, aby nie został usunięty w tym samym czasie:
if(auto barPtr = bar.lock()){ //operacje na barPtr }W ten sposób jednocześnie dowiemy się czy on już wygasł (funkcja lock zwróci nullptr, a wtedy warunek nie jest spełniony) oraz uzyskamy tymczasowy (w zasięgu tego warunku) wskaźnik współdzielony.
Przede wszystkim są to klasy szablonowe, stąd też konieczność użycia przy deklaracji nawiasów trójkątnych określających typ jaki ma przechowywać. Pomoże to nie tylko kompilatorowi, ale również też środowiskom programistycznym w podpowiadaniu kodu. Same w sobie są obiektami, których nie powinno się tworzyć na stercie(!), jedynie na stosie wewnątrz metod/funkcji lub jako pola klas. Wtedy mamy gwarancję że kompilator zadba o ich usunięcie i wywołanie destruktorów.
Największą zaletą inteligentnych wskaźników jest ich proste używanie niczym nieróżniące się od normalnych. Mamy takie metody i operatory jak:
UżywajÄ…c wskaźników shared należy pamiÄ™tać, że taki może siÄ™ „porozumiewać” z pozostaÅ‚ymi shared tylko wtedy, gdy TYLKO PIERWSZY otrzymaÅ‚ surowy normalny wskaźnik, a reszta zostaÅ‚a utworzona lub przypisana na podstawie jakiegoÅ› inteligentnego wskaźnika (i nie należy przy tym używać get()). W przeciwnym wypadku powstanÄ… dwa lub wiÄ™cej liczniki i obiekt zostanie usuniÄ™ty zanim zniszczeni zostanÄ… wszyscy użytkownicy.
Również jak wspomniałem wcześniej, inteligentne wskaźniki nie powinny wylądować bezpośrednio na stercie, nie należy ich tworzyć operatorem new gdyż wtedy tracą one sens.
Artemis 10.03.2014 o 20:12
Niezwykle gruntowne podejście do tematu i świetny artykuł. Gratulacje :)