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

brak opisu edycji
m (Wycofano edycje użytkownika Franekmat (dyskusja). Autor przywróconej wersji to 37.47.165.84.)
Znacznik: Wycofanie zmian
Nie podano opisu zmian
 
 
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:
<sourcesyntaxhighlight lang="cpp">
int *wektor = (int*) malloc (sizeof(int)*10);
free (wektor);
</syntaxhighlight>
</source>
 
Albo w stylu C++:
<sourcesyntaxhighlight lang="cpp">
int *wektor = new int[10];
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:
<sourcesyntaxhighlight lang="cpp">
int **wektory = new int *[5];
for (int i = 0; i < 5; ++i)
wektory[i] = new int [10];
</syntaxhighlight>
</source>
 
W ten sposób stworzono tablicę dwuwymiarową którą statycznie zadeklarowalibyśmy jako:
<sourcesyntaxhighlight lang="cpp">
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.
 
Ilość elementów poszczególnych wymiarów nie musi być jednakowa. Można np zadeklarować tablicę taką:
<sourcesyntaxhighlight lang="cpp">
int **wektory = new int *[2];
wektory[0] = new int [5];
wektory[1] = new int;
</syntaxhighlight>
</source>
 
 
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:
<sourcesyntaxhighlight lang="cpp">
int ***wektory; // deklarujemy tablicę 3-wymiarową
wektory = new int **[5]; // pierwszy wymiar
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.
Usuwanie tablic wielowymiarowych przebiega podobnie jak jednowymiarowych, z tą różnicą, że usuwanie zaczynamy od "najgłębszego" wymiaru:
<sourcesyntaxhighlight lang="cpp">
delete wektory[1][0]; // kasujemy pojedynczą zmienną
delete [] wektory[0][1];
// I wymiar
delete [] wektory;
</syntaxhighlight>
</source>
 
Zwrócić uwagę trzeba na dwie rzeczy:
 
Drugą zaletą jest fakt, że przy okazji alokacji pamięci możemy wywołać odpowiedni konstruktor inicjując wartości zmiennych obiektu, np.
<sourcesyntaxhighlight lang="cpp">
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>.
 
1. Domyślnie gdy przydział pamięci jest niemożliwy operator '''new''' zgłasza wyjątek '''std::bad_alloc''', np.
 
<sourcesyntaxhighlight lang="cpp" highlight="1,7,10">
#include <new> // wyjątek std::bad_alloc
#include <cstdio>
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.
 
<sourcesyntaxhighlight lang="cpp" highlight="1,5,10">
#include <new> // symbol std::nothrow
#include <cstdio>
return 0;
}
</syntaxhighlight>
</source>
 
==Placement new==
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.
 
<sourcesyntaxhighlight lang="cpp" highlight="12,14">
#include <new>
#include <cstdlib> // malloc
return 0;
}
</syntaxhighlight>
</source>
 
==Inteligentne wskaźniki==
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.
 
<sourcesyntaxhighlight lang="cpp">
#include <memory>
 
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.
Przykładowy program:
 
<sourcesyntaxhighlight lang="cpp" highlight="12,13,18">
#include <memory> // unique_ptr
#include <iostream>
std::cout << "p2 = " << p2.get() << '\n';
}
</syntaxhighlight>
</source>
 
Wypisze:
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:
 
<sourcesyntaxhighlight lang="cpp">
void funkcja() {
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ę.
Drugim podejściem jest złapanie wszystkich możliwych wyjątków:
 
<sourcesyntaxhighlight lang="cpp">
void funkcja_poprawiona() {
char* dane = nullptr;
}
}
</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:
 
<sourcesyntaxhighlight lang="cpp">
#include <memory>
 
// obliczenia ...
}
</syntaxhighlight>
</source>
 
 
Standard C++11 gwarantuje bezpieczeństwo w środowisku wielowątkowym.
 
<sourcesyntaxhighlight lang="cpp">
#include <memory>
#include <iostream>
przyklad();
}
</syntaxhighlight>
</source>
 
Program wyświetli:
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:
 
<sourcesyntaxhighlight lang="cpp">
#include <memory>
#include <iostream>
przyklad();
}
</syntaxhighlight>
</source>
 
Kiedy skompilujemy i uruchomimy powyższy program, na ekranie zostaną wypisane tylko dwa wiersze
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.
 
<sourcesyntaxhighlight lang="cpp">
weak_ptr<int> wp;
 
// tu działania na shared
}
</syntaxhighlight>
</source>
 
Poniżej kompletny przykład.
 
<sourcesyntaxhighlight lang="cpp">
#include <memory>
#include <iostream>
przyklad();
}
</syntaxhighlight>
</source>
 
Po uruchomieniu na ekranie wyświetli się: