C++/Zarządzanie pamięcią: Różnice pomiędzy wersjami

Z Wikibooks, biblioteki wolnych podręczników.
< C++
Usunięta treść Dodana treść
m Wycofano edycje użytkownika Franekmat (dyskusja). Autor przywróconej wersji to 37.47.165.84.
Znacznik: Wycofanie zmian
Nie podano opisu zmian
 
Linia 2: Linia 2:


Rozważmy prosty przykład. Załóżmy, że chcemy stworzyć wektor 10 liczb typu całkowitego. Możemy to zrobić na dwa sposoby. W stylu znanym z języka C:
Rozważmy prosty przykład. Załóżmy, że chcemy stworzyć wektor 10 liczb typu całkowitego. Możemy to zrobić na dwa sposoby. W stylu znanym z języka C:
<source lang="cpp">
<syntaxhighlight lang="cpp">
int *wektor = (int*) malloc (sizeof(int)*10);
int *wektor = (int*) malloc (sizeof(int)*10);
free (wektor);
free (wektor);
</syntaxhighlight>
</source>


Albo w stylu C++:
Albo w stylu C++:
<source lang="cpp">
<syntaxhighlight lang="cpp">
int *wektor = new int[10];
int *wektor = new int[10];
delete [] wektor;
delete [] wektor;
</syntaxhighlight>
</source>


Od razu widać, że drugi zapis jest łatwiejszy i przyjemniejszy w użyciu. To jest podstawowa zaleta operatora new - krótszy zapis. Wystarczy wiedzieć jakiego typu ma być obiekt, który chcemy powołać do życia, nie martwiąc się o rozmiar alokowanego bloku pamięci. Za pomocą operatora new można również tworzyć tablice wielowymiarowe:
Od razu widać, że drugi zapis jest łatwiejszy i przyjemniejszy w użyciu. To jest podstawowa zaleta operatora new - krótszy zapis. Wystarczy wiedzieć jakiego typu ma być obiekt, który chcemy powołać do życia, nie martwiąc się o rozmiar alokowanego bloku pamięci. Za pomocą operatora new można również tworzyć tablice wielowymiarowe:
<source lang="cpp">
<syntaxhighlight lang="cpp">
int **wektory = new int *[5];
int **wektory = new int *[5];
for (int i = 0; i < 5; ++i)
for (int i = 0; i < 5; ++i)
wektory[i] = new int [10];
wektory[i] = new int [10];
</syntaxhighlight>
</source>


W ten sposób stworzono tablicę dwuwymiarową którą statycznie zadeklarowalibyśmy jako:
W ten sposób stworzono tablicę dwuwymiarową którą statycznie zadeklarowalibyśmy jako:
<source lang="cpp">
<syntaxhighlight lang="cpp">
int wektory[5][10];
int wektory[5][10];
</syntaxhighlight>
</source>


Jednak w przeciwieństwie do <code>int wektory[5][10]</code>, która jest tablicą dwuwymiarową, nasze <code>int **wektory</code> jest tablicą tablic i może być rozrzucone po całej pamięci.
Jednak w przeciwieństwie do <code>int wektory[5][10]</code>, która jest tablicą dwuwymiarową, nasze <code>int **wektory</code> jest tablicą tablic i może być rozrzucone po całej pamięci.


Ilość elementów poszczególnych wymiarów nie musi być jednakowa. Można np zadeklarować tablicę taką:
Ilość elementów poszczególnych wymiarów nie musi być jednakowa. Można np zadeklarować tablicę taką:
<source lang="cpp">
<syntaxhighlight lang="cpp">
int **wektory = new int *[2];
int **wektory = new int *[2];
wektory[0] = new int [5];
wektory[0] = new int [5];
wektory[1] = new int;
wektory[1] = new int;
</syntaxhighlight>
</source>




Przy takiej deklaracji pierwszy wiersz ma 5 elementów (tablica) a drugi to jeden element.
Przy takiej deklaracji pierwszy wiersz ma 5 elementów (tablica) a drugi to jeden element.
Deklaracja tablic o większej ilości wymiarów przebiega podobnie:
Deklaracja tablic o większej ilości wymiarów przebiega podobnie:
<source lang="cpp">
<syntaxhighlight lang="cpp">
int ***wektory; // deklarujemy tablicę 3-wymiarową
int ***wektory; // deklarujemy tablicę 3-wymiarową
wektory = new int **[5]; // pierwszy wymiar
wektory = new int **[5]; // pierwszy wymiar
Linia 47: Linia 47:
wektory[1][0] = new int; // wymiar I = 1 -> wymiar II = 2 -> 1 element
wektory[1][0] = new int; // wymiar I = 1 -> wymiar II = 2 -> 1 element
...
...
</syntaxhighlight>
</source>




Stosując ten sposób, ogólnie można deklarować tablice n-wymiarowe bez większego problemu.
Stosując ten sposób, ogólnie można deklarować tablice n-wymiarowe bez większego problemu.
Usuwanie tablic wielowymiarowych przebiega podobnie jak jednowymiarowych, z tą różnicą, że usuwanie zaczynamy od "najgłębszego" wymiaru:
Usuwanie tablic wielowymiarowych przebiega podobnie jak jednowymiarowych, z tą różnicą, że usuwanie zaczynamy od "najgłębszego" wymiaru:
<source lang="cpp">
<syntaxhighlight lang="cpp">
delete wektory[1][0]; // kasujemy pojedynczą zmienną
delete wektory[1][0]; // kasujemy pojedynczą zmienną
delete [] wektory[0][1];
delete [] wektory[0][1];
Linia 61: Linia 61:
// I wymiar
// I wymiar
delete [] wektory;
delete [] wektory;
</syntaxhighlight>
</source>


Zwrócić uwagę trzeba na dwie rzeczy:
Zwrócić uwagę trzeba na dwie rzeczy:
Linia 69: Linia 69:


Drugą zaletą jest fakt, że przy okazji alokacji pamięci możemy wywołać odpowiedni konstruktor inicjując wartości zmiennych obiektu, np.
Drugą zaletą jest fakt, że przy okazji alokacji pamięci możemy wywołać odpowiedni konstruktor inicjując wartości zmiennych obiektu, np.
<source lang="cpp">
<syntaxhighlight lang="cpp">
Test *test = new Test(1,2);
Test *test = new Test(1,2);
</syntaxhighlight>
</source>
zakładając, że obiekt Test posiada dwie zmienne typu całkowitego i zdefiniowany konstruktor <code>Test(int,int)</code>.
zakładając, że obiekt Test posiada dwie zmienne typu całkowitego i zdefiniowany konstruktor <code>Test(int,int)</code>.


Linia 80: Linia 80:
1. Domyślnie gdy przydział pamięci jest niemożliwy operator '''new''' zgłasza wyjątek '''std::bad_alloc''', np.
1. Domyślnie gdy przydział pamięci jest niemożliwy operator '''new''' zgłasza wyjątek '''std::bad_alloc''', np.


<source lang="cpp" highlight="1,7,10">
<syntaxhighlight lang="cpp" highlight="1,7,10">
#include <new> // wyjątek std::bad_alloc
#include <new> // wyjątek std::bad_alloc
#include <cstdio>
#include <cstdio>
Linia 95: Linia 95:
return 0;
return 0;
}
}
</syntaxhighlight>
</source>


2. Można wyłączyć zgłaszanie wyjątku, zamiast tego w przypadku braku pamięci zostanie zwrócony pusty wskaźnik ('''nullptr'''). W tym celu po słowie kluczowym '''new''' trzeba podać symbol '''std::nothrow''', np.
2. Można wyłączyć zgłaszanie wyjątku, zamiast tego w przypadku braku pamięci zostanie zwrócony pusty wskaźnik ('''nullptr'''). W tym celu po słowie kluczowym '''new''' trzeba podać symbol '''std::nothrow''', np.


<source lang="cpp" highlight="1,5,10">
<syntaxhighlight lang="cpp" highlight="1,5,10">
#include <new> // symbol std::nothrow
#include <new> // symbol std::nothrow
#include <cstdio>
#include <cstdio>
Linia 113: Linia 113:
return 0;
return 0;
}
}
</syntaxhighlight>
</source>


==Placement new==
==Placement new==
Linia 126: Linia 126:
Jest to niezbyt powszechne użycie, ma też jedną wadę: nie działa operator '''delete''', trzeba ręcznie wywoływać destruktory obiektów. Zastosowanie tego mechanizmu ma głównie sens, gdy samodzielnie zarządzamy pamięcią, np. zawczasu rezerwujemy pamięć dla dużej liczby obiektów i w miarę potrzeb ją przydzielamy, oszczędzając tym samym czas na każdorazowe odwołanie do alokatora pamięci.
Jest to niezbyt powszechne użycie, ma też jedną wadę: nie działa operator '''delete''', trzeba ręcznie wywoływać destruktory obiektów. Zastosowanie tego mechanizmu ma głównie sens, gdy samodzielnie zarządzamy pamięcią, np. zawczasu rezerwujemy pamięć dla dużej liczby obiektów i w miarę potrzeb ją przydzielamy, oszczędzając tym samym czas na każdorazowe odwołanie do alokatora pamięci.


<source lang="cpp" highlight="12,14">
<syntaxhighlight lang="cpp" highlight="12,14">
#include <new>
#include <new>
#include <cstdlib> // malloc
#include <cstdlib> // malloc
Linia 144: Linia 144:
return 0;
return 0;
}
}
</syntaxhighlight>
</source>


==Inteligentne wskaźniki==
==Inteligentne wskaźniki==
Linia 164: Linia 164:
Klasa <tt>unique_ptr</tt> (unikalny wskaźnik) gwarantuje, że w systemie istnieć będzie dokładnie jedna aktywna instancja wskaźnika. Nie tylko nie istnieje możliwość skopiowania <tt>unique_ptr</tt>, ale nawet nie można dokonać prostego przypisania, tzn. <tt>a = b;</tt>. Dostępne jest jedynie przenoszenie wartości (<tt>std::move</tt>) ewentualnie należy wprost zwolnić wskaźnik metodą <tt>release</tt> i zastąpić przez <tt>reset</tt>. Np.
Klasa <tt>unique_ptr</tt> (unikalny wskaźnik) gwarantuje, że w systemie istnieć będzie dokładnie jedna aktywna instancja wskaźnika. Nie tylko nie istnieje możliwość skopiowania <tt>unique_ptr</tt>, ale nawet nie można dokonać prostego przypisania, tzn. <tt>a = b;</tt>. Dostępne jest jedynie przenoszenie wartości (<tt>std::move</tt>) ewentualnie należy wprost zwolnić wskaźnik metodą <tt>release</tt> i zastąpić przez <tt>reset</tt>. Np.


<source lang="cpp">
<syntaxhighlight lang="cpp">
#include <memory>
#include <memory>


Linia 174: Linia 174:
a = std::move(b);
a = std::move(b);
}
}
</syntaxhighlight>
</source>


Kiedy <tt>unique_ptr</tt> posiadający wskaźnik jest niszczony, niszczony jest również obiekt na który wskazuje. Usunięcie obiektu wykonuje tzw. deleter, jest to klasa będąca drugim argumentem szablonu. Domyślnie to <tt>std::default_deleter</tt> i nie trzeba jej wprost podawać; dopiero gdy potrzebujemy dodatkowych działań w chwili usuwania obiektu należy taką klasę zaimplementować, co jest raczej rzadko potrzebne.
Kiedy <tt>unique_ptr</tt> posiadający wskaźnik jest niszczony, niszczony jest również obiekt na który wskazuje. Usunięcie obiektu wykonuje tzw. deleter, jest to klasa będąca drugim argumentem szablonu. Domyślnie to <tt>std::default_deleter</tt> i nie trzeba jej wprost podawać; dopiero gdy potrzebujemy dodatkowych działań w chwili usuwania obiektu należy taką klasę zaimplementować, co jest raczej rzadko potrzebne.
Linia 180: Linia 180:
Przykładowy program:
Przykładowy program:


<source lang="cpp" highlight="12,13,18">
<syntaxhighlight lang="cpp" highlight="12,13,18">
#include <memory> // unique_ptr
#include <memory> // unique_ptr
#include <iostream>
#include <iostream>
Linia 203: Linia 203:
std::cout << "p2 = " << p2.get() << '\n';
std::cout << "p2 = " << p2.get() << '\n';
}
}
</syntaxhighlight>
</source>


Wypisze:
Wypisze:
Linia 218: Linia 218:
Warto zwrócić uwagę, że nigdzie w programie nie ma bezpośredniego wywołania '''delete''', mimo to destruktor klasy jest wołany. Z tej cechy można korzystać we własnych klasach: zamiast przechowywać wskaźniki do jakiś obiektów wykorzystywanych wewnętrznie, łatwiej mieć <tt>unique_ptr</tt>, dzięki czemu nie trzeba pamiętać o zwalnianiu pamięci w destruktorze. Analogicznie w przypadku funkcji <tt>unique_ptr</tt> załatwia za nas zwalnianie pamięci, także '''w przypadku wystąpienia wyjątku'''. Na przykład:
Warto zwrócić uwagę, że nigdzie w programie nie ma bezpośredniego wywołania '''delete''', mimo to destruktor klasy jest wołany. Z tej cechy można korzystać we własnych klasach: zamiast przechowywać wskaźniki do jakiś obiektów wykorzystywanych wewnętrznie, łatwiej mieć <tt>unique_ptr</tt>, dzięki czemu nie trzeba pamiętać o zwalnianiu pamięci w destruktorze. Analogicznie w przypadku funkcji <tt>unique_ptr</tt> załatwia za nas zwalnianie pamięci, także '''w przypadku wystąpienia wyjątku'''. Na przykład:


<source lang="cpp">
<syntaxhighlight lang="cpp">
void funkcja() {
void funkcja() {
Linia 229: Linia 229:
delete[] tymczasowe;
delete[] tymczasowe;
}
}
</syntaxhighlight>
</source>


Ta realizacja ma oczywistą wadę: w przypadku jakiegokolwiek wyjątku nastąpi wyciek pamięci (no, chyba, że nie uda się zaalokować pamięci na <tt>dane</tt>). Oprócz tego programista jest odpowiedzialny za zwolnienie zasobów. Czasem można zapomnieć, szczególnie gdy rozwija się już istniejącą funkcję.
Ta realizacja ma oczywistą wadę: w przypadku jakiegokolwiek wyjątku nastąpi wyciek pamięci (no, chyba, że nie uda się zaalokować pamięci na <tt>dane</tt>). Oprócz tego programista jest odpowiedzialny za zwolnienie zasobów. Czasem można zapomnieć, szczególnie gdy rozwija się już istniejącą funkcję.
Linia 235: Linia 235:
Drugim podejściem jest złapanie wszystkich możliwych wyjątków:
Drugim podejściem jest złapanie wszystkich możliwych wyjątków:


<source lang="cpp">
<syntaxhighlight lang="cpp">
void funkcja_poprawiona() {
void funkcja_poprawiona() {
char* dane = nullptr;
char* dane = nullptr;
Linia 255: Linia 255:
}
}
}
}
</syntaxhighlight>
</source>


To podejście jest lepsze, jednak również ma wadę z poprzedniego rozwiązania: ręczne zwalnianie pamięci, do tego powielone. Użycie <tt>unique_ptr</tt> skraca i znacząco upraszcza kod, eliminując wszystkie wymienione mankamenty:
To podejście jest lepsze, jednak również ma wadę z poprzedniego rozwiązania: ręczne zwalnianie pamięci, do tego powielone. Użycie <tt>unique_ptr</tt> skraca i znacząco upraszcza kod, eliminując wszystkie wymienione mankamenty:


<source lang="cpp">
<syntaxhighlight lang="cpp">
#include <memory>
#include <memory>


Linia 269: Linia 269:
// obliczenia ...
// obliczenia ...
}
}
</syntaxhighlight>
</source>




Linia 278: Linia 278:
Standard C++11 gwarantuje bezpieczeństwo w środowisku wielowątkowym.
Standard C++11 gwarantuje bezpieczeństwo w środowisku wielowątkowym.


<source lang="cpp">
<syntaxhighlight lang="cpp">
#include <memory>
#include <memory>
#include <iostream>
#include <iostream>
Linia 312: Linia 312:
przyklad();
przyklad();
}
}
</syntaxhighlight>
</source>


Program wyświetli:
Program wyświetli:
Linia 333: Linia 333:
O ile drugi problem dotyczy wąskiej grupy programów, które intensywnie wykorzystują wątki (gry, bazy danych, programy naukowe), tak pierwszy może wystąpić wszędzie. Oto ilustracja cyklu zależności:
O ile drugi problem dotyczy wąskiej grupy programów, które intensywnie wykorzystują wątki (gry, bazy danych, programy naukowe), tak pierwszy może wystąpić wszędzie. Oto ilustracja cyklu zależności:


<source lang="cpp">
<syntaxhighlight lang="cpp">
#include <memory>
#include <memory>
#include <iostream>
#include <iostream>
Linia 362: Linia 362:
przyklad();
przyklad();
}
}
</syntaxhighlight>
</source>


Kiedy skompilujemy i uruchomimy powyższy program, na ekranie zostaną wypisane tylko dwa wiersze
Kiedy skompilujemy i uruchomimy powyższy program, na ekranie zostaną wypisane tylko dwa wiersze
Linia 379: Linia 379:
Wzorce użycia tej klasy są dwa, można albo 1) najpierw testować metodą <tt>expired</tt>, a następnie użyć <tt>lock</tt>, albo 2) prościej od razu użyć <tt>lock</tt>, sprawdzając czy wynikowy wskaźnik nie będzie pusty.
Wzorce użycia tej klasy są dwa, można albo 1) najpierw testować metodą <tt>expired</tt>, a następnie użyć <tt>lock</tt>, albo 2) prościej od razu użyć <tt>lock</tt>, sprawdzając czy wynikowy wskaźnik nie będzie pusty.


<source lang="cpp">
<syntaxhighlight lang="cpp">
weak_ptr<int> wp;
weak_ptr<int> wp;


Linia 394: Linia 394:
// tu działania na shared
// tu działania na shared
}
}
</syntaxhighlight>
</source>


Poniżej kompletny przykład.
Poniżej kompletny przykład.


<source lang="cpp">
<syntaxhighlight lang="cpp">
#include <memory>
#include <memory>
#include <iostream>
#include <iostream>
Linia 432: Linia 432:
przyklad();
przyklad();
}
}
</syntaxhighlight>
</source>


Po uruchomieniu na ekranie wyświetli się:
Po uruchomieniu na ekranie wyświetli się:

Aktualna wersja na dzień 19:01, 9 lut 2021

W języku C++ do alokowania pamięci na stercie służy operator new, a do zwalniania - delete. W C można również stosować funkcje malloc i free, jednak należy być ostrożnym. Najczęstszym błędem jest mieszanie operatorów new i delete z funkcjami malloc i free, np. zwalnianie pamięci zaalokowanej przez new przy pomocy free.

Rozważmy prosty przykład. Załóżmy, że chcemy stworzyć wektor 10 liczb typu całkowitego. Możemy to zrobić na dwa sposoby. W stylu znanym z języka C:

int *wektor = (int*) malloc (sizeof(int)*10);
free (wektor);

Albo w stylu C++:

int *wektor = new int[10];
delete [] wektor;

Od razu widać, że drugi zapis jest łatwiejszy i przyjemniejszy w użyciu. To jest podstawowa zaleta operatora new - krótszy zapis. Wystarczy wiedzieć jakiego typu ma być obiekt, który chcemy powołać do życia, nie martwiąc się o rozmiar alokowanego bloku pamięci. Za pomocą operatora new można również tworzyć tablice wielowymiarowe:

int **wektory = new int *[5];
for (int i = 0; i < 5; ++i)
   wektory[i] = new int [10];

W ten sposób stworzono tablicę dwuwymiarową którą statycznie zadeklarowalibyśmy jako:

int wektory[5][10];

Jednak w przeciwieństwie do int wektory[5][10], która jest tablicą dwuwymiarową, nasze int **wektory jest tablicą tablic i może być rozrzucone po całej pamięci.

Ilość elementów poszczególnych wymiarów nie musi być jednakowa. Można np zadeklarować tablicę taką:

int **wektory = new int *[2];
wektory[0] = new int [5];
wektory[1] = new int;


Przy takiej deklaracji pierwszy wiersz ma 5 elementów (tablica) a drugi to jeden element. Deklaracja tablic o większej ilości wymiarów przebiega podobnie:

int ***wektory;             // deklarujemy tablicę 3-wymiarową
wektory = new int **[5];    // pierwszy wymiar
wektory[0] = new int *[10]; // pierwszy element pierwszego wymiaru
wektory[1] = new int *[3];  // drugi element pierwszego wymiaru
....
wektory[0][0] = new int [3] // wymiar I = 0 -> wymiar II = 1 -> 3 elementy(tablica)
wektory[0][1] = new int [5] // wymiar I = 0 -> wymiar II = 3 -> 5 elementów(tablica)
wektory[1][0] = new int;    // wymiar I = 1 -> wymiar II = 2 -> 1 element
...


Stosując ten sposób, ogólnie można deklarować tablice n-wymiarowe bez większego problemu. Usuwanie tablic wielowymiarowych przebiega podobnie jak jednowymiarowych, z tą różnicą, że usuwanie zaczynamy od "najgłębszego" wymiaru:

delete  wektory[1][0];        // kasujemy pojedynczą zmienną
delete [] wektory[0][1];
delete [] wektory[0][0];
// II wymiar
delete [] wektory[0];
delete [] wektory[1];
// I wymiar
delete [] wektory;

Zwrócić uwagę trzeba na dwie rzeczy:

  • delete [] używamy dla zmiennych tablicowych, a delete dla pojedynczych zmiennych
  • Kolejność zwalniania wymiarów jest odwrotna niż ich tworzenia


Drugą zaletą jest fakt, że przy okazji alokacji pamięci możemy wywołać odpowiedni konstruktor inicjując wartości zmiennych obiektu, np.

Test *test = new Test(1,2);

zakładając, że obiekt Test posiada dwie zmienne typu całkowitego i zdefiniowany konstruktor Test(int,int).

Kolejną korzyścią jest możliwość przeciążania. Jednak to już jest temat na inny rozdział.

Działanie w przypadku braku pamięci[edytuj]

1. Domyślnie gdy przydział pamięci jest niemożliwy operator new zgłasza wyjątek std::bad_alloc, np.

#include <new>      // wyjątek std::bad_alloc
#include <cstdio>

int main() {

    try {
        char* p = new char[1000000000000];
    } catch (std::bad_alloc& e) {
        // std::bad_alloc::what
        std::printf("Błąd alokacji: %s\n", e.what()); 
    }

    return 0;
}

2. Można wyłączyć zgłaszanie wyjątku, zamiast tego w przypadku braku pamięci zostanie zwrócony pusty wskaźnik (nullptr). W tym celu po słowie kluczowym new trzeba podać symbol std::nothrow, np.

#include <new>      // symbol std::nothrow
#include <cstdio>

int main() {
    
    char* p = new (std::nothrow) char[1000000000000];

    if (p == nullptr) {
        std::puts("Brak pamięci");
    }

    return 0;
}

Placement new[edytuj]

Jak zostało powiedziane operator new wykonuje dwie operacje:

  • zaalokowanie pamięci o żądanym rozmiarze,
  • wywołanie konstruktorów (domyślnych lub wyspecyfikowanych).

Można jednak użyć specjalnego wywołania new, tzw. "placement new", które jedynie wywołuje konstruktory na pamięci już zaalokowanej w innym miejscu; trzeba być jednak pewnym, że wskazany obszar pamięci jest odpowiedniego rozmiaru.

Jest to niezbyt powszechne użycie, ma też jedną wadę: nie działa operator delete, trzeba ręcznie wywoływać destruktory obiektów. Zastosowanie tego mechanizmu ma głównie sens, gdy samodzielnie zarządzamy pamięcią, np. zawczasu rezerwujemy pamięć dla dużej liczby obiektów i w miarę potrzeb ją przydzielamy, oszczędzając tym samym czas na każdorazowe odwołanie do alokatora pamięci.

#include <new>
#include <cstdlib>  // malloc

class Klasa {
    int numer;
};

int main() {

    void* wskaznik = malloc(sizeof(Klasa));

    Klasa* obiekt = new (wskaznik) Klasa;

    obiekt->~Klasa();

    return 0;
}

Inteligentne wskaźniki[edytuj]

Klasy nazywane "inteligentnymi wskaźnikami" pomagają ominąć część problemów związanych z czasem życia wskazywanych obiektów i ze współdzieleniem wskaźników. Często występujące problemy to:

  • wycieki pamięci,
  • przedwczesne kasowanie wskazywanych obiektów,
  • wielokrotne kasowanie obiektów (przez delete).

W standardzie zdefiniowano trzy klasy szablonowe:

  • std::unique_ptr,
  • std::shared_ptr,
  • std::weak_ptr.

unique_ptr[edytuj]

Klasa unique_ptr (unikalny wskaźnik) gwarantuje, że w systemie istnieć będzie dokładnie jedna aktywna instancja wskaźnika. Nie tylko nie istnieje możliwość skopiowania unique_ptr, ale nawet nie można dokonać prostego przypisania, tzn. a = b;. Dostępne jest jedynie przenoszenie wartości (std::move) ewentualnie należy wprost zwolnić wskaźnik metodą release i zastąpić przez reset. Np.

#include <memory>

int main() {
    std::unique_ptr<int> a(new int(5));
    std::unique_ptr<int> b;

    b.reset(a.release());
    a = std::move(b);
}

Kiedy unique_ptr posiadający wskaźnik jest niszczony, niszczony jest również obiekt na który wskazuje. Usunięcie obiektu wykonuje tzw. deleter, jest to klasa będąca drugim argumentem szablonu. Domyślnie to std::default_deleter i nie trzeba jej wprost podawać; dopiero gdy potrzebujemy dodatkowych działań w chwili usuwania obiektu należy taką klasę zaimplementować, co jest raczej rzadko potrzebne.

Przykładowy program:

#include <memory>       // unique_ptr
#include <iostream>

class Klasa {
public:
    Klasa()  { std::cout << "Konstrukor" << '\n'; }
    ~Klasa() { std::cout << "Destruktor" << '\n'; }
};

