[16] Zarządzanie pamięcią
(Część C++ FAQ Lite, Copyright © 1991-2002, Marshall Cline, cline@parashift.com)


FAQ - sekcja [16]:


[16.1] Co usuwa instrukcja delete p: wskaźnik p, czy dane wskazywane przez p?

Dane wskazywane przez wskaźnik.

Instrukcję delete p powinno się czytać raczej jak "skasuj dane wskazywane przez p", a nie dosłownie "skasuj p". Podobnie jest w przypadku wywołania funkcji free: free(p) nie oznacza "uwolnij p" tylko "uwolnij pamięć zajmowaną przez dane na które wskazuje p". [ang. delete oznacza "skasuj", natomiast free - "uwolnij"; przyp. tłum.]

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.2] Czy pamięć alokowaną przy użyciu new można zwolnić funkcją free? I odwrotnie - czy pamięć alokowaną funkcją "malloc" można zwolnić operatorem delete?

Nie!

Jest całkowicie legalnym, moralnym i dobrym stosowanie w jednym programie funkcji malloc() i free() wraz z operatorami new i delete. Jednak nielegalnym, amoralnym i nikczemnym jest zwalnianie funkcją free() pamięci alokowanej operatorem new, jak również zwalnianie operatorem delete pamięci alokowanej funkcją malloc().

Strzeż się! Od czasu do czasu dostaję e-maile od osób twierdzących, że tego rodzaju kombinacje funkcji i operatorów obsługujących pamięć działają u nich bez problemu na kompilatorze X pracującym na komputerze Y. To, że w prostym teście nie dostrzegają oni jakiegokolwiek problemu nie oznacza wcale, że problem ten nie istnieje i że nie doprowadzi on do zawieszenia bardziej skomplikowanego programu. Nawet jeśli wiadomo, że program napewno nie zawiesi się na jednym kompilatorze, nie wiadomo czy będzie tak na innym kompilatorze, innej platformie, a nawet na innej wersji tego samego kompilatora.

Strzeż się! Czasami ludzie mówią "Ale przecież chodzi mi tylko o prostą tablicę char'ów". Mimo tego, nie używaj malloc()'a i delete'a lub new i free() na tym samym wskaźniku! Jeżeli alokowano pamięć instrukcją p = new char[n] to musisz ją zwolnić przy użyciu delete[] p; nie możesz tu napisać free(p). Natomiast jeżeli alokowano pamięć przez p = malloc(n) to musisz ją zwolnić funkcją free(p); nie możesz tu napisać ani delete[] p ani delete p! Mieszanie ze sobą obu sposobów może doprowadzić do katastrofy w czasie wykonywania programu w sytuacji, gdy kod został przeniesiony na nową platformę sprzętową, na nowy kompilator lub nawet na nowszą wersję tego samego kompilatora.

Zostałeś ostrzeżony(a).

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.3] Dlaczego powinno się używać operatora new zamiast starego, dobrego malloc()?

Ze względu na konstruktory/destruktory, lepszą kontrolę typów i możliwość przeciążania.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.4] Czy mogę wywołać funkcję realloc() dla wskaźników utworzonych przez operator new?

Nie!

realloc(), kiedy musi zmienić położenie obiektu w pamięci, robi to kopiując obiekt bit po bicie, co dla większości obiektów w C++ może się skończyć rozdarciem na strzępy. Obiekty w języku C++ powinny mieć możliwość kopiowania "na własną rękę", przy użyciu ich własnych konstruktorów kopiujących lub operatorów przypisań.

Poza tym, sterta używana przez operator new wcale nie musi być tą samą stertą, której używają funkcje malloc() i realloc().

[Przypis tłumacza: Sterta jest to obszar pamięci programu, przeznaczony na dynamiczne alokacje]

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.5] Czy przy każdym użyciu operatora new muszę sprawdzać, czy nie zwrócił on wartości NULL?

Nie! (Ale jeśli używasz starego kompilatora, możliwe że będziesz musiał(a) "zmusić" operator new do zgłaszania wyjątku w przypadku braku pamięci.)

Konieczność sprawdzania przy każdej alokacji z użyciem operatora new, czy nie zwrócił on wskaźnika NULL, okazuje się naprawdę bolesna. Kod, taki jak poniżej, jest strasznie nużący:

 Fred* p = new Fred();
 if (p == NULL)
   throw std::bad_alloc();

Jeżeli Twój kompilator nie obsługuje (lub odmawia użycia) wyjątków, Twój kod może się stać jeszcze nudniejszy:

 Fred* p = new Fred();
 if (p == NULL) {
   std::cerr << "Nie powiodła się próba alokacji obiektu Fred" << std::endl;
   abort();
 }

Ale głowa do góry! W C++, jeżeli podczas wykonywania instrukcji p = new Fred() nie powiedzie się alokacja sizeof(Fred) bajtów pamięci, wtedy biblioteka czasu pracy programu [ang. runtime system] zgłosi wyjątek std::bad_alloc. W odróżnieniu od malloc()'a, new nigdy nie zwraca wskaźnika NULL!

Stąd możesz po prostu napisać:

 Fred* p = new Fred();   // Nie ma potrzeby sprawdzania czy p == NULL

Jednakże stare kompilatory nie obsługują tej właściwości języka. Możesz to sprawdzić w dokumentacji swojego kompilatora, w rozdziale na temat operatora new. Jeżeli dysponujesz starym kompilatorem, być może konieczne będzie wymuszenie na kompilatorze opisanego zachowania.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.6] W jaki sposób mogę przekonać mój (stary) kompilator, aby automatycznie sprawdzał, czy operator new nie zwrócił wskaźnika NULL?

Możliwe, że Twój kompilator już obsługuje tę właściwość.

Jeżeli jednak dysponujesz starym kompilatorem, który nie przeprowadza "automagicznie" "testu na NULL", możesz wymusić na bibliotece czasu pracy programu (ang. "runtime system") wykonywanie tego testu, instalując funkcję obsługującą operator new [ang. "new handler function"; chodzi o funkcję, która jest automatycznie wywoływana w przypadku braku pamięci - przyp. tłum.]. Funkcja ta może robić wszystko, co tylko sobie wymyślisz, np. zgłaszać wyjątek, usuwać kilka obiektów z pamięci i wracać do punktu wywołania (w takim przypadku operator new ponowi próbę alokacji), wyświetlać komunikat i przerywać działanie programu, itd.

Poniższy kod przedstawia taką przykładową funkcję "new handler", która wyświetla komuniakt a następnie wyrzuca wyjątek. Funkcja jest instalowana przy użyciu funkcji std::set_new_handler():

 #include <new>       // Stąd mamy std::set_new_handler
 #include <cstdlib>   
// Stąd mamy abort()
 #include <iostream>  
// Stąd mamy std::cerr
 
 
/* Klasa wyjątków zgłaszanych przy braku pamięci */
 class alloc_error : public std::exception {
 public:
   alloc_error() : exception() { }
 };
 
 void myNewHandler()
 {
   
// To jest nasza funkcja obsługująca operator new. Może ona robić co tylko
   
// sobie wymyślisz.
   throw alloc_error(); 
// Wyrzuć wyjątek klasy alloc_error()
 }
 
 int main()
 {
   std::set_new_handler(myNewHandler);   
// Instalacja funkcji obsługującej
  
// operator new
   
// ...
 }

Od momentu wykonania wiersza zawierającego std::set_new_handler(), za każdym razem, gdy operator new wykryje błąd w czasie alokacji pamięci, zostanie wywołana nasza funkcja myNewHandler(). Oznacza to, że operator new nigdy nie zwróci wartości NULL:

 Fred* p = new Fred();   // Nie ma potrzeby sprawdzania, czy p == NULL

Jeżeli Twój kompilator nie obsługuje wyjątków, możesz ewentualnie zmienić linijkę throw ...; na:

 std::cerr << "Nie powiodła się próba alokacji pamięci!" << std::endl;
 abort(); 
// Awaryjne wyjście z programu

Uwaga: Jeżeli operator new zostanie użyty w konstruktorze jakiegoś globalnego lub statycznego obiektu, to nie będzie on mógł wywołać naszej funkcji myNewHandler(), ponieważ konstruktor zostanie wywołany jeszcze przed uruchomieniem funkcji main(). Niestety, nie istnieje żaden wygodny sposób, który mógłby zapewnić, że std::set_new_handler() zostanie wywołany przed pierwszym użyciem operatora new. Przykładowo, jeżeli nawet umieścisz wywołanie std::set_new_handler() w konstruktorze któregoś z globalnych obiektów, to wciąż nie będziesz mieć pewności, czy moduł ("jednostka kompilacyjna") zawierający ten obiekt zostanie uwzględniony w trakcie inicjalizacji jako pierwszy, ostatni czy gdzieś pośrodku. Zatem wciąż nie mamy żadnej pewności, że funkcja std::set_new_handler() zostanie wywołana przed inicjalizacją jakiegokolwiek obiektu globalnego.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.7] Czy przed instrukcją delete p musze sprawdzać, czy wskaźnik nie jest równy NULL ?

Nie!

Język C++ gwarantuje, że instrukcja delete p nie zrobi nic, jeśli p będzie równy NULL.

Since you might get the test backwards, and since most testing methodologies force you to explicitly test every branch point, you should not put in the redundant if test.

Źle:

 if (p != NULL)
   delete p;

Dobrze:

 delete p;

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.8] Z jakich etapów składa się wykonanie instrukcji delete p?

delete p jest dwuetapową operacją: najpierw wywoływany jest destruktor, a następnie zwalniana jest pamięć zajmowana przez obiekt. Kod generowany w miejscu wystąpienia instrukcji delete p odpowiada funkcjonalnie poniższemu (przyjmując, że p jest tyou Fred*):

 // Oryginalny kod: delete p;
 if (p != NULL) {
   p->~Fred();
   operator delete(p);
 }

Instrukcja p->~Fred() wywołuje destruktor obiektu klasy Fred wskazywanego przez wskaźnik p.

Instrukcja operator delete(p) wywołuje funkcję void operator delete(void *p) służącą do dealokacji pamięci. W działaniu funkcja ta jest podobna do free(void *p). (Zwróć jednak uwagę, że obie te funkcje nie są wzajemnie wymienne, np. nie ma żadnej gwarancji, że obie używają tej samej sterty!)

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.9] Czy w instrukcji p = new Fred() może nastąpić "wyciek pamięci", jeśli konstruktor klasy Fred rzuci wyjątek?

Nie.

Język C++ gwarantuje, że jeśli konstruktor zgłosi wyjątek podczas wykonywania instrukcji p = new Fred(), to alokowane przed chwilą sizeof(Fred) bajtów pamięci zostanie "automagicznie" zwolnionych z powrotem na stertę.

Teraz szczegóły: wykonanie instrukcji new Fred() składa się z dwóch etapów:

  1. sizeof(Fred) bajtów pamięci zostaje alokowanych przy użyciu funkcji void* operator new(size_t nbytes). Funkcja ta działa podobnie jak malloc(size_t nbytes). (Zwróć jednak uwagę, że obie te funkcje nie są wymienne; np., nie ma żadnej gwarancji, że będą one korzystać z tej samej sterty!).
  2. Zostaje wywołany konstruktor klasy Fred, który tworzy obiekt w przed chwilą alokowanym obszarze. Wskaźnik zwrócony w pierwszym etapie, zostaje przekazany do konstruktora jako ukryty parametr this. Wywołanie konstruktora zawarte jest w bloku try ... catch, dzięki czemu możliwe jest obsłużenie wyjątków zgłaszanych przez konstruktor.

Zatem kod generowany w miejscu instrukcji new odpowiada funkcjonalnie poniższemu:

 // Originalny kod: Fred* p = new Fred();
 Fred* p = (Fred*) operator new(sizeof(Fred));
 try 
 {
   new(p) Fred();       
// Placement new
 }
 catch (...)        
// Jeżeli konstruktor zgłosi wyjątek to:
 {
   operator delete(p);  
// 1. Zwolnij pamięć
   throw;               
// 2. Prześlij wyjątek dalej
 }

W wierszu oznaczonym "Placement new" zostaje wywołany konstruktor klasy Fred. Wskaźnik this wewnątrz konstruktora Fred::Fred() przyjmuje wartość p.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.10] W jaki sposób mogę alokować / zwalniać tablice obiektów?

Przy użyciu p = new T[n] oraz delete[] p:

 Fred* p = new Fred[100];
 
// ...
 delete[] p;

Każdą tablicę alokowaną przy użyciu operatora new (zazwyczaj z [n] wewnątrz wyrażenia new) należy usuwać przy użyciu operatora delete z nawiasami []. Taki zapis jest niezbędny, gdyż nie ma żadnej składniowej różnicy między wskaźnikiem do obiektu a wskaźnikiem do tablicy obiektów (cecha odziedziczona po języku C).

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.11] Co się stanie, jeśli będę chciał(a) skasować tablicę alokowaną przy użyciu new T[n], a zapomnę napisać [] przy operatorze delete?

Życie nieuchronnie zmierza ku katastroficznemu zakończeniu.

Właściwe powiązanie instrukcji p = new T[n] i delete[] p należy do obowiązków programisty — nie kompilatora. Jeżeli zrobisz to źle, to nie otrzymasz żadnego komunikatu informującego o tej pomyłce - ani podczas kompilacji, ani podczas pracy programu. Jest bardzo prawdopodobne, że doprowadzi to do uszkodzenia sterty. Lub czegoś gorszego. Twój program prawdopodobnie "padnie" na skutek tego błędu.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.12] Czy mogę opuścić nawiasy [] w operatorze delete, gdy chcę skasować tablicę obiektów typu prostego (np. char, int)?

Nie!

Czasami programistom wydaje się, że nawiasy kwadratowe [] w operatorze delete[] p używa się tylko po to, aby przed skasowaniem tablicy wywołać destruktory wszystkich znajdujących się w niej obiektów. W związku z tym dochodzą do wniosku, że tablicę obiektów należących do typu prostego, np. char lub int, można usunąć bez stosowania nawiasów kwadratowych [] w operatorze delete. Zgodnie z tym założeniem uznają poniższy kod za prawidłowy:

 void userCode(int n)
 {
   char* p = new char[n];
   
// ...
   delete p;     
// <— BŁĄD! Powinno być delete[] p !
 }

Jednakże ten kod jest nieprawidłowy i może doprowadzić do katastrofy podczas wykonywania programu. Przyczyną tego jest fakt, że instrukcja delete p wywołuje funkcję operator delete(void*), natomiast instrukcja delete[] p wywołuje funkcję operator delete[](void*). Domyślnie, funkcja operator delete[](void*) po prostu wywołuje funkcję operator delete(void*) [tak że oba operatory faktycznie działają tak samo dla typów prostych - przyp. tłum.], jednakże użytkownik może zdefiniować własne wersje obu funkcji (co na ogół wiąże się również ze zdefiniowaniem nowej wersji funkcji operator new[](size_t)). Jeżeli utworzysz nową wersję operatora delete[], która nie będzie kompatybilna z operatorem delete, a następnie wywołasz nie ten operator, co trzeba (np. delete p zamiast delete[] p), może to spowodować katastrofę w czasie wykonywania programu.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.13] Mam tablicę alokowaną przy użyciu instrukcji p = new Fred[n]. Skąd kompilator wie, podczas jej kasowania instrukcją delete[] p, że znajduje się w niej n obiektów?

Krótka odpowiedź: czary-mary.

Dłuższa odpowiedź: Biblioteka czasu pracy programu (ang. run-time system) zapamiętuje liczbę obiektów n w taki sposób, że można ją pobrać znając wskaźnik do tablicy p. Istnieją dwie popularne techniki realizujące to zadanie. Obie są stosowane w komercyjnych kompilatorach, obie mają swoje wady i zalety, i żadna z nich nie jest doskonała. Oto one:

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.14] Czy legalnym (i moralnym) jest umieszczenie w treści metody instrukcji delete this?

Obiekt może bez problemu popełnić samobójstwo (delete this), o ile zachowasz pewną ostrożność.

"Ostrożność" definiuję tu w następujący sposób:

  1. Musisz mieć stuprocentową pewność, że obiekt został utworzony przy użyciu "zwykłego new" (nie new[], ani nie placement new). Nie może on zostać utworzony jako zwykły obiekt (lokalny na stosie, globalny lub jako składowa innego obiektu); po prostu musi on zostać alokowany przy użyciu zwykłego new.
  2. Musisz mieć stuprocentową pewność, że metoda, w której następuje usunięcie obiektu przy użyciu delete this, jest ostatnią metodą wywoływaną dla tego obiektu.
  3. Musisz mieć stuprocentową pewność, że pozostała część tej metody (znajdująca się poniżej wiersza z delete this) nie odwołuje się w żaden sposób do składowych obiektu (zarówno do metod jak i do pól).
  4. Musisz mieć stuprocentową pewność, że po usunięciu obiektu wskaźnik do niego (this) nie będzie używany w żaden sposób. Innymi słowy, nie możesz sprawdzać jego wartości, porównywać go zarówno z innym wskaźnikiem jak i z NULLem, wyświetlać go, rzutować - po prostu nie wolno Ci robić czegokolwiek z wskaźnikiem do tego obiektu.

Naturalnie w przypadku, gdy wskaźnik this jest wskaźnikiem do klasy nadrzędnej wobec klasy obiektu, a obiekt nie zawiera wirtualnego destruktora, pojawiają się dodatkowe problemy.

[Przypis tłumacza: Problem polega na tym, że wskaźnik this nie zawsze jest wskaźnikiem tego samego typu, co obiekt. Wyobraźmy sobie np., że mamy dwie klasy - A i B - przy czym B jest potomkiem klasy A. Obie klasy zawierają niewirtualną metodę zniszcz_mnie(), którą zdefiniowano tylko w klasie A i która zawiera instrukcję delete this. Szkopuł polega tu na tym, że - ponieważ metoda ta nie jest wirtualna i zdefiniowano ją tylko w klasie A - to wskaźnik this jest w niej interpretowany jako wskaźnik do obiektu klasy A; stąd instrukcja delete this zawsze spowoduje wywołanie destruktora klasy A (nawet jeśli typem obiektu jest B). Rozwiązaniem jest zadeklarowanie destruktorów obu klas jako metody wirtualne - wtedy, wewnątrz instrukcji delete this, nastąpi automatyczne rozpoznanie typu obiektu i wywołany zostanie właściwy destruktor.]

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.15] Jak, przy użyciu operatora new, mogę alokować tablicę wielowymiarową?

Można to zrobić na wiele sposobów, zależnych od tego jak elastycznie chcesz ustalać wymiary tablicy. W jednym ze skrajnym przypadków, gdy wymiary są znane podczas kompilacji, możesz alokować tablicę statycznie (jak w języku C):

 class Fred { /*...*/ };
 void someFunction(Fred& fred);
 
 void manipulateArray()
 {
   const unsigned nrows = 10;  
// Liczba wierszy jest znana podczas
        
// kompilacji.
   const unsigned ncols = 20;  
// Liczba kolumn jest znana podczas
        
// kompilacji.
   Fred matrix[nrows][ncols];
 
   for (unsigned i = 0; i < nrows; ++i) {
     for (unsigned j = 0; j < ncols; ++j) {
       
// Funkcja "someFunction" wykonuje jakąś operację na elemencie
       
// tablicy o współrzędnych (i,j):
       someFunction( matrix[i][j] );
 
       
// Możesz bezpiecznie opuścić funkcję bez usuwania tablicy (zostanie ona
       
// usunięta automatycznie, jak każda zmienna lokalna zdefiniowana w
       
// funkcji):
       if (today == "Tuesday" && moon.isFull())
         return;     
// Przerwij pracę, jeśli mamy wtorek i pełnię księżyca
     }
   }
 
   
// Pod koniec funkcji również nie musimy jawnie usuwać tablicy
 }

Jednak w większości przypadków, rozmiar tablicy jest znany dopiero podczas wykonywania programu; ponadto tablica ma prostokątny kształt (tak jak poprzednio). W takiej sytuacji tablicę należy już alokować dynamicznie na stercie, ale wciąż zajmuje ona jeden, ciągły obszar w pamięci.

 void manipulateArray(unsigned nrows, unsigned ncols)
 {
   Fred* matrix = new Fred[nrows * ncols];
 
   
// Ponieważ alokowaliśmy tablicę operatorem new, musimy teraz
   
// BARDZO uważać, aby przypadkiem nie przeskoczyć instrukcji
   
// delete. Stąd musimy przechwytywać wszystkie wyjątki
   
// z fragmentu kodu, w którym wykonywane są operacje na tablicy.
   
   try {
 
     
// Wywołaj funkcję someFunction() dla wszystkich elementów tablicy:
     for (unsigned i = 0; i < nrows; ++i) {
       for (unsigned j = 0; j < ncols; ++j) {
         someFunction( matrix[i*ncols + j] );
       }
     }
 
     
// Jeżeli chcemy aby funkcja przerywała działanie w każdą pełnię księżyca
     
// wypadającą we wtorek, musimy najpierw usunąć tablicę ze sterty - to
     
// samo przy każdym innym wystąpieniu instrukcji return
     if (today == "Tuesday" && moon.isFull()) {
       delete[] matrix;
       return;
     }
 
     
...miejsce na kod, który coś-tam robi z tablicą...
 
   }
   catch (...) {
     
// W przypadku wystąpienia wyjątku usuń tablicę ze sterty...
     delete[] matrix;
     throw;    
// ...a następnie prześlij wyjątek dalej
   }
 
   
// Pod koniec funkcji również musimy zapewnić, że pamięć zajmowana przez
   
// tablicę zostanie zwolniona:
   delete[] matrix;
 }

