Cyfroteka.pl

klikaj i czytaj online

Cyfro
Czytomierz
00094 008457 10460892 na godz. na dobę w sumie
C++. Kruczki i fortele w programowaniu - książka
C++. Kruczki i fortele w programowaniu - książka
Autor: Liczba stron: 272
Wydawca: Helion Język publikacji: polski
ISBN: 83-7361-346-3 Data wydania:
Lektor:
Kategoria: ebooki >> komputery i informatyka >> programowanie >> c++ - programowanie
Porównaj ceny (książka, ebook, audiobook).

'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ą:

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...\

Znajdź podobne książki Ostatnio czytane w tej kategorii

Darmowy fragment publikacji:

IDZ DO IDZ DO PRZYK£ADOWY ROZDZIA£ PRZYK£ADOWY ROZDZIA£ SPIS TREĎCI SPIS TREĎCI KATALOG KSI¥¯EK KATALOG KSI¥¯EK KATALOG ONLINE KATALOG ONLINE ZAMÓW DRUKOWANY KATALOG ZAMÓW DRUKOWANY KATALOG TWÓJ KOSZYK TWÓJ KOSZYK DODAJ DO KOSZYKA DODAJ DO KOSZYKA CENNIK I INFORMACJE CENNIK I INFORMACJE ZAMÓW INFORMACJE ZAMÓW INFORMACJE O NOWOĎCIACH O NOWOĎCIACH ZAMÓW CENNIK ZAMÓW CENNIK CZYTELNIA CZYTELNIA FRAGMENTY KSI¥¯EK ONLINE FRAGMENTY KSI¥¯EK 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 Wydawnictwo Helion ul. Chopina 6 44-100 Gliwice tel. (32)230-98-63 e-mail: helion@helion.pl 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++. Spis treści Wstęp ...................................................d................................................. 9 Podstawy ...................................................d.......................................... 13 Zagadnienie 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...................................................d............................................ 41 Zagadnienie 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...................................................d....................................... 63 Zagadnienie 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 6 C++. Kruczki i fortele w programowaniu Konwersje ...................................................d......................................... 75 Zagadnienie 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 ...................................................d..................................... 115 Zagadnienie 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 ...................................................d..... 149 Zagadnienie 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 ...................................................d..................................... 175 Zagadnienie 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 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 ...................................................d.................................... 207 Zagadnienie 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...................................................d............................. 231 Zagadnienie 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...................................................d...................................... 259 Skorowidz ...................................................d....................................... 261 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 nieco komplikuje, 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żna ich wzajemne interakcje uprościć. Jako że pamięć jest jednym z wielu zasobów wykorzystywanych przez program, warto się 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 danej typu nietablicowego i tablicy Czy obiekt 9KFIGV jest tym samym, co tablica obiektów 9KFIGV? 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 dio typów nietablicowych? Wiadomo, jak zaalokować i zwolnić pojedynczy obiekt 9KFIGV — należy użyć operato- rów PGY i FGNGVG: 9KFIGV YPGY9KFIGV CTI   FGNGVGY 150 Zarządzanie pamięcią i zasobami W przeciwieństwie do większości operatorów C++, zachowania operatora PGY nie moż- na modyfikować przez przeciążanie. Operator PGY zawsze wywołuje funkcję QRGTCVQT PGY, aby uzyskać pamięć, potem ewentualnie ją zainicjalizować. W przypadku poka- zanego powyżej obiektu 9KFIGV użycie operatora PGY spowoduje wywołanie funkcji QRGTCVQTPGY z jednym parametrem typu UKGAV, potem na niezainicjalizowanej pamię- ci zostanie wywołany konstruktor obiektu 9KFIGV. Operator FGNGVG wywołuje destruktor obiektu 9KFIGV i następnie wywołuje funkcję QRGTCVQTFGNGVG, która zwolni wcześniej zaalokowaną na obiekt 9KFIGV pamięć. Jeśli trzeba zmodyfikować sposób alokowania i zwalniania pamięci, korzysta się z prze- ciążania, podmiany lub ukrywania funkcji QRGTCVQTPGY i QRGTCVQTFGNGVG, a nie przez modyfikowanie operatorów PGY i FGNGVG. Wiadomo też, jak alokować i zwalniać tablice obiektów 9KFIGV. Nie używa się do tego operatorów PGY ani FGNGVG: YPGY9KFIGV=P?  FGNGVG=?Y Zamiast tego używa się operatorów PGY=? i FGNGVG=? (czytanych jako „tablicowe PGY” i „tablicowe FGNGVG”). Tak jak PGY i FGNGVG, tak i ich tablicowe odpowiedniki nie mogą być modyfikowane. Tablicowy PGY najpierw wywołuje funkcję QRGTCVQT PGY=? alo- kującą pamięć, potem (w miarę potrzeb) przeprowadza inicjalizację każdego zaalo- kowanego elementu tablicy, od pierwszego do ostatniego. Tablicowy FGNGVG niszczy wszystkie elementy tablicy w kolejności odwrotnej niż były one inicjalizowane, następ- nie wywołuje funkcję QRGTCVQTFGNGVG=? w celu odzyskania pamięci. Zauważmy na marginesie, że często lepiej zamiast tablic skorzystać obiektu XGEVQT z biblioteki standardowej. Obiekt XGEVQT działa niemalże równie szybko jak tablica a jego użycie jest bezpieczniejsze i jest on elastyczniejszy. Obiekt XGEVQT można zwy- kle traktować jako „inteligentną” tablicę. Jednak podczas niszczenia obiektu XGEVQT jego elementy są niszczone od pierwszego do ostatniiego — odwrotnie niż w tablicy. Funkcje zarządzające pamięcią muszą być odpowiednio sparowane. Jeśli za pomocą PGY alokujemy pamięć, musimy ją potem zwolnić za pomocą FGNGVG. Jeśli pamięć alo- kujemy za pomocą OCNNQE, musimy ją zwolnić za pomocą HTGG. Czasami użycie par HTGG i PGY lub OCNNQE i FGNGVG zadziała w pewnych warunkach, ale nie ma takiej gwariancji: KPV KRPGYKPV    HTGG KR ļNG KRUVCVKEAECUVKPV OCNNQE UKGQH KPV   KR  FGNGVGKRļNG Taka sama zasada dotyczy także alokacji i usuwania tablic. Typowym błędem jest alo- kowanie pamięci na tablicę za pomocą tablicowego PGY i potem zwalnianie tej pamięci za pomocą nietablicowego FGNGVG. Tak jak w przypadku użycia pary PGY/FGNGVG może to zadziałać ale jest to błąd i wcześniej czy później się ujawni: Zagadnienie 60: nieodróżnianie alokacji danej typu nietablicowego i tablicy 151 FQWDNG FRPGYFQWDNG=?  FGNGVGFRļNG Warto zauważyć, że kompilator nie może ostrzec o nieprawidłowym usuwania tablicy jako typu nietablicowego, gdyż nie jest w stanie odróżnić wskaźnika tablicy od wskaź- nika pojedynczego elementu. Zwykle tablicowy operator PGY obok rezerwowanej pa- mięci umieści też informacje o wielkości bloku i liczbie elementów tablicy. Informacje te są potem sprawdzane i wykorzystywane przez tablicowy operator FGNGVG podczas usuwania tablicy. Format tych informacji jest prawdopodobnie inny niż informacje uzyskiwane w przy- padku nietablicowego operatora PGY. Jeśli do zwolnienia pamięci zaalokowanej tabli- cowym PGY użyje się nietablicowego FGNGVG, informacje o wielkości i liczbie ele- mentów, przeznaczone dla tablicowego FGNGVG, zostaną przez nietablicowe FGNGVG prawdopodobnie źle zinterpretowane, czego skutki są trudne do przewidzenia. Może też się zdarzyć, że pamięć na dane typów nietablicowych i na dane tablicowe pochodzi z różnych obszarów. Użycie nietablicowego operatora FGNGVG do zwolnienia pamięci zaalokowanej na tablicę w obszarze tablic może doprowaidzić do katastrofy. FGNGVG=?FRRTCYKFđQYQ Opisane mylenie alokacji pamięci na dane typów nietablicowych i tablice wystę- puje także przy kodowaniu metod do zarządzania pamięcią: ENCUU9KFIGV] RWDNKE XQKF QRGTCVQTPGY UKGAV  XQKFQRGTCVQTFGNGVG XQKF UKGAV   _ Autor klasy 9KFIGV postanowił napisać specjalną wersję funkcji do zarządzania pamięcią na obiekty 9KFIGV ale nie wziął pod uwagę faktu, że tablicowe operatory PGY i FGNGVG mają inne nazwy funkcji niż ich nietablicowe odpowiedniki, wobec czego nie są ukrywane przez zdefiniowane w pokazanej klasie metody: 9KFIGV YPGY9KFIGV CTI 1-  FGNGVGY1- YPGY9KFIGV=P?WRU  FGNGVG=?YWRU W klasie 9KFIGV nie zadeklarowano funkcji QRGTCVQTPGY=? ani QRGTCVQTFGNGVG=?, więc do zarządzania pamięcią na tablice obiektów 9KFIGV będą używane ogólne wersje tych funkcji. Jest to prawdopodobnie sytuacja błędna, autor klasy 9KFIGV powinien podać 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” kod dodając „brakujące” funkcje. Najlepszym sposobem udokumentowania takiej decyzji projektowej jest wstawienie nie komentarza, ale odpowiiedniego kodu: 152 Zarządzanie pamięcią i zasobami ENCUU9KFIGV] RWDNKE XQKF QRGTCVQTPGY UKGAV  XQKFQRGTCVQTFGNGVG XQKF UKGAV  XQKF QRGTCVQTPGY=? UKGAVP ]TGVWTPQRGTCVQTPGY=? P _ XQKFQRGTCVQTFGNGVG=? XQKF RUKGAV ]QRGTCVQTFGNGVG=? R _  _ Jeśli powyższe funkcje zostaną zaimplementowane jako funkcje włączane (KPNKPG), koszt ich wykonania będzie zerowy a ich istnienie powinno odstręczyć wszystkich opiekunów od pisania ich „poprawionych” wersji uświadamiając im, że autor celowo stosuje globalne tablicowe funkcje PGY i FGNGVG. 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: DQQNGTTQTHCNUG 5VTKPI CTTC[PGY5VTKPI =P? KH CTTC[ ] HQT 5VTKPI RCTTC[RCTTC[ P R ] 5VTKPI VORPGY5VTKPI KH VOR  RVOR GNUG] GTTQTVTWG DTGCM _ _ _ GNUG GTTQTVTWG KH GTTQT QDUNWIC$NGFW  Taki sposób kodowania powoduje mnóstwo kłopotów, ale i tak byłby tego wart, gdyby można było w ten sposób wykryć ewentualne problemy z alokacją pamięci. Niestety, nie można. Sam konstruktor klasy 5VTKPI może spowodować błąd alokacji pamięci a 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 5VTKPI tworzył obiekt w pewnym akceptowalnym, błędnym stanie i ustawiał Zagadnienie 61: sprawdzanie alokacji pamięci 153 flagę, którą będzie mógł potem odczytać użytkownik. Nawet zakładając, że istnieje dostęp do klasy 5VTKPI, 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 bywa całkowicie poprawny a po pewnym czasie serwisowania zwykle jest już nieprawidłowy. Lepszym rozwiązaniem jest rezygnacja z jakichkolwiek kiontroli: 5VTKPI CTTC[PGY5VTKPI =P? HQT 5VTKPI RCTTC[RCTTC[ P R  RPGY5VTKPI Kod ten jest krótszy, bardziej zrozumiały i poprawny. Standardowe zachowanie PGY polega na wyrzucaniu w razie błędu alokacji pamięci wyjątku DCFACNNQE. Pozwala to zamknąć kod badania błędów w osobnym module, niezależnie od zasadniczej części programu. 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 PGY nigdy nie poinformuje użytkownika o błędzie, gdyż albo PGY zadziała prawidłowo, albo zostanie wyrzucony wyjątek: KPV KRPGYKPV KH KR ]YCTWPGMCYUGLGUVRTCYFKY[  _ GNUG] VGPMQFPKIF[UKúPKGY[MQPC _ Można też użyć funkcji QRGTCVQTPGY niewyrzucającej wyjątków, która w razie błędu zwraca wskaźnik pusty: KPV KRPGY PQVJTQY KPV KH KR ]YCTWPGMRTCYKGCYUGRTCYFKY[  _ GNUG] VGPMQFDCTFQTCFMQUKúY[MQPC _ Jednak w tym przypadku powraca się do problemów ze starą semantyką PGY a do tego programista 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 je kodować z użyciem operatora PGY 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 PGY używa wywołań dwóch funkcji: najpierw QRGTCVQTPGY alokującej pamięć a potem konstruktora inicjalizującego uzyskaną pamięć: QU VRPGY QU CTI  154 Zarządzanie pamięcią i zasobami W razie przechwycenia wyjątku DCFACNNQE 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 QU lub w konstruktorze obiektu QU. W pierwszym przypadku zwalnianie pamięci jest zbędne, gdyż wskaźnik VR nigdy nie wskazywał na nic. W drugim przy- padku należałoby zwolnić ze sterty (niezainicjalizowaną) pamięć wskazywaną przez VR. 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śli udało się zaalokować pamięć na obiekt QU, natomiast zawiódł konstruktor wyrzucając wyjątek, zostanie wywołana odpowiednia funkcja QRGTCVQTFGNGVG 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, globalnych funkcji QRGTCVQTPGY, QRGTCVQTFGNGVG i ich tablicowych odpowiedników, mimo że standard na to pozwala. Standardowe funkcje są zwykle bardzo dobrze zoptymalizo- wane pod kątem różnorodnych zastosowań, zaś wersje podawane przez użytkownika rzadko 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 konikretnej klasy czy hierarchii). Specjalne wersje funkcji QRGTCVQTPGY i QRGTCVQTFGNGVG 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 od domyślnej implementacji omawianych funkcji. Bezpieczniejszym rozwiązaniem jest przeciążenie globalnej funkcji QRGTCVQTPGY, a nie jej zastępowanie. Przykładowo, istnieje potrzeba wypełnienia zaalokowanej pamięci pewnym wzorcem (RCVVGTP poniżej): XQKF QRGTCVQTPGY UKGAVPEQPUVUVTKPIRCV ] EJCT RUVCVKEAECUVEJCT QRGTCVQTPGY P  EQPUVEJCT RCVVGTPRCVEAUVT  KH RCVVGTP=? RCVVGTP WYCICFYCPCMK07. EQPUVEJCT HRCVVGTP HQT KPVKKP K ] KH  H HRCVVGTP R=K? H  _ TGVWTPR _ Pokazana wersja funkcji QRGTCVQTPGY ma parametr UVTKPI kopiowany do nowo zaaloko- wanej pamięci. Kompilator odróżnia standardową funkcję od naszej dwuargumentowej funkcji będącej przeciążeniem wersji standardowej. Zagadnienie 62: zastąpienie globalnych new i delete 155 UVTKPIHKNN ICTDCIG   UVTKPI UVTKPIPGYUVTKPI *GNNQ YGTULCUVCPFCTFQYC UVTKPI UUVTKPI PGY HKNN UVTKPI 9QTNF YGTULCRTGEKæľQPC Standard zawiera też definicję przeciążonej funkcji QRGTCVQTPGY, mającej oprócz wymaganego pierwszego parametru typu UKGAV drugi parametr typu XQKF . Imple- mentacja po prostu zwraca drugi parametr (zapis VJTQY to opis wyjątków mówiących, że dana nie będzie przekazywała dalej żadnych wyjątków; można to spokojnie dalej pominąć). XQKF QRGTCVQTPGY UKGAVXQKF R VJTQY ]TGVWTPR_ Jest to wersja PGY tworząca obiekt we wskazanym miejscu (w przeciwieństwie jednak do standardowej, jednoparametrowej funkcji QRGTCVQTPGY, próba zastąpienia omawia- nej wersji funkcji jest niedopuszczalna). Pokazanej funkcji używa się przede wszystkim do wymuszenia na kompilatorze wywołania konstruktora. Przykładowo, w aplikacji zagnieżdżonej w innej można zechcieć zbudować obiekt „rejestru stanu” koniecznie pod wskazanym adresem: ENCUU5VCVWU4GIKUVGT]  _ XQKF TGI#FFTTGKPVGTRTGVAECUVXQKF :(    WOKGħèQDKGMVTGLGUVTWRQFCFTGUGOTGI#FFT 5VCVWU4GIKUVGT UTPGY TGI#FFT 5VCVWU4GIKUVGT Oczywiście tworzone tą metodą obiekty muszą być kiedyś zniszczone. Jednak, skoro tak naprawdę żadna pamięć nie jest alokowana, konieczne jest zagwarantowanie, że żadna pamięć nie zostanie zwolniona. Przypomnijmy, że ioperator FGNGVG najpierw wywołuje destruktor usuwanego obiektu, dopiero potem wywołuje funkcję QRGTCVQT FGNGVG zwalniającą pamięć. W przypadku obiektu „zaalokowanego” operatorem PGY z adresem trzeba jawnie wywołać destruktor, aby uniknąć próby zwalniania pamięci: UT `5VCVWU4GIKUVGT LCYPGY[YQđCPKGFGUVTWMVQTCDTCMQRGTCVQTFGNGVG Operator PGY 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 numer 47 opisaliśmy przykład tego z biblioteki standardowej). Zauważmy, że o ile możemy przeciążyć funkcję QRGTCVQTFGNGVG, przeciążone wersje nigdy nie zostaną wywołane przez standardowe wyrażeinie FGNGVG: XQKF QRGTCVQTPGY UKGAVP$WHHGTDWHHGT RTGEKæľQP[PGY XQKFQRGTCVQTFGNGVG XQKF R $WHHGTDWHHGT QFRQYKGFPKFGNGVG  6JKPI VJKPIPGY6JKPIWľ[EKGUVCPFCTFQYGIQQRGTCVQTCPGY $WHHGTDWH 6JKPI VJKPIPGY DWH 6JKPIWľ[EKGRTGEKæľQPGIQQRGTCVQTCPGY FGNGVGVJKPIPKGRTCYKFđQYQRQYKPPQD[èWľ[VGRTGEKæľQPGFGNGVG FGNGVGVJKPIRTCYKFđQYQWľ[VQUVCPFCTFQYGIQQRGTCVQTCFGNGVG 156 Zarządzanie pamięcią i zasobami Tymczasem, jak w przypadku obiektu tworzonego przez PGY z adresem, programista jest zmuszony do jawnego wywołania destruktora obiektu, a potem do jawnego zwol- nienia pamięci byłego obiektu przez wywołanie odpowieidniej funkcji QRGTCVQTFGNGVG: VJKPI `6JKPI RTCYKFđQYQPKUEGPKGQDKGMVW6JKPI QRGTCVQTFGNGVG VJKPIDWH RTCYKFđQYQWľ[VQRTGEKæľQPGIQFGNGVG W praktyce pamięć alokowana przez przeciążoną funkcję globalną QRGTCVQTPGY często jest błędnie zwalniana standardową funkcją globalną QRGTCVQTFGNGVG. Metodą unik- nięcia tego błędu jest upewnienie się, że wszelka pamięć alokowana przeciążoną funkcją QRGTCVQT PGY otrzyma pamięć od standardowej funkcji globalnej QRGTCVQT PGY. Tak właśnie postąpiono powyżej, w przypadku pierwszej implementacji funkcji przeciążonej, dlatego wersja ta działa prawidłowo ze standardową ifunkcją globalną QRGTCVQTFGNGVG: UVTKPIHKNN ICTDCIG   UVTKPI UVTKPIPGY HKNN UVTKPI 9QTNF   FGNGVGUVTKPIFKCđC Przeciążone wersje funkcji globalnej QRGTCVQTPGY powinny albo nie alokować żadnej pamię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 QRGTCVQTPGY, QRGTCVQTFGNGVG i ich tablicowych odpowiedników. Pod koniec zagadnienia numer 61 napomknęliśmy, że środowisko naszego programu wywoła „odpowiedni” operator FGNGVG w razie pojawienia się wyjątku w inicjalizacji wyrażenia PGY: 6JKPI VRPGY6JKPI CTI  Jeśli alokacja pamięci na obiekt 6JKPI powiedzie się ale konstruktor 6JKPI wyrzuci wyjątek, środowisko naszego systemu wywoła odpowiednią funkcję QRGTCVQTFGNGVG, która zwolni niezainicjalizowaną pamięć wskazywaną przez VR. W pokazanym wyżej przypadku odpowiednią będzie albo globalna QRGTCVQTFGNGVG XQKF , albo metoda QRGTCVQTFGNGVG z taką samą sygnaturą. Jednak użycie innej funkcji QRGTCVQTPGY wymusiłoby użycie innej funkcji QRGTCVQTFGNGVG: 6JKPI VRPGY DWH 6JKPI CTI  W tym przypadku odpowiednią funkcją jest dwuparametroiwa wersja QRGTCVQTFGNGVG odpowiadająca przeciążonej QRGTCVQTPGY użytej do alokacji obiektu 6JKPI, QRGTCVQT FGNGVG XQKF $WHHGT , 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 wersje funkcji QRGTCVQTPGY i QRGTCVQTFGNGVG wystarczają w większości sytuacji. Bardziej skomplikowanych rozwiązań trzeba się chwytać tylko wtedy, gdy naprawdę nie moż- na się bez nich obejść. Zagadnienie 63: mylenie zakresu i wywołanie metod new i delete 157 Zagadnienie 63: mylenie zakresu i wywołanie metod new i delete Metody QRGTCVQTPGY i QRGTCVQTFGNGVG są wywoływane, kiedy tworzone i niszczone są obiekty zawierającej je klasy. Zakres, w którym jest wykonywana alokacja, nie ma znaczenia: ENCUU5VTKPI] RWDNKE XQKF QRGTCVQTPGY UKGAV OGVQFCQRGTCVQTPGY XQKFQRGTCVQTFGNGVG XQKF  OGVQFCQRGTCVQTFGNGVG XQKF QRGTCVQTPGY=? UKGAV OGVQFCQRGTCVQTPGY=? XQKFQRGTCVQTFGNGVG=? XQKF  OGVQFCQRGTCVQTFGNGVG=? 5VTKPI EQPUVEJCT    _ XQKFH ] 5VTKPI URPGY5VTKPI 5VGTVC Wľ[YCP[LGUV5VTKPIQRGTCVQTPGY KPV KRPGYKPV  Wľ[YCP[LGUVQRGTCVQTPGY FGNGVGKRWľ[YCP[LGUVQRGTCVQTFGNGVG FGNGVGURWľ[YCP[LGUV5VTKPIQRGTCVQTFGNGVG _ Znowu, zakres alokacji nie ma znaczenia — to typ, na który alokowana jest pamięć, decyduje o tym, która funkcja zostanie użyta: 5VTKPI5VTKPI EQPUVEJCT U UA UVTER[ PGYEJCT=UVTNGP U ?U  ]_ Tablica znaków jest alokowana w zakresie klasy 5VTKPI ale alokacja wykorzystuje globalny operator tablicowy PGY, a nie takiż operator z klasy 5VTKPI: EJCT to inny typ niż 5VTKPI. Przydatna może być jawna kwalifikacja: 5VTKPI5VTKPI EQPUVEJCT U UA UVTER[ TGKPVGTRTGVAECUVEJCT  5VTKPIQRGTCVQTPGY=? UVTNGP U  U  ]_ Dobrze byłoby, gdyby można było w celu sięgnięcia do metody QRGTCVQTPGY=? klasy 5VTKPI napisać 5VTKPIPGY EJCT=UVTNGP U ? (zaleca się Czytelnikowi przeanalizo- wanie tego zagadnienia), ale taka składnia jest niepoprawna (choć możemy użyć zapisu PGY, aby sięgnąć do globalnej funkcji QRGTCVQTPGY i QRGTCVQTPGY=? oraz FGNGVG, aby sięgnąć do globalnej funkcji QRGTCVQTFGNGVG lub QRGTCVQTFGNGVG=?). 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ów prezentuje wyrzucanie literałów łańcuchowych: VJTQY0KGFQRGđPKGPKGUVQUW Autorzy ci wiedzą, że jest to niedobra praktyka, ale nadal ją stosują jako „przykład pedagogiczny”. Niestety, często zapominają uprzedzić swoich czytelników, że stoso- wanie się do takiego wzorca może spowodować wiele szkóid. Nigdy nie należy jako obiektów wyjątków wyrzucać literałów łańcuchowych. Wynika to stąd, że takie obiekty wyjątków powinny być przechwytywane, zaś przechwytywa- nie odbywa się na podstawie ich typu, a nie wartościi: VT[]  _ ECVEJ EQPUVEJCT OUI ] UVTKPIO OUI  KH OPKGFQRGđPKGPKGUVQUW  GNUGKH ORTGMTQEQPQECURQđæEGPKC  GNUGKH OPCTWUGPKGCUCFDGRKGEGēUVYC  GNUGVJQTY _ Praktycznym następstwem wyrzucania i przechwytywania literałów łańcuchowych jest to, że niemalże żadne informacje o wyjątku nie są zakodowane w typie obiektu wyjątku. Taki brak precyzji wymaga, aby fraza ECVEJ przechwytywała każdy wyjątek i spraw- dzała, czy pasuje on do niej. Co gorsza, porównanie wartości jest bardzo podatne na pomył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 nigdy nie zauważymy, że wystąpiło niedopełnienie stosu. Uwagi powyższe w takim samym stopniu dotyczą wyjątków innych predefiniowanych i standardowych typów. Wyrzucanie liczb całkowitych, zmiennoprzecinkowych, ciągów znaków UVTKPI 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 daje bardzo małe szanse na sukces. Typ wyjątku to abstrakcyjny typ danych reprezentujący wyjątek. Wytyczne do jego projektowania nie różnią się od wytycznych projektowania innych abstrakcyjnych ty- pów danych: należy zidentyfikować i nazwać pojęcie, zdecydować o dopuszczalnych operacjach danego pojęcia i zaimplementować je. Podczas implementacji trzeba wziąć 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 jiest 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: ENCUU5VCEM7PFGTHNQY]_ 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 zadeklarowanych metod. Tym niemniej możliwość dodania tekstu objaśniającego jest wygodna. Rzadziej w obiekcie wyjątku można zapisać też dalsze informacje oi zaistniałym wyjątku: ENCUU5VCEM7PFGTHNQY] RWDNKE 5VCEM7PFGTHNQY EQPUVEJCT OUIPKGFQRGđPKGPKGUVQUW  XKTVWCN`5VCEM7PFGTHNQY  XKTVWCNEQPUVEJCT YJCV EQPUV  _ Jeśli istnieje funkcja zwracająca komunikat tekstowy, powinna być ona wirtualną metodą o nazwie YJCV, mającą pokazaną powyżej sygnaturę. W rzeczywistości często dobrym pomysłem jest wyprowadzenie typu wyjątku z jednego ze standardowych typów wyjątków: ENCUU5VCEM7PFGTHNQYRWDNKEUVFTWPVKOGAGTTQT] RWDNKE GZRNKEKV5VCEM7PFGTHNQY EQPUVEJCT OUIPKGFQRGđPKGPKGUVQUW UVFTWPVKOGAGTTQT OUI ]_ _ Pozwala to przechwytywać wyjątki albo jako 5VCEM7PFGTHNQY, albo jako ogólniejszy typ TWPVKOGAGTTQT, albo jako bardzo ogólny typ GZEGRVKQP (GZEGRVKQP to publiczna klasa bazowa klasy TWPVKOGAGTTQT). 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ów wyjątków, które mogą być wyrzucane z danego modułu lub ibiblioteki: ENCUU QPVCKPGT(CWNV] RWDNKE XKTVWCN` QPVCKPGT(CWNV  XKTVWCNEQPUVEJCT YJCV EQPUV  _ ENCUU5VCEM7PFGTHNQY RWDNKEUVFTWPVKOGAGTTQTRWDNKE QPVCKPGT(CWNV] RWDNKE GZRNKEKV5VCEM7PFGTHNQY EQPUVEJCT OUIPKGFQRGđPKGPKGUVQUW UVFTWPVKOGAGTTQT OUI ]_ EQPUVEJCT YJCV EQPUV ]TGVWTPUVFTWPVKOGAGTTQTYJCV _ _ 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 do wyrzucenia wyjątku (zobacz zagadnienie numer 65). Skopiowany wyjątek, kiedy będzie już obsłużony, musi zostać zniszczony. Często można pozwolić kompilatorowi napisać za programistę te operacje (zobacz zagadnienie numeri 49). ENCUU5VCEM7PFGTHNQY RWDNKEUVFTWPVKOGAGTTQTRWDNKE QPVCKPGT(CWNV] RWDNKE GZRNKEKV5VCEM7PFGTHNQY EQPUVEJCT OUIPKGFQRGđPKGPKGUVQUW UVFTWPVKOGAGTTQT OUI ]_ 5VCEM7PFGTHNQY EQPUV5VCEM7PFGTHNQY  5VCEM7PFGTHNQYQRGTCVQT EQPUV5VCEM7PFGTHNQY  EQPUVEJCT YJCV EQPUV ]TGVWTPUVFTWPVKOGAGTTQTYJCV _ _ Teraz już użytkownicy naszego stosu mogą wykryć niedopełnienie stosu jako 5VCEM 7PFGTHNQY (wiedząc, że używają naszego typu, śledzą taki wyjątek), jako ogólniejszy QPVCKPGT(CWNV (wiedząc, że używają naszej biblioteki kontenerów, są gotowi do ob- służenia wyjątków związanych z kontenerami), jako TWPVKOGAGTTQT (nie wiedząc nic o naszej bibliotece kontenerów chcą jednak obsłużyć wszelkie typowe błędy wykona- nia) lub jako GZEGRVKQP (są gotowi obsłużyć wszelkie standardowe wyjątki). Zagadnienie 65: nieprawidłowe korzystanie z mechanizmu wyjątków Kwestie związane ogólnie z obsługą wyjątków i ich architekturą są nadal przedmiotem dyskusji. Jednak wytyczne zdefiniowane na niższym poziomie, dotyczące sposobu wyrzucania 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ów kopiuje obiekt wyjątku w „bezpieczne”, tymczasowe miejsce. Miejsce to w dużym stopniu 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 ECVEJ korzystającej z tego obiektu, nawet jeśli na rzecz tego tymczasowego obiektu wyjątku wywołanych zostanie szereg innych fraz ECVEJ. 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 okiem cyklonu obsługi wyjątków. Oto dlaczego nie należy wyrzucać wskaźników: VJTQYPGY5VCEM7PFGTHNQY UVQUQRGTCVQTC  Zagadnienie 65: nieprawidłowe korzystanie z mechanizmu wyjątków 161 Adres obiektu 5VCEM7PFGTHNQY na stercie jest kopiowany w bezpieczne miejsce ale wskazywana pamięć, umieszczona na stercie, nie jest chroniona. Takie rozwiązanie dopuszcza też sytuację, że wskaźnik może odwoływać się do pamięci znajdującej się na stosie roboczym: 5VCEM7PFGTHNQYG UVQURCTCOGVTÎY  VJTQYG W tym przypadku wskazywany obiekt wyjątku (należy pamiętać, że wyrzucany jest wskaźnik, a nie wskazywana przezeń wartość) odnosi się do pamięci, która może w chwili przechwycenia wyjątku być już niedostępna (a przy okazji: kiedy wyrzucamy literał łańcuchowy, cała tablica znaków jest kopiowana w tymczasowe miejsce, a nie adres pierwszego znaku; jest to jednak mało przydatna informacja, gdyż nigdy nie należy wyrzucać literałów łańcuchowych — zobacz zagadnienie numer 64). Co więcej, wskaź- nik może być pusty (PWNN). Komu takie komplikacje są potrzebne? Nie należy zatem wyrzucać wskaźników, lecz obiekty: 5VCEM7PFGTHNQYG UVQURCTCOGVTÎY^  VJTQYG Obiekt wyjątku jest natychmiast kopiowany przez mechanizm obsługi wyjątków do tymczasowej lokalizacji, więc deklaracja G jest zbędna. Zwyczajowo wyrzuca się obiekty anonimowe: VJTQY5VCEM7PFGTHNQY UVQURCTCOGVTÎY  Użycie obiektów tymczasowych jasno mówi, że obiekt 5VCEM7PFGTHNQY jest używany jedynie jako obiekt wyjątku, gdyż jego cykl życia jest ograniczony do wyrażenia wyrzu- cającego go. O ile jawnie zadeklarowana zmienna G zostanie także zniszczona podczas wykonywania 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ów tymczasowych pomaga także ukrócić niektóre „tfurcze” próbiy obsługi wyjątków: UVCVKE5VCEM7PFGTHNQYG UVQURCTCOGVTÎY  GZVGTP5VCEM7PFGTHNQY CTIUVCEMGTT CTIUVCEMGTTG VJTQYG W tym przypadku nasz błyskotliwy programista postanowił zapamiętać adres obiektu wyjątku na później, prawdopodobnie w jakiejś frazie ECVEJ. Niestety, wskaźnik CTI UVCEMGTT 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 dobrze ukrytych błędów; powinien być to kod tak prosty, jak to miożliwe. Jak najlepiej przechwytywać wyjątki? Na pewno nie przezi wartość: VT[]  _ ECVEJ  QPVCKPGT(CWNVHCWNV ]  _ 162 Zarządzanie pamięcią i zasobami Warto zastanowić się, co by się stało, gdyby taka fraza ECVEJ faktycznie przechwyciła wyrzucony gdzieś obiekt 5VCEM7PFGTHNQY. Odpowiedź brzmi: szatkowanie. Obiekt 5VCEM7PFGTHNQY jest przykładem QPVCKPGT(CWNV, można by zainicjalizować HCWNV wyrzuconym obiektem wyjątku, ale nastąpiłoby poszatkowanie wszystkich danych i metod klasy pochodnej (zobacz zagadnienie numer 30). W tym konkretnym przypadku nie byłoby jednak problemów z szatkowaniem, gdyż QPVCKPGT(CWNV — jako faktyczna klasa bazowa — jest klasą abstrakcyjną (zobacz zagadnienie numer 93). Wobec tego fraza ECVEJ jest błędna. Nie można przechwycić przez wartość obiektu wyjątku jako obiektu QPVCKPGT(CWNV. Przechwytywanie przez wartość naraża programistę na jieszcze inne ukryte problemy: CVEJ 5VCEM7PFGTHNQYHCWNV ] EúħEKQYQWFTCYKCO[U[VWCELú HCWNVOQFKH[5VCVG OQLCYKPC VJTQYRQPQYPKGY[TWECO[CMVWCNP[Y[LæVGM _ Nierzadko zdarza się, że jakaś fraza ECVEJ częściowo naprawia sytuację, zapisuje wynik tej 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 ECVEJ przechwytuje wyjątek, wykonuje częściową naprawę, zapisuje stan w lokalnej kopii obiektu wyjątku i ponownie wyrzuca (niezmienionyi) obiekt wyjątku. Dla uproszczenia i dla uniknięcia opisanych problemów zawsze wyrzuca się anonimowe obiekty tymczasowe a przechwytuje się je przez refereincję. Należy zachować ostrożność, aby nie doszło do ponownego przekazania kopii z opisem problemów do tej samej procedury obsługi. Zdarza się tak wtedy, gdy nowy wyjątek jest wyrzucany z procedury obsługi, podczas gdy ponownie powinien być wyrzucany wyjątek już istniejący: ECVEJ  QPVCKPGT(CWNVHCWNV ] TQDKO[EúħEKQYæPCRTCYú KH YCTWPGM VJTQYRQPQYPGY[TWEGPKG GNUG]  QPVCKPGT(CWNVO[(CWNV HCWNV  O[(CWNVOQFKH[5VCVG PCFCNOQLCYKPC VJTQYO[(CWNVPQY[QDKGMVY[LæVMW _ _ Tym razem zapisane zmiany nie zostaną utracone, ale utracony zostanie pierwotny typ wyjątku. Załóżmy, że pierwotnym typem było 5VCEM7PFGTHNQY. Kiedy obiekt ten jest przechwytywany jako referencja QPVCKPGT(CWNV, dynamicznym typem obiektu wyjątku nadal jest 5VCEM7PFGTHNQY, więc ponowne wyrzucenie tego obiektu pozwala znów prze- chwycić zarówno we frazie ECVEJ obiektu 5VCEM7PFGTHNQY, jak i QPVCKPGT(CWNV. Jednak w chwili wyrzucania nowego obiektu wyjątku O[(CWNV jest on typu QPVCKPGT(CWNV i nie może być przechwycony przez frazę ECVEJ obiektu 5VCEM7PFGTHNQY. Zwykle lepiej jest ponownie wyrzucać istniejący obiekt wyjątku, a nie obsługiwać pierwotny wyjątek. Zamiast tego lepiej jest wyrzucać nowy: Zagadnienie 66: nadużywanie adresów lokalnych 163 ECVEJ  QPVCKPGT(CWNVHCWNV ] TQDKO[EúħEKQYæPCRTCYú KH YCTWPGM HCWNVOQFKH[5VCVG  VJTQYRQPQYPGY[TWEGPKG _ Na szczęście klasa bazowa QPVCKPGT(CWNV jest abstrakcyjna, więc tutaj błąd nie może wystąpić. W ogóle klasy bazowe powinny być abstrakcyjne. Oczywiście, rada taka nie jest słuszna, jeśli konieczne jest wyrzucenie całkiemi innego typu wyjątku: ECVEJ  QPVCKPGT(CWNVHCWNV ] TQDKO[EúħEKQYæPCRTCYú KH QWVAQHAOGOQT[ VJTQYDCFACNNQE Y[TWECO[PQY[Y[LæVGM HCWNVOQFKH[5VCVG  VJTQYRQPQYPGY[TWEGPKG _ Inny typowy problem wiąże się z kolejnością fraz ECVEJ. Frazy te są sprawdzane kolej- no (jak warunki KHGNUGKH, a nie jak instrukcja UYKVEJ), więc typy wyjątków należy porządkować od najbardziej szczegółowych do najogólniejszych. Jeśli chodzi o typy wyjątków niedających się tak uporządkować, trzeba po prostu logicznie zastanowić się, jaka kolejność będzie najlepsza: ECVEJ  QPVCKPGT(CWNVHCWNV ] TQDKO[EúħEKQYæPCRTCYú HCWNVOQFKH[5VCVG PKGOQLCYKPC VJTQY _ ECVEJ 5VCEM7PFGTHNQYHCWNV ]  _ ECVEJ GZEGRVKQP ]  _ Pokazana powyżej kolejność fraz ECVEJ nie pozwoli nigdy przechwycić wyjątku 5VCEM 7PFGTHNQY, gdyż przed odpowiadającą mu frazą pojawia się fraza ogólniejszego wyjątku QPVCKPGT(CWNV. Mechanizm obsługi wyjątków łatwo może prowadzić do dużej złożoności kodu ale niekoniecznie 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ów ostrzega przed taką sytuacją i ostrzeżenie to należy itraktować poważnie. 164 Zarządzanie pamięcią i zasobami Znikanie ramek stosu Jeśli zmienna jest zmienną automatyczną, używana przez nią pamięć znika w chwili powrotu z procedury: EJCT PGY.CDGN ] UVCVKEKPVPT V EJCTDWHQT=?QDCECICFPKGPKGPWOGT URTKPVH DWHQTGV[MKGVCFPT V   TGVWTPDWHQT _ Funkcja powyższa ma tę nieprzyjemną cechę, że czasem działa. Po wyjściu z tej funkcji ramka stosu jej odpowiadająca jest usuwana a związana z nią pamięć jest zwalniana (w tym także DWHQT), aby mogła być wykorzystana przez następną funkcję. Jeśli jednak otrzymana z PGY.CDGN wartość zostanie przepisana przed wywołaniem następnej funkcji, uzyskany wskaźnik, choć już niepoprawny, może wskazywać jednak pożądaną wartość: EJCT WPKSWG.CDPGY.CDGN  EJCTO[DWH=? RO[DWHO[DWH YJKNG  RO[DWH  WPKSWG.CD   Nie jest to kod, który da się zbyt długo serwisować. Serwisant może zdecydować się na alokację bufora na stercie: EJCT RO[DWHPGYEJCT=? Serwisant może też zrezygnować z ręcznego kopiowania buifora: UVTER[ RO[DWHWPKSWG.CD  Serwisant może postanowić o użyciu bardziej abstrakcyjnego typu danych niż bufor znakowy: UVFUVTKPIO[DWH WPKSWG.CD  Każda z powyższych modyfikacji spowoduje, że zmodyfikowana będzie pamięć lokalna wskazywana przez WPKSWG.CD. Interferencje danych statycznych Jeśli zmienna jest statyczna, późniejsze wywołanie tej samej funkcji wpłynie na wynik wywołania wcześniejszego: EJCT PGY.CDGN ] UVCVKEKPVPT V UVCVKEEJCTDWHQT=? URTKPVH DWHQTGV[MKGVCFPT V   TGVWTPDWHQT _ Pamięć na bufor jest dostępna także po wyjściu z funkcji ale kolejne użycie tej samej funkcji może zmienić zawartość tej pamięci: Zagadnienie 66: nadużywanie adresów lokalnych 165 RT[RCFGM EQWVRKGTYUCPGY.CDGN    EQWVFTWICPGY.CDGN GPFN RT[RCFGM EQWVRKGTYUCPGY.CDGN   FTWICPGY.CDGN GPFN 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 PGY.CDGN, napisze tak 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 PGY.CDGN może wywołania połączyć powodując przez to błąd. Co gorsze, połączone wywołanie moż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 jej deklaracji. Pomocą mogą być tu komentarze (zobacz zagadnienie numer 1), ale ważne jest takie projektowanie funkcji, aby ułatwiać prawidłowie jej wykorzystywanie. Należy unikać zwracania referencji odwołującej się do pamięci zaalokowanej w danej funkcji. Użytkownicy będą notorycznie zapominać o zwolnieniu tej pamięci powodując przez to wycieki pamięci: KPVH ]TGVWTP PGYKPV  _  KPVKH Y[EKGMRCOKúEK Prawidłowy kod musi przekształcać referencję na adres lub kopiować wynik i zwalniać pamięć. Byle nie na mojej zmianie, kolego: KPV KRH LGFPCMQUOCTPCOGVQFC KPVVORH MQNGLPC KPVKVOR FGNGVGVOR Wyjątkowo źle sprawdza się to w przypadku przeciążonycih funkcji operatorów: GURQNQPCQRGTCVQT EQPUV GURQNQPCCEQPUV GURQNQPCD ]TGVWTP PGY GURQNQPC CTG DTGCKO DKO _  GURQNQPCCDE CD E C DOPÎUVYQY[EKGMÎY Należy zwracać wskaźnik do odpowiedniego miejsca w pamięci albo nie alokować pamięci i zwracać wynik przez wartość: KPV H ]TGVWTPPGYKPV  _ GURQNQPCQRGTCVQT  GURQNQPCC GURQNQPCD ]TGVWTP GURQNQPC CTG DTGCKO DKO _ 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 zwykle starają 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: XQKFNQECN5EQRG KPVZ ] EJCT ER KH Z ] EJCTDWH=?CUFH ERDWHMKGRUMKRQO[Uđ EJCTDWH=?SYGTV[ EJCT ERDWH  _ KH Z ] EJCT ERCEJQFKPCDWH!  _ KH ER RTKPVH ER D[èOQľGDđæF _ Kompilatory są bardzo elastyczne, jeśli chodzi o sposób układania pamięci na zmienne lokalne. W zależności od używanego systemu i opcji kompilatora pamięć na DWH i ER może się pokryć lub nie. Jest to dopuszczalne, gdyż DWH i ER mają rozłączne zakresy i rozłączne okresy życia. W przypadku pokrycia wartość DWH zostanie uszkodzona i zachowanie RTKPVH może się zmienić (prawdopodobnie nic nie zostanie pokazane). Aby uzyskać przenośność, nie należy opierać się na konkretnym sposobie działania ramek stosu. Korekta przez static Czasami programista staje wobec poważnego błędu i okazuje się, że zastosowanie specyfikatora UVCVKE powoduje, że wszystko nagle zaczyna działać poprawniei:  EJCTDWH=/#:? NQPIEQWPV  KPVK YJKNG K /#: KH DWH=K?   ] DWH=K?   EQWPV Zagadnienie 67: błąd pozyskania zasobu bierze się z inicjalizacji 167 _ CUUGTV EQWPVK   Kod ten zawiera kiepsko napisaną pętlę, która czasami pisze za końcem tablicy DWH powodując, że asercja zawodzi. Szukając na ślepo przyczyn błędu programista może zadeklarować EQWPV jako lokalną zmienną statyczną i kod nagle zacznie diziałać: EJCTDWH=/#:? UVCVKENQPIEQWPV  EQWPV KPVK YJKNG K /#: KH DWH=K?   ] DWH=K?   EQWPV _ CUUGTV EQWPVK  Wielu programistów nie będzie chciało narażać na szwank swojego szczęścia i zostawi swój kod w takiej postaci. Niestety, problem nie znikł: po prostu został przeniesiony. Siedzi sobie spokojnie i czeka, gotów do uderzenia w oidpowiedniej chwili. Zmiana lokalnej zmiennej EQWPV na statyczną spowodowało przeniesienie jej poza ram- kę stosu funkcji do całkiem innego obszaru pamięci, gdzie umieszczane są obiekty lokalne. Z powodu tego przeniesienia zmienna ta już nie jest nadpisywana. Jednak nie tylko zmienna EQWPV 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: EJCTDWH=/#:? NQPIEQWPV  KPVK HQT KPVKK/#: K KH DWH=K?   ] DWH=K?   EQWPV _  Zagadnienie 67: błąd pozyskania zasobu bierze się z inicjalizacji Wstyd, jak wielu programistów C++ nie docenia cudownej symetrii konstruktorów i 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 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 programowania w 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 elastyczniejszy pod względem możliwych sposobów stosowania języka. Nie chodzi o to, iżby nie było w 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”. Jest to 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. Podczas tworzenia obiektu klasy kolejność inicjalizacji zawsze jest taka sama: najpierw podo- biekty wirtualnej klasy bazowej (zgodnie ze standardem, „w takiej kolejności, w jakiej pojawiają 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 pola danych 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 klas robią swoje inicjalizacje w takiej samej kolejności, nawet jeśli lista inicjalizacyjna pól jest zapisana w innej kolejności (zobacz zagadnienie inumer 52). Efektem ubocznym lub wynikiem inicjalizacji jest to, że konstruktor zbiera zasoby w miarę tworzenia obiektu. Często kolejność pobierania tych zasobów jest istotna (przykładowo, przed zapisem bazy danych trzeba ją zablokować. Trzeba pobrać uchwyt pliku przed pisaniem do tego pliku) i zwykle destruktor zwalnia zasoby w kolejności odwrotnej 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 samiej kolejności. Tak na marginesie warto wspomnieć, że nie zawsze tak było. Krótko po powstaniu języka C++ kolejność inicjalizacji w konstruktorach nie była ustalona, co powodowało wiele trudności w przypadku złożonych projektów. Tak jak większość reguł języka C++, tak i ta jest wynikiem starannego projektu i prakitycznego doświadczenia. Symetria tworzenia i niszczenia zachodzi nawet wtedy, gdy przechodzimy od struktury obiektu do zastosowań wielu obiektów. Warto rozważyć proistą klasę śladu: ►►zagadnienie67/trace.h ENCUU6TCEG] RWDNKE 6TCEG EQPUVEJCT OUI OA OUI ]UVFEQWV9GLħEKGFQOAGPFN_ `6TCEG ]UVFEQWV9[LħEKGOAGPFN_ Zagadnienie 67: błąd pozyskania zasobu bierze się z inicjalizacji 169 RTKXCVG EQPUVEJCT OA _ Taka klasa śladu jest być może nieco zbyt prosta, gdyż zakładamy, że jej inicjalizator jest poprawny i będzie istniał przynajmniej tak długo, jak długo istniał będzie obiekt 6TCEG, ale dla naszych celów to wystarcza. Obiekt 6TCEG pokazuje komunikat przy każdym jego tworzeniu i ponownie przy niszczeniu, więc za jego pomocą można śle- dzić przebieg wykonania programu: ►►zagadnienie67/trace.cpp 6TCEGC QDUCTWINQDCNPGIQ  XQKFNQQR[ KPVEQPFKPVEQPF ] 6TCEGD VTGħEKHWPMELK  KV6TCEGE FCNUGLEúħEKVTGħEK  KH EQPFEQPF TGVWTP KH EQPF ] 6TCEGF KH  UVCVKE6TCEGUVCV NQMCNP[EJUVCV[EP[EJ  YJKNG EQPF ] 6TCEGG RúVNK  KH EQPFEQPF IQVQKV _ 6TCEGH RQRúVNK  _ 6TCEGI RQKH  _ Wywołanie funkcji NQQR[ z parametrami  i  da następujące wyniki: 9EJQFúFQQDUCTWINQDCNPGIQ 9EJQFúFQVTGħEKHWPMELK 9EJQFúFQFCNUGLEúħEKVTGħEK 9EJQFúFQKH 9EJQFúFQNQMCNP[EJUVCV[EP[EJ 9EJQFúFQRúVNK 9[EJQFúRúVNK 9EJQFúFQRúVNK 9[EJQFúRúVNK 9[EJQFúKH 9[EJQFúFCNUGLEúħEKVTGħEK 9EJQFúFQFCNUGLEúħEKVTGħEK 9[EJQFúFCNUGLEúħEKVTGħEK 9[EJQFúVTGħEKHWPMELK 9[EJQFúNQMCNP[EJUVCV[EP[EJ 9[EJQFúQDUCTWINQDCNPGIQ Z komunikatów jasno wynika, jak czas życie obiektu 6TCEG jest związany z aktualnym zakresem wykonania. W szczególności warto zwrócić uwagę na wpływ instrukcji IQVQ i TGVWTP na czas życia aktywnych obiektów 6TCEG. Każda z tych gałęzi stanowi przykład praktyki kodowania, ale są to konstrukcje podobne do tych, jakie pojawiać się będą w większości praktycznie spotykanego oprogramowania. 170 Zarządzanie pamięcią i zasobami XQKFTQD $ ] DNQMWL $  TÎDDCæFCP[EJEQVTGDC QFDNQMWL $  _ 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ędzy blokowaniem a odblokowaniem jest długi: XQKFTQD $ ] DNQMWL $   KH VCMAOKAUKGARQFQDC TGVWTP  QFDNQMWL $  _ Teraz błąd powstaje za każdym razem, kiedy tak się podoba funkcji TQD $: baza danych pozostanie zablokowana a to bez wątpienia przyczyni się do wielu kłopotów w innych miejscach systemu. Tak naprawdę nawet oryginalny kod nie był napisany poprawnie, gdyż wyrzucenie wyjątku po zablokowaniu bazy, ale przed jej zablokowaniem, da podobny efekt: baza będzie zablokowana. Można by próbować zaradzić temu jawnie uwzględniając wyjątki i utrudniając lekturę serwisantom: XQKFTQD $ ] CDNQMWL $  VT[] TÎDDCæFCP[EJEQVTGDC _ ECVEJ  ] QFDNQMWL $  VJTQY _ QFDNQMWL $  _ Takie rozwiązanie jest przegadane, mało eleganckie, trudne do serwisowani
Pobierz darmowy fragment (pdf)

Gdzie kupić całą publikację:

C++. Kruczki i fortele w programowaniu
Autor:

Opinie na temat publikacji:


Inne popularne pozycje z tej kategorii:


Czytaj również:


Prowadzisz stronę lub blog? Wstaw link do fragmentu tej książki i współpracuj z Cyfroteką: