new
Operator new, umożliwia nie tylko alokowanie pamięci pod nowo tworzone obiekty, ale również pod wskazany przez nas (wcześniej zaalokowany) obszar pamięci.
#include <iostream> int main() { int int_in_memory = 5; std::cout << "Memory (val) " << int_in_memory << std::endl; std::cout << "Memory (addr) " << &int_in_memory << std::endl; int *ptr = new (&int_in_memory) int(2); std::cout << "Ptr (val) " << *ptr << std::endl; std::cout << "Ptr (addr) " << ptr << std::endl; return 0; }W takim przypadku, new zawsze zwróci ten sam adres, który został mu przekazany. Nie można łączyć tego rodzaju inicjalizacji z nothrow (wersja która nie rzuca wyjątku)
Memory (val) 5 Memory (addr) 0xbf9c26f8 Ptr (val) 2 Ptr (addr) 0xbf9c26f8Każda pamięć jawnie zaalokowana przez nas musi być później zwolniona. W przypadku tablic trzeba użyć specjalnej formy delete [], aby wywołać destruktory obiektów, które się w niej znajdują.
#include <iostream> struct MyObject { MyObject(std::string n): name(n) { std::cout << "Constructor: " << name << std::endl; } ~MyObject() noexcept { std::cout << "Destructor: " << name << std::endl; } std::string name; }; int main() { MyObject *tab = new MyObject[3] { {"Elem1"}, {"Elem2"}, {"Elem3"} }; delete tab; // delete [] tab; return 0; }Jeżeli skorzystamy ze zwykłego delete, bez podania pustych nawiasów, zachowanie jest niezdefiniowane.
Constructor: Elem1 Constructor: Elem2 Constructor: Elem3 Destructor: Elem1 The program has unexpectedly finished.Nowy standard dodaje, wersje operatora new z nothrow i w razie wystąpienia błędu przy inicjalizacji pamięci zamiast wyjątku operator zwróci nullptr, aby poinformować nas o problemie.
#include <iostream> #include <memory> int main() { try { int *ptr = new int[1000000000]; } catch (std::bad_alloc& exception) { std::cout << "Exception: " << exception.what() << std::endl; } int *ptr = new (std::nothrow) int[1000000000]; if (ptr == nullptr) std::cout << "Not allocated" << std::endl; return 0; }Poniżej wyniki działania programu, w pierwszej wersji zwracany jest wyjątek, w drugiej jesteśmy informowani przez zwrócenie nullptr.
Exception: std::bad_alloc Not allocatedCo ciekawe, rzadko można w kodzie zauważyć sytuacje, gdy programista próbuje przestrzec się przed tego typu zdarzeniami (blok try-catch). W Linuxie jest to związane z domyślnie włączonym mechanizmem "opportunistic memory allocation", który nie sprawdza, czy pamięć jest dostępna gdy próbujemy ją zaalokować. W momencie, gdy próbujemy się do niej odwołać, a w systemie jej zabraknie program zostaje zterminowany. Ustawienie można sprawdzić przez:
cat /proc/sys/vm/overcommit_memory 0
smart pointers
W standardzie pojawiły się nowe inteligentne wskaźniki, zaczerpnięte z biblioteki boost. Są to unique_ptr (zdaje się odpowiednik boost::scoped_ptr), shared_ptr oraz weak_ptr. Herb Shutter daje kilka porad, odnośnie tego jaki i kiedy je stosować.
Można to podsumować w ten sposób. W nowoczesnym C++ zawsze powinno się używać inteligentnych wskaźników oraz surowych wskaźników nie posiadających prawa własności. unique_ptr powinno być preferowane nad shared_ptr (zawsze można będzie na niego przejść, jeżeli nasz zasób będzie musiał być współdzielony między komponentami o różnym czasie życia, albo gdy chcemy skorzystać z własnej metody do usunięcia zasobu z pamięci). Poniżej program, w którym m.in. shared_ptr, korzysta z funkcji "usuwającej" zasób z pamięci dostarczonej przez nas.
#include <iostream> #include <memory> struct MyObject { MyObject(std::string n): name(n) { std::cout << "Constructor: " << name << std::endl; } ~MyObject() noexcept { std::cout << "Destructor: " << name << std::endl; } std::string name; }; void myDeleter(MyObject* p) { std::cout << "Deleter: " << p->name << std::endl; } int main() { std::shared_ptr<MyObject> sha(new MyObject("shared"), myDeleter); std::unique_ptr<MyObject> uni1(new MyObject("unique")); std::unique_ptr<MyObject> uni2(uni1.release()); std::cout << "Uni1 -> " << (uni1 ? "exist" : "not exist") << std::endl; std::cout << "Uni2 -> " << (uni2 ? "exist" : "not exist") << std::endl; auto store = std::make_shared<MyObject>("weak"); std::weak_ptr<MyObject> wea(store); if (std::shared_ptr<MyObject> sp = wea.lock()) std::cout << "Locked: " << sp->name << std::endl; return 0; }A oto kolejność wywołania destruktorów dla dynamicznie stworzonych zasobów.
Constructor: shared Constructor: unique Uni1 -> not exist Uni2 -> exist Constructor: weak Locked: weak Destructor: weak Destructor: unique Deleter: sharedShutter (i nie tylko) zaleca tworzenie inteligentnych wskaźników za pomocą metod make_*. Ale to postanowiłem przetestować sobie w innym wpisie.
std::allocator
Ostatnią rzeczą, którą testowałem są alokatory. Pozwalają nam one na odseparowanie procesu alokowania od konstruowania. W przykładzie poniżej, szykujemy pamięć pod trzy elementy typu MyObject. Najpierw pamięć jest alokowana, a my uzyskujemy wskaźnik pod adres, gdzie powinien być skonstruowany pierwszy z naszych obiektów. Następnie korzystają z metody construct(), oraz przesuwając wskaźnik następuje proces konstruowania. Aby zniszczyć obiekt, należy skorzystać z metody destroy().
#include <iostream> #include <memory> struct MyObject { MyObject(std::string n): name(n) { std::cout << "Constructor: " << name << std::endl; } ~MyObject() noexcept { std::cout << "Destructor: " << name << std::endl; } std::string name; }; int main() { std::allocator<MyObject> alloc; std::cout << "Allocate" << std::endl; MyObject *ptr = alloc.allocate(3); std::cout << "Address: " << ptr << std::endl; alloc.construct(ptr, "Elem1"); ptr++; std::cout << "Address: " << ptr << std::endl; alloc.construct(ptr, "Elem2"); return 0; }Wynik działania.
Allocate Address: 0x8b2a008 Constructor: Elem1 Address: 0x8b2a00c Constructor: Elem2
Brak komentarzy:
Prześlij komentarz