W ostateczności możemy mieć do czynienia z drugim skrajnym przypadkiem, w którym nawet nie ma gwarancji, że macierz będzie miała prostokątny kształt. Przykładowo, jeśli wiersze będą mogły mieć różne długości, wtedy będziesz musiał(a) alokować każdy z nich osobno. W funkcji przedstawionej poniżej, ncols[i] oznacza liczbę kolumn w wierszu i, gdzie i należy do przedziału od 0 do nrows-1 [nrows oznacza liczbę wierszy - przyp. tłum.]

 void manipulateArray(unsigned nrows, unsigned ncols[])
 {
   typedef Fred* FredPtr;
 
   
// Jeżeli w tym miejscu wystąpi wyjątek to nie doprowadzi to do wycieku
   
// pamięci.
   FredPtr* matrix = new FredPtr[nrows];
 
   
// Ustaw wszystkie elementy na NULL, na wypadek gdyby w czasie ich
   
// inicjalizacji wystąpił wyjątek (szczegóły w komentarzu na początku
   
// bloku try)
   for (unsigned i = 0; i < nrows; ++i)
     matrix[i] = NULL;
 
   
// Ponieważ macierz została alokowana dynamicznie, stąd musimy BARDZO
   
// uważać, żeby niechcący nie przeskoczyć instrukcji delete. Dlatego
   
// w poniższym bloku kodu przechwytujemy wszystkie wyjątki.
   try {
 
     
// Teraz alokujemy wiersze wewnątrz macierzy. Jeżeli przy którymś z nich
     
// zostanie zgłoszony wyjątek, to wszystkie do tej pory alokowane wiersze
     
// zostaną usunięte (zobacz komentarz w bloku catch poniżej)
     for (unsigned i = 0; i < nrows; ++i)
       matrix[i] = new Fred[ ncols[i] ];
 
     
// Tu dokonujemy operacji na poszczególnych elementach (i,j)
     
// macierzy:
     for (unsigned i = 0; i < nrows; ++i) {
       for (unsigned j = 0; j < ncols[i]; ++j) {
         someFunction( matrix[i][j] );
       }
     }
 
     
// Jeżeli chcesz, żeby funkcja przerywała działanie w każdą
     
// pełnię księżyca przypadającą na wtorek, musisz najpierw usunąć macierz
     
// - to samo w przypadku każdego innego wystąpienia instrukcji 
     
// return
     if (today == "Tuesday" && moon.isFull()) {
       for (unsigned i = nrows; i > 0; --i)
         delete[] matrix[i-1];
       delete[] matrix;
       return;
     }
 
     
...miejsce na kod, który coś-tam robi z macierzą...
 
   }
   catch (...) {
     
// W przypadku wystąpienia wyjątku musimy usunąć macierz z pamięci.
     
// Zwróć uwagę, że niektóre ze wskaźników do wierszy mecierzy
     
// matrix[...] mogą mieć wartość NULL; jest to całkowicie w
     
// porządku, gdyż próba usunięcia danych spod adresu NULL operatorem
     
// delete (delete NULL) nie powoduje wystąpienia błędu.
     for (unsigned i = nrows; i > 0; --i)
       delete[] matrix[i-1];
     delete[] matrix;
     throw;    
// Prześlij wyjątek dalej
   }
 
   
// Pod koniec funkcji również musimy usunąć macierz z pamięci. Zauważ, że
   
// dealokacje następują w odwrotnej kolejności niż alokacje [od ostatniego
   
// wiersza do pierwszego - przyp. tłum.]
   for (unsigned i = nrows; i > 0; --i)
     delete[] matrix[i-1]; 
/* <-- Wyjaśnienie tej instrukcji poniżej */
   delete[] matrix;
 }

Zwróć uwagę na to śmieszne matrix[i-1], w przedostatnim miejscu w którym użyto operator delete (zaznaczona linijka). Pozwala ono uniknąć "przekręcenia" zmiennej i w przypadku zejścia poniżej zera (i jest zmienną bez znaku - unsigned). [pętla for zmienia wartość zmiennej i w zakresie od nrows do 1; stąd wartość i-1, tzn. numer kasowanego wiersza macierzy, zmienia się od nrows-1 do 0, czyli od ostatniego do pierwszego - przyp. tłum.]

Na koniec, zapamiętaj że wskaźniki i tablice są złe. Zazwyczaj o wiele lepszym rozwiązaniem jest enkapsulacja wskaźników w postaci klasy posiadającej bezpieczny i prosty interfejs. Metodę tą przedstawia kolejny faq.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.16] Ale kod w poprzednim faqu jest taki skomplikowany i podatny na błędy! Nie ma jakiegoś prostszego sposobu na to?

Ano jest.

Przyczyną skomplikowania i podatności na błędy kodu z poprzedniego faqa jest to, że używaliśmy w nim wskaźników, a jak już wiemy wskaźniki i tablice są złe. Problem ten można rozwiązać zastępując wskaźniki klasą o prostym i bezpiecznym interfejsie. Na przykład, możemy zdefiniować klasę Matrix służącą do obsługi prostokątnej macierzy, dzięki czemu nasz kod zyskałby znacznie na prostocie w porównaniu do przykładu z poprzedniego faqa:

 // Kod klasy Matrix przedstawiono w następnej ramce...
 void someFunction(Fred& fred);
 
 void manipulateArray(unsigned nrows, unsigned ncols)
 {
   Matrix matrix(nrows, ncols);   
// Utwórz macierz o nazwie matrix
 
   for (unsigned i = 0; i < nrows; ++i) {
     for (unsigned j = 0; j < ncols; ++j) {
       
// W ten sposób odwołujesz się do elementu (i,j) macierzy:
       someFunction( matrix(i,j) );
 
       
// Możesz w każdej chwili bezpiecznie opuścić funkcję, bez konieczności
       
// stosowania dodatkowych instrukcji delete:
       if (today == "Tuesday" && moon.isFull())
         return;     
// Przerwij pracę w środę gdy jest pełnia księżyca
     }
   }
 
   
// Również na końcu funkcji nie trzeba jawnie stosować instrukcji delete
 }

Szczególnie wart uwagi jest brak kodu usuwającego macierz z pamięci. Pomimo tego, że w powyższym przykładzie nie ma ani jednej instrukcji delete, to nie wystąpi tu żaden wyciek pamięci - pod warunkiem, że destruktor klasy Matrix dobrze spełni swoje zadanie.

Oto kod klasy Matrix spełniający powyższe założenia:

 class Matrix {
 public:
   Matrix(unsigned nrows, unsigned ncols);
   
// Zgłasza wyjątek BadSize gdy wysokość (nrows) lub szerokość (ncols)
   
// jest równa zero
   class BadSize { };
 
   
// Zgodnie z Prawem Wielkiej Trójki:
   
// [ang. 'Law Of The Big Three']
  ~Matrix();
   Matrix(const Matrix& m);
   Matrix& operator= (const Matrix& m);
 
   
// Metody umożliwiające dostęp do elementów macierzy ((i,j) -
   
// - współrzędne) 
   Fred&       operator() (unsigned i, unsigned j);
   const Fred& operator() (unsigned i, unsigned j) const;
   
// Metody te, w przypadku podania nieprawidłowych współrzędnych, zgłaszają
   
// wyjątek BoundsViolation
   class BoundsViolation { };
 
 private:
   Fred* data_;
   unsigned nrows_, ncols_;
 };
 
 inline Fred& Matrix::operator() (unsigned row, unsigned col)
 {
   if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
   return data_[row*ncols_ + col];
 }
 
 inline const Fred& Matrix::operator() (unsigned row, unsigned col) const
 {
   if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
   return data_[row*ncols_ + col];
 }
 
 Matrix::Matrix(unsigned nrows, unsigned ncols)
   : data_  (new Fred[nrows * ncols]),
     nrows_ (nrows),
     ncols_ (ncols)
 {
   if (nrows == 0 || ncols == 0)
     throw BadSize();
 }
 
 Matrix::~Matrix()
 {
   delete[] data_;
 }

Zwróć uwagę, że dzięki klasie Matrix udało się nam osiągnąć dwie rzeczy: przenieśliśmy uciążliwe zarządzanie pamięcia z kodu posługującego się macierzą (np. z funkcji main) do kodu klasy, oraz zmniejszyliśmy stopień skomplikowania "właściwego programu". To drugie jest bardzo ważne. Przykładowo, zakładając nawet, że klasa Matrix jest niezbyt często wykorzystywana, przeniesienie skomplikowania kodu z [wielu] użytkowników na [pojedynczą] klasę Matrix oznacza przeniesienie ciężaru z wielu ludzi na małą garstkę. Każdy, kto widział Star Trek 2 wie, że dobro wielu przeważa nad dobrem niewielu... lub jednego.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.17] Ale klasa Matrix z poprzedniego faqa współpracuje tylko z klasą Fred! Czy nie da się zrobić tej klasy tak, aby była bardziej uniwersalna?

