Dy­na­micz­na alo­ka­cja pa­mię­ci

• tech • 1106 słów • 6 minut czy­ta­nia

W ję­zy­ku C do dy­na­micz­ne­go przy­dzie­la­nia pa­mię­ci wy­ko­rzy­stu­je się funk­cje (bi­blio­te­ki stan­dar­do­wej) malloc(), calloc()realloc(), a zwal­nia­nia pa­mię­ci - free(). Funk­cje te są po­pu­lar­ne i prak­tycz­ne w uży­ciu, ale jak wspo­mniał Bruce Eckel, w swo­jej książ­ce “Thin­king In C++”, są rów­nież pry­mi­tyw­ne, bo wy­ma­ga­ją one za­rów­no zro­zu­mie­nia, jak i uwagi ze stro­ny pro­gra­mi­sty.

Niby wszyst­ko jasne, ale nie tak do końca jest to takie ko­lo­ro­we.

Ty­po­we przy­dzie­le­nie pa­mię­ci wy­glą­da tak:

Obj* obj = (Obj*)malloc(sizeof(Obj));

Jest to tro­chę nie wy­god­ne. Pro­gra­mi­sta musi okre­ślić wiel­kość obiek­tu, a na­stęp­nie jesz­cze do­dat­ko­wo rzu­to­wać zwra­ca­ną war­tość funk­cji malloc() do od­po­wied­nie­go typu. Funk­cja malloc() two­rzy ob­szar pa­mię­ci (a nie obiekt), dla­te­go zwra­ca war­tość typu void*.

Warto wspo­mnieć, że język C++ nie po­zwa­la na przy­pi­sa­nie war­to­ści typu void* ja­kie­mu­kol­wiek in­ne­mu wskaź­ni­ko­wi, dla­te­go wy­ma­ga­ne jest rzu­to­wa­nie.

Ko­lej­ny pro­blem wy­stę­pu­je, gdy malloc() nie przy­dzie­li wy­ma­ga­nej pa­mię­ci i zwró­ci war­tość ze­ro­wą. Wy­mu­sza to spraw­dza­nie zwra­ca­ne­go wskaź­ni­ka, aby mieć pew­ność, że pa­mięć zo­sta­ła po­praw­nie za­alo­ko­wa­na.

Gdy już o to wszyst­ko za­dba­ny nie za­po­mnij­my o ini­cja­li­za­cji obiek­tu, zanim z niego sko­rzy­sta­my, np.:

Obj->initialize();

Pa­mię­taj­my, że nie można tutaj użyć kon­struk­to­ra, po­nie­waż nie ma spo­so­bu wy­wo­ła­nia go w jawny spo­sób1.

Pro­blem po­le­ga na tym, że użyt­kow­nik może za­po­mnieć o do­ko­na­niu ini­cja­li­za­cji przed roz­po­czę­ciem uży­wa­nia obiek­tu, umiesz­cza­jąc w ten spo­sób w pro­gra­mie źró­dło po­ten­cjal­nych po­waż­nych błę­dów.

To tyle wspo­mnień z ję­zy­ka C.

Teraz mamy C++ i dużo ła­twiej­sze dy­na­micz­ne alo­ko­wa­nie/zwal­nia­nie pa­mię­ci. A wszyst­ko dzię­ki dwóm ope­ra­to­rom newdelete.

Uży­cie new do alo­ka­cji pa­mię­ci jest bar­dzo ela­stycz­ne i wy­god­ne. Nie tylko zo­sta­nie przy­dzie­lo­na na ster­cie od­po­wied­nia dla obiek­tu ilość pa­mię­ci, ale obiekt zo­sta­nie, także za­ini­cjo­wa­ny po­przez wy­wo­ła­nie od­po­wied­nie­go kon­struk­to­ra da­ne­go obiek­tu.

Obj* obj = new Obj(1, 2);

Tak, więc uży­cie po­wyż­szej in­struk­cji jest rów­no­waż­ne wy­wo­ła­niu funk­cji malloc(sizeof(Obj)) (czę­sto jest to rze­czy­wi­ście wy­wo­ła­nie funk­cji malloc()) oraz kon­struk­to­ra klasy Obj, z ad­re­sem przy­dzie­lo­nej pa­mię­ci w cha­rak­te­rze wskaź­ni­ka this oraz listą ar­gu­men­tów (1, 2).

