C++. Kruczki i fortele w programowaniu

29
Wydawnictwo Helion ul. Chopina 6 44-100 Gliwice tel. (32)230-98-63 e-mail: [email protected] PRZYK£ADOWY ROZDZIA£ PRZYK£ADOWY ROZDZIA£ IDZ DO IDZ DO ZAMÓW DRUKOWANY KATALOG ZAMÓW DRUKOWANY KATALOG KATALOG KSI¥¯EK KATALOG KSI¥¯EK TWÓJ KOSZYK TWÓJ KOSZYK CENNIK I INFORMACJE CENNIK I INFORMACJE ZAMÓW INFORMACJE O NOWOCIACH ZAMÓW INFORMACJE O NOWOCIACH ZAMÓW CENNIK ZAMÓW CENNI K CZYTELNIA CZYTELNIA FRAGMENTY KSI¥¯EK ONLINE FRAGMENTY KSI¥¯EK ONLINE SPIS TRECI SPIS TRECI DODAJ DO KOSZYKA DODAJ DO KOSZYKA KATALOG ONLINE KATALOG ONLINE C++. Kruczki i fortele w programowaniu Autor: Stephen C. Dewhurst T³umaczenie: Tomasz ¯mijewski ISBN: 83-7361-346-3 Tytu³ orygina³u: C++ Gotchas: Avoiding Common Problems in Coding and Design Format: B5, stron: 266 Przyk³ady na ftp: 28 kB „C++. Kruczki i fortele w programowaniu” to pomoc dla zawodowych programistów pozwalaj¹ca unikn¹æ lub poprawiæ dziewiêædziesi¹t dziewiêæ najczêciej pope³nianych i najbardziej szkodliwych b³êdów projektowych i programowych w C++. Jest to te¿ ksi¹¿ka, dziêki której mo¿na poznaæ niektóre niestandardowe cechy jêzyka C++ i techniki programistyczne. W ksi¹¿ce omówiono typowe b³êdy wystêpuj¹ce niemal¿e we wszystkich programach utworzonych w C++. Ka¿dy z nich zosta³ starannie opisany, przedstawiono równie¿ konsekwencje wynikaj¹ce z ich pojawienia siê w kodzie programu i szczegó³owy opis sposobów na ich unikniêcie. „C++. Kruczki i fortele w programowaniu” to ksi¹¿ka o tym, jak unikn¹æ najwiêkszych zagro¿eñ zwi¹zanych z programowaniem w C++. Gotowa i praktyczna wiedza dla programistów, która pozwoli im uzyskaæ status ekspertów. Omówione b³êdy dotycz¹: • Podstaw jêzyka C++ • Sk³adni jêzyka • Preprocesora • Konwersji • Inicjalizacji • Zarz¹dzania pamiêci¹ i zasobami • Polimorfizmu • Projektowania klas • Projektowania hierarchii Stephen C. Dewhurst by³ jednym z pierwszych u¿ytkowników C++ w Bell Labs. Ma ponad osiemnacie lat dowiadczenia w stosowaniu jêzyka C++ do takich zagadnieñ, jak projektowanie kompilatorów, zabezpieczenia, handel elektroniczny oraz elementy telekomunikacji wbudowane w wiêksze systemy. Jest wspó³autorem wydanej przez Prentice Hall w 1989 roku ksi¹¿ki „Programming in C++”, redaktorem „C/C++ Users Journal”, poza tym prowadzi³ kolumnê w „C++ Report”. Jest autorem dwóch kompilatorów C++, napisa³ wiele artyku³ów o budowie kompilatorów i technikach programowania w C++.

description

"C++. Kruczki i fortele w programowaniu" to pomoc dla zawodowych programistów pozwalająca uniknąć lub poprawić dziewięćdziesiąt dziewięć najczęściej popełnianych i najbardziej szkodliwych błędów projektowych i programowych w C++. Jest to też książka, dzięki której można poznać niektóre niestandardowe cechy języka C++ i techniki programistyczne.W książce omówiono typowe błędy występujące niemalże we wszystkich programach utworzonych w C++. Każdy z nich został starannie opisany, przedstawiono również konsekwencje wynikające z ich pojawienia się w kodzie programu i szczegółowy opis sposobów na ich uniknięcie. "C++. Kruczki i fortele w programowaniu” to książka o tym, jak uniknąć największych zagrożeń związanych z programowaniem w C++. Gotowa i praktyczna wiedza dla programistów, która pozwoli im uzyskać status ekspertów.Omówione błędy dotyczą:* Podstaw języka C++* Składni języka* Preprocesora* Konwersji* Inicjalizacji* Zarządzania pamięcią i zasobami* Polimorfizmu* Projektowania klas* Projektowania hierarchii O autorze: Stephen C. Dewhurst był jednym z pierwszych użytkowników języka C++ w laboratoriach Bell Labs. Ma ponad dwudziestoletnie doświadczenie w stosowaniu C++ do rozwiązywania problemów w takich dziedzinach, jak projektowanie kompilatorów, zabezpieczanie handlu elektronicznego czy telekomunikacja implementowana na bazie urządzeń wbudowanych [więcej...]

Transcript of C++. Kruczki i fortele w programowaniu

Page 1: C++. Kruczki i fortele w programowaniu

Wydawnictwo Helionul. Chopina 644-100 Gliwicetel. (32)230-98-63e-mail: [email protected]

PRZYK£ADOWY ROZDZIA£PRZYK£ADOWY ROZDZIA£

IDZ DOIDZ DO

ZAMÓW DRUKOWANY KATALOGZAMÓW DRUKOWANY KATALOG

KATALOG KSI¥¯EKKATALOG KSI¥¯EK

TWÓJ KOSZYKTWÓJ KOSZYK

CENNIK I INFORMACJECENNIK I INFORMACJE

ZAMÓW INFORMACJEO NOWO�CIACH

ZAMÓW INFORMACJEO NOWO�CIACH

ZAMÓW CENNIKZAMÓW CENNIK

CZYTELNIACZYTELNIA

FRAGMENTY KSI¥¯EK ONLINEFRAGMENTY KSI¥¯EK ONLINE

SPIS TRE�CISPIS TRE�CI

DODAJ DO KOSZYKADODAJ DO KOSZYKA

KATALOG ONLINEKATALOG ONLINE

C++. Kruczki i fortelew programowaniuAutor: Stephen C. DewhurstT³umaczenie: Tomasz ¯mijewskiISBN: 83-7361-346-3Tytu³ orygina³u: C++ Gotchas: AvoidingCommon Problems in Coding and DesignFormat: B5, stron: 266Przyk³ady na ftp: 28 kB

„C++. Kruczki i fortele w programowaniu” to pomoc dla zawodowych programistów pozwalaj¹ca unikn¹æ lub poprawiæ dziewiêædziesi¹t dziewiêæ najczê�ciej pope³nianych i najbardziej szkodliwych b³êdów projektowych i programowych w C++. Jest to te¿ ksi¹¿ka, dziêki której mo¿na poznaæ niektóre niestandardowe cechy jêzyka C++ i techniki programistyczne.

W ksi¹¿ce omówiono typowe b³êdy wystêpuj¹ce niemal¿e we wszystkich programach utworzonych w C++. Ka¿dy z nich zosta³ starannie opisany, przedstawiono równie¿ konsekwencje wynikaj¹ce z ich pojawienia siê w kodzie programu i szczegó³owy opis sposobów na ich unikniêcie. „C++. Kruczki i fortele w programowaniu” to ksi¹¿ka o tym, jak unikn¹æ najwiêkszych zagro¿eñ zwi¹zanych z programowaniem w C++. Gotowa i praktyczna wiedza dla programistów, która pozwoli im uzyskaæ status ekspertów.

Omówione b³êdy dotycz¹:

• Podstaw jêzyka C++ • Sk³adni jêzyka • Preprocesora • Konwersji • Inicjalizacji • Zarz¹dzania pamiêci¹ i zasobami • Polimorfizmu • Projektowania klas • Projektowania hierarchii

Stephen C. Dewhurst by³ jednym z pierwszych u¿ytkowników C++ w Bell Labs. Ma ponad osiemna�cie lat do�wiadczenia w stosowaniu jêzyka C++ do takich zagadnieñ, jak projektowanie kompilatorów, zabezpieczenia, handel elektroniczny oraz elementy telekomunikacji wbudowane w wiêksze systemy. Jest wspó³autorem wydanej przez Prentice Hall w 1989 roku ksi¹¿ki „Programming in C++”, redaktorem „C/C++ Users Journal”, poza tym prowadzi³ kolumnê w „C++ Report”. Jest autorem dwóch kompilatorów C++, napisa³ wiele artyku³ów o budowie kompilatorów i technikach programowania w C++.

Page 2: C++. Kruczki i fortele w programowaniu

Spis treści

Wstęp .................................................................................................... 9

Podstawy ............................................................................................. 13Zagadnienie 1: nadmiar komentarzy .................................................................................13

Zagadnienie 2: magiczne liczby........................................................................................16

Zagadnienie 3: zmienne globalne......................................................................................17

Zagadnienie 4: nierozróżnianie przeciążenia od inicjalizacji domyślnej..........................19

Zagadnienie 5: niezrozumienie referencji .........................................................................21

Zagadnienie 6: niezrozumienie deklaracji const ...............................................................24

Zagadnienie 7: nieznajomość podstaw języka ..................................................................25

Zagadnienie 8: nierozróżnianie dostępności i widoczności ..............................................29

Zagadnienie 9: błędy językowe.........................................................................................32

Zagadnienie 10: nieznajomość idiomów...........................................................................34

Zagadnienie 11: zbędna błyskotliwość .............................................................................37

Zagadnienie 12: dorosłe zachowania ................................................................................39

Składnia............................................................................................... 41Zagadnienie 13: pomylenie inicjalizacji z użyciem tablicy ..............................................41

Zagadnienie 14: nieokreślona kolejność wartościowania .................................................42

Zagadnienie 15: problemy z priorytetami .........................................................................47

Zagadnienie 16: przebojowa instrukcja for .......................................................................49

Zagadnienie 17: problemy największego kęsa..................................................................52

Zagadnienie 18: zmiana kolejności elementów deklaracji................................................53

Zagadnienie 19: nierozróżnianie funkcji i obiektu............................................................55

Zagadnienie 20: migracja kwalifikatorów typu ................................................................55

Zagadnienie 21: samoinicjalizacja ....................................................................................56

Zagadnienie 22: typy static i extern ..................................................................................58

Zagadnienie 23: anomalia szukania funkcji operatora......................................................58

Zagadnienie 24: specyfika operatora -> ............................................................................60

Preprocesor.......................................................................................... 63Zagadnienie 25: literały w #define....................................................................................63

Zagadnienie 26: pseudofunkcje w #define........................................................................65

Zagadnienie 27: nadużywanie #if .....................................................................................68

Zagadnienie 28: efekty uboczne w asercjach....................................................................72

Page 3: C++. Kruczki i fortele w programowaniu

6 C++. Kruczki i fortele w programowaniu

Konwersje ............................................................................................ 75Zagadnienie 29: konwersja przez void *...........................................................................75

Zagadnienie 30: szatkowanie ............................................................................................78

Zagadnienie 31: niezrozumienie konwersji wskaźnika na stałą........................................80

Zagadnienie 32: niezrozumienie konwersji wskaźnika na wskaźnik na stałą...................80

Zagadnienie 33: niezrozumienie konwersji wskaźników wskaźników klas bazowych....84

Zagadnienie 34: problemy ze wskaźnikami tablic wielowymiarowych ...........................84

Zagadnienie 35: niesprawdzone rzutowanie w dół ...........................................................86

Zagadnienie 36: nieprawidłowe użycie operatorów konwersji.........................................87

Zagadnienie 37: niezamierzona konwersja konstruktora ..................................................90

Zagadnienie 38: rzutowanie w przypadku wielokrotnego dziedziczenia..........................93

Zagadnienie 39: rzutowanie niepełnych typów.................................................................94

Zagadnienie 40: rzutowanie w starym stylu......................................................................96

Zagadnienie 41: rzutowanie statyczne ..............................................................................97

Zagadnienie 42: inicjalizacja parametrów formalnych tymczasowymi wartościami .......99

Zagadnienie 43: czas życia wartości tymczasowych ......................................................102

Zagadnienie 44: referencje i obiekty tymczasowe ..........................................................104

Zagadnienie 45: niejednoznaczność dynamic_cast.........................................................107

Zagadnienie 46: niezrozumienie przeciwwariancji.........................................................110

Inicjalizacja ........................................................................................ 115Zagadnienie 47: mylenie przypisania i inicjalizacji ........................................................115

Zagadnienie 48: nieprawidłowy zasięg zmiennych ........................................................118

Zagadnienie 49: niedocenianie operacji kopiowania w C++ ..........................................120

Zagadnienie 50: bitowe kopiowanie obiektów................................................................124

Zagadnienie 51: myląca inicjalizacja i przypisania w konstruktorach............................126

Zagadnienie 52: zmienna kolejność pól na liście inicjalizacji ........................................128

Zagadnienie 53: inicjalizacja domyślna bazowej klasy wirtualnej .................................129

Zagadnienie 54: inicjalizacja klasy bazowej konstruktora kopiującego .........................133

Zagadnienie 55: kolejność inicjalizacji danych statycznych w programie .....................135

Zagadnienie 56: inicjalizacja bezpośrednia a kopiowanie ..............................................137

Zagadnienie 57: bezpośrednia inicjalizacja parametrów ................................................140

Zagadnienie 58: nieznajomość optymalizacji wartości zwracanych...............................142

Zagadnienie 59: inicjalizacja pola statycznego w konstruktorze ....................................145

Zarządzanie pamięcią i zasobami ........................................................ 149Zagadnienie 60: nieodróżnianie alokacji danej typu nietablicowego i tablicy ...............149

Zagadnienie 61: sprawdzanie alokacji pamięci...............................................................152

Zagadnienie 62: zastąpienie globalnych new i delete .....................................................154

Zagadnienie 63: mylenie zakresu i wywołanie metod new i delete ................................157

Zagadnienie 64: wyrzucanie literałów łańcuchowych ....................................................158

Zagadnienie 65: nieprawidłowe korzystanie z mechanizmu wyjątków..........................160

Zagadnienie 66: nadużywanie adresów lokalnych..........................................................163

Zagadnienie 67: błąd pozyskania zasobu bierze się z inicjalizacji .................................167

Zagadnienie 68: nieprawidłowe użycie auto_ptr ............................................................171

Polimorfizm ........................................................................................ 175Zagadnienie 69: kody typów...........................................................................................175

Zagadnienie 70: niewirtualny destruktor klasy bazowej.................................................179

Zagadnienie 71: ukrywanie funkcji niewirtualnych........................................................183

Zagadnienie 72: zbyt elastyczne korzystanie z wzorca Template Method .....................186

Zagadnienie 73: przeciążanie funkcji wirtualnych..........................................................187

Zagadnienie 74: funkcje wirtualne z inicjalizacją parametrów domyślnych ..................188

Zagadnienie 75: funkcje wirtualne w konstruktorach i destruktorach ............................190

Page 4: C++. Kruczki i fortele w programowaniu

Spis treści 7

Zagadnienie 76: przypisanie wirtualne ...........................................................................192

Zagadnienie 77: nierozróżnianie przeciążania, nadpisywania i ukrywania ....................195

Zagadnienie 78: funkcje wirtualne i nadpisywanie.........................................................199

Zagadnienie 79: dominacja .............................................................................................204

Projekt klas ....................................................................................... 207Zagadnienie 80: interfejsy get/set ...................................................................................207

Zagadnienie 81: pola stałe i referencje............................................................................210

Zagadnienie 82: niezrozumienie metod const .................................................................212

Zagadnienie 83: nierozróżnianie agregacji i przypadków użycia ...................................216

Zagadnienie 84: nieprawidłowe przeciążanie operatorów..............................................220

Zagadnienie 85: priorytety a przeciążanie ......................................................................223

Zagadnienie 86: metody przyjacielskie a operatory........................................................224

Zagadnienie 87: problemy z inkrementacją i dekrementacją..........................................225

Zagadnienie 88: niezrozumienie operacji kopiowania z szablonów...............................228

Projekt hierarchii................................................................................ 231Zagadnienie 89: tablice obiektów ...................................................................................... 231

Zagadnienie 90: nieprawidłowe używanie kontenerów .................................................... 233

Zagadnienie 91: niezrozumienie dostępu chronionego ..................................................... 236

Zagadnienie 92: dziedziczenie publiczne metodą wielokrotnego wykorzystania kodu ... 239

Zagadnienie 93: konkretne publiczne klasy bazowe ......................................................... 243

Zagadnienie 94: użycie zdegenerowanych hierarchii........................................................ 243

Zagadnienie 95: nadużywanie dziedziczenia..................................................................... 244

Zagadnienie 96: struktury kontrolne działające na podstawie typów ............................... 248

Zagadnienie 97: kosmiczne hierarchie............................................................................... 250

Zagadnienie 98: zadawanie obiektowi niedyskretnych pytań ........................................... 253

Zagadnienie 99: zapytania o możliwości........................................................................... 255

Bibliografia......................................................................................... 259

Skorowidz .......................................................................................... 261

Page 5: C++. Kruczki i fortele w programowaniu

Zarządzanie pamięciąi zasobami

C++ cechuje się ogromną elastycznością w zarządzaniu pamięcią ale niewielu progra-mistów C++ w pełni rozumie dostępne mechanizmy. W tym obszarze języka przecią-żanie, ukrywanie nazw, konstruktory, destruktory, wyjątki, funkcje statyczne wirtualne,funkcje operatory i funkcje zwykłe — wszystkie razem zapewniają niezwykłą elastycz-ność i możliwość sterowania zarządzaniem pamięcią. Niestety, wszystko się tu niecokomplikuje, co jest chyba zresztą nieuniknione.

W tym rozdziale przyjrzymy się różnym cechom języka C++ związanym z zarządza-niem pamięcią, pokażemy, jak czasem cechy te przedziwnie się splatają i jak możnaich wzajemne interakcje uprościć.

Jako że pamięć jest jednym z wielu zasobów wykorzystywanych przez program, wartosię dowiedzieć, w jaki sposób można ją wiązać z innymi zasobami i jak można zarzą-dzać nią i innymi zasobami.

Zagadnienie 60:nieodróżnianie alokacji danejtypu nietablicowego i tablicy

Czy obiekt ������ jest tym samym, co tablica obiektów ������? Oczywiście, że nie.Dlatego zatem tak wielu programistów C++ ze zdziwieniem stwierdza, że inne ope-ratory służą do alokacji i zwalniania tablic, a inne do typów nietablicowych?

Wiadomo, jak zaalokować i zwolnić pojedynczy obiekt ������ — należy użyć operato-rów ��� i �����:

��������������������� �������������������

Page 6: C++. Kruczki i fortele w programowaniu

150 Zarządzanie pamięcią i zasobami

W przeciwieństwie do większości operatorów C++, zachowania operatora ��� nie moż-na modyfikować przez przeciążanie. Operator ��� zawsze wywołuje funkcję ��� �����, aby uzyskać pamięć, potem ewentualnie ją zainicjalizować. W przypadku poka-zanego powyżej obiektu ������ użycie operatora ��� spowoduje wywołanie funkcji��� ������ z jednym parametrem typu ������, potem na niezainicjalizowanej pamię-ci zostanie wywołany konstruktor obiektu ������.

Operator ����� wywołuje destruktor obiektu ������ i następnie wywołuje funkcję��� ��������, która zwolni wcześniej zaalokowaną na obiekt ������ pamięć.

Jeśli trzeba zmodyfikować sposób alokowania i zwalniania pamięci, korzysta się z prze-ciążania, podmiany lub ukrywania funkcji ��� ������ i ��� ��������, a nie przezmodyfikowanie operatorów ��� i �����.

Wiadomo też, jak alokować i zwalniać tablice obiektów ������. Nie używa się do tegooperatorów ��� ani �����:

��������������������������������

Zamiast tego używa się operatorów ������ i �������� (czytanych jako „tablicowe ���”i „tablicowe �����”). Tak jak ��� i �����, tak i ich tablicowe odpowiedniki nie mogąbyć modyfikowane. Tablicowy ��� najpierw wywołuje funkcję ��� �������� alo-kującą pamięć, potem (w miarę potrzeb) przeprowadza inicjalizację każdego zaalo-kowanego elementu tablicy, od pierwszego do ostatniego. Tablicowy ����� niszczywszystkie elementy tablicy w kolejności odwrotnej niż były one inicjalizowane, następ-nie wywołuje funkcję ��� ���������� w celu odzyskania pamięci.

Zauważmy na marginesie, że często lepiej zamiast tablic skorzystać obiektu �����z biblioteki standardowej. Obiekt ����� działa niemalże równie szybko jak tablicaa jego użycie jest bezpieczniejsze i jest on elastyczniejszy. Obiekt ����� można zwy-kle traktować jako „inteligentną” tablicę. Jednak podczas niszczenia obiektu �����jego elementy są niszczone od pierwszego do ostatniego — odwrotnie niż w tablicy.

Funkcje zarządzające pamięcią muszą być odpowiednio sparowane. Jeśli za pomocą��� alokujemy pamięć, musimy ją potem zwolnić za pomocą �����. Jeśli pamięć alo-kujemy za pomocą � �, musimy ją zwolnić za pomocą ����. Czasami użycie par ����i ��� lub � � i ����� zadziała w pewnych warunkach, ale nie ma takiej gwarancji:

��������������������������

�������������������

������ ����� �������� �! ��"�����#�"������������������������

������������������

Taka sama zasada dotyczy także alokacji i usuwania tablic. Typowym błędem jest alo-kowanie pamięci na tablicę za pomocą tablicowego ��� i potem zwalnianie tej pamięciza pomocą nietablicowego �����. Tak jak w przypadku użycia pary ���/�����może to zadziałać ale jest to błąd i wcześniej czy później się ujawni:

Page 7: C++. Kruczki i fortele w programowaniu

Zagadnienie 60: nieodróżnianie alokacji danej typu nietablicowego i tablicy 151

�"$%������������"$%������������

������������������

Warto zauważyć, że kompilator nie może ostrzec o nieprawidłowym usuwania tablicyjako typu nietablicowego, gdyż nie jest w stanie odróżnić wskaźnika tablicy od wskaź-nika pojedynczego elementu. Zwykle tablicowy operator ��� obok rezerwowanej pa-mięci umieści też informacje o wielkości bloku i liczbie elementów tablicy. Informacjete są potem sprawdzane i wykorzystywane przez tablicowy operator ����� podczasusuwania tablicy.

Format tych informacji jest prawdopodobnie inny niż informacje uzyskiwane w przy-padku nietablicowego operatora ���. Jeśli do zwolnienia pamięci zaalokowanej tabli-cowym ��� użyje się nietablicowego �����, informacje o wielkości i liczbie ele-mentów, przeznaczone dla tablicowego �����, zostaną przez nietablicowe �����prawdopodobnie źle zinterpretowane, czego skutki są trudne do przewidzenia. Możeteż się zdarzyć, że pamięć na dane typów nietablicowych i na dane tablicowe pochodziz różnych obszarów. Użycie nietablicowego operatora ����� do zwolnienia pamięcizaalokowanej na tablicę w obszarze tablic może doprowadzić do katastrofy.

������������������� ��&""

Opisane mylenie alokacji pamięci na dane typów nietablicowych i tablice wystę-puje także przy kodowaniu metod do zarządzania pamięcią:

�� ����������'��$%���(���)"����"��� �"��������#���������)"���"��� �"����������)"����*���#������

���������

+�

Autor klasy ������ postanowił napisać specjalną wersję funkcji do zarządzania pamięciąna obiekty ������ ale nie wziął pod uwagę faktu, że tablicowe operatory ��� i �����mają inne nazwy funkcji niż ich nietablicowe odpowiedniki, wobec czego nie sąukrywane przez zdefiniowane w pokazanej klasie metody:

��������������������� ���������,-������������������,-

�������������������������������$������������������������������������������$���

W klasie ������ nie zadeklarowano funkcji ��� �������� ani ��� ����������,więc do zarządzania pamięcią na tablice obiektów ������ będą używane ogólne wersjetych funkcji. Jest to prawdopodobnie sytuacja błędna, autor klasy ������ powinienpodać tablicowe funkcje zarządzania pamięcią.

Jeśli jednak jest to postępowanie celowe, autor klasy powinien jasno wskazać to przy-szłym serwisantom kodu, gdyż inaczej wcześniej czy później ktoś „poprawi” koddodając „brakujące” funkcje. Najlepszym sposobem udokumentowania takiej decyzjiprojektowej jest wstawienie nie komentarza, ale odpowiedniego kodu:

Page 8: C++. Kruczki i fortele w programowaniu

152 Zarządzanie pamięcią i zasobami

�� ����������'��$%���(���)"����"��� �"��������#���������)"���"��� �"����������)"����*���#���������)"����"��� �"����������#�������������'���$���(("��� �"�����������+���)"���"��� �"������������)"�����*���#�����������'�(("��� �"���������������+���������+�

Jeśli powyższe funkcje zostaną zaimplementowane jako funkcje włączane (�����),koszt ich wykonania będzie zerowy a ich istnienie powinno odstręczyć wszystkichopiekunów od pisania ich „poprawionych” wersji uświadamiając im, że autor celowostosuje globalne tablicowe funkcje ��� i �����.

Zagadnienie 61:sprawdzanie alokacji pamięci

Niektórych pytań po prostu nie należy zadawać. Jednym z nich jest pytanie, czy konkret-ny fragment pamięci został zaalokowany.

Warto się przekonać, jak naprawdę wygląda alokowanie pamięci w C++. Oto kod,w którym następuje staranne, każdorazowe sprawdzenie, czy alokowanie pamięci siępowiodło:

%""�����"���� ����.�������� �� /�����.��������������� �� /���'����"���.����������� �� /������ �� /0���00����'������.��������!������.�����������������!�����������������!�������������'������������"�����$�����������%�� 1�������+���++����������"�����$���������"������"%��$� 2���$���

Taki sposób kodowania powoduje mnóstwo kłopotów, ale i tak byłby tego wart, gdybymożna było w ten sposób wykryć ewentualne problemy z alokacją pamięci. Niestety,nie można. Sam konstruktor klasy ������ może spowodować błąd alokacji pamięcia w takim przypadku nie ma prostej metody propagacji tego błędu poza konstruktor.Można — choć włos się jeży na głowie na samą myśl o tym — spowodować, aby kon-struktor ������ tworzył obiekt w pewnym akceptowalnym, błędnym stanie i ustawiał

Page 9: C++. Kruczki i fortele w programowaniu

Zagadnienie 61: sprawdzanie alokacji pamięci 153

flagę, którą będzie mógł potem odczytać użytkownik. Nawet zakładając, że istniejedostęp do klasy ������, który pozwoli na zrealizowanie takiej implementacji, rozwią-zanie to wymusza na autorze kodu i przyszłych serwisantach sprawdzanie jeszcze jed-nego dodatkowego warunku.

Można też nie przejmować się błędami. Kod sprawdzający błędy rzadko od razu bywacałkowicie poprawny a po pewnym czasie serwisowania zwykle jest już nieprawidłowy.Lepszym rozwiązaniem jest rezygnacja z jakichkolwiek kontroli:

.�������� �� /�����.������������"���.����������� �� /������ �� /0���00�������������.������

Kod ten jest krótszy, bardziej zrozumiały i poprawny. Standardowe zachowanie ���polega na wyrzucaniu w razie błędu alokacji pamięci wyjątku � �� �. Pozwala tozamknąć kod badania błędów w osobnym module, niezależnie od zasadniczej częściprogramu. W ten sposób uzyskuje się czystszy, bardziej zrozumiały i szybciej działa-jący program.

Tak czy inaczej próba sprawdzania wyniku standardowego wywołania ��� nigdy niepoinformuje użytkownika o błędzie, gdyż albo ��� zadziała prawidłowo, albo zostaniewyrzucony wyjątek:

�������������������������'���� �$��1�# �#��3������ �#�/���������+�����'����������1"������/���4�����/1"� +

Można też użyć funkcji ��� ������ niewyrzucającej wyjątków, która w razie błęduzwraca wskaźnik pusty:

��������������"�5�"���������������'���� �$��1��� ���# �#���� �#�/���������+�����'����������1"��% ��#"��# �1"���4�/1"� +

Jednak w tym przypadku powraca się do problemów ze starą semantyką ��� a do tegoprogramista jest zmuszony do korzystania z iście paskudnej składni. Lepiej unikaćtakiej zgodności ze starszymi wersjami i po prostu projektować programy i potem jekodować z użyciem operatora ��� wyrzucającego wyjątki.

Środowisko opisywanego programu także automatycznie będzie obsługiwało szczegól-nie paskudny problem związany z błędem alokacji pamięci. Przypomnijmy, że operator��� używa wywołań dwóch funkcji: najpierw ��� ������ alokującej pamięć a potemkonstruktora inicjalizującego uzyskaną pamięć:

6"����������6"��� �����

Page 10: C++. Kruczki i fortele w programowaniu

154 Zarządzanie pamięcią i zasobami

W razie przechwycenia wyjątku � �� � wiadomo, że wystąpił błąd alokacji pamięci— pytanie brzmi, gdzie on wystąpił. Błąd ten mógł pojawić się podczas alokacji pamię-ci na obiekt �� lub w konstruktorze obiektu ��. W pierwszym przypadku zwalnianiepamięci jest zbędne, gdyż wskaźnik �� nigdy nie wskazywał na nic. W drugim przy-padku należałoby zwolnić ze sterty (niezainicjalizowaną) pamięć wskazywaną przez��. Jednak sprawdzenie, z którym z przypadków ma się do czynienia, może być trud-ne lub niemożliwe.

Na szczęście środowisko opisywanego programu potrafi taką sytuację obsłużyć. Jeśliudało się zaalokować pamięć na obiekt ��, natomiast zawiódł konstruktor wyrzucającwyjątek, zostanie wywołana odpowiednia funkcja ��� �������� zwalniająca pamięć(zobacz zagadnienie numer 62).

Zagadnienie 62:zastąpienie globalnych new i delete

Bardzo rzadko zdarza się, aby wskazane było zastępowanie standardowych, globalnychfunkcji ��� ������, ��� �������� i ich tablicowych odpowiedników, mimo żestandard na to pozwala. Standardowe funkcje są zwykle bardzo dobrze zoptymalizo-wane pod kątem różnorodnych zastosowań, zaś wersje podawane przez użytkownikarzadko będą lepsze (choć często rozsądne jest użycie operacji zarządzających pamięciądo udoskonalenia zarządzania pamięcią na potrzeby konkretnej klasy czy hierarchii).

Specjalne wersje funkcji ��� ������ i ��� �������� implementujące inne zacho-wanie niż wersje standardowe zwykle zawierają mniej lub bardziej dokuczliwe błędy,gdyż niezawodność dużej części biblioteki standardowej i innych bibliotek zależy oddomyślnej implementacji omawianych funkcji.

Bezpieczniejszym rozwiązaniem jest przeciążenie globalnej funkcji ��� ������, a niejej zastępowanie. Przykładowo, istnieje potrzeba wypełnienia zaalokowanej pamięcipewnym wzorcem (� ����� poniżej):

)"����"��� �"��������#�����*��"�����������7� ����'����5 �������� ����� ����5 ��� �(("��� �"���������������"�����5 ���� �������� ������������������� ������8���������� �������9:89�����$ � (�� �#� 1��;<=����"�����5 ������� ����������"����������8���������00����'���������������������������� ��������������������00����+������$�����+

Pokazana wersja funkcji ��� ������ ma parametr ������ kopiowany do nowo zaaloko-wanej pamięci. Kompilator odróżnia standardową funkcję od naszej dwuargumentowejfunkcji będącej przeciążeniem wersji standardowej.

Page 11: C++. Kruczki i fortele w programowaniu

Zagadnienie 62: zastąpienie globalnych new i delete 155

�������������9�� �% �� 9�������������������������������9>���"9����������3 ��� �� ��" ��������������������������������������9�"����9�����������������3 ���#���?@"�

Standard zawiera też definicję przeciążonej funkcji ��� ������, mającej opróczwymaganego pierwszego parametru typu ������ drugi parametr typu �����. Imple-mentacja po prostu zwraca drugi parametr (zapis ������ to opis wyjątków mówiących,że dana nie będzie przekazywała dalej żadnych wyjątków; można to spokojnie dalejpominąć).

)"����"��� �"��������#���*�)"���������5�"�����'����$������+

Jest to wersja ��� tworząca obiekt we wskazanym miejscu (w przeciwieństwie jednakdo standardowej, jednoparametrowej funkcji ��� ������, próba zastąpienia omawia-nej wersji funkcji jest niedopuszczalna). Pokazanej funkcji używa się przede wszystkimdo wymuszenia na kompilatorze wywołania konstruktora. Przykładowo, w aplikacjizagnieżdżonej w innej można zechcieć zbudować obiekt „rejestru stanu” konieczniepod wskazanym adresem:

�� ���.� �$�A��������'���������+�)"�������B������������������ ���)"���� �8CDE8888�����������$!��FG�"%��1����3����$��"�� �����!����B���.� �$�A��������������������B�����.� �$�A��������

Oczywiście tworzone tą metodą obiekty muszą być kiedyś zniszczone. Jednak, skorotak naprawdę żadna pamięć nie jest alokowana, konieczne jest zagwarantowanie, żeżadna pamięć nie zostanie zwolniona. Przypomnijmy, że operator ����� najpierwwywołuje destruktor usuwanego obiektu, dopiero potem wywołuje funkcję ��� ������� zwalniającą pamięć. W przypadku obiektu „zaalokowanego” operatorem ���z adresem trzeba jawnie wywołać destruktor, aby uniknąć próby zwalniania pamięci:

��H I.� �$�A��������������3 ���/"& ���������$1�"� *�%� 1�"��� �"��������

Operator ��� z adresem i jawne niszczenie są oczywiście przydatne, ale równie oczywi-ste jest, że w przypadku nieostrożnego użycia są niebezpieczne (w zagadnieniu numer47 opisaliśmy przykład tego z biblioteki standardowej).

Zauważmy, że o ile możemy przeciążyć funkcję ��� ��������, przeciążone wersjenigdy nie zostaną wywołane przez standardowe wyrażenie �����:

)"����"��� �"��������#�����*�2$�����7%$�������������#���?@"�/���)"���"��� �"����������)"�����*���2$�����7%$��������������������������������������"��"������������������J5������5���������J5���������������������������$@/������ �� ��"��"�"��� �"� ���2$�����%$��J5������5����������%$���J5���������������������$@/������#���?@"���"�"��� �"� ���

��������5���������������������������� ��&""*��"���"�%/G�$@/�����#���?@"���������

��������5������������������������� ��&""*�$@/�"��� �� ��"��"�"��� �"� �������

Page 12: C++. Kruczki i fortele w programowaniu

156 Zarządzanie pamięcią i zasobami

Tymczasem, jak w przypadku obiektu tworzonego przez ��� z adresem, programistajest zmuszony do jawnego wywołania destruktora obiektu, a potem do jawnego zwol-nienia pamięci byłego obiektu przez wywołanie odpowiedniej funkcji ��� ��������:

�5����H IJ5�������������������������� ��&""*����#�#�����"%��1�$�J5���"��� �"�����������5����*�%$���������� ��&""*�$@/�"���#���?@"���"�������

W praktyce pamięć alokowana przez przeciążoną funkcję globalną ��� ������ częstojest błędnie zwalniana standardową funkcją globalną ��� ��������. Metodą unik-nięcia tego błędu jest upewnienie się, że wszelka pamięć alokowana przeciążoną funkcją��� ������ otrzyma pamięć od standardowej funkcji globalnej ��� ������. Takwłaśnie postąpiono powyżej, w przypadku pierwszej implementacji funkcji przeciążonej,dlatego wersja ta działa prawidłowo ze standardową funkcją globalną ��� ��������:

�������������9�� �% �� 9��������������������������������������9�"����9�����������������������������#� &

Przeciążone wersje funkcji globalnej ��� ������ powinny albo nie alokować żadnejpamięci, albo powinny być obudową standardowej funkcji.

Często najlepiej jest po prostu unikać robienia czegokolwiek związanego z globalny-mi funkcjami zarządzającymi pamięcią, a za to dopracować zarządzanie pamięciąw poszczególnych klasach lub w całej ich hierarchii przez użycie metod ��� ������,��� �������� i ich tablicowych odpowiedników.

Pod koniec zagadnienia numer 61 napomknęliśmy, że środowisko naszego programuwywoła „odpowiedni” operator ����� w razie pojawienia się wyjątku w inicjalizacjiwyrażenia ���:

J5������������J5����� �����

Jeśli alokacja pamięci na obiekt ����� powiedzie się ale konstruktor ����� wyrzuciwyjątek, środowisko naszego systemu wywoła odpowiednią funkcję ��� ��������,która zwolni niezainicjalizowaną pamięć wskazywaną przez ��. W pokazanym wyżejprzypadku odpowiednią będzie albo globalna ��� ���������������, albo metoda��� �������� z taką samą sygnaturą. Jednak użycie innej funkcji ��� ������wymusiłoby użycie innej funkcji ��� ��������:

J5�������������%$���J5����� �����

W tym przypadku odpowiednią funkcją jest dwuparametrowa wersja ��� ��������odpowiadająca przeciążonej ��� ������ użytej do alokacji obiektu �����, ��� �������������� �!"�����#�, i tak właśnie wersja zostanie wywołana.

C++ pozwala na dużą elastyczność przy definiowaniu zachowania funkcji zarządzają-cych pamięcią, ale ceną za tę elastyczność jest złożoność. Standardowe, globalne wersjefunkcji ��� ������ i ��� �������� wystarczają w większości sytuacji. Bardziejskomplikowanych rozwiązań trzeba się chwytać tylko wtedy, gdy naprawdę nie moż-na się bez nich obejść.

Page 13: C++. Kruczki i fortele w programowaniu

Zagadnienie 63: mylenie zakresu i wywołanie metod new i delete 157

Zagadnienie 63:mylenie zakresu

i wywołanie metod new i delete

Metody ��� ������ i ��� �������� są wywoływane, kiedy tworzone i niszczonesą obiekty zawierającej je klasy. Zakres, w którym jest wykonywana alokacja, nie maznaczenia:

�� ���.������'��$%���(���)"����"��� �"��������#�����������������!��"� �"��� �"�������)"���"��� �"����������)"����������������!��"� �"��� �"�����������)"����"��� �"����������#���������������!��"� �"��� �"���������)"���"��� �"��������������)"������������!��"� �"��� �"�������������.��������"�����5 �����99������������+�)"�������'���.��������������.�������9.���� 9�������$@/ �/�3����.�����(("��� �"���������������������������������������������$@/ �/�3����(("��� �"�����������������������������������������������$@/ �/�3����(("��� �"���������������������������������������������������$@/ �/�3����.�����(("��� �"��������+

Znowu, zakres alokacji nie ma znaczenia — to typ, na który alokowana jest pamięć,decyduje o tym, która funkcja zostanie użyta:

.�����((.��������"�����5 ���������(����������/������5 �����������0��*���������'+

Tablica znaków jest alokowana w zakresie klasy ������ ale alokacja wykorzystujeglobalny operator tablicowy ���, a nie takiż operator z klasy ������: �� � to inny typniż ������. Przydatna może być jawna kwalifikacja:

.�����((.��������"�����5 ���������(����������/��������������� ����5 ��� �������.�����(("��� �"����������������0����*��������'+

Dobrze byłoby, gdyby można było w celu sięgnięcia do metody ��� �������� klasy������ napisać ������$$������ ����������%&� (zaleca się Czytelnikowi przeanalizo-wanie tego zagadnienia), ale taka składnia jest niepoprawna (choć możemy użyć zapisu$$���, aby sięgnąć do globalnej funkcji ��� ������ i ��� �������� oraz $$�����,aby sięgnąć do globalnej funkcji ��� �������� lub ��� ����������).

Page 14: C++. Kruczki i fortele w programowaniu

158 Zarządzanie pamięcią i zasobami

Zagadnienie 64:wyrzucanie literałów łańcuchowych

Wielu autorów piszących o programowaniu w C++ jako przykład użycia wyjątkówprezentuje wyrzucanie literałów łańcuchowych:

�5�"�9;���"��&���������"�$�9�

Autorzy ci wiedzą, że jest to niedobra praktyka, ale nadal ją stosują jako „przykładpedagogiczny”. Niestety, często zapominają uprzedzić swoich czytelników, że stoso-wanie się do takiego wzorca może spowodować wiele szkód.

Nigdy nie należy jako obiektów wyjątków wyrzucać literałów łańcuchowych. Wynikato stąd, że takie obiekty wyjątków powinny być przechwytywane, zaś przechwytywa-nie odbywa się na podstawie ich typu, a nie wartości:

��/�'���������+� ��5���"�����5 ���!�����'����������!��!������������!��9����"��&���������"�$9���������������������!��9��#�1�"�#"�"��# ���"&?�#��� 9���������������������!��9� �$�#�����# � ��%�#����#�K�� 9������������������5"��+

Praktycznym następstwem wyrzucania i przechwytywania literałów łańcuchowych jestto, że niemalże żadne informacje o wyjątku nie są zakodowane w typie obiektu wyjątku.Taki brak precyzji wymaga, aby fraza � ��� przechwytywała każdy wyjątek i spraw-dzała, czy pasuje on do niej. Co gorsza, porównanie wartości jest bardzo podatne napomyłki, często w trakcie serwisowania przestaje ono działać wskutek różnicy w wiel-kości liter czy zmiany formatowania „komunikatów”. W powyższym przykładzie nigdynie zauważymy, że wystąpiło niedopełnienie stosu.

Uwagi powyższe w takim samym stopniu dotyczą wyjątków innych predefiniowanychi standardowych typów. Wyrzucanie liczb całkowitych, zmiennoprzecinkowych, ciągówznaków ������ czy (przy wyjątkowo złym humorze) zbiorów wektorów liczb zmien-noprzecinkowych spowoduje podobne problemy. Krótko mówiąc, problem z wyrzu-caniem jako wyjątków polega na tym, że kiedy taki wyjątek zostanie przechwycony,nie bardzo wiadomo, co on oznacza, więc nie można adekwatnie zareagować. Z miej-sca wyrzucenia wyjątku otrzymuje się komunikat: „Stało się coś bardzo, bardzo złego.Zgadnij, co!”. Nie ma żadnego innego wyboru jak przystąpić do zgadywania, co dajebardzo małe szanse na sukces.

Typ wyjątku to abstrakcyjny typ danych reprezentujący wyjątek. Wytyczne do jegoprojektowania nie różnią się od wytycznych projektowania innych abstrakcyjnych ty-pów danych: należy zidentyfikować i nazwać pojęcie, zdecydować o dopuszczalnychoperacjach danego pojęcia i zaimplementować je. Podczas implementacji trzeba wziąć

Page 15: C++. Kruczki i fortele w programowaniu

Zagadnienie 64: wyrzucanie literałów łańcuchowych 159

pod uwagę inicjalizację, kopiowanie i konwersje. Proste. Użycie literału łańcuchowe-go do reprezentowania wyjątku ma tyle sensu, co użycie literału jako liczby zespolo-nej. Teoretycznie może to zadziałać, ale praktycznie jest uciążliwe i podatne na błędy.

Jakie pojęcie abstrakcyjne chcemy opisać wyrzucając wyjątek odpowiadający niedo-pełnieniu stosu? No tak:

�� ���.� �1<������"�'+�

Nierzadko zdarza się, że sam typ obiektu wyjątku zawiera wszystkie niezbędne informa-cje o tym wyjątku, często też typ wyjątku wystarcza do wyboru jawnie zadeklarowanychmetod. Tym niemniej możliwość dodania tekstu objaśniającego jest wygodna. Rzadziejw obiekcie wyjątku można zapisać też dalsze informacje o zaistniałym wyjątku:

�� ���.� �1<������"�'��$%���(���.� �1<������"���"�����5 ���!����9����"��&���������"�$9������)���$ ��I.� �1<������"������)���$ ���"�����5 ���5 �����"�������������+�

Jeśli istnieje funkcja zwracająca komunikat tekstowy, powinna być ona wirtualnąmetodą o nazwie �� �, mającą pokazaną powyżej sygnaturę. W rzeczywistości częstodobrym pomysłem jest wyprowadzenie typu wyjątku z jednego ze standardowychtypów wyjątków:

�� ���.� �1<������"�(��$%�������((�$���!�����"��'��$%���(����L�������.� �1<������"���"�����5 ���!����9����"��&���������"�$9��������(����((�$���!�����"���!�����'++�

Pozwala to przechwytywać wyjątki albo jako �� �'(������, albo jako ogólniejszy typ�"����������, albo jako bardzo ogólny typ �)������ (�)������ to publiczna klasabazowa klasy �"����������). Często też dobrze jest podać ogólniejszy ale niestan-dardowy typ wyjątku. Zwykle typ taki służy jako klasa bazowa wszystkich typówwyjątków, które mogą być wyrzucane z danego modułu lub biblioteki:

�� ���6"�� ����D $���'��$%���(���)���$ ��I6"�� ����D $��������)���$ ���"�����5 ���5 �����"�����8����������+��� ���.� �1<������"���(��$%�������((�$���!�����"�*��$%����6"�� ����D $���'��$%���(����L�������.� �1<������"���"�����5 ���!����9����"��&���������"�$9��������(����((�$���!�����"���!�����'+����"�����5 ���5 �����"���������'����$������((�$���!�����"�((5 �����++�

Page 16: C++. Kruczki i fortele w programowaniu

160 Zarządzanie pamięcią i zasobami

W końcu trzeba też opisać prawidłowy sposób kopiowania i niszczenia typów wyjąt-ków. W szczególności wyrzucenie wyjątku oznacza, że dopuszczalne musi być two-rzenie obiektu przez kopiowanie, gdyż jest ono używane podczas pracy programu dowyrzucenia wyjątku (zobacz zagadnienie numer 65). Skopiowany wyjątek, kiedy będziejuż obsłużony, musi zostać zniszczony. Często można pozwolić kompilatorowi napisaćza programistę te operacje (zobacz zagadnienie numer 49).

�� ���.� �1<������"���(��$%�������((�$���!�����"�*��$%����6"�� ����D $���'��$%���(����L�������.� �1<������"���"�����5 ���!����9����"��&���������"�$9��������(����((�$���!�����"���!�����'+������.� �1<������"���"����.� �1<������"�7���������.� �1<������"�7"��� �"�����"����.� �1<������"�7�������"�����5 ���5 �����"���������'����$������((�$���!�����"�((5 �����++�

Teraz już użytkownicy naszego stosu mogą wykryć niedopełnienie stosu jako �� �'*(������ (wiedząc, że używają naszego typu, śledzą taki wyjątek), jako ogólniejszy��� ����+ "� (wiedząc, że używają naszej biblioteki kontenerów, są gotowi do ob-służenia wyjątków związanych z kontenerami), jako �"���������� (nie wiedząc nico naszej bibliotece kontenerów chcą jednak obsłużyć wszelkie typowe błędy wykona-nia) lub jako �)������ (są gotowi obsłużyć wszelkie standardowe wyjątki).

Zagadnienie 65:nieprawidłowe korzystaniez mechanizmu wyjątków

Kwestie związane ogólnie z obsługą wyjątków i ich architekturą są nadal przedmiotemdyskusji. Jednak wytyczne zdefiniowane na niższym poziomie, dotyczące sposobuwyrzucania wyjątków i ich przechwytywania są równie dobrze rozumiane, co nagmin-nie naruszane.

Kiedy wykonywane jest wyrażenie wyrzucające wyjątek, mechanizm obsługi wyjątkówkopiuje obiekt wyjątku w „bezpieczne”, tymczasowe miejsce. Miejsce to w dużymstopniu zależy od używanego systemu ale zawsze gwarantuje się, że będzie ono dostęp-ne aż do chwili obsługi wyjątku. Oznacza to, że tymczasowy obiekt będzie mógł byćużyty aż do skończenia wykonywania ostatniej fraz � ��� korzystającej z tego obiektu,nawet jeśli na rzecz tego tymczasowego obiektu wyjątku wywołanych zostanie szereginnych fraz � ���. Jest to ważne, gdyż — mówiąc najprościej — po wyrzuceniu wyjąt-ku nagle wszystko staje się możliwe. Tymczasowy obiekt wyjątku jest cichym okiemcyklonu obsługi wyjątków.

Oto dlaczego nie należy wyrzucać wskaźników:

�5�"����.� �1<������"��9��"��"��� �"� 9���

Page 17: C++. Kruczki i fortele w programowaniu

Zagadnienie 65: nieprawidłowe korzystanie z mechanizmu wyjątków 161

Adres obiektu �� �'(������ na stercie jest kopiowany w bezpieczne miejsce alewskazywana pamięć, umieszczona na stercie, nie jest chroniona. Takie rozwiązaniedopuszcza też sytuację, że wskaźnik może odwoływać się do pamięci znajdującej sięna stosie roboczym:

.� �1<������"����9��"��� � !���M9����5�"�7��

W tym przypadku wskazywany obiekt wyjątku (należy pamiętać, że wyrzucany jestwskaźnik, a nie wskazywana przezeń wartość) odnosi się do pamięci, która możew chwili przechwycenia wyjątku być już niedostępna (a przy okazji: kiedy wyrzucamyliterał łańcuchowy, cała tablica znaków jest kopiowana w tymczasowe miejsce, a nieadres pierwszego znaku; jest to jednak mało przydatna informacja, gdyż nigdy nie należywyrzucać literałów łańcuchowych — zobacz zagadnienie numer 64). Co więcej, wskaź-nik może być pusty (�"). Komu takie komplikacje są potrzebne? Nie należy zatemwyrzucać wskaźników, lecz obiekty:

.� �1<������"����9��"��� � !���MN����5�"���

Obiekt wyjątku jest natychmiast kopiowany przez mechanizm obsługi wyjątków dotymczasowej lokalizacji, więc deklaracja � jest zbędna. Zwyczajowo wyrzuca się obiektyanonimowe:

�5�"�.� �1<������"��9��"��� � !���M9���

Użycie obiektów tymczasowych jasno mówi, że obiekt �� �'(������ jest używanyjedynie jako obiekt wyjątku, gdyż jego cykl życia jest ograniczony do wyrażenia wyrzu-cającego go. O ile jawnie zadeklarowana zmienna � zostanie także zniszczona podczaswykonywania instrukcji wyrzucającej wyjątek, to jej zakres sięga końca bloku zawie-rającego jej deklarację i w tym zakresie jest dostępna. Użycie anonimowych obiektówtymczasowych pomaga także ukrócić niektóre „tfurcze” próby obsługi wyjątków:

�� ����.� �1<������"����9��"��� � !���M9����L�����.� �1<������"�� ���� �1���� ���� �1�����7���5�"���

W tym przypadku nasz błyskotliwy programista postanowił zapamiętać adres obiektuwyjątku na później, prawdopodobnie w jakiejś frazie � ���. Niestety, wskaźnik ��*�� �'��� nie wskazuje obiektu wyjątku (jest to tymczasowy obiekt w celowo nieujaw-nianym miejscu), ale wskazuje zniszczony już obiekt użyty do inicjalizacji tego wskaź-nika. Kod obsługi wyjątków nie jest najlepszym miejscem na umieszczanie dobrzeukrytych błędów; powinien być to kod tak prosty, jak to możliwe.

Jak najlepiej przechwytywać wyjątki? Na pewno nie przez wartość:

��/�'���������+� ��5��6"�� ����D $���� $�����'���������+

Page 18: C++. Kruczki i fortele w programowaniu

162 Zarządzanie pamięcią i zasobami

Warto zastanowić się, co by się stało, gdyby taka fraza � ��� faktycznie przechwyciławyrzucony gdzieś obiekt �� �'(������. Odpowiedź brzmi: szatkowanie. Obiekt�� �'(������ jest przykładem ��� ����+ "�, można by zainicjalizować � "�wyrzuconym obiektem wyjątku, ale nastąpiłoby poszatkowanie wszystkich danychi metod klasy pochodnej (zobacz zagadnienie numer 30).

W tym konkretnym przypadku nie byłoby jednak problemów z szatkowaniem, gdyż��� ����+ "� — jako faktyczna klasa bazowa — jest klasą abstrakcyjną (zobaczzagadnienie numer 93). Wobec tego fraza � ��� jest błędna. Nie można przechwycićprzez wartość obiektu wyjątku jako obiektu ��� ����+ "�.

Przechwytywanie przez wartość naraża programistę na jeszcze inne ukryte problemy:

6 ��5��.� �1<������"�� $�����'�������#4F��""�$#�� � !/��/�$ �34������� $���!"���/.� ���������!"3 ��� ����5�"��������������������"�"����/�#$� !/� 1�$ ��/�/3?��1+

Nierzadko zdarza się, że jakaś fraza � ��� częściowo naprawia sytuację, zapisuje wyniktej naprawy w obiekcie wyjątku i ponownie wyrzuca tenże obiekt wyjątku, aby prze-twarzanie było kontynuowane. Niestety, w pokazanym kodzie tak się nie dzieje. Fraza� ��� przechwytuje wyjątek, wykonuje częściową naprawę, zapisuje stan w lokalnejkopii obiektu wyjątku i ponownie wyrzuca (niezmieniony) obiekt wyjątku.

Dla uproszczenia i dla uniknięcia opisanych problemów zawsze wyrzuca się anonimoweobiekty tymczasowe a przechwytuje się je przez referencję.

Należy zachować ostrożność, aby nie doszło do ponownego przekazania kopii z opisemproblemów do tej samej procedury obsługi. Zdarza się tak wtedy, gdy nowy wyjątekjest wyrzucany z procedury obsługi, podczas gdy ponownie powinien być wyrzucanywyjątek już istniejący:

� ��5��6"�� ����D $���7� $�����'�������"%�!/��#4F��"?�� �� 4���������� �$��1���������5�"����������������������"�"���/�#$�������������'

������6"�� ����D $���!/D $����� $�����������!/D $���!"���/.� ���������� � ��!"3 ��� �������5�"�!/D $����������������"/�"%��1��/3?�1$

���++

Tym razem zapisane zmiany nie zostaną utracone, ale utracony zostanie pierwotny typwyjątku. Załóżmy, że pierwotnym typem było �� �'(������. Kiedy obiekt ten jestprzechwytywany jako referencja ��� ����+ "�, dynamicznym typem obiektu wyjątkunadal jest �� �'(������, więc ponowne wyrzucenie tego obiektu pozwala znów prze-chwycić zarówno we frazie � ��� obiektu �� �'(������, jak i ��� ����+ "�. Jednakw chwili wyrzucania nowego obiektu wyjątku �,+ "� jest on typu ��� ����+ "� i niemoże być przechwycony przez frazę � ��� obiektu �� �'(������. Zwykle lepiej jestponownie wyrzucać istniejący obiekt wyjątku, a nie obsługiwać pierwotny wyjątek.Zamiast tego lepiej jest wyrzucać nowy:

Page 19: C++. Kruczki i fortele w programowaniu

Zagadnienie 66: nadużywanie adresów lokalnych 163

� ��5��6"�� ����D $���7� $�����'�������"%�!/��#4F��"?�� �� 4����������� �$��1��������� $���!"���/.� ���������5�"������"�"���/�#$�����+

Na szczęście klasa bazowa ��� ����+ "� jest abstrakcyjna, więc tutaj błąd nie możewystąpić. W ogóle klasy bazowe powinny być abstrakcyjne. Oczywiście, rada taka niejest słuszna, jeśli konieczne jest wyrzucenie całkiem innego typu wyjątku:

� ��5��6"�� ����D $���7� $�����'�������"%�!/��#4F��"?�� �� 4����������"$��"��!�!"�/���������5�"�% �� ��"��������/�#$� !/��"/�/3?��1���� $���!"���/.� ���������5�"���������������������"�"���/�#$�����+

Inny typowy problem wiąże się z kolejnością fraz � ���. Frazy te są sprawdzane kolej-no (jak warunki ��*�����, a nie jak instrukcja ������), więc typy wyjątków należyporządkować od najbardziej szczegółowych do najogólniejszych. Jeśli chodzi o typywyjątków niedających się tak uporządkować, trzeba po prostu logicznie zastanowić się,jaka kolejność będzie najlepsza:

� ��5��6"�� ����D $���7� $�����'�������"%�!/��#4F��"?�� �� 4������� $���!"���/.� �������������!"3 ��� ����5�"�+

� ��5��.� �1<������"�7� $�����'���������+

� ��5���L�����"��7���'���������+

Pokazana powyżej kolejność fraz � ��� nie pozwoli nigdy przechwycić wyjątku �� �'*(������, gdyż przed odpowiadającą mu frazą pojawia się fraza ogólniejszego wyjątku��� ����+ "�.

Mechanizm obsługi wyjątków łatwo może prowadzić do dużej złożoności kodu aleniekoniecznie trzeba iść tą drogą. Wyrzucając i przechwytując wyjątki należy zachowaćprostotę.

Zagadnienie 66:nadużywanie adresów lokalnych

Nie należy zwracać wskaźników do zmiennych lokalnych. Większość kompilatorówostrzega przed taką sytuacją i ostrzeżenie to należy traktować poważnie.

Page 20: C++. Kruczki i fortele w programowaniu

164 Zarządzanie pamięcią i zasobami

Znikanie ramek stosu

Jeśli zmienna jest zmienną automatyczną, używana przez nią pamięć znika w chwilipowrotu z procedury:

�5 �����= %������'����� ����������E���8�����5 ��%$�"���O������#"% �#�# � ���������$!����������������%$�"�*�9��/1��� P�9*���E�00���������$���%$�"��+

Funkcja powyższa ma tę nieprzyjemną cechę, że czasem działa. Po wyjściu z tej funkcjiramka stosu jej odpowiadająca jest usuwana a związana z nią pamięć jest zwalniana(w tym także �"��), aby mogła być wykorzystana przez następną funkcję. Jeśli jednakotrzymana z ���- ��& wartość zostanie przepisana przed wywołaniem następnej funkcji,uzyskany wskaźnik, choć już niepoprawny, może wskazywać jednak pożądaną wartość:

�5 ���$��Q$�= %����= %�������5 ��!/%$���O�*���!/%$���!/%$��5�������!/%$�00���$��Q$�= %00���

Nie jest to kod, który da się zbyt długo serwisować. Serwisant może zdecydować sięna alokację bufora na stercie:

�5 ����!/%$�������5 ���O��

Serwisant może też zrezygnować z ręcznego kopiowania bufora:

�����/���!/%$�*�$��Q$�= %���

Serwisant może postanowić o użyciu bardziej abstrakcyjnego typu danych niż buforznakowy:

���((�������!/%$���$��Q$�= %���

Każda z powyższych modyfikacji spowoduje, że zmodyfikowana będzie pamięć lokalnawskazywana przez "��."�- �.

Interferencje danych statycznych

Jeśli zmienna jest statyczna, późniejsze wywołanie tej samej funkcji wpłynie na wynikwywołania wcześniejszego:

�5 �����= %������'����� ����������E���8������ �����5 ��%$�"���O��������������%$�"�*�9��/1��� P�9*���E�00���������$���%$�"��+

Pamięć na bufor jest dostępna także po wyjściu z funkcji ale kolejne użycie tej samejfunkcji może zmienić zawartość tej pamięci:

Page 21: C++. Kruczki i fortele w programowaniu

Zagadnienie 66: nadużywanie adresów lokalnych 165

�����#/� ��1���"$�����9�����# (�9������= %���������R�R��"$�����9��$� (�9������= %��������������

�����#/� ��1���"$�����9�����# (�9������= %���������R�R��������9��$� (�9������= %��������������

W pierwszym przypadku zostaną pokazane dwie różne etykiety. W drugim — prawdo-podobnie (choć niekoniecznie) ta sama etykieta zostanie pokazana dwa razy. Prawdo-podobnie ktoś, kto jest świadom niezwykłej implementacji funkcji ���- ��/, napiszetak jak w przypadku 1 — rozdzielając wywołania etykiet na odrębne instrukcje. Ktoś,kto będzie używał tej funkcji w przyszłości nie znając błędnej implementacji ���- ��/może wywołania połączyć powodując przez to błąd. Co gorsze, połączone wywołaniemoże też działać prawidłowo, ale zachowywać się nieprzewidywalnie w przyszłości(zobacz zagadnienie numer 14).

Problemy z idiomami

Trzeba pamiętać o jeszcze jednym niebezpieczeństwie. Wiadomo, że użytkownicy funk-cji zwykle nie mają dostępu do jej implementacji i muszą opierać się jedynie na jejdeklaracji. Pomocą mogą być tu komentarze (zobacz zagadnienie numer 1), ale ważnejest takie projektowanie funkcji, aby ułatwiać prawidłowe jej wykorzystywanie.

Należy unikać zwracania referencji odwołującej się do pamięci zaalokowanej w danejfunkcji. Użytkownicy będą notorycznie zapominać o zwolnieniu tej pamięci powodującprzez to wycieki pamięci:

����7������'����$������������S����+���������������������/���1�� !�4���

Prawidłowy kod musi przekształcać referencję na adres lub kopiować wynik i zwalniaćpamięć. Byle nie na mojej zmianie, kolego:

���������7��������3��� �1"�#! �� �!��"� ����7�!�����������1"��3� ��������!���������7�!��

Wyjątkowo źle sprawdza się to w przypadku przeciążonych funkcji operatorów:

T���"�"� �7"��� �"��0���"����T���"�"� �7 *��"����T���"�"� �7%�����'����$�������T���"�"� �� ���0%���*� ��!0%��!����+������T���"�"� � *�%*��� ��%�0���0� �0�%�����!�M��"�/���1M�

Należy zwracać wskaźnik do odpowiedniego miejsca w pamięci albo nie alokowaćpamięci i zwracać wynik przez wartość:

���������'����$����������S���+T���"�"� �"��� �"��0��T���"�"� � *�T���"�"� �%�����'����$���T���"�"� �� ���0%���*� ��!0%��!����+

Page 22: C++. Kruczki i fortele w programowaniu

166 Zarządzanie pamięcią i zasobami

Użytkownicy funkcji zwracającej wskaźnik spodziewają się, że mogą być odpowie-dzialni za ewentualne zwalniania pamięci wskazywanej takim wskaźnikiem i zwyklestarają się sprawdzić, czy faktycznie są za to odpowiedzialni (zwykle oznacza to prze-czytanie komentarzy). Użytkownicy funkcji zwracającej referencję rzadko czują sięodpowiedzialni za zwolnienie pamięci.

Problemy z zasięgiem lokalnym

Problemy, jakie napotykamy w związku z czasem życia zmiennych lokalnych mogąpojawiać się nie tylko na granicy między funkcjami, ale także w przypadku zagnież-dżonych zakresów w pojedynczych funkcjach:

)"����"� �.�"��������L���'����5 �������8��������L���'�������5 ��%$������9 ���9�����������%$�����������1����1���"!/�&��������5 ��%$������9Q���/9��������5 ��������%$������������������+�������LH����'�������5 ��������8�����# �5"�#��� �%$��U���������������+�����������������������������������%/G�!"@��%&?����+

Kompilatory są bardzo elastyczne, jeśli chodzi o sposób układania pamięci na zmiennelokalne. W zależności od używanego systemu i opcji kompilatora pamięć na �"�& i ��/może się pokryć lub nie. Jest to dopuszczalne, gdyż �"�& i ��/ mają rozłączne zakresyi rozłączne okresy życia. W przypadku pokrycia wartość �"�& zostanie uszkodzonai zachowanie ������ może się zmienić (prawdopodobnie nic nie zostanie pokazane).Aby uzyskać przenośność, nie należy opierać się na konkretnym sposobie działaniaramek stosu.

Korekta przez static

Czasami programista staje wobec poważnego błędu i okazuje się, że zastosowaniespecyfikatora �� ��� powoduje, że wszystko nagle zaczyna działać poprawnie:

�������5 ��%$��VBC���"����"$����8��������������8�5������00���VBC���������%$������R:8R���'������%$������R�R�������00�"$���

Page 23: C++. Kruczki i fortele w programowaniu

Zagadnienie 67: błąd pozyskania zasobu bierze się z inicjalizacji 167

���+ ��������"$���������������

Kod ten zawiera kiepsko napisaną pętlę, która czasami pisze za końcem tablicy �"�powodując, że asercja zawodzi. Szukając na ślepo przyczyn błędu programista możezadeklarować �"�� jako lokalną zmienną statyczną i kod nagle zacznie działać:

�5 ��%$��VBC���� �����"����"$����8��������"$����8��������8�5������00���VBC���������%$������R:8R���'������%$������R�R�������00�"$������+ ��������"$���������

Wielu programistów nie będzie chciało narażać na szwank swojego szczęścia i zostawiswój kod w takiej postaci. Niestety, problem nie znikł: po prostu został przeniesiony.Siedzi sobie spokojnie i czeka, gotów do uderzenia w odpowiedniej chwili.

Zmiana lokalnej zmiennej �"�� na statyczną spowodowało przeniesienie jej poza ram-kę stosu funkcji do całkiem innego obszaru pamięci, gdzie umieszczane są obiektylokalne. Z powodu tego przeniesienia zmienna ta już nie jest nadpisywana. Jednak nietylko zmienna �"�� jest przedmiotem problemów, które określiliśmy wcześniej jako„interferencja zmiennych statycznych”. Dotyczy to każdej innej zmiennej lokalnej,ewentualnie zmiennych, które powstaną — wszystkie one mogą być nadpisywane.Prawidłowym rozwiązaniem jest oczywiście nie ukrywanie błędu, lecz jego usunięcie:

�5 ��%$��VBC���"����"$����8��������������8��"�����������������VBC��00����������%$������R:8R���'������%$������R�R�������00�"$������+������

Zagadnienie 67:błąd pozyskania zasobubierze się z inicjalizacji

Wstyd, jak wielu programistów C++ nie docenia cudownej symetrii konstruktorówi destruktorów. W większości przypadków chodzi tu o programistów, którzy korzy-stali z języków, które trzymały ich z daleka od kaprysów wskaźników i problemów

Page 24: C++. Kruczki i fortele w programowaniu

168 Zarządzanie pamięcią i zasobami

zarządzania pamięcią. Bezpieczeństwo i nieświadomość. Błoga nieświadomość. Progra-mowanie zgodnie z metodą wytyczoną przez projektanta języka wymaga programowaniaw jeden, z góry określony sposób. Właściwy sposób.

Na szczęście C++ ma więcej względów dla praktyków i jest znacznie elastyczniejszypod względem możliwych sposobów stosowania języka. Nie chodzi o to, iżby nie byłow C++ ogólnych zasad i idiomów (zobacz zagadnienie numer 10). Jednym z najważ-niejszych idiomów jest to, że „pozyskiwanie zasobów bierze się z inicjalizacji”. Jestto dość enigmatyczne stwierdzenie ale jego konsekwencje pozwalają prosto i elastycz-nie wiązać zasoby z pamięcią i zarządzać jednymi i drugimi w przewidywalny sposób.

Operacje tworzenia i niszczenia stanowią swoje wzajemne odbicia lustrzane. Podczastworzenia obiektu klasy kolejność inicjalizacji zawsze jest taka sama: najpierw podo-biekty wirtualnej klasy bazowej (zgodnie ze standardem, „w takiej kolejności, w jakiejpojawiają się przy analizie w głąb od lewej do prawej skierowanego grafu acyklicz-nego klas bazowych”), potem bezpośrednie klasy bazowe w kolejności ich występo-wania na liście klas bazowych w definicji omawianej klasy, potem niestatyczne poladanych w kolejności ich deklaracji, potem treść konstruktora. Niszczenie odbywa sięw kolejności odwrotnej: treść destruktora, pola w odwrotnej kolejności deklaracji, bez-pośrednie klasy bazowe w odwrotnej kolejności deklaracji, wirtualne klasy bazowe.Dobrze jest wyobrazić sobie budowanie jako proces wkładania pewnego ciągu na stos,a niszczenia — jako zdejmowanie ze stosu, oczywiście w odwrotnej kolejności. Syme-tria budowania i niszczenia jest uważana za tak ważną, że wszystkie konstruktory klasrobią swoje inicjalizacje w takiej samej kolejności, nawet jeśli lista inicjalizacyjna póljest zapisana w innej kolejności (zobacz zagadnienie numer 52).

Efektem ubocznym lub wynikiem inicjalizacji jest to, że konstruktor zbiera zasobyw miarę tworzenia obiektu. Często kolejność pobierania tych zasobów jest istotna(przykładowo, przed zapisem bazy danych trzeba ją zablokować. Trzeba pobrać uchwytpliku przed pisaniem do tego pliku) i zwykle destruktor zwalnia zasoby w kolejnościodwrotnej do kolejności ich pobierania. Może istnieć wiele konstruktorów, ale istnie-nie zawsze dokładnie jednego destruktora oznacza, że wszystkie konstruktory musząwykonywać inicjalizacje swoich składników w takiej samej kolejności.

Tak na marginesie warto wspomnieć, że nie zawsze tak było. Krótko po powstaniujęzyka C++ kolejność inicjalizacji w konstruktorach nie była ustalona, co powodowałowiele trudności w przypadku złożonych projektów. Tak jak większość reguł językaC++, tak i ta jest wynikiem starannego projektu i praktycznego doświadczenia.

Symetria tworzenia i niszczenia zachodzi nawet wtedy, gdy przechodzimy od strukturyobiektu do zastosowań wielu obiektów. Warto rozważyć prostą klasę śladu:

►►zagadnienie67/trace.h

�� ���J� ���'���$%���(����J� �����"�����5 ���!������������(�!���!�����'����((�"$�����9��3F�����"�9����!�����������+����IJ� ������������'����((�"$�����9�/3F����#�9����!�����������+

Page 25: C++. Kruczki i fortele w programowaniu

Zagadnienie 67: błąd pozyskania zasobu bierze się z inicjalizacji 169

�����) ��(�����"�����5 ���!��+�

Taka klasa śladu jest być może nieco zbyt prosta, gdyż zakładamy, że jej inicjalizatorjest poprawny i będzie istniał przynajmniej tak długo, jak długo istniał będzie obiekt�� ��, ale dla naszych celów to wystarcza. Obiekt �� �� pokazuje komunikat przykażdym jego tworzeniu i ponownie przy niszczeniu, więc za jego pomocą można śle-dzić przebieg wykonania programu:

►►zagadnienie67/trace.cpp

J� ��� ��9"%�# �$���"% ����"9���

)"����""�/�������"���*������"������'����J� ���%��9���F����$�1�3�9�����(�J� ������9� ��#�3��#4F������F��9������������"������"����������������$������������"���H����'��������J� ������9��9������������� ����J� ����� ���9�"1 ��/�5��� �/�#�/�59�����������5�����HH�"������'������������J� ������9�4���9��������������������"������"����������������������"�"������������+��������J� ������9�"��4���9�������+����J� ������9�"���9���+

Wywołanie funkcji �, z parametrami 0 i / da następujące wyniki:

��5"�#4��"�"%�# �$���"% ����"��5"�#4��"����F����$�1�3���5"�#4��"�� ��#�3��#4F������F����5"�#4��"�����5"�#4��"��"1 ��/�5��� �/�#�/�5��5"�#4��"��4����/�5"�#4�#��4�����5"�#4��"��4����/�5"�#4�#��4����/�5"�#4�#����/�5"�#4�#�� ��#�3��#4F������F����5"�#4��"�� ��#�3��#4F������F���/�5"�#4�#�� ��#�3��#4F������F���/�5"�#4�#����F����$�1�3��/�5"�#4�#��"1 ��/�5��� �/�#�/�5�/�5"�#4�#�"%�# �$���"% ����"

Z komunikatów jasno wynika, jak czas życie obiektu �� �� jest związany z aktualnymzakresem wykonania. W szczególności warto zwrócić uwagę na wpływ instrukcji ��i ���"�� na czas życia aktywnych obiektów �� ��. Każda z tych gałęzi stanowi przykładpraktyki kodowania, ale są to konstrukcje podobne do tych, jakie pojawiać się będąw większości praktycznie spotykanego oprogramowania.

Page 26: C++. Kruczki i fortele w programowaniu

170 Zarządzanie pamięcią i zasobami

)"���#�"%W2���'���%�"1$3W2���������#�M%�#�% #?�� �/�5��"���#�% ������"�%�"1$3W2���+

W powyższym kodzie przed skorzystaniem z bazy danych najpierw ją zablokowano,a po wykonaniu zaplanowanych czynności odblokowano ją. Niestety, tego typu ostroż-ność załamuje się podczas serwisowania kodu, szczególnie, jeśli fragment kodu międzyblokowaniem a odblokowaniem jest długi:

)"���#�"%W2���'���%�"1$3W2�������������������� 1�!�������"�"% �����������$���������������"�%�"1$3W2���+

Teraz błąd powstaje za każdym razem, kiedy tak się podoba funkcji ���1!: baza danychpozostanie zablokowana a to bez wątpienia przyczyni się do wielu kłopotów w innychmiejscach systemu. Tak naprawdę nawet oryginalny kod nie był napisany poprawnie,gdyż wyrzucenie wyjątku po zablokowaniu bazy, ale przed jej zablokowaniem, dapodobny efekt: baza będzie zablokowana.

Można by próbować zaradzić temu jawnie uwzględniając wyjątki i utrudniając lekturęserwisantom:

)"���#�"%W2���'���# %�"1$3W2��������/�'���������#�M%�#�% #?�� �/�5��"���#�% ������+���� ��5��������'������"�%�"1$3W2����������5�"����+���"�%�"1$3W2���+

Takie rozwiązanie jest przegadane, mało eleganckie, trudne do serwisowania i spowo-duje, że jego twórca będzie postrzegany jako przedstawiciel Wydziału Wydziału Nad-miarowości. Prawidłowo napisany, bezpieczny z uwagi na wyjątki kod zwykle zawierakilka bloków ��,. Zamiast tego można skorzystać z zasady pozyskiwania zasobóww ramach inicjalizacji:

�� ���W22�"1$3�'��$%���(���W22�"1$3���'�%�"1$3W2����+���IW22�"1$3���'�"�%�"1$3W2����++�

)"���#�"%W2���'���W22�"1$3��"�1��������$� 3��"%�!/�#�% #?�� �/�5��"���#�% ���+

Page 27: C++. Kruczki i fortele w programowaniu

Zagadnienie 68: nieprawidłowe użycie auto_ptr 171

Tworzenie obiektu 1!!' � powoduje, że jest pozyskiwana blokada bazy danych.Kiedy obiekt 1!!' � wychodzi poza zakres z jakiegokolwiek powodu, jego destruktorodzyska zasób i odblokuje bazę danych. Idiom taki jest tak często używany w C++, żeczęsto w ogóle się go nie zauważa. Jednak za każdym razem, kiedy używa się stan-dardowego ������, �����, ��� lub innego typu, korzysta się z tego właśnie idiomu.

Na marginesie warto ostrzec przed dwoma typowymi problemami związanymi z użyciemklas uchwytów zasobów takich, jak 1!!' � :

)"���#�"%W2���'���W22�"1 � �%�"1 � �������"�� ���

���W22�"1 � �%�"1 � ��������$������W22�"1 � �������$���

�������$� 3��"%�!/�#�% #?�� �/�5��"���#�% ���+

Deklaracja zmiennej �' � & jest prawidłowa: mamy obiekt 1!!' � , który pojawiasię w zakresie tuż przed średnikiem w deklaracji i wychodzi poza zakres pod koniecbloku zawierającego jego deklarację (tutaj jest to koniec funkcji). Deklaracja �' � /jest deklaracją funkcji bezparametrowej, zwracającej 1!!' � (zobacz zagadnienienumer 19). Nie jest to błąd, ale prawdopodobnie nie o to chodziło programiście, gdyżnie zachodzi ani blokowanie, ani odblokowywanie.

Następny wiersz to deklaracja tworząca anonimowy obiekt tymczasowy 1!!' � . Tofaktycznie zablokuje bazę danych, ale z uwagi na to, że anonimowy obiekt tymczasowywychodzi poza zakres zaraz za zawierającym go wyrażeniem (tuż przed średnikiem),baza danych natychmiast zostanie odblokowana. Prawdopodobnie nie o to chodziło.

Standardowy szablon "����� jest przydatnym uchwytem zasobu ogólnego przezna-czenia obiektu alokowanego na stercie. Zobacz zagadnienia numer 10 i 68.

Zagadnienie 68:nieprawidłowe użycie auto_ptr

Standardowy szablon "����� jest prostym i wygodnym uchwytem zasobu, mającymniezwykłą semantykę kopiowania (zobacz zagadnienie numer 10). Większość przypad-ków użycia "����� nie wymaga komentarza:

��!�� �����/��� !��J )"����������6"�� �����J �7����'��� $�"������X����J � ��������X������������"����H �����������H �"�������H ��L������'�������"$������H ���������������������L !������������+���������3 ����#/�#�#�������+

Page 28: C++. Kruczki i fortele w programowaniu

172 Zarządzanie pamięcią i zasobami

Szablonu "����� często używa się, aby zapewnić zwalnianie pamięci i zasobóww formie obiektów alokowanych na stercie, które są zwalniane, kiedy wskazujący jewskaźnik wychodzi poza zakres (bardziej skomplikowaną analizę hierarchii możnazobaczyć w zagadnieniu 90). Powyżej zakładamy, że pamięć na 2���3�4 zwracana przez���2��� jest alokowana ze sterty. Wobec tego "�����3�2���3�4�4 wywoła operator�����, aby odzyskać pamięć obiektu w przypadku, kiedy "����� wyjdzie poza zakres.

Jednak z użyciem "����� wiążą się dwa typowe błędy. Pierwszy to założenie, że "����� może odnosić się do tablicy.

)"���� �����"$%��������*�����������'����"$%�����!�������"$%�������������������������������!��+

Funkcja � � jest podatna na błędy, gdyż zaalokowana tablica ��� nie zostanie odzy-skana, jeśli podczas wykonania funkcji pojawi się wyjątek lub jeśli niedbały serwisantspowoduje wcześniejsze wyjście z funkcji. Potrzebny jest uchwyt zasobu, jest nimstandardowo "�����:

)"���� �����"$%��������*�����������'��� $�"������"$%�� ��!�������"$%�������������������+

Jednak "����� to standardowy uchwyt zasobu pojedynczego obiektu, a nie tablicyobiektów. Kiedy ��� wychodzi poza zakres i aktywowany jest jego destruktor, do tablicywartości �"�� zaalokowanej tablicowym operatorem ��� (zobacz zagadnienie numer 60)stosowane jest nietablicowe zwalnianie pamięci. Wynika to stąd, że, niestety, kompi-lator nie potrafi odróżnić wskaźnika tablicy od wskaźnika pojedynczego obiektu.Szczęśliwym zbiegiem okoliczności kod ten może czasem działać w niektórych sys-temach, zaś problem może zostać wykryty jedynie przy przenoszeniu oprogramowaniana inny system lub podczas aktualizacji nowej wersji systemu istniejącego.

Lepszym rozwiązaniem jest użycie do standardowej struktury ����� zamiast �� ,.Standardowa struktura ����� to w zasadzie uchwyt zasobu tablicy, rodzaj „autotablicy”wzbogacony o szereg dodatkowych możliwości. Jednocześnie zapewne dobrym pomy-słem jest pozbycie się prymitywnego i niebezpiecznego używania jako parametrówformalnych wskaźników przysłaniających tablicę:

)"���� ����)���"���"$%�� �7������'���)���"���"$%�� ��!�������#�������������+

Innym typowym błędem jest użycie "����� jako typu elementu z kontenera STL.Kontenery STL nie narzucają zbyt dużych wymagań na swoje elementy, ale wymagająklasycznej semantyki kopiowania.

W standardzie "����� zdefiniowano w taki sposób, że niepoprawne jest ukonkret-nienie kontenera STL typem elementu "�����. Próba spowoduje wyświetlenie błędukompilacji (prawdopodobnie dość mocno ukrytego). Jednak wiele istniejących imple-mentacji nadal nie nadąża za standardem.

Page 29: C++. Kruczki i fortele w programowaniu

Zagadnienie 68: nieprawidłowe użycie auto_ptr 173

W powszechnych, przestarzałych implementacjach "����� jego semantyka kopiidoskonale odpowiada użyciu go jako typu elementu kontenera. Oznacza to, że do chwiliotrzymania innej lub nowszej wersji biblioteki standardowej, taki kod nie będzie siękompilował. Bardzo to niedogodne, ale łatwo to naprawić.

Gorsza sytuacja zachodzi, kiedy implementacja "����� nie w pełni jest zgodna zestandardem, więc można użyć jej do ukonkretnienia kontenera STL, ale semantykakopiowania nie jest tym, czego wymaga STL. Jak opisano w zagadnieniu numer 10,kopiowanie "����� przenosi kontrolę wskazywanego obiektu i ustawia źródło kopio-wania na �":

$�"�����Y� �"��1 ��������Z"�#��"/��� $�"�����Y� �"��1 �����������������3�����$�������������������������������������3�����$��

Opisana właściwość jest w pewnych sytuacjach całkiem przydatna, ale nie o nią chodziw przypadku elementów będących kontenerami STL:

)���"��� $�"�����Y� �"��1 � ����� �� �������������� $�"�����Y� �"��1 � ���!���"�/������ �� ��%������*����� �� ������*��� � ��������#���!�����

W niektórych systemach taki kod da się skompilować i uruchomić, ale zwykle nie wartosię tym zajmować. Struktura ����� obiektów 5� ����' zostanie skopiowana na listę, alepo zakończeniu kopiowania ����� będzie zawierała wszystkie puste wskaźniki (�")!

Należy unikać używania "����� jako kontenera STL, nawet jeśli używany aktualniesystem na to pozwala.