Owszem; po prostu użyj wzorców klas:

Możesz je zastosować w następujący sposób:

 #include "Fred.hpp"     // definicja klasy Fred
 
 
// Kod klasy Matrix<T> przedstawiono w jednej z kolejnych ramek...
 void someFunction(Fred& fred);
 
 void manipulateArray(unsigned nrows, unsigned ncols)
 {
   Matrix<Fred> matrix(nrows, ncols);   
// Utwórz macierz Matrix<Fred>
         
// o nazwie matrix
 
   for (unsigned i = 0; i < nrows; ++i) {
     for (unsigned j = 0; j < ncols; ++j) {
       
// W ten sposób odwołujemy się do elementu (i,j) macierzy:
       someFunction( matrix(i,j) );
 
       
// Możesz spokojnie wyjść z funkcji bez stosowania żadnego dodatkowego
       
// kodu usuwającego macierz:
       if (today == "Tuesday" && moon.isFull())
         return;     
// Wyjdź w każdą pełnię księżyca przypadającą na wtorek
     }
   }
 
   
// Macierzy nie trzeba usuwać jawnie na końcu funkcji
 }

Teraz z łatwością można użyć macierzy Matrix<T> do przechowywania innych obiektów niż Fred. Na przykład, w poniższym kodzie wykorzystano macierz napisów typu std::string (std::string jest standardową klasą do obsługi napisów):

 #include <string>
 
 void someFunction(std::string& s);
 
 void manipulateArray(unsigned nrows, unsigned ncols)
 {
   Matrix<std::string> matrix(nrows, ncols);   
// Skonstruuj macierz Matrix<std::string>
 
   for (unsigned i = 0; i < nrows; ++i) {
     for (unsigned j = 0; j < ncols; ++j) {
       
// W ten sposób uzyskujemy dostęp do elementu (i,j) macierzy:
       someFunction( matrix(i,j) );
 
       
// Można wyjść z funkcji bez żadnego dodatkowego kodu usuwającego
       
// macierz z pamięci
       if (today == "Tuesday" && moon.isFull())
         return;     
// Wyjście w każdą pełnię księżyca przypadającą na wtorek
     }
   }
 
   
// Macierz nie wymaga jawnego usuwania na końcu funkcji
 }

W związku z tym dzięki jednemu wzorcowi masz dostęp do całej rodziny klas, takich jak np. Matrix<Fred>, Matrix<std::string>, Matrix<Matrix<std::string> >, itd.

Poniżej przedstawiono jeden ze sposobów zaimplementowania wzorca naszej macierzy:

 template<class T>  // Patrz: rozdział dot. wzorców
 class Matrix {
 public:
   Matrix(unsigned nrows, unsigned ncols);
   
// Zgłasza wyjątek BadSize jeśli jeden z wymiarów jest równy zero
   class BadSize { };
 
   
// Zgodnie z Prawem Wielkiej Trójki [ang. Law Of The Big Three]:
  ~Matrix();
   Matrix(const Matrix<T>& m);
   Matrix<T>& operator= (const Matrix<T>& m);
 
   
// Metody dostępu, zwracające (i,j)-ty element macierzy:
   T&       operator() (unsigned i, unsigned j);
   const T& operator() (unsigned i, unsigned j) const;
   
// Zgłaszają one wyjątek BoundsViolation w przypadku gdy jedna ze
   
// współrzędnych i lub j jest zbyt duża
   class BoundsViolation { };
 
 private:
   T* data_;
   unsigned nrows_, ncols_;
 };
 
 template<class T>
 inline T& Matrix<T>::operator() (unsigned row, unsigned col)
 {
   if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
   return data_[row*ncols_ + col];
 }
 
 template<class T>
 inline const T& Matrix<T>::operator() (unsigned row, unsigned col) const
 {
   if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
   return data_[row*ncols_ + col];
 }
 
 template<class T>
 inline Matrix<T>::Matrix(unsigned nrows, unsigned ncols)
   : data_  (new T[nrows * ncols])
   , nrows_ (nrows)
   , ncols_ (ncols)
 {
   if (nrows == 0 || ncols == 0)
     throw BadSize();
 }
 
 template<class T>
 inline Matrix<T>::~Matrix()
 {
   delete[] data_;
 }

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.18] W jaki inny sposób można zaprojektować wzorzec klasy Matrix?

Można użyć standardowej klasy wzorcowej vector, i utworzyć macierz w formie wektora zawierającego kolejne wektory.

W poniższym przykładzie zastosowano klasę vector<vector<T> > (zwróć uwagę na spację między znakami >).

 #include <vector>
 
 template<class T>  
// Patrz: rozdział dot. wzorców
 class Matrix {
 public:
   Matrix(unsigned nrows, unsigned ncols);
   
// Konstruktor zgłasza wyjątek BadSize jeśli jeden z wymiarów
   
// jest równy zero
   class BadSize { };
 
   
// Tym razem nie potrzebujemy Wielkiej Trójki!
 
   
// Metody dostępu, zwracające (i,j)-ty element macierzy:
   T&       operator() (unsigned i, unsigned j);
   const T& operator() (unsigned i, unsigned j) const;
   
// Metody te zgłaszają wyjątek BoundsViolation w przypadku gdy jedna
   
// ze współrzędnych i lub j jest zbyt duża
   class BoundsViolation { };
 
 private:
   vector<vector<T> > data_;
 };
 
 template<class T>
 inline T& Matrix<T>::operator() (unsigned row, unsigned col)
 {
   if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
   return data_[row][col];
 }
 
 template<class T>
 inline const T& Matrix<T>::operator() (unsigned row, unsigned col) const
 {
   if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
   return data_[row][col];
 }
 
 template<class T>
 Matrix<T>::Matrix(unsigned nrows, unsigned ncols)
   : data_ (nrows)
 {
   if (nrows == 0 || ncols == 0)
     throw BadSize();
   for (unsigned i = 0; i < nrows; ++i)
     data_[i].resize(ncols);
 }

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.19] Czy w C++ są tablice, których wymiary można ustawić w czasie działania programu?

Tak, w tym sensie, że biblioteka standardowa zawiera wzorzec std::vector, który daje taką możliwość.

Nie, w tym sensie, że typ tablicowy wbudowany do języka wymaga, aby tablica miała swój rozmiar ustalony w trakcie kompilacji.

Tak, w tym sensie, że nawet we wbudowanych tablicach można ustawić rozmiar pierwszego wymiaru tablicy w trakcie działania programu. Nawiązując do poprzedniego faqa, jeżeli wystarczy Ci możliwość dowolnego ustalania tylko pierwszego wymiaru, to możesz po prostu poprosić new o utworzenie tablicy tablic, a nie tablicy wskaźników:

 const unsigned ncols = 100;           // ncols = liczba kolumn w tablicy
 
 class Fred { 
/*...*/ };
 
 void manipulateArray(unsigned nrows)  
// nrows = liczba wierszy w tablicy
 {
   Fred (*matrix)[ncols] = new Fred[nrows][ncols];
   
// ...
   delete[] matrix;
 }

Przy użyciu tej metody nie można zmienić niczego poza pierwszym wymiarem tablicy.

Proszę jednak, nie używaj tablic, chyba że jest to konieczne. Tablice są złe! Jeśli możesz, używaj zamiast nich obiektu jakiejś klasy.