W chwi­li przy­pi­sa­nia ad­re­su wskaź­ni­ko­wi obj wska­zy­wa­ny ob­szar pa­mię­ci jest już ist­nie­ją­cym, za­ini­cjo­wa­nym obiek­tem, a nawet jest d razu typu Obj, dzię­ki czemu nie ma po­trze­by rzu­to­wa­nia wskaź­ni­ka.

W wy­ra­że­niu new można wy­ko­rzy­stać do­wol­ny do­stęp­ny kon­struk­tor klasy.

Do­myśl­nie ope­ra­tor new upew­nia się, że przy­dzie­le­nie pa­mię­ci za­koń­czy­ło się po­wo­dze­niem, zanim jesz­cze prze­ka­że adres kon­struk­to­ro­wi. Nie ma, więc po­trze­by jaw­ne­go spraw­dza­nia, czy jego wy­wo­ła­nie za­koń­czy­ło się po­myśl­nie.

Gdy za­brak­nie nam do­stęp­nej pa­mię­ci (free store), new zwró­ci war­tość ze­ro­wą (0 lub NULL), wtedy au­to­ma­tycz­nie zo­sta­nie wy­wo­ła­na funk­cja ob­słu­gi ope­ra­to­ra new. Do­myśl­nie funk­cja ta zgło­si od­po­wied­ni wy­ją­tek.

Mo­że­my jed­nak sami za­sto­so­wać swoją funk­cje jako funk­cje ob­słu­gi ope­ra­to­ra new. Aby tego do­ko­nać na­le­ży wy­wo­łać set_new_handler(), po­da­jąc jej jako pa­ra­metr adres na­szej funk­cji.

Funk­cja ob­słu­gi ope­ra­to­ra new nie może po­bie­rać ar­gu­men­tów i musi zwra­cać war­tość void.

Przy­kład, pre­zen­tu­ją­cy dzia­ła­nie:

// 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 zwal­nia­nia pa­mię­ci alo­ko­wa­nej przez new służy ope­ra­tor delete. Ope­ra­tor ten naj­pierw wy­wo­łu­je de­struk­tor obiek­tu, a do­pie­ro póź­niej zwal­nia przy­dzie­la­na mu pa­mięć (wy­wo­łu­jąc czę­sto free()).

Na­le­ży pa­mię­tać, że delete może być użyte tylko do zwal­nia­nia pa­mię­ci za­alo­ko­wa­nej przez new.

Je­że­li uży­je­my go w sto­sun­ku do za­alo­ko­wa­nej pa­mię­ci przez malloc() to jego dzia­ła­nie nie jest zde­fi­nio­wa­ne. Czę­sto jed­nak w do­myśl­nych im­ple­men­ta­cjach newdelete wy­ko­rzy­stu­je się funk­cje malloc()free(), wiec praw­do­po­dob­nie zo­sta­nie zwol­nio­na pa­mięć, ale nie zo­sta­nie wy­wo­ła­ny de­struk­tor, co do­pro­wa­dzi do wy­cie­ków pa­mię­ci.

I wła­śnie o tym chcia­łem coś na­pi­sać - o wy­cie­kach pa­mię­ci. Prze­glą­da­jąc “Thin­king In C++” za­cie­ka­wił mnie ten pod­roz­dział, ale wy­szło jak wy­szło ;)

Próba usu­nię­cia ze­ro­we­go wskaź­ni­ka przez delete nie wy­wo­ła żad­nych szkód, po pro­stu nic się nie sta­nie, kom­pu­ter prze­cież wie, ze nie ist­nie­je adres 0. Dla­te­go za­le­ca się po zwol­nie­niu pa­mię­ci przy­pi­sać da­ne­mu wskaź­ni­ko­wi war­tość NULL. Bo próba usu­nię­cia po­wtór­nie tego sa­me­go obiek­tu (cze­goś czego już nie ma, zo­sta­ło usu­nię­te prę­dzej) może do­pro­wa­dzić do ka­ta­stro­fy Błąd nie musi się od razu ujaw­nić, przez co trud­no póź­niej go zna­leźć.

