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

Dodane 10 213 bajtów ,  8 lat temu
+inteligentne wskaźniki
(rozszerzenie opisu new)
(+inteligentne wskaźniki)
</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::noexceptnothrow''', np.
 
<source lang="cpp" highlight="1,5,10">
}
</source>
 
==Inteligentne wskaźniki==
 
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:
 
* <tt>std::unique_ptr</tt>,
* <tt>std::shared_ptr</tt>,
* <tt>std::weak_ptr</tt>.
 
===unique_ptr===
 
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">
#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);
}
</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.
 
Przykładowy program:
 
<source lang="cpp" highlight="12,13,18">
#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';
}
</source>
 
Wypisze:
 
<pre>
Konstrukor
p1 = 0x894b008
p2 = 0
p1 = 0
p2 = 0x894b008
Destruktor
</pre>
 
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">
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;
}
</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ę.
 
Drugim podejściem jest złapanie wszystkich możliwych wyjątków:
 
<source lang="cpp">
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;
}
}
</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:
 
<source lang="cpp">
#include <memory>
 
void funkcja_najlepsza() {
std::unique_ptr<char> dane(new char[100000]);
std::unique_ptr<char> tymczasowe(new char[5000]);
 
// obliczenia ...
}
</source>
 
 
===shared_ptr i weak_ptr===
 
Klasa <tt>shared_ptr</tt> (współdzielony wskaźnik) umożliwia istnienie wielu wskaźników do tego samego, podtrzymując go przy życiu tak długo, jak istnieje przynajmniej jeden <tt>shared_ptr</tt>, który by go zawierał. Klasa <tt>shared_ptr</tt> w istocie realizuje '''odśmiecanie pamięci ze zliczaniem referencji''' - z każdym obiektem związana jest liczba odwołań (odczyt metodą <tt>use_count</tt>), która jest automatycznie aktualizowana, a kiedy osiągnie zero obiekt jest niszczony. Standard C++11 gwarantuje bezpieczeństwo w środowisku wielowątkowym.
 
<source lang="cpp">
#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();
}
</source>
 
Program wyświetli:
 
<pre>
Konstruktor
1
2
3
3
1
Destruktor
</pre>
 
Używając <tt>shared_ptr</tt> 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:
 
<source lang="cpp">
#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();
}
</source>
 
Kiedy skompilujemy i uruchomimy powyższy program, na ekranie zostaną wypisane tylko dwa wiersze
 
<pre>
Konstrukor
Konstrukor
</pre>
 
pochodzące z konstruktorów obiektów <tt>A</tt> i <tt>B</tt>. Mimo że po wyjściu z funkcji wskaźniki <tt>shared_ptr</tt> są niszczone, to obiekty już nie, ponieważ same posiadają dodatkowe <tt>shared_ptr</tt> 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 <tt>weak_ptr</tt>, 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ć <tt>weak_ptr</tt> na <tt>shared_ptr</tt>, który podtrzyma przy życiu obiekt - robi to metoda <tt>lock</tt>. Instancja <tt>weak_ptr</tt> 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 <tt>expired</tt>.
 
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>, wynikowy wskaźnik będzie pusty.
 
<source lang="cpp">
weak_ptr<int> wp;
 
// ...
 
if (!wp.expired()) {
auto shared = wp.lock();
// tu działania na shared
}
 
if (auto shared = wp.lock()) {
// tu działania na shared
}
</source>
 
Poniżej kompletny przykład.
 
<source lang="cpp">
#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();
}
</source>
 
Po uruchomieniu na ekranie wyświetli się:
 
<pre>
>>>>
licznik referencji = 1
wartość = 71, licznik referencji = 2
<<<<
<brak danych>
</pre>
 
Jak widać po przypisaniu <tt>shared_ptr</tt> do <tt>weak_ptr</tt> nie zmienia licznika, dopiero w funkcji wyświetl jest on zmieniany. Po wyjściu z zakresu, <tt>p</tt> jest niszczone i wskaźnik staje się nieważny.
 
<noinclude>
516

edycji