Zamiast nich, jeśli tylko możesz, użyj obiektu jakieś klasy. Tablice stosuj tylko wtedy gdy musisz.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.20] W jaki sposób zmusić klasę, aby jej obiekty można było tworzyć wyłącznie przy użyciu operatora new, a nie deklarując je jako zmienne lokalne lub statyczne?

Można w tym celu użyć "nazwanych konstruktorów" [ang. Named Constructor Idiom].

Tak jak to ma zazwyczaj miejsce w metodzie "nazwanych konstruktorów", wszystkie prawdziwe konstruktory są prywatne lub chronione, a ponadto dodaje się jedną lub więcej metod create() (są one wspomnianymi "nazwanymi konstruktorami"), które deklaruje się jako publiczne i statyczne. W naszym przypadku, metody create() alokują obiekt operatorem new [a następnie zwracają wskaźnik - przyp. tłum.]. Ponieważ prawdziwe konstruktory nie są publiczne, nie da się w inny sposób utworzyć obiektów naszej klasy.

 class Fred {
 public:
   
// "Nazwane konstruktory" - metody create()
   static Fred* create()                 { return new Fred();     }
   static Fred* create(int i)            { return new Fred(i);    }
   static Fred* create(const Fred& fred) { return new Fred(fred); }
   
// ...
 
 private:
   
// "Właściwe konstruktory" są chronione (protected) lub prywatne (private)
   Fred();
   Fred(int i);
   Fred(const Fred& fred);
   
// ...
 };

Od tej pory obiekty klasy Fred można tworzyć tylko za pośrednictwem metody Fred::create():

 int main()
 {
   Fred* p = Fred::create(5);
   
// ...
   delete p;
 }

Jeżeli zakładasz, że z klasy Fred będą dziedziczyć inne klasy, to musisz się upewnić, że "właściwe konstruktory" są zadeklarowane jako chronione (protected).

Warto zauważyć, że można zadeklarować inną klasę, np. Wilma, jako przyjaciela (friend) klasy Fred, umożliwiając jej w ten sposób posiadanie zwykłych pól tej klasy [jest to spowodowane tym, że Wilma, jako przyjaciel klasy Fred, ma dostęp do jej wszystkich składowych, w tym również do ukrytych konstruktorów; może więc bezkarnie tworzyć jej obiekty w dowolny sposób - przyp. tłum.] -- oczywiście jednak mijałoby się to z zamierzonym celem, jakim było udostępnienie tylko jednej możliwości tworzenia obiektów klasy Fred - przy użyciu operatora new.

of Fred if you want to allow a Wilma to have a member object of class Fred, but of course this is a softening of the original goal, namely to force Fred objects to be allocated via new. -->

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.21] W jaki sposób mogę wprowadzić prosty licznik odniesień [ang. reference counting] ?

Jeżeli chcesz aby obiekt został "automagicznie" usunięty w chwili usunięcia ostatniego wskaźnika odnoszącego się do niego [czyli wtedy gdy "licznik odniesień" będzie równy zero - przyp. tłum.], to możesz zastosować coś w rodzaju poniższej klasy "inteligentnego wskaźnika" [ang. "smart pointer"]:

 // Fred.h
 
 class FredPtr;
 
 class Fred {
 public:
   
// Wszystkie konstruktory muszą ustawić count_ na 0 !
   Fred() : count_(0) 
/*...*/ { } 
   
// ...
 private:
   friend FredPtr;     
// Zaprzyjaźniona klasa
   unsigned count_;
   
// count_ musi być ustawiony na 0 przez wszystkie konstruktory
   
// count_ jest liczbą obiektów FredPtr wskazujących na ten
   
// obiekt [czyli jest licznikiem odniesień - przyp. tłum.]
 };
 
 class FredPtr {
 public:
   Fred* operator-> () { return p_; }
   Fred& operator* ()  { return *p_; }
   FredPtr(Fred* p)    : p_(p) { ++p_->count_; }  
// p nie może być NULLem
  ~FredPtr()           { if (--p_->count_ == 0) delete p_; }
   FredPtr(const FredPtr& p) : p_(p.p_) { ++p_->count_; }
   FredPtr& operator= (const FredPtr& p)
         { 
// NIE ZMIENIAJ KOLEJNOŚCI PONIŻSZYCH INSTRUKCJI!
           
// (Dzięki takiej kolejności jaka jest, poniższy kod prawidłowo
    
// obsługuje pseudo-przypisania)
           ++p.p_->count_;
           if (--p_->count_ == 0) delete p_;
           p_ = p.p_;
           return *this;
         }
 private:
   Fred* p_;    
// p_ is never NULL
 };

Oczywiście możesz utworzyć zagnieżdżoną klasę, żeby zamiast FredPtr używać nazwy w stylu Fred::Ptr.

Zauważ, że można złagodzić obowiązującą w powyższym kodzie zasadę, że "p nie może być NULLem", jeżeli doda się sprawdzanie argumentu w konstruktorze (zarówno zwykłym jak i kopiującym), operatorze przypisania oraz w destruktorze. Jeśli tak postąpisz, to możesz również umieścić sprawdzanie czy p_ != NULL w definicjach operatorów "*" i "->" (przynajmniej z zastosowaniem makra assert()). Odradzałbym natomiast definiowania metody operator Fred*() [operator rzutowania na wskaźnik do Freda - przyp. tłum.], gdyż na skutek jej użycia użytkownik mógłby uzyskać zwykły wskaźnik Fred* [znajdujący się poza kontrolą mechanizmu zliczającego odwołania do obiektu - przyp. tłum.].

Jednym z ograniczeń zastosowań klasy FredPtr, wynikających z jej działania, jest to że może ona wskazywać tylko na te obiekty klasy Fred które utworzono przy użyciu operatora new. Jeżeli chcesz aby Twój kod był naprawdę bezpieczny, możesz wymusić na klasie Fred dostosowanie się do tego ograniczenia, deklarując wszystkie jej konstruktory jako prywatne oraz definiując dla każdego konstruktora odpowiadającą mu publiczną i statyczną metodę create(), która tworzyłaby obiekt operatorem new i zwracała wskaźnik FredPtr (nie Fred*) do niego. Dzięki temu jedynym sposobem utworzenia obiektu Fred jest uzyskanie wskaźnika FredPtr (należy wtedy zamienić zapis "Fred* p = new Fred()" na "FredPtr p = Fred::create()"). Uniemożliwiłoby to komukolwiek obejście mechanizmu zliczającego odwołania do obiektu.

Przykładowo, jeśli Fred zawiera dwa konstruktory - Fred::Fred() i Fred::Fred(int i, int j) - to zmiany w deklaracji klasy wyglądałyby następująco:

 class Fred {
 public:
   static FredPtr create();              
// Zdefiniowany poniżej class FredPtr {...}
   static FredPtr create(int i, int j);  
// Zdefiniowany poniżej class FredPtr {...}
   
// ...
 private:
   Fred();
   Fred(int i, int j);
   
// ...
 };
 
 class FredPtr { 
/* ... */ };
 
 inline FredPtr Fred::create()             { return new Fred(); }
 inline FredPtr Fred::create(int i, int j) { return new Fred(i,j); }

W końcowym rezultacie otrzymaliśmy prosty mechanizm liczący odwołania do obiektu, oparty na "składni wskaźnikowej". Użytkownicy klasy Fred muszą jawnie stosować obiekty FredPtr, które zachowują się mniej więcej jak zwykłe wskaźniki Fred*. Zaletą tego rozwiązania jest to, że użytkownicy mogą utworzyć dowolną ilość "inteligentnych wskaźników" FredPtr, a obiekt wskazywany przez nie zostanie "automagicznie" usunięty wraz ze zniknięciem ostatniego z tych wskaźników.

Jeżeli wolał(a)byś zastosować "składnię referencyjną" zamiast "składni wskaźnikowej" to możesz użyć licznika odniesień typu "copy on write".

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.22] W jaki sposób mogę wprowadzić licznik odniesień typu "copy-on-write"?

Licznik odniesień może stosować zarówno "składnię wskaźnikową" jak i "składnię referencyjną". W poprzednim faqu przedstawiono licznik odniesień korzystający ze "składni wskaźnikowej". Ten faq ma na celu zaprezentowanie licznika odniesień ze "składnią referencyjną".

Pomysł polega na tym, aby użytkownikowi wydawało się, że kopiuje jeden obiekt klasy Fred do drugiego, podczas gdy w rzeczywistości żadne kopiowanie się nie odbywa aż do czasu, gdy nastąpi próba modyfikacji obiektu.

Klasa Fred::Data przechowuje wszystkie dane, które normalnie byłyby umieszczone od razu w klasie Fred. Fred::Data ma również dodatkowe pole, count_, zawierające licznik odniesień. Natomiast sama klasa Fred staje się "inteligentną referencją", która (wewnętrznie) odnosi się do obiektu klasy Fred::Data.

 class Fred {
 public:
 
   Fred();                               
// Domyślny konstruktor
   Fred(int i, int j);                   
// Zwykły konstruktor
 
   Fred(const Fred& f);
   Fred& operator= (const Fred& f);
  ~Fred();
 
   void sampleInspectorMethod() const;   
// Metoda niezmieniająca obiektu
   void sampleMutatorMethod();           
// Metoda zmieniająca obiekt
 
   
// ...
 
 private:
 
   class Data {
   public:
     Data();
     Data(int i, int j);
     Data(const Data& d);
 
     
// Ponieważ tylko klasa Fred ma dostęp do Fred::Data, stąd
     
// jeśli chcesz, możesz zadeklarować wszystkie dane klasy
     
// Fred::Data jako publiczne. Jeżeli jednak takie ustawienie
     
// sprawiałoby Ci pewien dyskomfort, to możesz zadeklarować dane jako
     
// prywatne oraz uczynić Fredklasą zaprzyjaźnioną
     
// przy użyciu instrukcji friend Fred;
     
     
...tu trafiają dane klasy...
 
     unsigned count_;
     
// count_ jest licznikiem obiektów Fred wskazujących na ten
     
// obiekt.
     
// count_ musi być inicjowany na 1 przez każdy konstruktor
     
// (licznik zaczyna od 1, ponieważ dane są już wskazywane przez ten obiekt
     
// klasy Fred, który je utworzył)
   };
 
   Data* data_;
 };
 
 Fred::Data::Data()              : count_(1) 
/*inicjalizacja reszty danych*/ { }
 Fred::Data::Data(int i, int j)  : count_(1) 
/*inicjalizacja reszty danych*/ { }
 Fred::Data::Data(const Data& d) : count_(1) 
/*inicjalizacja reszty danych*/ { }
 
 Fred::Fred()             : data_(new Data()) { }
 Fred::Fred(int i, int j) : data_(new Data(i, j)) { }
 
 Fred::Fred(const Fred& f)
   : data_(f.data_)
 {
   ++ data_->count_;
 }
 
 Fred& Fred::operator= (const Fred& f)
 {
   
// NIE ZMIENIAJ KOLEJNOŚCI PONIŻSZYCH INSTRUKCJI
   
// (Taka kolejność zapewnia prawidłową obsługę pseudo-przypisań)
   ++ f.data_->count_;
   if (--data_->count_ == 0) delete data_;
   data_ = f.data_;
   return *this;
 }
 
 Fred::~Fred()
 {
   if (--data_->count_ == 0) delete data_;
 }
 
 void Fred::sampleInspectorMethod() const
 {
   
// Ta metoda zapewnia ("const") że nie zmieni ona czegokolwiek w danych
   
// (*data_). 
   
 }
 
 void Fred::sampleMutatorMethod()
 {
   
// Z kolei ta metoda może chcieć coś pozmieniać w *data_
   
// Stąd musi ona najpierw sprawdzić czy this jest jedynym wskaźnikiem do
   
// *data_
   if (data_->count_ > 1) {
     Data* d = new Data(*data_);    
// Wywołaj konstruktor kopiujący Fred::Data
     -- data_->count_;
     data_ = d;
   }
   assert(data_->count_ == 1);
 
   
// Od tego miejsca można już się normalnie odwoływać do "data_->..."
 }

Jeżeli wywołania domyślnego konstruktora klasy Fred zdarzają się dość często, możesz uniknąć ciągłego alokowania nowych obiektów operatorem new - wystarczy, że wszystkie obiekty klasy Fred konstruowane przy użyciu Fred::Fred() będą współdzieliły pewien wspólny obiekt klasy Fred::Data. Aby uniknąć problemu kolejności inicjalizacji statycznych obiektów, ten współdzielony obiekt jest tworzony wewnątrz funkcji, przy pierwszej próbie jego użycia. Poniżej przedstawiono zmiany jakie można wprowadzić do wcześniejszego kodu (zwróć uwagę, że destruktor wspólnego obiektu Fred::Data nigdy nie zostanie wywołany; jeżeli stanowi to dla Ciebie problem to możesz albo mieć nadzieję, że nie wystąpi problem kolejności inicjalizacji obiektów statycznych, albo możesz powrócić do poprzedniego rozwiązania).

 class Fred {
 public:
   
// ...
 private:
   
// ...
   static Data* defaultData();
 };
 
 Fred::Fred()
 : data_(defaultData())
 {
   ++ data_->count_;
 }
 
 Fred::Data* Fred::defaultData()
 {
   static Data* p = NULL;
   if (p == NULL) {
     p = new Data();
     ++ p->count_; 
// Zapewnia, że licznik nigdy nie spadnie do zera
   }
   return p;
 }

Notka: Możesz również wprowadzić licznik odniesień dla całej hierarchii klas, jeżeli Twoja klasa Fred ma być bazowa dla innych klas.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.23] W jaki sposób mogę wprowadzić licznik odniesień typu "copy-on-write" dla całej hierarchii klas ?

W poprzednim faqu przedstawiono już licznik odniesień ze "składnią referencyjną" - działa on jednak tylko w obrębie jednej klasy, a nie całej hierarchii. Ten faq opisuje jak rozszerzyć wspomnianą technikę na całą rodzinę klas. Główna różnica polega na tym, że klasa Fred::Data stanowi teraz wierzchołek hierarchii, co oznacza, że prawdopodobnie będzie ona musiała zawierać również pewne wirtualne metody. W dalszej części tego faqa zwróć uwagę, że klasa Fred sama w sobie wciąż nie będzie zawierała żadnych wirtualnych metod.

