FAQ - sekcja [16]:
- [16.1] Co usuwa instrukcja delete p: wskaźnik p,
czy dane wskazywane przez p?
- [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?
- [16.3] Dlaczego powinno się używać operatora new zamiast
starego, dobrego malloc()?
- [16.4] Czy mogę wywołać funkcję realloc() dla wskaźników
utworzonych przez operator new?
- [16.5] Czy przy każdym użyciu operatora new muszę
sprawdzać, czy nie zwrócił on wartości NULL?
- [16.6] W jaki sposób mogę przekonać mój (stary)
kompilator, aby automatycznie sprawdzał, czy operator new nie zwrócił
wskaźnika NULL?
- [16.7] Czy przed instrukcją delete p musze sprawdzać, czy
wskaźnik nie jest równy NULL ?
- [16.8] Z jakich etapów składa się wykonanie instrukcji
delete p?
- [16.9] Czy w instrukcji p = new Fred()
może nastąpić "wyciek pamięci", jeśli konstruktor klasy Fred rzuci wyjątek?
- [16.10] W jaki sposób mogę alokować / zwalniać tablice obiektów?
- [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?
- [16.12] Czy mogę opuścić nawiasy [] w operatorze
delete, gdy chcę skasować tablicę obiektów typu prostego (np. char,
int)?
- [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?
- [16.14] Czy legalnym (i moralnym) jest umieszczenie w treści metody
instrukcji delete this?
- [16.15] Jak, przy użyciu operatora new, mogę alokować tablicę
wielowymiarową?
- [16.16] Ale kod w poprzednim faqu jest taki skomplikowany i
podatny na błędy! Nie ma jakiegoś prostszego sposobu na to?
- [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?
- [16.18] W jaki inny sposób można zaprojektować wzorzec klasy
Matrix?
- [16.19] Czy w C++ są tablice, których wymiary można ustawić w
czasie działania programu?
- [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?
- [16.21] W jaki sposób mogę wprowadzić prosty licznik odniesień
[ang. reference counting] ?
- [16.22] W jaki sposób mogę wprowadzić licznik odniesień typu
"copy-on-write"?
- [16.23] W jaki sposób mogę wprowadzić licznik
odniesień typu "copy-on-write" dla całej hierarchii klas ?
- [16.24] Can you absolutely prevent people
from subverting the reference counting mechanism, and if so, should
you?
- [16.25] Can I use a garbage collector in C++?
- [16.26] What are the two kinds of garbage collectors for
C++?
- [16.27] Where can I get more info on garbage
collectors for C++?
[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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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.
- Konstruktory/destruktory: w odróżnieniu od
malloc(sizeof(Fred)), new Fred() automatycznie wywołuje
konstruktor klasy Fred. Analogicznie, delete p wywołuje destruktor obiektu
*p [wskazywanego przez wskaźnik p - przyp. tłum.].
- Lepsza kontrola typów: malloc() zwraca wskaźnik typu void*,
który nie jest bezpieczny pod względem kontroli typów [można go przekształcić
do dowolnego typu wskaźnikowego - przyp. tłum.]. Natomiast new Fred()
zwraca wskaźnik zgodny z typem tworzonego obiektu (Fred*).
- Możliwość przeciążania: new jest operatorem, dzięki czemu może
zostać przeciążony wewnątrz klasy; natomiast malloc()'a nie da się
przeciążać osobno dla każdej klasy.
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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:
- 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!).
- 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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:
- 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.
- 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.
- 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).
- 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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ć Freda klasą 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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:
- 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->();.
- 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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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:
- less portable
- usually more efficient (especially when the average object size is
small or in multithreaded environments)
- able to handle "cycles" in the data (reference counting techniques
normally "leak" if the data structures can form a cycle)
- sometimes leak other objects (since the garbage collectors are
necessarily conservative, they sometimes see a random bit pattern that appears
to be a pointer into an allocation, especially if the allocation is large;
this can allow the allocation to leak)
- work better with existing libraries (since smart pointers need to
be used explicitly, they may be hard to integrate with existing
libraries)
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj 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++:
-
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:
-
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óra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]
[16.27] Where can I get more info on garbage
collectors for C++?
For more information, see the
Garbage Collector FAQ.
[ Góra | Dół | Poprzednia sekcja | Następna sekcja | Szukaj w FAQ ]
E-mail the author
[ C++ FAQ Lite
| Spis treści
| Skorowidz
| O autorze
| ©
| Pobierz swoją własną kopię ]
Ostatnia aktualizacja Jun 17, 2002
Wersja polska: 0.1i Jul 12, 2004