punkt08






Scott Meyers
"Język C++ bardziej efektywny"




Scott Meyers
“JÄ™zyk C++ bardziej efektywny"
 
Punkt 8 : Zrozum różne znaczenie operatorów new oraz delete
 
Można czasem odnieść wrażenie, że różni ludzie usilnie starali się o to, aby uczynić terminologię języka C++ trudną do zrozumienia. Przykładem tego może być rozróżnienie między operatorem new i funkcją operator new.
PiszÄ…c instrukcjÄ™
 
string *wn = new string(“ZarzÄ…dzanie pamiÄ™ciÄ…");
 
użyliśmy słowa new jako operatora, który jest wbudowany do języka, podobnie jak np. operator sizeof. Nie możesz zmienić znaczenia operatora wbudowanego
zawsze będzie robił to samo. Operator new ma dwa zadania do spełnienia. Po pierwsze, przydziela odpowiedni obszar pamięci dla obiektu żądanego typu. W powyższym przekładzie przydziela pamięć, w której będzie przechowywany obiekt klasy string,
czyli napis Zarządzanie pamięcią. Po drugie, wywołuje konstruktor w celu zainicjowania obiektu w przydzielonej pamięci. Operator new zawsze wykonuje te dwa zadania i w żaden sposób nie można tego zmienić.
Można natomiast zmienić to, jak będzie przydzielona pamięć dla danego obiektu. Operator new wywołuje funkcję, która ma dokonać żądanego przydziału pamięci. Możesz napisać tę funkcję od nowa lub ją przeciążyć, tak aby zmieniła swoje zachowanie. Funkcja, którą operator new wywołuje, aby przydzieliła pamięć, nazywa się operator new. Naprawdę.
Funkcja operator new jest zazwyczaj zadeklarowana tak jak poniżej:


void *operator new(size_t rozmiar);


Wartość przekazywana przez funkcję jest typu void*, ponieważ funkcja ta przekazuje wskaźnik do niezainicjowanej pamięci. (Jeśli chcesz, możesz napisać taką wersję funkcji operator new, która umieszcza w pamięci jakąś wartość przed przekazaniem wskaźnika do niej, ale takie podejście nie jest zbyt popularne). Argument typu size_t określa, jak duży obszar ma być przydzielony w pamięci. Możesz przeciążyć funkcję operator new, dodając jej drugi argument, ale pierwszy argument musi zawsze być typu size_t.
Przypuszczalnie nigdy nie zechcesz bezpośrednio wywoływać funkcji operator new, ale gdyby jednak do tego doszło, to wywołuje się ją tak samo jak każdą funkcję:
 
void *pamięćNiezainicjowana = operator new(sizeof(string));
 
Funkcja operator new przekaże wówczas wskaźnik do porcji pamięci, w której pomieści się obiekt klasy string.
Podobnie jak funkcja malloc, funkcja operator new odpowiada jedynie za przydział pamięci. Funkcja ta nie wie nic o konstruktorach, potrafi tylko przydzielać pamięć. Zadaniem operatora new jest pobranie porcji niezainicjowanej pamięci, do której wskaźnik przekazała mu funkcja operator new, a następnie przekształcenie zawartości tej pamięci w pewien obiekt. Kiedy kompilatory odczytają instrukcję:
 
string *wn = new string("zarządzanie pamięcią");
 
wtedy muszą utworzyć kod odpowiadający w przybliżeniu czemuś takiemu:
 
void *obszarNiezainicjowany = // pobierz obszar niezainicjowanej pamięci


operator new(sizeof(string)); // dla obiektu klasy string wywołaj string::string("Zarządzanie pamięcią") dla obszarNiezainicjowany; // zainicjuj ten obiekt w pamięci
string *wn = // niech wn wskazuje na nowy obiekt
static cast<string*>(obszarNiezainicjowany);
 


Zauważmy, że w drugim kroku w powyższym postępowaniu jest wywołanie konstruktora, czyli wykonuje się coś, czego zwykłemu programiście robić zabroniono. Ale nasze kompilatory nie mają takich ograniczeń, jak zwykli śmiertelnicy, i mogą robić co chcą. Właśnie dlatego musisz używać operatora new, jeżeli chcesz, aby powstał nowy obiekt w stercie, ponieważ nie możesz bezpośrednio wywołać konstruktora niezbędnego do zainicjowania obiektu (włącznie z takimi jego zasadniczymi składnikami, jak tablica wirtualna - zob. punkt 24).
 
UmieszczajÄ…cy operator new
 
Bywa tak, że rzeczywiście chcemy bezpośrednio wywołać konstruktor. Wywoływanie konstruktora dla istniejącego obiektu nie ma sensu, ponieważ konstruktory inicjują wartości obiektów, obiekt zaś można tylko raz zainicjować jego pierwszą wartością. Ale zdarza się, że mamy jakiś obszar niezainicjowanej pamięci, który był już raz przydzielony, a musimy skonstruować obiekt w tym samym obszarze pamięci. Możemy w tym celu użyć specjalnej składni operatora new; operator new napisany przy użyciu tej składni nazywa się umieszczającym operatorem new (ang. placement new).
Rozważmy poniższy przykład użycia umieszczającego operatora new:
 
class Cokolwiek { public: Cokolwiek(int rozmiarCokolwiek);
...


};Cokolwiek * skonstruujCokolwiekWBuforze(void *bufor, int rozmiarCokolwiek)
{ return new (bufor) Cokolwiek(rozmiarCokolwiek);


}
 
Funkcja ta przekazuje wskaźnik do obiektu klasy Cokolwiek, który jest skonstruowany wewnątrz bufora przekazywanego jako argument funkcji. Taką funkcją można się z pożytkiem posłużyć w programach korzystających z pamięci wspólnej (dzielonej) lub z operacji wejścia-wyjścia odwzorowanych na miejsca w pamięci, ponieważ obiekty, na których działają takie programy, muszą być umieszczane pod specjalnymi adresami albo w pamięci przydzielanej przez specjalne procedury. (Inny przykład tego, jak można zastosować umieszczający operator new, podano w punkcie 4).
Wewnątrz funkcji skonstruujCokolwiekWBuforze jest przekazywane wyrażenie
new (bufor) Cokolwiek(rozmiarCokolwiek)
 
Na pierwszy rzut oka wygląda to dziwnie, ale jest to właśnie takie zastosowanie operatora new, w którym dodatkowy argument (bufor) jest wyspecyfikowany dla niejawnego wywołania funkcji operator new przez operator new. Wywołana zatem funkcja operator new musi oprócz obowiązkowego argumentu typu size_t pobrać argument typu void*, który wskazuje na początek obszaru pamięci przeznaczonego dla konstruowanego obiektu. Ta funkcja operator new jest umieszczającym operatorem new i wygląda następująco:
 
void * operator new(size_t, void *umiejscowienie)


{


return umiejscowienie;




}
 
Prawdopodobnie może się to wydawać zaskakująco proste, ale jest to wszystko, co umieszczający operator new musi zrobić. W końcu zadaniem funkcji operator new jest znalezienie pamięci dla obiektu i przekazanie jako wyniku wskaźnika do tej pamięci. W przypadku umieszczającego operatora new w chwili wywołania funkcji wiadomo, jaki powinien być ten wskaźnik, ponieważ wiadomo, gdzie zamierza się umieścić dany obiekt. Operator ten musi zatem jedynie przekazać wskaźnik, który mu przesłano. (Nieużywany (ale obowiązkowy) argument typu size_t jest pozbawiony nazwy - ma to powstrzymywać kompilatory przed utyskiwaniem, że nie użyto tego argumentu; zob. punkt 6). Umieszczający operator new należy do standardowej biblioteki języka C++. Aby go użyć, należy jedynie napisać w programie dyrektywę #include<new.h>.
Jeżeli na chwilę powrócimy do rozważań poprzedzających omawianie umieszczającego operatora new, to - pomimo, być może, trochę mylącej terminologii - dostrzeżemy łatwą do zrozumienia zależność między operatorem new a funkcją operator new. Jeżeli chcesz utworzyć obiekt, który będzie umieszczony w stercie, to użyj operatora new. Zarówno przydzieli on pamięć, jak i wywoła konstruktor obiektu. Jeżeli chcesz tylko przydzielić pamięć, to wywołaj funkcję operator new; konstruktor nie będzie wywołany. Jeżeli przydzielanie pamięci chcesz dostosować do swoich potrzeb, co zdarza się wówczas, gdy tworzy się obiekt w stercie, to napisz własną wersję funkcji operator new i użyj operatora new; wywoła on automatycznie twoją przystosowaną wersję funkcji operator new. Jeżeli chcesz skonstruować obiekt w pamięci, do której masz już wskaźnik, to użyj umieszczającego operatora new.
 
Usunięcie obiektu i uwolnienie pamięci
 
Aby nie powstawały luki w zasobach, każdemu dynamicznemu przydzieleniu obszaru pamięci musi odpowiadać uwolnienie tego obszaru. Funkcja operator delete jest tym dla wbudowanego do języka C++ operatora delete, czym dla operatora new jest funkcja operator new. Jeżeli napiszemy takie instrukcje:
 


string *ws ;delete ws; // użyj operatora delete
 


to kompilatory muszą utworzyć kod zarówno w celu usunięcia obiektu wskazywanego przez ws, jak i w celu uwolnienia pamięci zajmowanej przez ten obiekt.
Do uwalniania pamięci służy funkcja operator delete, którą zazwyczaj deklaruje się tak jak poniżej :
 
void operator delete(void *uwalnianaPamięć);
 
Tak więc instrukcja
delete ws;
spowoduje, że kompilatory utworzą kod, który w przybliżeniu odpowiada poniższym instrukcjom:
ws->~string(); // wywołaj destruktor obiektu operator
delete(ws); // uwolnij pamięć zajętą przez obiekt
 
Wynika stąd, że jeżeli chcesz działać tylko na niezainicjowanej pamięci przydzielanej dynamicznie, to nie możesz korzystać z operatorów new oraz delete. W zamian musisz wywołać dwie funkcje: operator new, aby uzyskać pamięć, i operator delete, aby zwrócić ją systemowi:
 
void *bufor = // przydziel dostateczny
operator new(50*sizeof(char)); // obszar pamięci dla 50
... // znaków; nie wywołuj konstruktora operator
delete(bufor); // uwolnij pamięć; nie wywołuj destruktora
 
W języku C++ jest to odpowiednik wywołania funkcji malloc i free.
Jeżeli chcąc utworzyć obiekt w pewnym obszarze pamięci, korzysta się z umieszczającego operatora new, to należy unikać używania operatora delete w stosunku do tej pamięci. Wynika to stąd, że operator delete wywołuje funkcję operator delete w celu uwolnienia przydzielonej pamięci, ale obszaru pamięci zawierającego ten obiekt nie przydzieliła poprzednio funkcja operator new; umieszczający operator new przekazuje tylko wskaźnik, który mu przesłano. Któż może wiedzieć, skąd wziął się ten wskaźnik? Należy zatem jawnie wywołać destruktor obiektu, aby unieważnić wynik działania konstruktora:
 
// funkcje służące do przydzielania i uwalniania obszaru w pamięci wspólnej
void * przydzielPamWsp(size_t rozmiar);










// odpowiednik funkcji malloc










void uwolnijPamWsp(void *obszar); // odpowiednik funkcji free
void *obszarPamWsp = przydzielPamWsp(sizeof(Cokolwiek));
Cokolwiek *wc = // jak powyżej: użyto
skonstruujCokolwiekWBuforze(obszarPamWsp, 10)










// umieszczajÄ…cego operatora new delete wc;
// niezdefiniowane: obszarPamWsp
// pochodzi od przydzielPamWsp,
// nie od funkcji operator new










wc->~Cokolwiek(); // dobrze, niszczy obiekt klasy










// Cokolwiek, na który wskazuje
// wc, ale nie uwalnia pamięci
// zajętej przez ten obiekt










uwolnijPamWsp(wc); // dobrze, uwalnia pamięć wskazywaną przez wc,










// ale nie wywołuje destruktora
 










Jak widać z tego przykładu, jeżeli umieszczający operator new ma działać na niezainicjowanej pamięci, której dany obszar był już dynamicznie przydzielony (w pewien niekonwencjonalny sposób), to musisz jeszcze uwolnić ten obszar, aby uniknąć pozostawiania luk w pamięci.
 
Tablice
 
Na razie wszystko w porządku, ale teraz musimy iść dalej . Do tej pory wszystkie nasze rozważania dotyczyły sytuacji, w której za każdym razem mieliśmy do czynienia z jednym tylko obiektem. A co z przydzielaniem pamięci dla tablicy? Co tutaj może się wydarzyć?

string *ws = new string[10); // przydziel pamięć dla tablicy obiektów
 
Użyte powyżej słowo new jest nadal operatorem new, ale ponieważ ma być utworzona tablica, więc operator ten zachowuje się trochę inaczej niż wówczas, gdy ma powstać pojedynczy obiekt. Po pierwsze, pamięci nie przydziela już funkcja operator new. Zamiast niej pamięć przydziela jej odpowiednik dla tablic, funkcja operator new[ ] (zwana często tablicowym operatorem new). Funkcja ta może być przeciążana podobnie jak funkcja operator new. Pozwala to nam przejmować kontrolę nad przydzielaniem pamięci dla tablic - w taki sam sposób, jak możemy nadzorować przydzielanie pamięci dla pojedynczych obiektów.
(Funkcja operator new[ ] jest względnie nowym dodatkiem do języka C++, więc niektóre kompilatory mogą jej jeszcze nie mieć. Jeżeli jej brak, to globalna wersja funkcji operator new posłuży do przydzielania pamięci dla każdej tablicy, niezależnie od typu jej elementów. W takich kompilatorach przystosowywanie (ang. customizing) funkcji przydzielającej pamięć dla tablicy do wymagań użytkownika jest czynnością zniechęcającą, ponieważ wymaga to napisania od nowa globalnej funkcji operator new. Nie jest to łatwe zadanie. Domyślnie funkcja operator new obsługuje każde dynamiczne przydzielanie pamięci w programie, więc dowolna zmiana jej zachowania ma dramatyczne i dotkliwe konsekwencje. Co więcej, istnieje tylko jedna globalna funkcja operator new, więc jeżeli postanowisz zażądać jej na własność, to tym samym sprawisz, że Twoje oprogramowanie będzie niezgodne z każdą biblioteką, w której podjęto taką samą decyzję (zob. też jeszcze punkt 27). Można z tych rozważań wyciągnąć wniosek, że przystosowywanie zarządzania pamięcią w kompilatorach, w których nie udostępniono funkcji operator new[ ], na ogół nie jest rozsądne). Drugim czynnikiem, który odróżnia zachowanie operatora new dla tablic od jego zachowania w odniesieniu do pojedynczych obiektów, jest liczba wywoływanych przez niego konstruktorów. Musi być wywołany konstruktor dla każdego obiektu należącego do tablicy:
 
string *ws = // wywołaj operator new[ ], aby
new string[10]; // przydzielił pamięć dla 10 obiektów klasy string, następnie wywołuj




// domyślny konstruktor klasy string
// dla każdego elementu tablicy
 




Podobnie operator delete zastosowany do tablicy wywołuje destruktor dla każdego elementu tablicy, następnie zaś wywołuje funkcję operator delete [ ] w celu uwolnienia pamięci:
 
delete [ ] ws; // wywołuj destruktor klasy string
// dla każdego elementu tablicy, następnie wywołaj operator




// delete[ ] w celu uwolnienia pamięci zajętej przez tablic
 




Podobnie jak można zastąpić inną funkcją lub przeciążyć funkcję operator delete, tak też można zastąpić lub przeciążyć funkcję operator delete [ ]. Jednak na sposób przeciążania nałożono pewne ograniczenia; w celu szczegółowego zaznajomienia się z tym zagadnieniem polecam Ci przeczytanie dobrych tekstów o języku C++ (aby dowiedzieć się, co to są dobre teksty o języku C++, przeczytaj Literaturę zalecaną) .
Podsumujmy nasze rozważania. Operatory new oraz delete są wbudowane do języka i pozostają poza kontrolą programisty, ale nie zawsze wywołują one funkcje służące do przydzielania i uwalniania pamięci. Kiedy zastanawiasz się nad przystosowywaniem zachowania operatorów new oraz delete do swoich potrzeb, wtedy pamiętaj, że tak naprawdę nie wolno Ci tego uczynić. Możesz zmienić tylko sposób, w jaki one robią to, co mają robić, ale już to, co robią, jest ustalone w języku C++.



Wyszukiwarka

Podobne podstrony:
punkt02
punkt06
punkt01
punkt04
punkt09

więcej podobnych podstron