int main() {

    std::unique_ptr<Klasa> p1(new Klasa());
    std::unique_ptr<Klasa> p2;

    std::cout << "p1 = " << p1.get() << '\n';
    std::cout << "p2 = " << p2.get() << '\n';

    p2 = std::move(p1);

    std::cout << "p1 = " << p1.get() << '\n';
    std::cout << "p2 = " << p2.get() << '\n';
}

Wypisze:

Konstrukor
p1 = 0x894b008
p2 = 0
p1 = 0
p2 = 0x894b008
Destruktor

Warto zwrócić uwagę, że nigdzie w programie nie ma bezpośredniego wywołania delete, mimo to destruktor klasy jest wołany. Z tej cechy można korzystać we własnych klasach: zamiast przechowywać wskaźniki do jakiś obiektów wykorzystywanych wewnętrznie, łatwiej mieć unique_ptr, dzięki czemu nie trzeba pamiętać o zwalnianiu pamięci w destruktorze. Analogicznie w przypadku funkcji unique_ptr załatwia za nas zwalnianie pamięci, także w przypadku wystąpienia wyjątku. Na przykład:

void funkcja() {
    
    char* dane = new char[100000];      // wyjątek może wystąpić tutaj
    char* tymczasowe = new char[5000];  // i tutaj też

    // obliczenia ...                   // podczas obliczeń również

    delete[] dane;
    delete[] tymczasowe;
}

Ta realizacja ma oczywistą wadę: w przypadku jakiegokolwiek wyjątku nastąpi wyciek pamięci (no, chyba, że nie uda się zaalokować pamięci na dane). Oprócz tego programista jest odpowiedzialny za zwolnienie zasobów. Czasem można zapomnieć, szczególnie gdy rozwija się już istniejącą funkcję.

Drugim podejściem jest złapanie wszystkich możliwych wyjątków:

void funkcja_poprawiona() {
    char* dane = nullptr;
    char* tymczasowe = nullptr;

    try {
        dane = new char[100000];
        tymczasowe = new char[5000];

        // obliczenia ...

        delete[] dane;
        delete[] tymczasowe;
    } catch (...) {
        delete[] dane;
        delete[] tymczasowe;

        throw;
    }
}

To podejście jest lepsze, jednak również ma wadę z poprzedniego rozwiązania: ręczne zwalnianie pamięci, do tego powielone. Użycie unique_ptr skraca i znacząco upraszcza kod, eliminując wszystkie wymienione mankamenty:

#include <memory>

void funkcja_najlepsza() {
    
    std::unique_ptr<char> dane(new char[100000]);
    std::unique_ptr<char> tymczasowe(new char[5000]);

    // obliczenia ...
}


shared_ptr i weak_ptr[edytuj]

Klasa shared_ptr (współdzielony wskaźnik) umożliwia istnienie wielu wskaźników do tego samego obiektu, podtrzymując go przy życiu tak długo, jak istnieje przynajmniej jeden shared_ptr, który by go zawierał. Klasa shared_ptr w istocie realizuje odśmiecanie pamięci ze zliczaniem referencji. Z każdym obiektem związana jest liczba odwołań (odczyt metodą use_count), która jest automatycznie aktualizowana. Kiedy osiągnie zero obiekt jest niszczony.

Standard C++11 gwarantuje bezpieczeństwo w środowisku wielowątkowym.

#include <memory>
#include <iostream>

class Klasa {
public:
    Klasa()  {std::cout << "Konstruktor" << '\n';}
    ~Klasa() {std::cout << "Destruktor" << '\n';}
};

void przyklad() {

    std::shared_ptr<Klasa> p(new Klasa());  
    std::cout << p.use_count() << '\n';         // licznik referencji = 1
    {
        std::shared_ptr<Klasa> p1(p);
        std::cout << p.use_count() << '\n';     // licznik referencji = 2

        {
            std::shared_ptr<Klasa> p2(p);       // licznik referencji = 3
            std::cout << p.use_count() << '\n';
        }                                       // licznik referencji = 2 - p2 jest niszczony

        std::shared_ptr<Klasa> p3(p1);          // licznik referencji = 3
        std::cout << p.use_count() << '\n';
    }                                           // licznik referencji = 1 - p1 i p3 są niszczone

    std::cout << p.use_count() << '\n';

}                                               // licznik referencji = 0 - p jest niszczony, niszczona jest też instancja Klasa

int main() {
    przyklad();
}

Program wyświetli:

Konstruktor
1
2
3
3
1
Destruktor