Wy­wo­ła­nie ope­ra­to­ra delete dla wskaź­ni­ka typu void* jest błę­dem. Chyba, że obiekt, wska­zy­wa­ny przez ten wskaź­nik, jest bar­dzo pro­sty (nie po­sia­da de­struk­to­ra).

Po­niż­szy przy­kład po­ka­zu­je, co się w takim przy­pad­ku dzie­je:

// 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;
}

Wy­ni­ki dzia­ła­nia pro­gra­mu są na­stę­pu­ją­ce:

Konstrukcja obiektu a. size = 40
Destrukcja obiektu a
Konstrukcja obiektu b. size = 40

Jak wi­dzi­my bar­dzo ważne jest, aby ope­ra­tor delete znał typ obiek­tu, w sto­sun­ku do któ­re­go zo­stał użyty.

Po­nie­waż wy­ra­że­nie delete a “wie”, że zmien­na a jest wskaź­ni­kiem do obiek­tu klasy Object, wy­wo­ły­wa­ny jest de­struk­tor i tym samym na­stę­pu­je zwal­nia­nie pa­mię­ci wska­zy­wa­nej przez skła­do­wą data. Jed­nak­że w przy­pad­ku gdy ope­ru­je się obiek­tem, wy­ko­rzy­stu­jąc wskaź­nik typu void*, jak w wy­ra­że­niu delete b, zwal­nia­na jest tylko pa­mięć, przy­dzie­lo­na obiek­to­wi wska­zy­wa­ne­mu przez ten wskaź­nik. Nie jest na­to­miast wy­wo­ły­wa­ny de­struk­tor obiek­tu, a w związ­ku z czym nie jest rów­nież zwal­nia­na pa­mięć, wska­zy­wa­na przez skła­do­wą data. Pod­czas kom­pi­la­cji tego pro­gra­mu nie po­ja­wią się praw­do­po­dob­nie żadne ostrze­że­nia - kom­pi­la­tor za­ło­ży, że pro­gra­mi­sta wie, co robi. Efek­tem bę­dzie na­to­miast bar­dzo po­wol­ne wy­cie­ka­nie pa­mię­ci.

J. Gre­bosz w książ­ce “Sym­fo­nia C++” przed­sta­wił krót­ko za­le­ty sto­so­wa­nia newdelete w sto­sun­ku do malloc()free().

Wspo­mniał o wszyst­kich przed­sta­wio­nych tutaj pro­ble­mach i za­le­tach, o tym, że newdelete wy­wo­łu­ją kon­struk­to­ry/de­struk­to­ry, czego w C nie było.

Do­dat­ko­wo malloc() zwra­ca wskaź­nik typu void*, który może zo­stać przy­pi­sa­ny gdzie­kol­wiek po rzu­to­wa­niu, przez co łatwo o po­mył­kę. W C++ new zwra­ca kon­kret­ny typ, dla­te­go ja­ka­kol­wiek próba przy­pi­sa­nia go do in­ne­go typu wskaź­ni­ka za­koń­czy się błę­dem sy­gna­li­zo­wa­nym przez kom­pi­la­tor.

Wspo­mniał także o pro­ble­mie tzw. mo­de­lu pa­mię­ci. Jeśli model był “samll” to uży­wa­ło się malloc()free(), a gdy “huge” to farmalloc()farfree(). W C++ uży­wa­jąc newdelete nie mu­si­my się tym przej­mo­wać.

To by było na tyle, tro­chę dłu­ga­wa notka wy­szła. Wy­ko­rzy­sta­no frag­men­ty z dwóch wy­mie­nio­nych w tre­ści ksią­żek, to wła­śnie jedna z nich skło­ni­ła mnie do głęb­sze­go wnik­nię­cia i po­ru­sze­nia tego te­ma­tu ;)


Przy­pi­sy

  1. W rze­czy­wi­sto­ści w C++ ist­nie­je me­cha­nizm “new pla­ce­ment”, który umoż­li­wia wy­wo­ła­nie kon­struk­to­ra i po­praw­ne stwo­rze­nia obiek­tu we wcze­śniej za­alo­ko­wa­nym ob­sza­rze pa­mię­ci. ↩︎

Ko­men­ta­rze (5)

Nedroxn avatar
Ne­droxn
20070130-175540-​nedroxn

Nie­źle!

Mo­głeś wspo­mniec o ry­zy­kow­nym prze­ła­do­wy­wa­niu glo­bal­nych ope­ra­to­rów newdelete.

MalCom avatar
Mal­Com
20070130-175821-​malcom

Wła­śnie prze­cią­że­nia to już inny temat, w za­mie­rze­niu miało być o deletevoid*, ale wy­szło tro­chę wię­cej.

Do tego wiele zdań jest iden­tycz­nych jak w książ­ce, bo się na niej wzo­ro­wa­łem, IMHO chyba bar­dziej przy­stęp­niej niż tam nie da­ło­by się tego opi­sać ;)

coorter avatar
co­or­ter
20070426-173829-​coorter

A jak zwol­nic pa­miec dla przy­dzie­lo­nej w spo­sob dy­na­micz­ny dla ta­bli­cy wska­zni­kow? Przy­dzie­lo­na jest na­spet­pu­ja­co:

Auto **miejsca=new Auto *[rozmiar];

zwol­nie­nie pa­mie­cie ma na­ste­po­wac w de­struk­to­rze.

Endru avatar
Endru
20080817-203014-​endru

Na­pi­sał Pan, że nie da się jaw­nie wy­wo­łać kon­struk­to­ra obiek­tu uży­wa­jąc bloku pa­mię­ci za­alo­ko­wa­ne­go za po­mo­cą malloc. Jest to nie­zgod­ne z praw­dą, po­nie­waż w ję­zy­ku C++ ist­nie­je me­cha­nizm “pla­ce­ment new”, który po­zwa­la wy­wo­łać kon­struk­tor klasy na wcze­śniej za­alo­ko­wa­nym ob­sza­rze pa­mię­ci. Wy­wo­ła­nie wy­glą­da np. tak:

void *block = malloc(sizeof(Klasa));
Klasa *obj = new (block) Klasa();

Za­sad­ni­czo ope­ra­tor new ję­zy­ka C++ robi dwie rze­czy:

  1. Alo­ku­je pa­mięć.
  2. Wy­wo­łu­je kon­struk­tor obiek­tu.

Punkt 2 ma new­ral­gicz­ne zna­cze­nie pod­czas ini­cja­li­za­cji obiek­tów klas dzie­dzi­czo­nych wie­lo­krot­nie, z funk­cja­mi wir­tu­al­ny­mi, gdyż nie­jaw­nie w kon­struk­to­rze usta­wia­na jest ta­bli­ca wskaź­ni­ków funk­cji wir­tu­al­nych (vir­tu­al table), co jest nie­zbęd­ne do po­praw­ne­go dzia­ła­nia me­cha­ni­zmów ję­zy­ka C++. Uży­cie pro­po­no­wa­ne­go Obj->initialize(); nie roz­wią­że tego pro­ble­mu. Pla­ce­ment new po­zwa­la wy­wo­łać kon­struk­tor nie alo­ku­jąc pa­mię­ci.

MalCom avatar
Mal­Com
20080817-204253-​malcom

Dzię­ku­ję za ob­szer­ny ko­men­tarz i wspo­mnie­nie o “pla­ce­ment new”, oczy­wi­ście ma Pan rację.

Nie­mniej wspo­mnia­ne wy­wo­ła­nie Obj->initialize() było przed­sta­wio­ne w kon­tek­ście ję­zy­ka C i “emu­la­cji” za­cho­wa­nia ope­ra­to­ra new za po­mo­cą “pry­mi­ty­wów” tego ję­zy­ka. Przy­naj­mniej takie było za­mie­rze­nie, ale przy ko­lej­nych edy­cjach tek­stu tro­chę się to zmie­ni­ło i roz­je­cha­ło ;)

Po­sta­ram się tro­chę prze­re­da­go­wać wpis, aby le­piej przed­sta­wić in­ten­cje, lub cho­ciaż za­mie­ścić wzmian­kę o me­cha­ni­zmie “pla­ce­ment new”, aby nie wpro­wa­dzać czy­tel­ni­ków i szcze­gól­nie po­cząt­ku­ją­cych pro­gra­mi­stów w błąd.

Dodaj ko­men­tarz

/do­zwo­lo­ny mark­down/

/nie zo­sta­nie opu­bli­ko­wa­ny/