Zaawansowane CPP/Wykład 14: Zarządzanie pamięcią: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Bartek (dyskusja | edycje)
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 68 wersji utworzonych przez 5 użytkowników)
Linia 1: Linia 1:
''Uwaga: przekonwertowane latex2mediawiki; prawdopodobnie trzeba wprowadzić poprawki''
==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 przydzialanie lub zwalnianie pamięci. Wyrażenie
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ść  
wykorzystanie w tym celu właśnych implementacji
wykorzystania w tym celu własnych implementacji.
Napisanie jednak  bardziej wydajnego alokatora
Napisanie jednak  bardziej wydajnego alokatora
pamięci, niż alokator standardowy nie jest łatwe.
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). { inicjalizator}
(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ęć, służy do tego
Najpierw przydzielana jest "goła" (raw) pamięć. Służy do tego
funkcja przydziału pamieci (alokator) <code>operator new()</code>:  
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) obiektu w
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
  new</code> jest napierw 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> zawraca <code>void *</code>: W przypadku
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 umieszany w przydzielonej pamięci.  Np.
który jest umieszczany w przydzielonej pamięci.  Np:


  X *p <nowiki>=</nowiki> X(1);
  X *p <nowiki>=</nowiki> X(1);
 
spowoduje wywolanie konstruktora:
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ęć, w "trybie awaryjnym".
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
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>.  
  <code>operator new</code> odbywa sie za pomocą odpowiadajacej mu wersji
  <code>operator delete</code>.  


  void operator delete(void *p,lista_argumentow) throw();
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  
{ 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, przekazywanamu jest lista dodatkowych
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 przestrzenie globalnej.  Jeśli kompilator nie znajdzie żadnej
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->&nbsp;X();
  p->&nbsp;~X();
 
Jeśli to wywołanie się niepowiedzie (zostanie rzucony wyjątek) to mamy
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:
   &nbsp;X() {throw 0;}
   &nbsp;~X() {throw 0;}
  };
  };
   
   
Linia 276: Linia 270:
   }
   }
  }
  }
 
Jeśli jednak nic złego się nie wydarzy, to po wywolaniu destruktora,
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
  delete()</code>. O zwalnianiu pamięci dużo już napisałem przy omowianiu
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> nie będących typu
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, jaki <code>operator new</code> został użyty do
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;
 
spowoduja wywołanie:
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>, jeśli przydział
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 biorać <code>operator new</code> rzuca wyjatek tylko wtedy
Dokładniej rzecz biorąc <code>operator new</code> rzuca wyjątek tylko wtedy
jeśli nie ustawiona jest funkcja obsługi błedów.  Do jej ustawiania  
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 błedów i
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>, musi więc albo uzyskać więcej pamięci,  rzucić
Funkcja <code>handler</code> musi więc uzyskać więcej pamięci,  rzucić
wyjątek albo przerwać program, może też ustawić inną funkcję obsługi,
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ć wersję operatora <code>new</code> korzystamy  
zerowy (null). Aby wywołać 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, jak wersja jednoargumentowa i
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 wględem pobić działanie standardowej
rozmiarów.  Trudno będzie pod tym względem pobić działanie standardowej
wersji <code>operator new</code>. Dlatego cześciej będziemy chcieli definiować
wersji <code>operator new</code>. Dlatego częściej będziemy chcieli definiować
operatory cd{new} we własnych klasach.   
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 państwa, że do każdego operatora <code>new</code>
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, w sytuacji w
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, jeśli w jednej klasie
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ć, aby <code>operator new</code> albo rzucał wyjątek, albo
Musimy też zadbać aby <code>operator new</code> albo rzucał wyjątek, albo
zwracał wskaźnik pusty w razie niemożnośći przydziału pamięci.  W
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 probować tworzyć obiekt w nie przydzielonej
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 póżniej któryś z
zewnętrznego interfejsu klasy, to prędzej czy później któryś z
użytkowników, może się postarać skorzystać z <code>set_new_handler()</code>.
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, po to, aby skorzystać z
<code>operator new</code>. Zwykle robimy to po to, aby skorzystać z
istiejacego już kodu który używa wyrażeń <code>new</code>.  Jeśli chcemy
istniejącego już kodu, który używa wyrażeń <code>new</code>.  Jeśli chcemy
tworzyć obiekty i przydzielać pamięc, ale nie zależy nam na
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, o takim samym
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, za pomocą  
Jednym z prostszych sposobów jest przydzielenie za pomocą  
stadardowego alokatora, pewnej ilości pamięci, a następnie przydzielanie
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.