Używając shared_ptr należy mieć jednak na uwadze pewne niedogodności:

  • Zliczanie referencji nie radzi sobie z cyklami w zależnościach, tzn. jeśli obiekt A wskazuje na B i jednocześnie B wskazuje na A, to nigdy nie zostaną zwolnione, nawet jeśli oba są zbędne.
  • W środowisku wieloprocesorowym zliczanie referencji nie jest najszybsze, ze względu na konieczność synchronizacji pamięci między procesorami.

O ile drugi problem dotyczy wąskiej grupy programów, które intensywnie wykorzystują wątki (gry, bazy danych, programy naukowe), tak pierwszy może wystąpić wszędzie. Oto ilustracja cyklu zależności:

#include <memory>
#include <iostream>

class Klasa {
private:
    std::shared_ptr<Klasa> sasiad;

public:
    Klasa()  {std::cout << "Konstruktor" << '\n';}
    ~Klasa() {std::cout << "Destruktor" << '\n';}

    void ustaw_sasiada(std::shared_ptr<Klasa> s) {
        sasiad = s;
    }
};

void przyklad() {

    std::shared_ptr<Klasa> A(new Klasa());  // licznik A = 1
    std::shared_ptr<Klasa> B(new Klasa());  // licznik B = 1 

    A->ustaw_sasiada(B);                    // licznik B = 2
    B->ustaw_sasiada(A);                    // licznik A = 2
}                                           // licznik A, B = 1

int main() {
    przyklad();
}

Kiedy skompilujemy i uruchomimy powyższy program, na ekranie zostaną wypisane tylko dwa wiersze

Konstrukor
Konstrukor

pochodzące z konstruktorów obiektów A i B. Mimo że po wyjściu z funkcji wskaźniki shared_ptr są niszczone, to obiekty już nie, ponieważ same posiadają dodatkowe shared_ptr podbijające licznik do niezerowej wartości. W tym przypadku doszło do wycieku pamięci.

Jeśli to możliwe należy nie dopuszczać do tworzenia cykli, bo jak widzimy prowadzi to do kłopotów. Pół biedy kiedy jesteśmy ich świadomi, ale gdy taki cykl powstanie przypadkowo, trudniej będzie dojść przyczyny wycieków.

Kiedy jednak cykle są nie do uniknięcia można użyć klasy weak_ptr, która również trzyma wskaźnik do obiektu, jednak nie wpływa na licznik referencji, mówi się, że "przerywa" cykl. Nie istnieje też możliwość dereferencji tego wskaźnika, należy tymczasowo skonwertować weak_ptr na shared_ptr, który podtrzyma przy życiu obiekt - robi to metoda lock. Instancja weak_ptr może istnieć dłużej niż wskazywany obiekt, dlatego konieczne jest stwierdzenie, czy obiekt jest jeszcze ważny – służy do tego funkcja expired.

Wzorce użycia tej klasy są dwa, można albo 1) najpierw testować metodą expired, a następnie użyć lock, albo 2) prościej od razu użyć lock, sprawdzając czy wynikowy wskaźnik nie będzie pusty.

weak_ptr<int> wp;

// ...

// 1)
if (!wp.expired()) {
    auto shared = wp.lock();
    // tu działania na shared
}

// 2)
if (auto shared = wp.lock()) {
    // tu działania na shared
}

Poniżej kompletny przykład.

#include <memory>
#include <iostream>

void wyswietl(std::weak_ptr<int> ptr) {
    if (ptr.expired()) {
        std::cout << "<brak danych>" << '\n';
    } else {
        auto shared = ptr.lock();
        std::cout << "wartość = " << *shared << ", "
                  << "licznik referencji = " << shared.use_count() << '\n';
    }
}

void przyklad() {

    std::weak_ptr<int> wp;

    {
        std::cout << ">>>>" << '\n';
        std::shared_ptr<int> p(new int(71));
        wp = p;
        std::cout << "licznik referencji = " << p.use_count() << '\n';

        wyswietl(wp);
        std::cout << "<<<<" << '\n';
    }

    wyswietl(wp);
}

int main() {
    przyklad();
}

Po uruchomieniu na ekranie wyświetli się:

>>>>
licznik referencji = 1
wartość = 71, licznik referencji = 2
<<<<
<brak danych>

Jak widać po przypisaniu shared_ptr do weak_ptr nie zmienia licznika, dopiero w funkcji wyświetl jest on zmieniany. Po wyjściu z zakresu, p jest niszczone i wskaźnik staje się nieważny.