Zaawansowane CPP/Wykład 14: Zarządzanie pamięcią: Różnice pomiędzy wersjami
m Zastępowanie tekstu - "<div class="thumb t(.*)"><div style="width:(.*)px;"> <flashwrap>file=(.*).swf\|size=small<\/flashwrap> <div\.thumbcaption>(.*)<\/div> <\/div><\/div>" na "$2x$2px|thumb|$1|$4" |
|||
(Nie pokazano 71 wersji utworzonych przez 6 użytkowników) | |||
Linia 1: | Linia 1: | ||
==Wstęp== | ==Wstęp== | ||
Linia 8: | Linia 6: | ||
używane są raczej wyrażenia | używane są raczej wyrażenia | ||
<code>new</code> i <code>delete</code>. Ta zmiana ma poważną przyczynę: te wyrażenia | <code>new</code> i <code>delete</code>. Ta zmiana ma poważną przyczynę: te wyrażenia | ||
robią więcej niż tylko | robią więcej niż tylko przydzielanie lub zwalnianie pamięci. Wyrażenie | ||
<code>new</code> tworzy nowy obiekt, a więc nie tylko przydziela pamięc, ale | <code>new</code> tworzy nowy obiekt, a więc nie tylko przydziela pamięc, ale | ||
również inicjalizuje go używając odpowiedniego konstruktora. Wyrażenie | również inicjalizuje go, używając odpowiedniego konstruktora. Wyrażenie | ||
<code>delete</code> niszczy obiekt wywołując jego destruktor i dopiero potem | <code>delete</code> niszczy obiekt, wywołując jego destruktor i dopiero potem | ||
zwalnia zajętą przez niego pamięć. W tym wykładzie pokażę, co tak | zwalnia zajętą przez niego pamięć. W tym wykładzie pokażę, co tak | ||
naprawdę się dzieje gdy dynamicznie tworzymy lub niszczymy obiekty. | naprawdę się dzieje, gdy dynamicznie tworzymy lub niszczymy obiekty. | ||
Wyrażenia <code>new</code> i <code>delete</code> posługują się systemowymi alokatorami | Wyrażenia <code>new</code> i <code>delete</code> posługują się systemowymi alokatorami | ||
i dealokatorami pamięci. C++ daje nam możliwość | i dealokatorami pamięci. C++ daje nam możliwość | ||
wykorzystania w tym celu własnych implementacji. | |||
Napisanie jednak bardziej wydajnego alokatora | Napisanie jednak bardziej wydajnego alokatora | ||
pamięci | pamięci niż alokator standardowy nie jest łatwe. | ||
Można jednak próbować zwiększyć wydajność przydzielania i zwalniania | Można jednak próbować zwiększyć wydajność przydzielania i zwalniania | ||
pamięci w sytuacjach szczególnych np. jeśli | pamięci w sytuacjach szczególnych, np. jeśli | ||
używamy dużej ilości małych obiektów o stałym rozmiarze, które muszą | używamy dużej ilości małych obiektów o stałym rozmiarze, które muszą | ||
być dynamicznie tworzone i niszczone. Pod koniec wykładu podamy prosty | być dynamicznie tworzone i niszczone. Pod koniec wykładu podamy prosty | ||
schemat obsługi pamięci mający zastosowanie w takiej sytuacji. | schemat obsługi pamięci mający zastosowanie w takiej sytuacji. | ||
==new== | ==new== | ||
Linia 32: | Linia 30: | ||
X *p <nowiki>=</nowiki> new X inicjalizator; | X *p <nowiki>=</nowiki> new X inicjalizator; | ||
lub | lub | ||
X *p <nowiki>=</nowiki> new (lista_argumentow) X inicjalizator; | X *p <nowiki>=</nowiki> new (lista_argumentow) X inicjalizator; | ||
Druga forma jest nazywana z przyczyn historycznych "placement new" | Druga forma jest nazywana z przyczyn historycznych "placement new" | ||
(pochodzenie tej nazwy wyjaśnię poniżej) | (pochodzenie tej nazwy wyjaśnię poniżej); <code>inicjalizator</code> | ||
może być dowolnym wyrażeniem inicjalizującym np.: | może być dowolnym wyrażeniem inicjalizującym, np.: | ||
X *p1 <nowiki>=</nowiki> new X; | X *p1 <nowiki>=</nowiki> new X; | ||
Linia 47: | Linia 45: | ||
X *p4 <nowiki>=</nowiki> new X(y); | X *p4 <nowiki>=</nowiki> new X(y); | ||
X *p5 <nowiki>=</nowiki> new X(0); | X *p5 <nowiki>=</nowiki> new X(0); | ||
Oczywiście zakładamy istnienie odpowiednich konstruktorów. | Oczywiście zakładamy istnienie odpowiednich konstruktorów. | ||
====Przydział pamięci==== | ====Przydział pamięci==== | ||
Najpierw przydzielana jest "goła" (raw) pamięć | Najpierw przydzielana jest "goła" (raw) pamięć. Służy do tego | ||
funkcja przydziału | funkcja przydziału pamięci (alokator) <code>operator new()</code>: | ||
void *tmp <nowiki>=</nowiki> operator new(sizeof(X)); | void *tmp <nowiki>=</nowiki> operator new(sizeof(X)); | ||
lub | lub | ||
void *tmp <nowiki>=</nowiki> operator new(sizeof(X),lista_argumentow); | void *tmp <nowiki>=</nowiki> operator new(sizeof(X),lista_argumentow); | ||
jeśli użyliśmy formy placement. Nazwa placement pochodzi od operatora | jeśli użyliśmy formy placement. Nazwa placement pochodzi od operatora | ||
<code>new</code> dostarczanego w bibliotece standardowej, który przyjmuje drugi | <code>new</code> dostarczanego w bibliotece standardowej, który przyjmuje drugi | ||
Linia 66: | Linia 64: | ||
void* operator new(std::size_t size, void* ptr) throw() {return ptr;}; | void* operator new(std::size_t size, void* ptr) throw() {return ptr;}; | ||
Operator ten nie przydziela żadnej pamięci tylko zwraca wskaźnik | Operator ten nie przydziela żadnej pamięci tylko zwraca wskaźnik | ||
<code>ptr</code>. Jego wywołanie nie może się nie powieść dlatego nie rzuca | <code>ptr</code>. Jego wywołanie nie może się nie powieść, dlatego nie rzuca | ||
żadnych wyjątków. Ta forma operatora służy do | żadnych wyjątków. Ta forma operatora służy do | ||
umieszczania (placement) | umieszczania (placement) obiektu w | ||
zadanym obszarze pamięci: | zadanym obszarze pamięci: | ||
void *p <nowiki>=</nowiki>malloc(sizeof(X)); | void *p <nowiki>=</nowiki>malloc(sizeof(X)); | ||
X *px<nowiki>=</nowiki>new (p) X; | X *px<nowiki>=</nowiki>new (p) X; | ||
stąd jego nazwa. | stąd jego nazwa. | ||
Linia 83: | Linia 81: | ||
void* operator new(std::size_t size) throw(std::bad_alloc) ; | void* operator new(std::size_t size) throw(std::bad_alloc) ; | ||
void* operator new(std::size_t size, std::nothrow_t) throw(); | void* operator new(std::size_t size, std::nothrow_t) throw(); | ||
ale użytkownik może podać własne definicje, zarówno | ale użytkownik może podać własne definicje, zarówno | ||
globalne, jak i dla pojedynczych klas. Odpowiednia funkcja <code>operator | globalne, jak i dla pojedynczych klas. Odpowiednia funkcja <code>operator new</code> jest najpierw wyszukiwana w klasie <code>X</code>, a następnie w | ||
przestrzeni globalnej. Jeśli nie znajdzie się definicja odpowiadająca | przestrzeni globalnej. Jeśli nie znajdzie się definicja odpowiadająca | ||
podanym argumentom, to wystąpi błąd kompilacji. Np. jeśli zażądamy | podanym argumentom, to wystąpi błąd kompilacji. Np. jeśli zażądamy | ||
Linia 92: | Linia 89: | ||
X *p <nowiki>=</nowiki> new X; | X *p <nowiki>=</nowiki> new X; | ||
to kompilator będzie szukał funkcji: | to kompilator będzie szukał funkcji: | ||
X::operator new(size_t); | X::operator new(size_t); | ||
a w drugiej kolejności: | a w drugiej kolejności: | ||
void *tmp <nowiki>=</nowiki> ::operator new(sizeof(X)); | void *tmp <nowiki>=</nowiki> ::operator new(sizeof(X)); | ||
Wyrażenie | Wyrażenie | ||
X *p <nowiki>=</nowiki> new (3.15) X; | X *p <nowiki>=</nowiki> new (3.15) X; | ||
spowoduje poszukiwanie funkcji: | spowoduje poszukiwanie funkcji: | ||
X::operator new(size_t,double); | X::operator new(size_t,double); | ||
lub | lub | ||
::operator new(size_t,double); | ::operator new(size_t,double); | ||
Pierwszy argument każdej funkcji <code>operator new</code> musi być typu | Pierwszy argument każdej funkcji <code>operator new</code> musi być typu | ||
<code>size_t</code> i przekazywany jest przez niego rozmiar żądanego obszaru | <code>size_t</code> i przekazywany jest przez niego rozmiar żądanego obszaru | ||
pamięci. | pamięci. | ||
Każda funkcja <code>operator new </code> | Każda funkcja <code>operator new </code> zwraca <code>void *</code>. W przypadku | ||
powodzenia zwracany jest wskaźnik do przydzielonego obszaru pamięci: | powodzenia zwracany jest wskaźnik do przydzielonego obszaru pamięci: | ||
void *p <nowiki>=</nowiki> operator new(1000); | void *p <nowiki>=</nowiki> operator new(1000); | ||
W przypadku niepowodzenia <code>operator new</code> może rzucić wyjątek | W przypadku niepowodzenia <code>operator new</code> może rzucić wyjątek | ||
<code>std::bad_alloc</code> lub zwrócić wskaźnik zerowy. | <code>std::bad_alloc</code> lub zwrócić wskaźnik zerowy. | ||
====Tworzenie obiektu ==== | ====Tworzenie obiektu ==== | ||
Linia 130: | Linia 127: | ||
niezerowy wskaźnik, to następuje wywołanie konstruktora klasy <code>X</code> | niezerowy wskaźnik, to następuje wywołanie konstruktora klasy <code>X</code> | ||
w celu stworzenia obiektu, | w celu stworzenia obiektu, | ||
który jest | który jest umieszczany w przydzielonej pamięci. Np: | ||
X *p <nowiki>=</nowiki> X(1); | X *p <nowiki>=</nowiki> X(1); | ||
spowoduje | spowoduje wywołanie konstruktora: | ||
X::X(int); | X::X(int); | ||
jeśli takowy istnieje. Jeśli konstrukcja się powiedzie (nie rzuci | jeśli takowy istnieje. Jeśli konstrukcja się powiedzie (nie rzuci | ||
wyjątku), to proces się kończy. Jeśli jednak wywołany konstruktor | wyjątku), to proces się kończy. Jeśli jednak wywołany konstruktor | ||
rzuci wyjątek, który zostanie złapany, to wyrażenie <code>delete</code> | rzuci wyjątek, który zostanie złapany, to wyrażenie <code>delete</code> | ||
postara się zwolnić przydzieloną pamięć | postara się zwolnić przydzieloną pamięć w "trybie awaryjnym". | ||
====Awaryjne zwolnienie pamięci==== | ====Awaryjne zwolnienie pamięci==== | ||
W ramach takiej obsługi przerwania, zwalnianie pamięci przydzielonej przez <code>operator new</code> odbywa sie za pomocą odpowiadajacej mu wersji <code>operator delete</code>. | |||
void operator delete(void *p,lista_argumentow) throw(); | |||
<code>operator delete</code> odpowiada wersji operatora <code>new</code> z taką sama | <code>operator delete</code> odpowiada wersji operatora <code>new</code> z taką sama listą argumentów. Jeśli lista argumentów jest niepusta, to | ||
taki operator nazywamy placement delete. | taki operator nazywamy placement delete. | ||
Przy wywoływaniu placement delete, | Przy wywoływaniu placement delete, przekazywana mu jest lista dodatkowych | ||
argumentów, identyczna z listą dodatkowych argumentów operatora placement new | argumentów, identyczna z listą dodatkowych argumentów operatora placement new, | ||
który pamięć przydzielił. | który pamięć przydzielił. | ||
Linia 161: | Linia 155: | ||
<code>delete</code>, odpowiadających trzem wspomnianym powyżej operatorom | <code>delete</code>, odpowiadających trzem wspomnianym powyżej operatorom | ||
<code>new</code>, ale można też dodawać własne definicje, zarówno w klasie jak | <code>new</code>, ale można też dodawać własne definicje, zarówno w klasie jak | ||
i w | i w przestrzeni globalnej. Jeśli kompilator nie znajdzie żadnej | ||
odpowiedniej definicji <code>operator delete</code>, to żadna funkcja | odpowiedniej definicji <code>operator delete</code>, to żadna funkcja | ||
zwalniająca nie zostanie wywołana. | zwalniająca nie zostanie wywołana. | ||
Rozważmy następujący przykład | Rozważmy następujący przykład: | ||
struct X { | struct X { | ||
Linia 171: | Linia 165: | ||
void *operator delete(void *p) throw(); | void *operator delete(void *p) throw(); | ||
} | } | ||
Wyrażenie | Wyrażenie: | ||
try { | try { | ||
Linia 178: | Linia 172: | ||
} | } | ||
catch(int){}; | catch(int){}; | ||
spowoduje wywołanie | spowoduje wywołanie: | ||
void *tmp<nowiki>=</nowiki>X::operator new(sizeof(X)); | void *tmp<nowiki>=</nowiki>X::operator new(sizeof(X)); | ||
X::operator delete(tmp); | X::operator delete(tmp); | ||
Dodanie do klasy <code>X</code> dwu operatorów | Dodanie do klasy <code>X</code> dwu operatorów: | ||
void *operator new(size_t,double) throw(std::bad_alloc); | void *operator new(size_t,double) throw(std::bad_alloc); | ||
void *operator delete(void *p,double) throw(); | void *operator delete(void *p,double) throw(); | ||
spowoduje, że wyrażenie | spowoduje, że wyrażenie: | ||
try { | try { | ||
Linia 195: | Linia 189: | ||
} | } | ||
catch(int){}; | catch(int){}; | ||
wywoła | wywoła: | ||
void *tmp<nowiki>=</nowiki>X::operator new(sizeof(X),3.14); | void *tmp<nowiki>=</nowiki>X::operator new(sizeof(X),3.14); | ||
X::operator delete(tmp,3.14); | X::operator delete(tmp,3.14); | ||
Tę logikę zaburza trochę fakt istnienia wyróżnionej wersji funkcji | Tę logikę zaburza trochę fakt istnienia wyróżnionej wersji funkcji | ||
<code>operator delete</code>. Są to składowe klas posiadające drugi parametr | <code>operator delete</code>. Są to składowe klas posiadające drugi parametr | ||
Linia 206: | Linia 200: | ||
void X::operator delete(void *p,size_t size); | void X::operator delete(void *p,size_t size); | ||
Jeśli klasa nie posiada jednoargumentowego operatora <code>delete</code>, to | Jeśli klasa nie posiada jednoargumentowego operatora <code>delete</code>, to | ||
powyższy operator jest traktowany jak jednoargumentowy (non | powyższy operator jest traktowany jak jednoargumentowy (non | ||
Linia 217: | Linia 211: | ||
void *operator delete(void *p,size_t) throw(); | void *operator delete(void *p,size_t) throw(); | ||
} | } | ||
Wyrażenie | Wyrażenie: | ||
try { | try { | ||
Linia 224: | Linia 218: | ||
} | } | ||
catch(int){}; | catch(int){}; | ||
wywoła | wywoła: | ||
void *tmp<nowiki>=</nowiki>X::operator new(sizeof(X)); | void *tmp<nowiki>=</nowiki>X::operator new(sizeof(X)); | ||
X::operator delete(tmp,sizeof(X)); | X::operator delete(tmp,sizeof(X)); | ||
a wyrażenie | a wyrażenie: | ||
try { | try { | ||
Linia 236: | Linia 230: | ||
} | } | ||
catch(int){}; | catch(int){}; | ||
wywoła | wywoła: | ||
void *tmp<nowiki>=</nowiki>X::operator new(sizeof(X),3); | void *tmp<nowiki>=</nowiki>X::operator new(sizeof(X),3); | ||
X::operator delete(tmp,3); | X::operator delete(tmp,3); | ||
Proszę zwrócić uwagę na różnicę w wartości drugiego argumentu przekazanego | Proszę zwrócić uwagę na różnicę w wartości drugiego argumentu przekazanego | ||
do <code>operator delete</code>. | do <code>operator delete</code>. | ||
==delete== | ==delete== | ||
Linia 250: | Linia 244: | ||
delete p; | delete p; | ||
które najpierw wywołuje destruktor klasy <code>X</code>: | które najpierw wywołuje destruktor klasy <code>X</code>: | ||
p-> X(); | p-> ~X(); | ||
Jeśli to wywołanie się | Jeśli to wywołanie się nie powiedzie (zostanie rzucony wyjątek) to mamy | ||
kłopot, bo nie zostanie wywołany <code>operator delete</code> w celu | kłopot, bo nie zostanie wywołany <code>operator delete</code> w celu | ||
zwolnienia pamięci. Jest to kolejny powód aby nie rzucać wyjątków z | zwolnienia pamięci. Jest to kolejny powód aby nie rzucać wyjątków z | ||
Linia 264: | Linia 258: | ||
char a[100000]; | char a[100000]; | ||
public: | public: | ||
X() {throw 0;} | ~X() {throw 0;} | ||
}; | }; | ||
Linia 276: | Linia 270: | ||
} | } | ||
} | } | ||
Jeśli jednak nic złego się nie wydarzy, to po | Jeśli jednak nic złego się nie wydarzy, to po wywołaniu destruktora | ||
przydzielona pamięć zostaje zwolniona za pomocą funkcji <code>operator | przydzielona pamięć zostaje zwolniona za pomocą funkcji <code>operator delete()</code>. O zwalnianiu pamięci dużo już napisałem przy omawianiu wyrażenia <code>new</code>. W przypadku wyrażenia <code>delete</code> dzieje się to jednak trochę inaczej. Wyrażenie <code>delete</code> używa do zwolnienia pamięci tylko funkcji <code>operator delete()</code> niebędących typu | ||
wyrażenia <code>new</code>. W przypadku wyrażenia <code>delete</code> dzieje się to | |||
jednak trochę inaczej. Wyrażenie <code>delete</code> używa do zwolnienia | |||
pamięci tylko funkcji <code>operator delete()</code> | |||
placement, tzn. posiadające jeden lub ewentualnie dwa argumenty: | placement, tzn. posiadające jeden lub ewentualnie dwa argumenty: | ||
void operator delete(void p) throw(); | void operator delete(void p) throw(); | ||
void operator delete(void p,size_t) throw(); | void operator delete(void p,size_t) throw(); | ||
Jest to niezależne od tego | Jest to niezależne od tego jaki <code>operator new</code> został użyty do | ||
przydzielenia pamięci. Czyli jeśli zdefiniujemy np: | przydzielenia pamięci. Czyli jeśli zdefiniujemy, np.: | ||
struct X { | struct X { | ||
Linia 299: | Linia 289: | ||
void *operator delete(void *p,double) throw(); | void *operator delete(void *p,double) throw(); | ||
} | } | ||
to wyrażenia: | to wyrażenia: | ||
Linia 308: | Linia 298: | ||
delete p2; | delete p2; | ||
delete p1; | delete p1; | ||
spowodują wywołanie: | |||
void *tmp1 <nowiki>=</nowiki> X::operator new(sizeof(X)); | void *tmp1 <nowiki>=</nowiki> X::operator new(sizeof(X)); | ||
Linia 317: | Linia 307: | ||
X::operator delete(tmp2,sizeof(X)); | X::operator delete(tmp2,sizeof(X)); | ||
X::operator delete(tmp1,sizeof(X)); | X::operator delete(tmp1,sizeof(X)); | ||
==operator new== | ==operator new== | ||
Linia 331: | Linia 321: | ||
void* operator new(std::size_t size) throw(std::bad_alloc); | void* operator new(std::size_t size) throw(std::bad_alloc); | ||
Jeśli wszystko pójdzie dobrze, to <code>operator new</code> zwraca wskaźnik | Jeśli wszystko pójdzie dobrze, to <code>operator new</code> zwraca wskaźnik | ||
do obszaru pamięci o rozmiarze co najmniej <code>size</code> | do obszaru pamięci o rozmiarze co najmniej <code>size</code>; jeśli przydział | ||
się nie powiedzie, to rzuca wyjątek <code>std::bad_alloc</code>. | się nie powiedzie, to rzuca wyjątek <code>std::bad_alloc</code>. | ||
Dokładniej rzecz | Dokładniej rzecz biorąc <code>operator new</code> rzuca wyjątek tylko wtedy | ||
jeśli nie ustawiona jest funkcja obsługi | jeśli nie ustawiona jest funkcja obsługi błędów. Do jej ustawiania | ||
służy funkcja: | służy funkcja: | ||
Linia 344: | Linia 334: | ||
new_hadler set_new_handler(new_handler f); | new_hadler set_new_handler(new_handler f); | ||
} | } | ||
Funkcja <code>set_new_handler</code> ustawia nową funkcję obsługi | Funkcja <code>set_new_handler</code> ustawia nową funkcję obsługi błędów i | ||
zwraca wskaźnik do poprzedniej funkcji obsługi lub <code>null</code>, jeśli | zwraca wskaźnik do poprzedniej funkcji obsługi lub <code>null</code>, jeśli | ||
funkcja nie była ustawiona. Przekazanie wskaźnika <code>null</code> jako | funkcja nie była ustawiona. Przekazanie wskaźnika <code>null</code> jako | ||
argumentu powoduje że nie będzie ustawiona żadna funkcja obsługi. To co | argumentu powoduje, że nie będzie ustawiona żadna funkcja obsługi. To co | ||
się dzieje wewnątrz <code>operator new</code> wygląda mniej wiecej tak: | się dzieje wewnątrz <code>operator new</code> wygląda mniej wiecej tak: | ||
Linia 361: | Linia 351: | ||
throw std::bad_alloc(); | throw std::bad_alloc(); | ||
} | } | ||
Funkcja <code>handler</code> | Funkcja <code>handler</code> musi więc uzyskać więcej pamięci, rzucić | ||
wyjątek albo przerwać program | wyjątek albo przerwać program. Może też ustawić inną funkcję obsługi, | ||
inaczej program będzie się wykonywał w niekończącej się pętli. | inaczej program będzie się wykonywał w niekończącej się pętli. | ||
Linia 371: | Linia 361: | ||
standardowej to wersja <code>no_throw</code>. Operator <code>new</code> nie musi | standardowej to wersja <code>no_throw</code>. Operator <code>new</code> nie musi | ||
rzucać wyjątku w razie niepowodzenia, ale musi wtedy zwrócić wskaźnik | rzucać wyjątku w razie niepowodzenia, ale musi wtedy zwrócić wskaźnik | ||
zerowy (null). Aby wywołać | zerowy (null). Aby wywołać tę wersję operatora <code>new</code> korzystamy | ||
z tego że posiada ona drugi argument typu <code>nothrow_t</code>. | z tego, że posiada ona drugi argument typu <code>nothrow_t</code>. | ||
void* operator new(std::size_t size, | void* operator new(std::size_t size, | ||
const std::nothrow_t&) throw(); | const std::nothrow_t&) throw(); | ||
W tym celu zdefiniowana została globalna stała typu <code>std::nothrow_t</code>: | W tym celu zdefiniowana została globalna stała typu <code>std::nothrow_t</code>: | ||
Linia 383: | Linia 373: | ||
extern const nothrow_t nothrow; | extern const nothrow_t nothrow; | ||
} | } | ||
Wersję <code>nothrow</code> używamy więc następująco: | Wersję <code>nothrow</code> używamy więc następująco: | ||
X *p<nowiki>=</nowiki>new (std::nothrow) X; | X *p<nowiki>=</nowiki>new (std::nothrow) X; | ||
if(!p) { | if(!p) {...}; | ||
==operator delete== | ==operator delete== | ||
Linia 395: | Linia 385: | ||
void operator delete(void* ptr) throw(); | void operator delete(void* ptr) throw(); | ||
Może to być wskaźnik zerowy, wtedy <code>operator new</code> nic nie robi. | Może to być wskaźnik zerowy, wtedy <code>operator new</code> nic nie robi. | ||
Operator <code>delete</code> nie rzuca wyjątków. | Operator <code>delete</code> nie rzuca wyjątków. | ||
Linia 401: | Linia 391: | ||
void operator delete(void* ptr) throw(); | void operator delete(void* ptr) throw(); | ||
zachowuje się, w większości przypadków | zachowuje się, w większości przypadków jak wersja jednoargumentowa i | ||
drugi argument zostaje automatycznie inicjalizowany rozmiarem zwalnianej | drugi argument zostaje automatycznie inicjalizowany rozmiarem zwalnianej | ||
pamięci. | pamięci. | ||
==Tablice== | ==Tablice== | ||
W powyższej dyskusji ograniczyłem się do tworzenia pojedynczych obiektów. | |||
C++ zezwala na tworzenie tablic obiektów: | |||
X *px <nowiki> =</nowiki> new X[10]; | |||
Powyższe wyrażenie przydziela pamięć na 10 obiektów klasy <code>X</code> i | |||
tworzy je za pomocą konstruktorów standardowych. | |||
W przypadku niepowodzenia konstrukcji niszczy skonstruowane obiekty (jeśli takowe istnieją) i zwalnia pamięć. Alokacja gołej pamięci jest dokonywana poprzez: | |||
void * operator new[] {size_t); | |||
i zwalniana za pomocą: | |||
void * operator new[] {size_t); | |||
==Przeładowywanie operatorów new i delete== | ==Przeładowywanie operatorów new i delete== | ||
Linia 415: | Linia 420: | ||
jakiejś klasy. Globalny alokator pamięci to poważna sprawa: dotyczy | jakiejś klasy. Globalny alokator pamięci to poważna sprawa: dotyczy | ||
działania całego programu i musi przydzielać pamięć dowolnych | działania całego programu i musi przydzielać pamięć dowolnych | ||
rozmiarów. Trudno będzie pod tym | rozmiarów. Trudno będzie pod tym względem pobić działanie standardowej | ||
wersji <code>operator new</code>. Dlatego | wersji <code>operator new</code>. Dlatego częściej będziemy chcieli definiować | ||
operatory | operatory <code>new</code> we własnych klasach. | ||
Na co musimy zwrócić w takim przypadku uwagę? Mam | Na co musimy zwrócić w takim przypadku uwagę? Mam | ||
nadzieję, że przekonałem | nadzieję, że przekonałem Państwa, że do każdego operatora <code>new</code> | ||
należy dopisać odpowiednią wersję operatora <code>delete</code>, inaczej nie | należy dopisać odpowiednią wersję operatora <code>delete</code>, inaczej nie | ||
będziemy w stanie zapewnić bezpiecznego zachowania | będziemy w stanie zapewnić bezpiecznego zachowania w sytuacji, w | ||
której zostanie rzucony wyjątek z konstruktora. | której zostanie rzucony wyjątek z konstruktora. | ||
Musimy też uważać na jawne zwalnianie pamięci | Musimy też uważać na jawne zwalnianie pamięci. Jeśli w jednej klasie | ||
zdefiniujemy kilka operatorów placement <code>new</code>, to nie będziemy | zdefiniujemy kilka operatorów placement <code>new</code>, to nie będziemy | ||
mogli ich rozróżnić w poleceniu <code>delete</code>!. | mogli ich rozróżnić w poleceniu <code>delete</code>!. | ||
Musimy też zadbać | Musimy też zadbać aby <code>operator new</code> albo rzucał wyjątek, albo | ||
zwracał wskaźnik pusty w razie | zwracał wskaźnik pusty w razie niemożności przydziału pamięci. W | ||
innym przypadku wyrażenie <code>new</code> nie rozpozna, że przydział pamięci | innym przypadku wyrażenie <code>new</code> nie rozpozna, że przydział pamięci | ||
się nie powiódł i będzie | się nie powiódł i będzie próbować tworzyć obiekt w nieprzydzielonej | ||
pamięci. | pamięci. | ||
Linia 438: | Linia 443: | ||
implementować i jeśli robimy to tylko na własny użytek, to pewnie nic | implementować i jeśli robimy to tylko na własny użytek, to pewnie nic | ||
złego się nie stanie. Ale jeśli <code>operator new</code> jest częścią | złego się nie stanie. Ale jeśli <code>operator new</code> jest częścią | ||
zewnętrznego interfejsu klasy, to prędzej czy | zewnętrznego interfejsu klasy, to prędzej czy później któryś z | ||
użytkowników | użytkowników może się postarać skorzystać z <code>set_new_handler()</code>. | ||
W końcu jeżeli nazywamy naszą funkcję <code>operator new</code>, a nie np. | W końcu jeżeli nazywamy naszą funkcję <code>operator new</code>, a nie np. | ||
<code>allocate()</code>, to sugerujemy, że będzie ona miała funkcjonalność | <code>allocate()</code>, to sugerujemy, że będzie ona miała funkcjonalność | ||
<code>operator new</code>. Zwykle robimy to | <code>operator new</code>. Zwykle robimy to po to, aby skorzystać z | ||
istniejącego już kodu, który używa wyrażeń <code>new</code>. Jeśli chcemy | |||
tworzyć obiekty i przydzielać | tworzyć obiekty i przydzielać pamięć, ale nie zależy nam na | ||
interfejsie <code>new</code>, lepiej nazwać nasze alokatory inaczej. | interfejsie <code>new</code>, lepiej nazwać nasze alokatory inaczej. | ||
==Memory pool== | ==Memory pool== | ||
Linia 452: | Linia 457: | ||
operatora <code>new</code> pojawia się, gdy chcemy uzyskać wydajność lepszą | operatora <code>new</code> pojawia się, gdy chcemy uzyskać wydajność lepszą | ||
niż oferowana przez standardowy <code>operator new</code>. Rozważmy więc | niż oferowana przez standardowy <code>operator new</code>. Rozważmy więc | ||
sytuację, kiedy używamy wielu małych przedmiotów | sytuację, kiedy używamy wielu małych przedmiotów o takim samym | ||
rozmiarze. Typowy przykład to inteligentne wskaźniki. W jaki więc | rozmiarze. Typowy przykład to inteligentne wskaźniki. W jaki więc | ||
sposób moglibyśmy wydajnie przydzielać i zwalniać pamięć dla takich obiektów? | sposób moglibyśmy wydajnie przydzielać i zwalniać pamięć dla takich obiektów? | ||
Jednym z prostszych sposobów jest przydzielenie | Jednym z prostszych sposobów jest przydzielenie za pomocą | ||
stadardowego alokatora | stadardowego alokatora pewnej ilości pamięci, a następnie przydzielanie | ||
z niej po kawałku pamięci na pojedyncze obiekty. | z niej po kawałku pamięci na pojedyncze obiekty. | ||
Dokładniej wygląda to tak (zob. | {{kotwica|am.14.1|}}[[File:cpp-12-pool2.mp4|250x250px|thumb|right|Animacja 14.1. Działanie zasobnika pamięci.]] | ||
obszar pamięci mogący | |||
rozmiaru. Na początku | Dokładniej wygląda to tak (zob. [[#am.14.1|animacja 14.1]]): przydzielamy | ||
obszar pamięci mogący pomieścić <code>N</code> kawałków żądanego przez nas | |||
rozmiaru. Na początku wszystkie te kawałki łączymy w listę. Ponieważ na | |||
liście bedą się znajdowały tylko kawałki nieprzydzielonej pamięci, | liście bedą się znajdowały tylko kawałki nieprzydzielonej pamięci, | ||
możemy umieścić wskaźnik do następnego kawałka na liście | możemy umieścić wskaźnik do następnego kawałka na liście w samym | ||
kawałku. Nasz alokator nie ma więc żadnego narzutu pamięci | kawałku. Nasz alokator nie ma więc żadnego narzutu pamięci poza | ||
wskaźnikiem do pierwszego elementu listy (głowa). | wskaźnikiem do pierwszego elementu listy (głowa). | ||
Kiedy potrzebujemy przydzielić | Kiedy potrzebujemy przydzielić pamięć, to usuwamy z listy jej pierwszy | ||
element i zwracamy wskaźnik do niego. Kiedy chcemy zwolnić pamięć | element i zwracamy wskaźnik do niego. Kiedy chcemy zwolnić pamięć | ||
przyłączamy zwalniany kawałek na początku listy. Obie te operacje są | przyłączamy zwalniany kawałek na początku listy. Obie te operacje są | ||
bardzo szybkie. Jeśli mamy wystarczająco dużo pamięci, możemy | bardzo szybkie. Jeśli mamy wystarczająco dużo pamięci, możemy | ||
zrezygnować ze zwalniania jej pojedynczo, tylko zwolnić cały obszar | zrezygnować ze zwalniania jej pojedynczo, tylko zwolnić cały obszar naraz, gdy nie będzie nam już potrzebny. | ||
Kiedy będziemy potrzebowali więcej kawałków niż może ich pomieścić | Kiedy będziemy potrzebowali więcej kawałków niż może ich pomieścić | ||
Linia 483: | Linia 485: | ||
Napisanie klasy obsługującej taki schemat pozostawiam jako ćwiczenie. | Napisanie klasy obsługującej taki schemat pozostawiam jako ćwiczenie. | ||
Tutaj wykorzystam gotową klasę <code>pool</code> dostarczaną w bibliotece | Tutaj wykorzystam gotową klasę <code>pool</code> dostarczaną w bibliotece <code>boost::pool</code>. | ||
Ewidentnie taki schemat nie nadaje się do implementacji globalnej wersji | Ewidentnie taki schemat nie nadaje się do implementacji globalnej wersji | ||
<code>operator new</code>, która przydziela pamięć dowolnych rozmiarów. | <code>operator new</code>, która przydziela pamięć dowolnych rozmiarów. | ||
Idealnie pasuje jednak do klasowego operatora <code>new</code>. | Idealnie pasuje jednak do klasowego operatora <code>new</code>. | ||
Deklarujemy więc | Deklarujemy więc: | ||
#include<boost/pool/pool.hpp> | #include<boost/pool/pool.hpp> | ||
struct X { | struct X { | ||
int _val; | int _val; | ||
Linia 501: | Linia 502: | ||
void operator delete(void *) throw(); | void operator delete(void *) throw(); | ||
}; | }; | ||
([[media:X_new.h | Źródło: X_new.h]]) | |||
Składowa <code>pool</code> jest składową statyczną, ponieważ musi istnieć | Składowa <code>pool</code> jest składową statyczną, ponieważ musi istnieć | ||
niezależnie od obiektów klasy. Podobnie operatory <code>new</code> i | niezależnie od obiektów klasy. Podobnie operatory <code>new</code> i | ||
Linia 509: | Linia 511: | ||
boost::pool<> X::pool(sizeof(X)); | boost::pool<> X::pool(sizeof(X)); | ||
która tworzy obiekt <code>X::pool</code> służący do przydzielania pamięci w kawałkach | która tworzy obiekt <code>X::pool</code> służący do przydzielania pamięci w kawałkach | ||
po <code>sizeof(X)</code> bajtów. | po <code>sizeof(X)</code> bajtów. | ||
Linia 527: | Linia 529: | ||
} | } | ||
} | } | ||
([[media:X_new.cpp | Źródło: X_new.cpp]]) | |||
Sam przydział pamięci jest najłatwiejszy: korzystamy z gotowej | Sam przydział pamięci jest najłatwiejszy: korzystamy z gotowej | ||
funkcji <code>malloc()</code> z klasy <code>boost::pool<></code>. Reszta kodu | funkcji <code>malloc()</code> z klasy <code>boost::pool<></code>. Reszta kodu | ||
Linia 540: | Linia 543: | ||
pool.free(p); | pool.free(p); | ||
} | } | ||
==Alokatory== | ==Alokatory== | ||
Trudno omawiać zarządzanie pamięcią | Trudno omawiać zarządzanie pamięcią w wykładzie dotyczącym | ||
programowania uogólnionego i nie wspomnieć o alokatorach STL. W | programowania uogólnionego i nie wspomnieć o alokatorach STL. W | ||
poprzedniej części wykładu używałem słowa alokator na określenie | poprzedniej części wykładu używałem słowa alokator na określenie | ||
każdej funkcji przydzielającej | każdej funkcji przydzielającej pamięć. W STL alokator jest konceptem i | ||
oznacza klasy, których obiekty służą do przydzielania pamięci dla | oznacza klasy, których obiekty służą do przydzielania pamięci dla | ||
standardowych kontenerów. Biblioteka C++ dostarcza standardową | standardowych kontenerów. Biblioteka C++ dostarcza standardową | ||
implementację | implementację szablonu alokatora, której konkretyzacje przekazywane są | ||
jako domyślny drugi lub trzeci argument szablonów kontenerów: | jako domyślny drugi lub trzeci argument szablonów kontenerów: | ||
Linia 560: | Linia 563: | ||
class set; | class set; | ||
} | } | ||
Dlatego | Dlatego | ||
można używać kontenerów | można używać kontenerów i nie wiedzieć nawet, że alokatory istnieją. | ||
Wymagane elementy szablonu kontenera opiszę na przykładzie możliwej | Wymagane elementy szablonu kontenera opiszę na przykładzie możliwej | ||
implementacji alokatora standardowego | implementacji alokatora standardowego allocator.h: | ||
template <class T> class allocator {...}; | |||
Alokator jest szablonem przyjmujacym jako argument typ obiektów, dla | |||
których będzie przydzielał pamięć. Ponieważ parametrem szablonu | |||
Alokator jest szablonem | |||
których będzie | |||
kontenera jest typ, a nie szablon, można by sądzić, że alokator klasą | kontenera jest typ, a nie szablon, można by sądzić, że alokator klasą | ||
być nie musi, bo możemy tworzyć konkretne alokatory dla konkretnych | być nie musi, bo możemy tworzyć konkretne alokatory dla konkretnych | ||
typów np.: | typów, np.: | ||
vector<int,alokator_int> v; | vector<int,alokator_int> v; | ||
Alokator musi jednak być szablonem, bo wewnątrz kontenera może zajść | Alokator musi jednak być szablonem, bo wewnątrz kontenera może zajść | ||
potrzeba przydzielenia pamięci dla obiektu innego typu niż typ | potrzeba przydzielenia pamięci dla obiektu innego typu niż typ | ||
przechowywany <code>T</code>. Dzieje się tak | przechowywany <code>T</code>. Dzieje się tak w przypadku kontenerów opartych na | ||
węzłach, takich jak listy czy kontenery asocjacyjne. Taki kontener | |||
musi przydzielić pamięć na | musi przydzielić pamięć na węzęł, a nie na element typu <code>T</code>. Żeby | ||
móc to zrobić alokator posiada wewnętrzną strukturę: | móc to zrobić alokator posiada wewnętrzną strukturę: | ||
template <class U> | template <class U> | ||
struct rebind { typedef pool_allocator | struct rebind { typedef pool_allocator U other; }; | ||
Kontener może z niej korzystać następująco: | Kontener może z niej korzystać następująco: | ||
typedef typename allocator<T>::rebind<node<T> >::other node_allocator; | typedef typename allocator<T>::rebind<node<T> >::other node_allocator; | ||
Obiekty klasy <code>node_allocator</code> przydzielają pamięć na obiekty klasy | Obiekty klasy <code>node_allocator</code> przydzielają pamięć na obiekty klasy | ||
<code>node<T></code>, a nie <code>T</code>. | <code>node<T></code>, a nie <code>T</code>. | ||
Linia 605: | Linia 607: | ||
typedef size_t size_type; | typedef size_t size_type; | ||
typedef size_t difference_type; | typedef size_t difference_type; | ||
i operator | i operator zwracający adres elementu typu <code>T</code>: | ||
pointer address(reference x) const { return &x; } | pointer address(reference x) const { return &x; } | ||
Linia 612: | Linia 614: | ||
return x; | return x; | ||
}; | }; | ||
Miało to umożliwić używanie | Miało to umożliwić używanie | ||
niestandardowych typów | niestandardowych typów wskaźnikowych i referencyjnych. W praktyce te | ||
typy i funkcje muszą być zdefiniowane dokładnie tak jak powyżej (zob. | typy i funkcje muszą być zdefiniowane dokładnie tak jak powyżej (zob. | ||
S. Meyers <i>"STL w praktyce. 50 sposobów efektywnego wykorzystania"</i> oraz N.M. Josuttis <i>"C++ Biblioteka Standardowa. Podręcznik programisty"</i>). | |||
Kontener używa obiektów klasy <code>allocator</code>, wobec czego musimy mieć | Kontener używa obiektów klasy <code>allocator</code>, wobec czego musimy mieć | ||
możność tworzenia i | możność tworzenia i niszczenia ich: | ||
pool_allocator() {} | pool_allocator() {} | ||
pool_allocator(const pool_allocator&) {} | pool_allocator(const pool_allocator&) {} | ||
pool_allocator() {} | ~pool_allocator() {} | ||
Ważnym ograniczeniem narzuconym przez standard C++ jest wymaganie, aby | Ważnym ograniczeniem narzuconym przez standard C++ jest wymaganie, aby | ||
każde dwa obiekty alokatora tej samej klasy były równoważne. | każde dwa obiekty alokatora tej samej klasy były równoważne. | ||
Rownoważność oznacza, że pamięc przydzielona przez jeden obiekt | Rownoważność oznacza, że pamięc przydzielona przez jeden obiekt | ||
alokatora | alokatora może być zwolniona przez drugi. W naszym przypadku alokator | ||
nie posiada żadnego stanu i jego konstruktory i destruktor nic nie | nie posiada żadnego stanu i jego konstruktory i destruktor nic nie | ||
robią, zatem ten warunek jest spełniony. Alokator nie musi posiadać | |||
operatora przypisania, więc uniemożliwimy jego użycie: | operatora przypisania, więc uniemożliwimy jego użycie: | ||
private: | private: | ||
void operator<nowiki>=</nowiki>(const pool_allocator&); | void operator<nowiki>=</nowiki>(const pool_allocator&); | ||
Dochodzimy wreszcie do funkcji, które zarządzają | Dochodzimy wreszcie do funkcji, które zarządzają pamięcią. Funkcja: | ||
pointer allocate(size_type n, const_pointer <nowiki>=</nowiki> 0) { | pointer allocate(size_type n, const_pointer <nowiki>=</nowiki> 0) { | ||
return static_cast<pointer>(::operator new(n)); | return static_cast<pointer>(::operator new(n)); | ||
}; | }; | ||
przydziela pamięć dla <code>n</code> elementów typu <code>T</code>. Pamięć nie jest | |||
inicjalizowana. Proszę zwrócić uwagę, że w przeciwieństwie do | inicjalizowana. Proszę zwrócić uwagę, że w przeciwieństwie do | ||
<code>operator new</code> czy <code>malloc</code>, <code>allocate</code> zwraca wskaźnik | <code>operator new</code> czy <code>malloc</code>, <code>allocate</code> zwraca wskaźnik | ||
<code>T *</code>, a nie <code>void *</code>. Drugi | <code>T *</code>, a nie <code>void *</code>. Drugi parametr funkcji <code>allocate</code> może | ||
być użyty przez bardziej wyrafinowane schematy przydziału pamięci. | być użyty przez bardziej wyrafinowane schematy przydziału pamięci. | ||
Tworzeniem obiektu wewnątrz przydzielonej pamięci zajmuje się funkcja | Tworzeniem obiektu wewnątrz przydzielonej pamięci zajmuje się funkcja | ||
Linia 652: | Linia 654: | ||
new(p) value_type(x); | new(p) value_type(x); | ||
} | } | ||
Funkcja | korzystająca ze standardowego placement <code>new</code>. Funkcja: | ||
void deallocate(pointer p, size_type n) throw() { | void deallocate(pointer p, size_type n) throw() { | ||
::operator delete(p); | ::operator delete(p); | ||
} | } | ||
zwalnia pamięć wskazywaną przez | zwalnia pamięć wskazywaną przez wskaźnik <code>p</code>. | ||
Funkcja <code>deallocate()</code> nie wywołuje destruktora. Robi to funkcja | Funkcja <code>deallocate()</code> nie wywołuje destruktora. Robi to funkcja: | ||
void destroy(pointer p) { | void destroy(pointer p) { | ||
p-> value_type(); | p-> ~value_type(); | ||
} | } | ||
Na koniec została pomocnicza funkcja: | Na koniec została pomocnicza funkcja: | ||
Linia 673: | Linia 673: | ||
return static_cast<size_type>(-1) / sizeof(T); | return static_cast<size_type>(-1) / sizeof(T); | ||
} | } | ||
która zwraca największą wartość | która zwraca największą wartość możliwą do przekazania do funkcji | ||
<code>allocate</code>. Nie oznacza to jednak, że przydział tej pamięci musi | <code>allocate</code>. Nie oznacza to jednak, że przydział tej pamięci musi | ||
się powieść. | się powieść. | ||
Koncept alokatora wymaga jeszcze dwu operatorów testujących równoważność | Koncept alokatora wymaga jeszcze dwu operatorów testujących równoważność | ||
obiektów alokatora. Ponieważ kontenery wymagają aby każde dwa obiekty | obiektów alokatora. Ponieważ kontenery wymagają, aby każde dwa obiekty | ||
były równoważne, te operatory zdefiniowane są następująco: | były równoważne, te operatory zdefiniowane są następująco: | ||
Linia 693: | Linia 693: | ||
return false; | return false; | ||
} | } | ||
Na koniec zabezpieczmy się jeszcze tylko na wypadek możliwości | Na koniec zabezpieczmy się jeszcze tylko na wypadek możliwości | ||
skonkretyzowania szablonu <code>allocator<void></code> poprzez odpowiednią | skonkretyzowania szablonu <code>allocator<void></code> poprzez odpowiednią | ||
Linia 704: | Linia 704: | ||
template <class U> | template <class U> | ||
struct rebind { typedef allocator<U> other; }; | struct rebind { typedef allocator<nowiki><</nowiki>U> other; }; | ||
}; | }; | ||
Aktualna wersja na dzień 13:43, 3 paź 2021
Wstęp
Dynamiczna alokacja pamięci to bardzo ważny element języka C. W C do
przydziału i zwolnienia pamięci służą odpowiedno funkcje malloc
(i jego "kuzyni") i free
. W C++ są one również dostępne, ale
używane są raczej wyrażenia
new
i delete
. Ta zmiana ma poważną przyczynę: te wyrażenia
robią więcej niż tylko przydzielanie lub zwalnianie pamięci. Wyrażenie
new
tworzy nowy obiekt, a więc nie tylko przydziela pamięc, ale
również inicjalizuje go, używając odpowiedniego konstruktora. Wyrażenie
delete
niszczy obiekt, wywołując jego destruktor i dopiero potem
zwalnia zajętą przez niego pamięć. W tym wykładzie pokażę, co tak
naprawdę się dzieje, gdy dynamicznie tworzymy lub niszczymy obiekty.
Wyrażenia new
i delete
posługują się systemowymi alokatorami
i dealokatorami pamięci. C++ daje nam możliwość
wykorzystania w tym celu własnych implementacji.
Napisanie jednak bardziej wydajnego alokatora
pamięci niż alokator standardowy nie jest łatwe.
Można jednak próbować zwiększyć wydajność przydzielania i zwalniania
pamięci w sytuacjach szczególnych, np. jeśli
używamy dużej ilości małych obiektów o stałym rozmiarze, które muszą
być dynamicznie tworzone i niszczone. Pod koniec wykładu podamy prosty
schemat obsługi pamięci mający zastosowanie w takiej sytuacji.
new
Przyjrzyjmy się najpierw dokładnie procesowi tworzenia pojedynczego, nowego
obiektu za pomocą wyrażenia new
:
X *p = new X inicjalizator;
lub
X *p = new (lista_argumentow) X inicjalizator;
Druga forma jest nazywana z przyczyn historycznych "placement new"
(pochodzenie tej nazwy wyjaśnię poniżej); inicjalizator
może być dowolnym wyrażeniem inicjalizującym, np.:
X *p1 = new X; X *p2 = new X(); X x,y; X *p3 = new X = x ; X *p4 = new X(y); X *p5 = new X(0);
Oczywiście zakładamy istnienie odpowiednich konstruktorów.
Przydział pamięci
Najpierw przydzielana jest "goła" (raw) pamięć. Służy do tego
funkcja przydziału pamięci (alokator) operator new()
:
void *tmp = operator new(sizeof(X));
lub
void *tmp = operator new(sizeof(X),lista_argumentow);
jeśli użyliśmy formy placement. Nazwa placement pochodzi od operatora
new
dostarczanego w bibliotece standardowej, który przyjmuje drugi
argument typu void *
:
void* operator new(std::size_t size, void* ptr) throw() {return ptr;};
Operator ten nie przydziela żadnej pamięci tylko zwraca wskaźnik
ptr
. Jego wywołanie nie może się nie powieść, dlatego nie rzuca
żadnych wyjątków. Ta forma operatora służy do
umieszczania (placement) obiektu w
zadanym obszarze pamięci:
void *p =malloc(sizeof(X)); X *px=new (p) X;
stąd jego nazwa.
Środowisko C++ dostarcza jeszcze dwu wersji globalnych funkcji
operator new()
:
void* operator new(std::size_t size) throw(std::bad_alloc) ; void* operator new(std::size_t size, std::nothrow_t) throw();
ale użytkownik może podać własne definicje, zarówno
globalne, jak i dla pojedynczych klas. Odpowiednia funkcja operator new
jest najpierw wyszukiwana w klasie X
, a następnie w
przestrzeni globalnej. Jeśli nie znajdzie się definicja odpowiadająca
podanym argumentom, to wystąpi błąd kompilacji. Np. jeśli zażądamy
stworzenia obiektu wyrażeniem:
X *p = new X;
to kompilator będzie szukał funkcji:
X::operator new(size_t);
a w drugiej kolejności:
void *tmp = ::operator new(sizeof(X));
Wyrażenie
X *p = new (3.15) X;
spowoduje poszukiwanie funkcji:
X::operator new(size_t,double);
lub
::operator new(size_t,double);
Pierwszy argument każdej funkcji operator new
musi być typu
size_t
i przekazywany jest przez niego rozmiar żądanego obszaru
pamięci.
Każda funkcja operator new
zwraca void *
. W przypadku
powodzenia zwracany jest wskaźnik do przydzielonego obszaru pamięci:
void *p = operator new(1000);
W przypadku niepowodzenia operator new
może rzucić wyjątek
std::bad_alloc
lub zwrócić wskaźnik zerowy.
Tworzenie obiektu
Jeśli przydział pamięci powiedzie się, tzn. operator new
zwróci
niezerowy wskaźnik, to następuje wywołanie konstruktora klasy X
w celu stworzenia obiektu,
który jest umieszczany w przydzielonej pamięci. Np:
X *p = X(1);
spowoduje wywołanie konstruktora:
X::X(int);
jeśli takowy istnieje. Jeśli konstrukcja się powiedzie (nie rzuci
wyjątku), to proces się kończy. Jeśli jednak wywołany konstruktor
rzuci wyjątek, który zostanie złapany, to wyrażenie delete
postara się zwolnić przydzieloną pamięć w "trybie awaryjnym".
Awaryjne zwolnienie pamięci
W ramach takiej obsługi przerwania, zwalnianie pamięci przydzielonej przez operator new
odbywa sie za pomocą odpowiadajacej mu wersji operator delete
.
void operator delete(void *p,lista_argumentow) throw();
operator delete
odpowiada wersji operatora new
z taką sama listą argumentów. Jeśli lista argumentów jest niepusta, to
taki operator nazywamy placement delete.
Przy wywoływaniu placement delete, przekazywana mu jest lista dodatkowych
argumentów, identyczna z listą dodatkowych argumentów operatora placement new,
który pamięć przydzielił.
Biblioteka C++ dostarcza globalnych implementacji operatorów
delete
, odpowiadających trzem wspomnianym powyżej operatorom
new
, ale można też dodawać własne definicje, zarówno w klasie jak
i w przestrzeni globalnej. Jeśli kompilator nie znajdzie żadnej
odpowiedniej definicji operator delete
, to żadna funkcja
zwalniająca nie zostanie wywołana.
Rozważmy następujący przykład:
struct X { X(int); /* rzuca wyjątek typu int*/ void *operator new(size_t) throw(std::bad_alloc); void *operator delete(void *p) throw(); }
Wyrażenie:
try { X *p = new X(1); } catch(int){};
spowoduje wywołanie:
void *tmp=X::operator new(sizeof(X)); X::operator delete(tmp);
Dodanie do klasy X
dwu operatorów:
void *operator new(size_t,double) throw(std::bad_alloc); void *operator delete(void *p,double) throw();
spowoduje, że wyrażenie:
try { X *p = new (3.14) X(1); } catch(int){};
wywoła:
void *tmp=X::operator new(sizeof(X),3.14); X::operator delete(tmp,3.14);
Tę logikę zaburza trochę fakt istnienia wyróżnionej wersji funkcji
operator delete
. Są to składowe klas posiadające drugi parametr
typu size_t
:
void X::operator delete(void *p,size_t size);
Jeśli klasa nie posiada jednoargumentowego operatora delete
, to
powyższy operator jest traktowany jak jednoargumentowy (non
placement). Za drugi argument podstawiany jest automatycznie rozmiar
zwalnianego obiektu. Rozważmy, więc teraz taki przykład:
struct X { X(int); /* rzuca wyjątek typu int*/ void *operator new(size_t) throw(std::bad_alloc); void *operator delete(void *p,size_t) throw(); }
Wyrażenie:
try { X *p = new X(1); } catch(int){};
wywoła:
void *tmp=X::operator new(sizeof(X)); X::operator delete(tmp,sizeof(X));
a wyrażenie:
try { X *p = new (3) X(1); } catch(int){};
wywoła:
void *tmp=X::operator new(sizeof(X),3); X::operator delete(tmp,3);
Proszę zwrócić uwagę na różnicę w wartości drugiego argumentu przekazanego
do operator delete
.
delete
Stworzony dynamicznie obiekt niszczymy wyrażeniem
delete p;
które najpierw wywołuje destruktor klasy X
:
p-> ~X();
Jeśli to wywołanie się nie powiedzie (zostanie rzucony wyjątek) to mamy
kłopot, bo nie zostanie wywołany operator delete
w celu
zwolnienia pamięci. Jest to kolejny powód aby nie rzucać wyjątków z
destruktora. Poniższy programik ilustruje ten problem, doprowadzając
do szybkiego wyczerpania pamięci:
class X { char a[100000]; public: ~X() {throw 0;} }; main() { while(1){ X *p = new X; try { delete p; } catch(int) {}; } }
Jeśli jednak nic złego się nie wydarzy, to po wywołaniu destruktora
przydzielona pamięć zostaje zwolniona za pomocą funkcji operator delete()
. O zwalnianiu pamięci dużo już napisałem przy omawianiu wyrażenia new
. W przypadku wyrażenia delete
dzieje się to jednak trochę inaczej. Wyrażenie delete
używa do zwolnienia pamięci tylko funkcji operator delete()
niebędących typu
placement, tzn. posiadające jeden lub ewentualnie dwa argumenty:
void operator delete(void p) throw(); void operator delete(void p,size_t) throw();
Jest to niezależne od tego jaki operator new
został użyty do
przydzielenia pamięci. Czyli jeśli zdefiniujemy, np.:
struct X { X(int); /* rzuca wyjątek typu int*/ void *operator new(size_t) throw(std::bad_alloc); void *operator new(size_t,size_t) throw(std::bad_alloc); void *operator new(size_t,double) throw(std::bad_alloc); void *operator delete(void *p,size_t) throw(); void *operator delete(void *p,double) throw(); }
to wyrażenia:
X p1 = new X; X p2 = new (1) X; X p3 = new (3.14) X; delete p3; delete p2; delete p1;
spowodują wywołanie:
void *tmp1 = X::operator new(sizeof(X)); void *tmp2 = X::operator new(sizeof(X),1); void *tmp3 = X::operator new(sizeof(X),3.14); X::operator delete(tmp3,sizeof(X)); X::operator delete(tmp2,sizeof(X)); X::operator delete(tmp1,sizeof(X));
operator new
Z powyższego opisu widać, że wpływ na proces dynamicznego tworzenia
obiektu możemy mieć tylko poprzez własne definicje przydzielającego
pamięć operatora new
. Zanim jednak napiszemy własną wersję
takiego operatora, przyjrzymy się dokładniej właściwościom
standardowego operatora new
.
Jak już wiemy funkcja operator new
musi posiadać co najmniej jeden
argument typu size_t
. Standardowy operator new
posiada
tylko ten jeden argument:
void* operator new(std::size_t size) throw(std::bad_alloc);
Jeśli wszystko pójdzie dobrze, to operator new
zwraca wskaźnik
do obszaru pamięci o rozmiarze co najmniej size
; jeśli przydział
się nie powiedzie, to rzuca wyjątek std::bad_alloc
.
Dokładniej rzecz biorąc operator new
rzuca wyjątek tylko wtedy
jeśli nie ustawiona jest funkcja obsługi błędów. Do jej ustawiania
służy funkcja:
namespace std { typedef void (new_handler*)(); new_hadler set_new_handler(new_handler f); }
Funkcja set_new_handler
ustawia nową funkcję obsługi błędów i
zwraca wskaźnik do poprzedniej funkcji obsługi lub null
, jeśli
funkcja nie była ustawiona. Przekazanie wskaźnika null
jako
argumentu powoduje, że nie będzie ustawiona żadna funkcja obsługi. To co
się dzieje wewnątrz operator new
wygląda mniej wiecej tak:
while(1) { void *p = przydziel pamiec; if(proba powiodla sie) return p; new_handler handler = set_new_handler(0); set_new_handler(handler); if(handler) handler(); else throw std::bad_alloc(); }
Funkcja handler
musi więc uzyskać więcej pamięci, rzucić
wyjątek albo przerwać program. Może też ustawić inną funkcję obsługi,
inaczej program będzie się wykonywał w niekończącej się pętli.
nothrow
Trzecia forma operatora new
dostarczonego w bibliotece
standardowej to wersja no_throw
. Operator new
nie musi
rzucać wyjątku w razie niepowodzenia, ale musi wtedy zwrócić wskaźnik
zerowy (null). Aby wywołać tę wersję operatora new
korzystamy
z tego, że posiada ona drugi argument typu nothrow_t
.
void* operator new(std::size_t size, const std::nothrow_t&) throw();
W tym celu zdefiniowana została globalna stała typu std::nothrow_t
:
namespace std { struct nothrow_t {}; extern const nothrow_t nothrow; }
Wersję nothrow
używamy więc następująco:
X *p=new (std::nothrow) X; if(!p) {...};
operator delete
Operator delete
musi posiadać co najmniej jeden parametr będący
wskaźnikiem na zwalniany obszar pamięci:
void operator delete(void* ptr) throw();
Może to być wskaźnik zerowy, wtedy operator new
nic nie robi.
Operator delete
nie rzuca wyjątków.
Jak już opisałem to powyżej, dwuargumentowa wersja będąca składową klasy:
void operator delete(void* ptr) throw();
zachowuje się, w większości przypadków jak wersja jednoargumentowa i drugi argument zostaje automatycznie inicjalizowany rozmiarem zwalnianej pamięci.
Tablice
W powyższej dyskusji ograniczyłem się do tworzenia pojedynczych obiektów. C++ zezwala na tworzenie tablic obiektów:
X *px = new X[10];
Powyższe wyrażenie przydziela pamięć na 10 obiektów klasy X
i
tworzy je za pomocą konstruktorów standardowych.
W przypadku niepowodzenia konstrukcji niszczy skonstruowane obiekty (jeśli takowe istnieją) i zwalnia pamięć. Alokacja gołej pamięci jest dokonywana poprzez:
void * operator new[] {size_t);
i zwalniana za pomocą:
void * operator new[] {size_t);
Przeładowywanie operatorów new i delete
Po tym przydługim, technicznym wprowadzeniu, możemy wreszcie pokusić
się o napisanie własnego operatora new
lub przeładowanie
istniejącego. Do wyboru mamy wersję globalną lub funkcję składową
jakiejś klasy. Globalny alokator pamięci to poważna sprawa: dotyczy
działania całego programu i musi przydzielać pamięć dowolnych
rozmiarów. Trudno będzie pod tym względem pobić działanie standardowej
wersji operator new
. Dlatego częściej będziemy chcieli definiować
operatory new
we własnych klasach.
Na co musimy zwrócić w takim przypadku uwagę? Mam
nadzieję, że przekonałem Państwa, że do każdego operatora new
należy dopisać odpowiednią wersję operatora delete
, inaczej nie
będziemy w stanie zapewnić bezpiecznego zachowania w sytuacji, w
której zostanie rzucony wyjątek z konstruktora.
Musimy też uważać na jawne zwalnianie pamięci. Jeśli w jednej klasie
zdefiniujemy kilka operatorów placement new
, to nie będziemy
mogli ich rozróżnić w poleceniu delete
!.
Musimy też zadbać aby operator new
albo rzucał wyjątek, albo
zwracał wskaźnik pusty w razie niemożności przydziału pamięci. W
innym przypadku wyrażenie new
nie rozpozna, że przydział pamięci
się nie powiódł i będzie próbować tworzyć obiekt w nieprzydzielonej
pamięci.
A co z obsługą new_handler
? W zasadzie możemy jej nie
implementować i jeśli robimy to tylko na własny użytek, to pewnie nic
złego się nie stanie. Ale jeśli operator new
jest częścią
zewnętrznego interfejsu klasy, to prędzej czy później któryś z
użytkowników może się postarać skorzystać z set_new_handler()
.
W końcu jeżeli nazywamy naszą funkcję operator new
, a nie np.
allocate()
, to sugerujemy, że będzie ona miała funkcjonalność
operator new
. Zwykle robimy to po to, aby skorzystać z
istniejącego już kodu, który używa wyrażeń new
. Jeśli chcemy
tworzyć obiekty i przydzielać pamięć, ale nie zależy nam na
interfejsie new
, lepiej nazwać nasze alokatory inaczej.
Memory pool
Jak już sygnalizowałem na wstępie, konieczność zdefiniowania własnego
operatora new
pojawia się, gdy chcemy uzyskać wydajność lepszą
niż oferowana przez standardowy operator new
. Rozważmy więc
sytuację, kiedy używamy wielu małych przedmiotów o takim samym
rozmiarze. Typowy przykład to inteligentne wskaźniki. W jaki więc
sposób moglibyśmy wydajnie przydzielać i zwalniać pamięć dla takich obiektów?
Jednym z prostszych sposobów jest przydzielenie za pomocą stadardowego alokatora pewnej ilości pamięci, a następnie przydzielanie z niej po kawałku pamięci na pojedyncze obiekty.
Dokładniej wygląda to tak (zob. animacja 14.1): przydzielamy
obszar pamięci mogący pomieścić N
kawałków żądanego przez nas
rozmiaru. Na początku wszystkie te kawałki łączymy w listę. Ponieważ na
liście bedą się znajdowały tylko kawałki nieprzydzielonej pamięci,
możemy umieścić wskaźnik do następnego kawałka na liście w samym
kawałku. Nasz alokator nie ma więc żadnego narzutu pamięci poza
wskaźnikiem do pierwszego elementu listy (głowa).
Kiedy potrzebujemy przydzielić pamięć, to usuwamy z listy jej pierwszy element i zwracamy wskaźnik do niego. Kiedy chcemy zwolnić pamięć przyłączamy zwalniany kawałek na początku listy. Obie te operacje są bardzo szybkie. Jeśli mamy wystarczająco dużo pamięci, możemy zrezygnować ze zwalniania jej pojedynczo, tylko zwolnić cały obszar naraz, gdy nie będzie nam już potrzebny.
Kiedy będziemy potrzebowali więcej kawałków niż może ich pomieścić nasz obszar, możemy przydzielić nowy.
Napisanie klasy obsługującej taki schemat pozostawiam jako ćwiczenie.
Tutaj wykorzystam gotową klasę pool
dostarczaną w bibliotece boost::pool
.
Ewidentnie taki schemat nie nadaje się do implementacji globalnej wersji
operator new
, która przydziela pamięć dowolnych rozmiarów.
Idealnie pasuje jednak do klasowego operatora new
.
Deklarujemy więc:
#include<boost/pool/pool.hpp> struct X { int _val; char c[1000];/* to tylko zwiększa rozmiar klasy*/ static boost::pool<> pool; public: X(int i=0):_val(i) {}; operator int() {return _val;}; void *operator new(size_t) throw(std::bad_alloc); void operator delete(void *) throw(); };
Składowa pool
jest składową statyczną, ponieważ musi istnieć
niezależnie od obiektów klasy. Podobnie operatory new
i
delete
automatycznie są traktowane jako metody statyczne.
Ponieważ składowe statyczne klasy są inicjalizowane na zewnątrz,
dodajemy do kodu linijkę:
boost::pool<> X::pool(sizeof(X));
która tworzy obiekt X::pool
służący do przydzielania pamięci w kawałkach
po sizeof(X)
bajtów.
Następnie definiujemy operator new
:
void * X::operator new(size_t size) throw(std::bad_alloc) { while(1) { void *p = pool.malloc(); if(p) return p; std::new_handler handler = std::set_new_handler(0); std::set_new_handler(handler); if(handler) handler(); else throw std::bad_alloc(); } }
Sam przydział pamięci jest najłatwiejszy: korzystamy z gotowej
funkcji malloc()
z klasy boost::pool<>
. Reszta kodu
implementuje zachowanie się operatora w przypadku braku pamięci, co
funkcja _pool.malloc()
sygnalizuje poprzez zwrócenie wskaźnika
zerowego.
Operator delete
jest dużo prostszy:
void X::operator delete(void *p) throw() { if(p) pool.free(p); }
Alokatory
Trudno omawiać zarządzanie pamięcią w wykładzie dotyczącym programowania uogólnionego i nie wspomnieć o alokatorach STL. W poprzedniej części wykładu używałem słowa alokator na określenie każdej funkcji przydzielającej pamięć. W STL alokator jest konceptem i oznacza klasy, których obiekty służą do przydzielania pamięci dla standardowych kontenerów. Biblioteka C++ dostarcza standardową implementację szablonu alokatora, której konkretyzacje przekazywane są jako domyślny drugi lub trzeci argument szablonów kontenerów:
namespace std { template<class T, class Allocator=allocator<T> > class vector; template<class T, class Compare = less<T>, class Allocator = allocator<T> > class set; }
Dlatego można używać kontenerów i nie wiedzieć nawet, że alokatory istnieją.
Wymagane elementy szablonu kontenera opiszę na przykładzie możliwej implementacji alokatora standardowego allocator.h:
template <class T> class allocator {...};
Alokator jest szablonem przyjmujacym jako argument typ obiektów, dla których będzie przydzielał pamięć. Ponieważ parametrem szablonu kontenera jest typ, a nie szablon, można by sądzić, że alokator klasą być nie musi, bo możemy tworzyć konkretne alokatory dla konkretnych typów, np.:
vector<int,alokator_int> v;
Alokator musi jednak być szablonem, bo wewnątrz kontenera może zajść
potrzeba przydzielenia pamięci dla obiektu innego typu niż typ
przechowywany T
. Dzieje się tak w przypadku kontenerów opartych na
węzłach, takich jak listy czy kontenery asocjacyjne. Taki kontener
musi przydzielić pamięć na węzęł, a nie na element typu T
. Żeby
móc to zrobić alokator posiada wewnętrzną strukturę:
template <class U> struct rebind { typedef pool_allocator U other; };
Kontener może z niej korzystać następująco:
typedef typename allocator<T>::rebind<node<T> >::other node_allocator;
Obiekty klasy node_allocator
przydzielają pamięć na obiekty klasy
node<T>
, a nie T
.
Alokator definiuje szereg typów stowarzyszonych:
public: typedef T value_type; typedef value_type* pointer; typedef const value_type* const_pointer; typedef value_type& reference; typedef const value_type& const_reference; typedef size_t size_type; typedef size_t difference_type;
i operator zwracający adres elementu typu T
:
pointer address(reference x) const { return &x; } const_pointer address(const_reference x) const { return x; };
Miało to umożliwić używanie niestandardowych typów wskaźnikowych i referencyjnych. W praktyce te typy i funkcje muszą być zdefiniowane dokładnie tak jak powyżej (zob. S. Meyers "STL w praktyce. 50 sposobów efektywnego wykorzystania" oraz N.M. Josuttis "C++ Biblioteka Standardowa. Podręcznik programisty").
Kontener używa obiektów klasy allocator
, wobec czego musimy mieć
możność tworzenia i niszczenia ich:
pool_allocator() {} pool_allocator(const pool_allocator&) {} ~pool_allocator() {}
Ważnym ograniczeniem narzuconym przez standard C++ jest wymaganie, aby każde dwa obiekty alokatora tej samej klasy były równoważne. Rownoważność oznacza, że pamięc przydzielona przez jeden obiekt alokatora może być zwolniona przez drugi. W naszym przypadku alokator nie posiada żadnego stanu i jego konstruktory i destruktor nic nie robią, zatem ten warunek jest spełniony. Alokator nie musi posiadać operatora przypisania, więc uniemożliwimy jego użycie:
private: void operator=(const pool_allocator&);
Dochodzimy wreszcie do funkcji, które zarządzają pamięcią. Funkcja:
pointer allocate(size_type n, const_pointer = 0) { return static_cast<pointer>(::operator new(n)); };
przydziela pamięć dla n
elementów typu T
. Pamięć nie jest
inicjalizowana. Proszę zwrócić uwagę, że w przeciwieństwie do
operator new
czy malloc
, allocate
zwraca wskaźnik
T *
, a nie void *
. Drugi parametr funkcji allocate
może
być użyty przez bardziej wyrafinowane schematy przydziału pamięci.
Tworzeniem obiektu wewnątrz przydzielonej pamięci zajmuje się funkcja
void construct(pointer p, const value_type& x) { new(p) value_type(x); }
korzystająca ze standardowego placement new
. Funkcja:
void deallocate(pointer p, size_type n) throw() { ::operator delete(p); }
zwalnia pamięć wskazywaną przez wskaźnik p
.
Funkcja deallocate()
nie wywołuje destruktora. Robi to funkcja:
void destroy(pointer p) { p-> ~value_type(); }
Na koniec została pomocnicza funkcja:
size_type max_size() const { return static_cast<size_type>(-1) / sizeof(T); }
która zwraca największą wartość możliwą do przekazania do funkcji
allocate
. Nie oznacza to jednak, że przydział tej pamięci musi
się powieść.
Koncept alokatora wymaga jeszcze dwu operatorów testujących równoważność obiektów alokatora. Ponieważ kontenery wymagają, aby każde dwa obiekty były równoważne, te operatory zdefiniowane są następująco:
template <class T> inline bool operator==(const allocator<T>&, const allocator<T>&) { return true; } template <class T> inline bool operator!=(const allocator<T>&, const allocator<T>&) { return false; }
Na koniec zabezpieczmy się jeszcze tylko na wypadek możliwości
skonkretyzowania szablonu allocator<void>
poprzez odpowiednią
specjalizację:
template<> class allocator<void> { typedef void value_type; typedef void* pointer; typedef const void* const_pointer; template <class U> struct rebind { typedef allocator<U> other; }; };