Dynamiczna alokacja pamięci
• tech • 1106 słów • 6 minut czytania
W języku C do dynamicznego przydzielania pamięci wykorzystuje się funkcje (biblioteki standardowej) malloc()
, calloc()
i realloc()
, a zwalniania pamięci - free()
. Funkcje te są popularne i praktyczne w użyciu, ale jak wspomniał Bruce Eckel, w swojej książce “Thinking In C++”, są również prymitywne, bo wymagają one zarówno zrozumienia, jak i uwagi ze strony programisty.
Niby wszystko jasne, ale nie tak do końca jest to takie kolorowe.
Typowe przydzielenie pamięci wygląda tak:
Obj* obj = (Obj*)malloc(sizeof(Obj));
Jest to trochę nie wygodne. Programista musi określić wielkość obiektu, a następnie jeszcze dodatkowo rzutować zwracaną wartość funkcji malloc()
do odpowiedniego typu. Funkcja malloc()
tworzy obszar pamięci (a nie obiekt), dlatego zwraca wartość typu void*
.
Warto wspomnieć, że język C++ nie pozwala na przypisanie wartości typu void*
jakiemukolwiek innemu wskaźnikowi, dlatego wymagane jest rzutowanie.
Kolejny problem występuje, gdy malloc()
nie przydzieli wymaganej pamięci i zwróci wartość zerową. Wymusza to sprawdzanie zwracanego wskaźnika, aby mieć pewność, że pamięć została poprawnie zaalokowana.
Gdy już o to wszystko zadbany nie zapomnijmy o inicjalizacji obiektu, zanim z niego skorzystamy, np.:
Obj->initialize();
Pamiętajmy, że nie można tutaj użyć konstruktora, ponieważ nie ma sposobu wywołania go w jawny sposób1.
Problem polega na tym, że użytkownik może zapomnieć o dokonaniu inicjalizacji przed rozpoczęciem używania obiektu, umieszczając w ten sposób w programie źródło potencjalnych poważnych błędów.
To tyle wspomnień z języka C.
Teraz mamy C++ i dużo łatwiejsze dynamiczne alokowanie/zwalnianie pamięci. A wszystko dzięki dwóm operatorom new
i delete
.
Użycie new
do alokacji pamięci jest bardzo elastyczne i wygodne. Nie tylko zostanie przydzielona na stercie odpowiednia dla obiektu ilość pamięci, ale obiekt zostanie, także zainicjowany poprzez wywołanie odpowiedniego konstruktora danego obiektu.
Obj* obj = new Obj(1, 2);
Tak, więc użycie powyższej instrukcji jest równoważne wywołaniu funkcji malloc(sizeof(Obj))
(często jest to rzeczywiście wywołanie funkcji malloc()
) oraz konstruktora klasy Obj
, z adresem przydzielonej pamięci w charakterze wskaźnika this
oraz listą argumentów (1, 2)
.
W chwili przypisania adresu wskaźnikowi obj
wskazywany obszar pamięci jest już istniejącym, zainicjowanym obiektem, a nawet jest d razu typu Obj
, dzięki czemu nie ma potrzeby rzutowania wskaźnika.
W wyrażeniu new
można wykorzystać dowolny dostępny konstruktor klasy.
Domyślnie operator new
upewnia się, że przydzielenie pamięci zakończyło się powodzeniem, zanim jeszcze przekaże adres konstruktorowi. Nie ma, więc potrzeby jawnego sprawdzania, czy jego wywołanie zakończyło się pomyślnie.
Gdy zabraknie nam dostępnej pamięci (free store), new
zwróci wartość zerową (0 lub NULL), wtedy automatycznie zostanie wywołana funkcja obsługi operatora new
. Domyślnie funkcja ta zgłosi odpowiedni wyjątek.
Możemy jednak sami zastosować swoją funkcje jako funkcje obsługi operatora new
. Aby tego dokonać należy wywołać set_new_handler()
, podając jej jako parametr adres naszej funkcji.
Funkcja obsługi operatora new
nie może pobierać argumentów i musi zwracać wartość void
.
Przykład, prezentujący działanie:
// Zmiana funkcji obsługi operatora new
#include <iostream>
#include <cstdlib>
#include <new>
using namespace std;
int count = 0;
void out_of_memory() {
cout << "pamiec wyczerpała sie po " << count << " przydziałach!" << endl;
exit(l);
}
int main() {
set_new_handler(out_of_memory);
while(l) {
count++;
new int[1000];
}
}
Do zwalniania pamięci alokowanej przez new
służy operator delete
. Operator ten najpierw wywołuje destruktor obiektu, a dopiero później zwalnia przydzielana mu pamięć (wywołując często free()
).
Należy pamiętać, że delete
może być użyte tylko do zwalniania pamięci zaalokowanej przez new
.
Jeżeli użyjemy go w stosunku do zaalokowanej pamięci przez malloc()
to jego działanie nie jest zdefiniowane. Często jednak w domyślnych implementacjach new
i delete
wykorzystuje się funkcje malloc()
i free()
, wiec prawdopodobnie zostanie zwolniona pamięć, ale nie zostanie wywołany destruktor, co doprowadzi do wycieków pamięci.
I właśnie o tym chciałem coś napisać - o wyciekach pamięci. Przeglądając “Thinking In C++” zaciekawił mnie ten podrozdział, ale wyszło jak wyszło ;)
Próba usunięcia zerowego wskaźnika przez delete
nie wywoła żadnych szkód, po prostu nic się nie stanie, komputer przecież wie, ze nie istnieje adres 0
. Dlatego zaleca się po zwolnieniu pamięci przypisać danemu wskaźnikowi wartość NULL
. Bo próba usunięcia powtórnie tego samego obiektu (czegoś czego już nie ma, zostało usunięte prędzej) może doprowadzić do katastrofy Błąd nie musi się od razu ujawnić, przez co trudno później go znaleźć.
Wywołanie operatora delete
dla wskaźnika typu void*
jest błędem. Chyba, że obiekt, wskazywany przez ten wskaźnik, jest bardzo prosty (nie posiada destruktora).
Poniższy przykład pokazuje, co się w takim przypadku dzieje:
// Usuwanie wskaźników typu void* może powodować wyciekanie pamięci
#include <iostream>
using namespace std;
class Object {
void* data; // jakiś obszar pamięci
const int size;
const char id;
public:
Object(int sz, char c) : size(sz). id(c) {
data = new char[size];
cout << "Konstrukcja obiektu " << id << ". size - " << size << endl;
}
~Object() {
cout << "Destrukcja obiektu " << id << endl;
delete []data; // w porządku, tylko zwalniana jest pamięć
// - nie ma potrzeby wywoływania destruktora
}
};
int main() {
Object* a = new Object(40. 'a');
delete a;
void* b = new Object(40. 'b');
delete b;
}
Wyniki działania programu są następujące:
Konstrukcja obiektu a. size = 40
Destrukcja obiektu a
Konstrukcja obiektu b. size = 40
Jak widzimy bardzo ważne jest, aby operator delete
znał typ obiektu, w stosunku do którego został użyty.
Ponieważ wyrażenie
delete a
“wie”, że zmiennaa
jest wskaźnikiem do obiektu klasyObject
, wywoływany jest destruktor i tym samym następuje zwalnianie pamięci wskazywanej przez składowądata
. Jednakże w przypadku gdy operuje się obiektem, wykorzystując wskaźnik typuvoid*
, jak w wyrażeniudelete b
, zwalniana jest tylko pamięć, przydzielona obiektowi wskazywanemu przez ten wskaźnik. Nie jest natomiast wywoływany destruktor obiektu, a w związku z czym nie jest również zwalniana pamięć, wskazywana przez składowądata
. Podczas kompilacji tego programu nie pojawią się prawdopodobnie żadne ostrzeżenia - kompilator założy, że programista wie, co robi. Efektem będzie natomiast bardzo powolne wyciekanie pamięci.
J. Grebosz w książce “Symfonia C++” przedstawił krótko zalety stosowania new
i delete
w stosunku do malloc()
i free()
.
Wspomniał o wszystkich przedstawionych tutaj problemach i zaletach, o tym, że new
i delete
wywołują konstruktory/destruktory, czego w C nie było.
Dodatkowo malloc()
zwraca wskaźnik typu void*
, który może zostać przypisany gdziekolwiek po rzutowaniu, przez co łatwo o pomyłkę. W C++ new
zwraca konkretny typ, dlatego jakakolwiek próba przypisania go do innego typu wskaźnika zakończy się błędem sygnalizowanym przez kompilator.
Wspomniał także o problemie tzw. modelu pamięci. Jeśli model był “samll” to używało się malloc()
i free()
, a gdy “huge” to farmalloc()
i farfree()
. W C++ używając new
i delete
nie musimy się tym przejmować.
To by było na tyle, trochę długawa notka wyszła. Wykorzystano fragmenty z dwóch wymienionych w treści książek, to właśnie jedna z nich skłoniła mnie do głębszego wniknięcia i poruszenia tego tematu ;)
Przypisy
-
W rzeczywistości w C++ istnieje mechanizm “new placement”, który umożliwia wywołanie konstruktora i poprawne stworzenia obiektu we wcześniej zaalokowanym obszarze pamięci. ↩︎
Komentarze (5)
Nieźle!
Mogłeś wspomniec o ryzykownym przeładowywaniu globalnych operatorów
new
idelete
.Właśnie przeciążenia to już inny temat, w zamierzeniu miało być o
delete
ivoid*
, ale wyszło trochę więcej.Do tego wiele zdań jest identycznych jak w książce, bo się na niej wzorowałem, IMHO chyba bardziej przystępniej niż tam nie dałoby się tego opisać ;)
A jak zwolnic pamiec dla przydzielonej w sposob dynamiczny dla tablicy wskaznikow? Przydzielona jest naspetpujaco:
zwolnienie pamiecie ma nastepowac w destruktorze.
Napisał Pan, że nie da się jawnie wywołać konstruktora obiektu używając bloku pamięci zaalokowanego za pomocą
malloc
. Jest to niezgodne z prawdą, ponieważ w języku C++ istnieje mechanizm “placement new”, który pozwala wywołać konstruktor klasy na wcześniej zaalokowanym obszarze pamięci. Wywołanie wygląda np. tak:Zasadniczo operator
new
języka C++ robi dwie rzeczy:Punkt 2 ma newralgiczne znaczenie podczas inicjalizacji obiektów klas dziedziczonych wielokrotnie, z funkcjami wirtualnymi, gdyż niejawnie w konstruktorze ustawiana jest tablica wskaźników funkcji wirtualnych (virtual table), co jest niezbędne do poprawnego działania mechanizmów języka C++. Użycie proponowanego
Obj->initialize();
nie rozwiąże tego problemu. Placement new pozwala wywołać konstruktor nie alokując pamięci.Dziękuję za obszerny komentarz i wspomnienie o “placement new”, oczywiście ma Pan rację.
Niemniej wspomniane wywołanie
Obj->initialize()
było przedstawione w kontekście języka C i “emulacji” zachowania operatoranew
za pomocą “prymitywów” tego języka. Przynajmniej takie było zamierzenie, ale przy kolejnych edycjach tekstu trochę się to zmieniło i rozjechało ;)Postaram się trochę przeredagować wpis, aby lepiej przedstawić intencje, lub chociaż zamieścić wzmiankę o mechanizmie “placement new”, aby nie wprowadzać czytelników i szczególnie początkujących programistów w błąd.