<div class="thumb tright"><div style="width:250px;">
{{kotwica|am.14.1|}}[[File:cpp-12-pool2.mp4|250x250px|thumb|right|Animacja 14.1. Działanie zasobnika pamięci.]]
<flashwrap>file=cpp-12-pool2.swf|size=small</flashwrap>
<div.thumbcaption>Rysunek 12.1. Działanie zasobnika pamięci.</div>
</div></div>


Dokładniej wygląda to tak (zob.&nbsp;rys.&nbsp;12.1.): przydzielamy
Dokładniej wygląda to tak (zob. [[#am.14.1|animacja 14.1]]): przydzielamy
obszar pamięci mogący pomięścić <code>N</code> kawałków żądanego przez nas
obszar pamięci mogący pomieścić <code>N</code> kawałków żądanego przez nas
rozmiaru. Na początku wszytkie te kawałki łączymy w listę. Ponieważ na
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, w samym
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
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ć pamięc, to usuwamy z listy jej pierwszy
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 na
zrezygnować ze zwalniania jej pojedynczo, tylko zwolnić cały obszar naraz, gdy nie będzie nam już potrzebny.
raz, kiedy 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 484: 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>.
{}{boost::pool}.


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 sobie:
Deklarujemy więc:
#include<boost/pool/pool.hpp>
#include<boost/pool/pool.hpp>
  struct X {
  struct X {
   int _val;
   int _val;
Linia 502: 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 510: 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 528: 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 544: Linia 546:
==Alokatory==
==Alokatory==


Trudno omawiać zarządzanie pamięcią na wykładzie dotyczącym
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 pamięc. W STL alokator jest konceptem i
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ę szablomu alokatora, której konkretyzacje przekazywane są
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 561: Linia 563:
  class set;
  class set;
  }
  }
 
Dlatego
Dlatego
można używać kontenerów, nie wiedzać nawet, że alokatory istnieją.
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:
{mode12/code/allocator.h}{allocator.h}:
 
template <class T> class      allocator {...};


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, przyjmujacym jako argument typ obiektów dla
których będzie przydzialał pamięć.  Ponieważ parametrem szablonu
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, w przypadku kontenerów opartych na
przechowywany <code>T</code>. Dzieje się tak w przypadku kontenerów opartych na
wezłach, takich jak listy czy kontenery asocjacyjne. Taki kontener
węzłach, takich jak listy czy kontenery asocjacyjne. Taki kontener
musi przydzielić pamięć na wezęł, a nie na element typu <code>T</code>. Żeby
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<nowiki><</nowiki>U> other; };
   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 606: Linia 607:
   typedef size_t            size_type;
   typedef size_t            size_type;
   typedef size_t            difference_type;
   typedef size_t            difference_type;
 
i operator zwracającu adres elementu typu <code>T</code>:
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 613: Linia 614:
     return x;
     return x;
   };
   };
 
Miało to umożliwić używanie
Miało to umożliwić używanie
niestandardowych typów wskażnikowych i referencyjnych. W praktyce te
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.
{MeyersSTL}  oraz {josuttis}).  
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 zniszczenia ich:
możność tworzenia i niszczenia ich:


  pool_allocator() {}  
  pool_allocator() {}  
   pool_allocator(const pool_allocator&) {}
   pool_allocator(const pool_allocator&) {}
   &nbsp;pool_allocator() {}
   &nbsp;~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, może być zwolniona przez drugi. W naszym przypadku alokator
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
robia, tak że ten warunek jest spełniony. Alokator nie musi posiadać
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ą pamięcia. Funkcja  
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
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 paremetr funckji <code>allocate</code> może
<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 653: Linia 654:
     new(p) value_type(x);  
     new(p) value_type(x);  
   }
   }
korzystająca ze standardowego  placement <code>new</code>.


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 wskażnik <code>p</code>.  
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->&nbsp;value_type();  
     p->&nbsp;~value_type();  
   }
   }
 
Na koniec została pomocnicza funkcja:
Na koniec została pomocnicza funkcja:


Linia 674: Linia 673:
     return static_cast<size_type>(-1) / sizeof(T);
     return static_cast<size_type>(-1) / sizeof(T);
   }
   }
 
która zwraca największą wartość, możliwą do przekazania do funkcji
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 694: 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 707: Linia 706:
   struct rebind { typedef allocator<nowiki><</nowiki>U> other; };
   struct rebind { typedef allocator<nowiki><</nowiki>U> other; };
  };
  };
==Podsumowanie==

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.

Animacja 14.1. Działanie zasobnika pamięci.

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();
}; 

( Źródło: X_new.h)

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();    
  }   
}

( Źródło: X_new.cpp)

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