Kopiowanie danych obiektu (klasa Fred::Data) odbywa się przy użyciu wirtualnego konstruktora [ang. Virtual Constructor Idiom]. Wyboru klasy do której ma należeć tworzony obiekt dokonuje się, wywołując jeden z "nazwanych konstruktorów" [ang. Named Constructor Idiom], można jednak stosować również inne techniki (np. instrukcja switch wewnątrz konstruktora). The sample code assumes two derived classes: Der1 and Der2. Methods in the derived classes are unaware of the reference counting.

 class Fred {
 public:
 
   static Fred create1(const std::string& s, int i);
   static Fred create2(float x, float y);
 
   Fred(const Fred& f);
   Fred& operator= (const Fred& f);
  ~Fred();
 
   void sampleInspectorMethod() const;   
// No changes to this object
   void sampleMutatorMethod();           
// Change this object
 
   
// ...
 
 private:
 
   class Data {
   public:
     Data() : count_(1) { }
     Data(const Data& d) : count_(1) { }              
// Do NOT copy the 'count_' member!
     Data& operator= (const Data&) { return *this; }  
// Do NOT copy the 'count_' member!
     virtual ~Data() { assert(count_ == 0); }         
// A virtual destructor
     virtual Data* clone() const = 0;                 
// A virtual constructor
     virtual void sampleInspectorMethod() const = 0;  
// A pure virtual function
     virtual void sampleMutatorMethod() = 0;
   private:
     unsigned count_;   
// count_ doesn't need to be protected
     friend Fred;       
// Allow Fred to access count_
   };
 
   class Der1 : public Data {
   public:
     Der1(const std::string& s, int i);
     virtual void sampleInspectorMethod() const;
     virtual void sampleMutatorMethod();
     virtual Data* clone() const;
     
// ...
   };
 
   class Der2 : public Data {
   public:
     Der2(float x, float y);
     virtual void sampleInspectorMethod() const;
     virtual void sampleMutatorMethod();
     virtual Data* clone() const;
     
// ...
   };
 
   Fred(Data* data);
   
// Creates a Fred smart-reference that owns *data
   
// It is private to force users to use a createXXX() method
   
// Requirement: data must not be NULL
 
   Data* data_;   
// Invariant: data_ is never NULL
 };
 
 Fred::Fred(Data* data) : data_(data)  { assert(data != NULL); }
 
 Fred Fred::create1(const std::string& s, int i) { return Fred(new Der1(s, i)); }
 Fred Fred::create2(float x, float y)            { return Fred(new Der2(x, y)); }
 
 Fred::Data* Fred::Der1::clone() const { return new Der1(*this); }
 Fred::Data* Fred::Der2::clone() const { return new Der2(*this); }
 
 Fred::Fred(const Fred& f)
   : data_(f.data_)
 {
   ++ data_->count_;
 }
 
 Fred& Fred::operator= (const Fred& f)
 {
   
// DO NOT CHANGE THE ORDER OF THESE STATEMENTS!
   
// (This order properly handles self-assignment)
   ++ f.data_->count_;
   if (--data_->count_ == 0) delete data_;
   data_ = f.data_;
   return *this;
 }
 
 Fred::~Fred()
 {
   if (--data_->count_ == 0) delete data_;
 }
 
 void Fred::sampleInspectorMethod() const
 {
   
// This method promises ("const") not to change anything in *data_
   
// Therefore we simply "pass the method through" to *data_:
   data_->sampleInspectorMethod();
 }
 
 void Fred::sampleMutatorMethod()
 {
   
// This method might need to change things in *data_
   
// Thus it first checks if this is the only pointer to *data_
   if (data_->count_ > 1) {
     Data* d = data_->clone();   
// The Virtual Constructor Idiom
     -- data_->count_;
     data_ = d;
   }
   assert(data_->count_ == 1);
 
   
// Now we "pass the method through" to *data_:
   data_->sampleInspectorMethod();
 }

Naturally the constructors and sampleXXX methods for Fred::Der1 and Fred::Der2 will need to be implemented in whatever way is appropriate.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.24] Can you absolutely prevent people from subverting the reference counting mechanism, and if so, should you?

No, and (normally) no.

There are two basic approaches to subverting the reference counting mechanism:

  1. The scheme could be subverted if someone got a Fred* (rather than being forced to use a FredPtr). Someone could get a Fred* if class FredPtr has an operator*() that returns a Fred&: FredPtr p = Fred::create(); Fred* p2 = &*p;. Yes it's bizarre and unexpected, but it could happen. This hole could be closed in two ways: overload Fred::operator&() so it returns a FredPtr, or change the return type of FredPtr::operator*() so it returns a FredRef (FredRef would be a class that simulates a reference; it would need to have all the methods that Fred has, and it would need to forward all those method calls to the underlying Fred object; there might be a performance penalty for this second choice depending on how good the compiler is at inlining methods). Another way to fix this is to eliminate FredPtr::operator*() — and lose the corresponding ability to get and use a Fred&. But even if you did all this, someone could still generate a Fred* by explicitly calling operator->(): FredPtr p = Fred::create(); Fred* p2 = p.operator->();.
  2. The scheme could be subverted if someone had a leak and/or dangling pointer to a FredPtr Basically what we're saying here is that Fred is now safe, but we somehow want to prevent people from doing stupid things with FredPtr objects. (And if we could solve that via FredPtrPtr objects, we'd have the same problem again with them). One hole here is if someone created a FredPtr using new, then allowed the FredPtr to leak (worst case this is a leak, which is bad but is usually a little better than a dangling pointer). This hole could be plugged by declaring FredPtr::operator new() as private, thus preventing someone from saying new FredPtr(). Another hole here is if someone creates a local FredPtr object, then takes the address of that FredPtr and passed around the FredPtr*. If that FredPtr* lived longer than the FredPtr, you could have a dangling pointer — shudder. This hole could be plugged by preventing people from taking the address of a FredPtr (by overloading FredPtr::operator&() as private), with the corresponding loss of functionality. But even if you did all that, they could still create a FredPtr& which is almost as dangerous as a FredPtr*, simply by doing this: FredPtr p; ... FredPtr& q = p; (or by passing the FredPtr& to someone else).

And even if we closed all those holes, C++ has those wonderful pieces of syntax called pointer casts. Using a pointer cast or two, a sufficiently motivated programmer can normally create a hole that's big enough to drive a proverbial truck through. (By the way, pointer casts are evil.)

So the lessons here seems to be: (a) you can't prevent espionage no matter how hard you try, and (b) you can easily prevent mistakes.

So I recommend settling for the "low hanging fruit": use the easy-to-build and easy-to-use mechanisms that prevent mistakes, and don't bother trying to prevent espionage. You won't succeed, and even if you do, it'll (probably) cost you more than it's worth.

So if we can't use the C++ language itself to prevent espionage, are there other ways to do it? Yes. I personally use old fashioned code reviews for that. And since the espionage techniques usually involve some bizarre syntax and/or use of pointer-casts and unions, you can use a tool to point out most of the "hot spots."

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.25] Can I use a garbage collector in C++?

Yes.

Compared with the "smart pointer" techniques (see [16.21], the two kinds of garbage collector techniques (see [16.26]) are:

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.26] What are the two kinds of garbage collectors for C++?

In general, there seem to be two flavors of garbage collectors for C++:

  1. Conservative garbage collectors. These know little or nothing about the layout of the stack or of C++ objects, and simply look for bit patterns that appear to be pointers. In practice they seem to work with both C and C++ code, particularly when the average object size is small. Here are some examples, in alphabetical order:

  2. Hybrid garbage collectors. These usually scan the stack conservatively, but require the programmer to supply layout information for heap objects. This requires more work on the programmer's part, but may result in improved performance. Here are some examples, in alphabetical order:

Since garbage collectors for C++ are normally conservative, they can sometimes leak if a bit pattern "looks like" it might be a pointer to an otherwise unused block. Also they sometimes get confused when pointers to a block actually point outside the block's extent (which is illegal, but some programmers simply must push the envelope; sigh) and (rarely) when a pointer is hidden by a compiler optimization. In practice these problems are not usually serious, however providing the collector with hints about the layout of the objects can sometimes ameliorate these issues.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


[16.27] Where can I get more info on garbage collectors for C++?

For more information, see the Garbage Collector FAQ.

GóraDółPoprzednia sekcjaNastępna sekcjaSzukaj w FAQ ]


E-Mail E-mail the author
C++ FAQ LiteSpis treściSkorowidzO autorze©Pobierz swoją własną kopię ]
Ostatnia aktualizacja Jun 17, 2002
Wersja polska: 0.1i Jul 12, 2004