[C++] Szablony klas inteligentnych wskaźników

opublikował: Razi91, 2014-03-09

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.

Stos i sterta

Jak wiele języków (środowisk) w C++ pamięć dzieli się na stos oraz stertę.

Stos

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

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.

Zarządzanie pamięcią

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.

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: