Ten artykuł pochodzi z mojego starego bloga na kacperkolodziej.pl.
Nowe standardy języka C++ dodają nie tylko całkiem nowe możliwości,
ale także udostępniają nam udogodnienia, dzięki którym możemy
wyeliminować na zawsze niektóre problemy, które były poważnym
utrapieniem dla programistów. Jednym z takich problemów są wycieki
pamięci przy jej dynamicznym alokowaniu. Wielu z nas często zapomina o
zwalnianiu (przy użyciu operatorów delete i delete[]) pamięci
zarezerwowanej przy pomocy new oraz new[]. Nowe udogodnienia w
postaci wskaźników unikalnych i współdzielonych powodują, że z operatora
delete będziemy korzystać coraz rzadziej. Czym właściwie są nowe
rodzaje wskaźników?
Wspólne cechy wskaźników unikalnych i współdzielonych
Zacznijmy od tego, że nie są to nowe konstrukcje języka, a jedynie
szablony klasy z biblioteki szablonów STL (Standard Template Library).
Chodzi konkretnie o std::unique_ptr i std::shared_ptr. Szablony te
wymagają co najmniej jednego typu - typu danych na jakie wskaźnik ma
pokazywać. Drugim przekazywanym parametrem szablonu może być specjalna
klasa obiektów funkcyjnych, która zwalnia pamięć po obiekcie.
Standardowo jest używany default_delete z biblioteki standardowej -
czyli obiekt, który wykorzystuje wbudowany w język operator delete.
Aby wykorzystywać te typy wskaźników należy dołączyć do projektu plik
nagłówkowy memory: #include <memory>.
Obiekty klas unique_ptr i shared_ptr w praktyce
Dzięki przeładowaniu odpowiednich operatorów (operator->,
operator*, operator= i operator bool) możemy korzystać z nowych
wskaźników tak samo jak z tych, które są konstrukcją języka. Dodatkowo
możemy uzyskać dostęp do zwyczajnego wskaźnika dzięki funkcji
składowej get() (obecnej zarówno w std::unique_ptr jak i
std::shared_ptr). Dzięki temu wskaźnik możemy wykorzystywać również
w przypadku gdy wymagane jest wykorzystywanie tradycyjnych wskaźników.
Wskaźniki unikalne - unique_ptr
Cechy charakterystyczne wskaźników unikalnych to:
- może istnieć tylko jeden wskaźnik tego typu pokazujący na dany obiekt,
- w momencie kiedy przestaje istnieć wskaźnik, usuwany jest także obiekt, na który ten wskaźnik pokazywał, a pamięć, którą zajmował jest zwalniana,
- obiekt na który wskazuje wskaźnik jest także usuwany kiedy do wskaźnika przypisujemy inny obiekt,
- obiekt wskazywany przez
unique_ptrnie może być zarządzany przez inny obiekt tego typu ani teżshared_ptr.
Przykład użycia wskaźników unikalnych:
#include <iostream>
#include <memory>
using namespace std;
struct MyClass
{
static int nextId;
int id;
MyClass() : id(++nextId)
{
cout << "Tworze obiekt klasy MyClass. id = " << id << "\n";
}
~MyClass()
{
cout << "Usuwam obiekt klasy MyClass. id = " << id << "\n";
}
void f()
{
cout << "Wywołanie funkcji f() dla obiektu id: " << id << "\n";
}
};
struct MyClassDeleter
{
void operator()(MyClass* ptr)
{
cout << "--- Uruchomienie usuwania obiektu id: "
<< ptr->id << " przy pomocy MyClassDeleter\n";
delete ptr;
cout << "--- Obiekt id: " << ptr->id << " usuniety\n";
}
};
int MyClass::nextId = 0;
int main()
{
unique_ptr<MyClass> ptr1(new MyClass);
ptr1->f();
(*ptr1).f();
unique_ptr<MyClass, MyClassDeleter> ptr2(new MyClass);
return 0;
}
W powyższym przykładzie zdefiniowaliśmy klasę MyClass, która posiada konstruktor i destruktor oraz funkcję
składową void f(). Każda z tych funkcji wyświetla stosowny
komunikat. Kolejna struktura to "usuwacz". Kiedy wskaźnik ma usunąć
obiekt, wykorzystuje obiekt tej struktury jako funkcję (czyli
uruchamia operator()). W funkcji main() tworzymy dwa unikalne
wskaźniki i korzystamy z funkcji MyClass::f(). Po uruchomieniu
programu widzimy, kiedy tworzone są obiekty, kiedy wywoływane ich
funkcje składowe i kiedy są one usuwane z pamięci. Jak widać, do tego
ostatniego nie przyłożyliśmy nawet ręki, a pamięć została zwolniona.
Wskaźniki współdzielone - shared_ptr
Cechy charakterystyczne wskaźników współdzielonych to:
- może istnieć wiele wskaźników do tego samego obiektu
- obiekt jest usuwany w momencie gdy przestaje istnieć ostatni
wskaźnik typu
shared_ptr
Przykład użycia wskaźników współdzielonych:
#include <iostream>
#include <memory>
using namespace std;
struct MyClass
{
static int nextId;
int id;
MyClass() : id(++nextId)
{
cout << "Tworze obiekt klasy MyClass. id = " << id << "\n";
}
~MyClass()
{
cout << "Usuwam obiekt klasy MyClass. id = " << id << "\n";
}
void f()
{
cout << "Wywołanie funkcji f() dla obiektu id: " << id << "\n";
}
};
int MyClass::nextId = 0;
int main()
{
shared_ptr<MyClass> ptr1(new MyClass);
cout << "Tworze 2 kolejne wskazniki shared_ptr do obiektu o id: "
<< ptr1->id << "\n";
shared_ptr<MyClass> ptr2(ptr1);
shared_ptr<MyClass> ptr3(ptr1);
cout << "Uzywam funkcji f we wszystkich wskaźnikach\n";
ptr1->f();
(*ptr2).f();
ptr3->f();
cout << "Program usuwa wskazniki\n";
return 0;
}
Ponieważ std::shared_ptr nie udostępnia możliwości ustawienia klasy
kasującej obiekt, wykorzystamy tutaj tylko MyClass. Na samym
początku tworzymy jeden wskaźnik współdzielony do nowego obiektu
zaalokowanego w pamięci. Następnie tworzymy dwa współdzielone
wskaźniki. Musimy pamiętać, aby każdy następny tworzyć nie z
tradycyjnego wskaźnika do pamięci, a z istniejącego już
współdzielonego wskaźnika do pamięci. Następnie wykonujemy funkcję
MyClass::f() wykorzystując operator*, służący do wyłuskania
obiektu i operator-> służący do wyłuskania funkcji składowej tego obiektu.