c2.6.doc

(131 KB) Pobierz

2.6 Deklaratory

"Z całej składni języka C najbardziej nie lubię składni deklaracji."

Bjarne Stroustrup

Na zakończenie tej części, ponieważ poznaliśmy już wszystkie sposoby deklarowania wszystkiego co się da, wspomnę teraz o największej bolączce C++ (burzliwe dyskusje ntt. prawdopodobnie trwają nadal, ale aktualny standard niczego jeszcze nie ulepszył w tej kwestii). Język C, jako język spartański, przyjął najprostszą możliwą postać deklaracji i w związku z tym jest ona pogmatwana na każdy możliwy sposób. Właściwie może nie byłby tak pogmatwana, gdyby nie operatory przedrostkowe, a właściwie tylko jeden - *. Problem w tym, że jest on w deklaratorach chyba najczęściej używany. Różni profesorzy, doktorzy i magistrzy na wyższych uczelniach uwielbiają gnębić studentów podając im deklarator i żądając objaśnienia, co taki deklarator deklaruje (jak sobie przypomnę koszmar lekcji języka polskiego z liceum i tłumaczenie "co autor miał na myśli", to mam trochę dziwne wzajemne skojarzenia... pocieszać się można tylko tym, że kompilator to i tak zrozumie zawsze jednoznacznie). Ze swojej strony tylko osobiście podpowiem, że staram się zawsze wprowadzać definicje pośrednie, jeśli muszę już używać jakichś skomplikowanych typów, jednak zazwyczaj cały kłopot sprowadza się do problemów z odpowiednim umieszczeniem i opakowaniem nawiasami jednego operatora - *.

W deklaratorach występują operatory, podobne do takich, jakie się używa potem na obiektach tak deklarowanych typów. A więc, * do wskaźników, [] do tablic i () do funkcji (& do referencji tutaj jest wyjątkiem, ale wydaje mi się, że ten operator jest wystarczająco czytelny). W sumie nawet deklaratory nie są czymś skomplikowanym. Tzn. nie byłyby, gdyby nie *.

I wbrew pozorom, * jest jedynym operatorem, z którym są takie problemy; operatory przyrostkowe są całkowicie bezproblemowe, a referencje mają zbyt ograniczone możliwości. Praktycznie w deklaracji jakiejkolwiek referencji nie ma żadnego innego operatora oprócz `&' - tablic referencji tworzyć się nie da, a referencji do tablicy lub funkcji aż tak często się nie używa.

Postać deklaratora wygląda następująco:

<typ czołowy> <operatory przedrostkowe> nazwa <operatory przyrostkowe>;

Przy czym operatory [] i () to operatory przyrostkowe, a * i & - przedrostkowe. Proszę zgadnąć zatem, co takiego może deklarować taki deklarator, który dla dodatkowego zmylenia zapiszę tak, jak "profesjonalni" programiści C:


int *tab[20];


A więc - czym jest `tab'? Wskaźnikiem do tablicy 20 elementów typu int, czy tablicą o 20 elementach typu `int*'?

Odpowiem może tak: co się stanie, jeśli dodamy odpowiednio nawiasy:


int (*tab)[20];


Otóż ta właśnie deklaracja deklaruje wskaźnik do tablicy ... itd., podczas gdy tamten poprzedni typ oznaczał właśnie to drugie.

Oczywiście, że w int* tab[20]; lepiej wiadomo, o co chodzi. Niestety to tylko pozór. Gdyby bowiem chcieć wstawić w tą deklarację nawiasy tam, gdzie one powinny być, należałoby je umieścić w następujący sposób:


int (*tab[20]);


I ciekaw jestem, komu skojarzyłoby się to z tablicą 20 int*?

Ale to jeszcze nic. Przykładowo, mamy w bibliotece standardowej C taką funkcję `signal' (zostanie ona dalej przedstawiona). Służy ona do ustanowienia nowej funkcji obsługującej konkretny sygnał wysłany do programu, zwracając starą. Funkcja ta ma posiadać nagłówek: `void sighandler( int )'. Funkcja `signal' zatem przyjmuje i zwraca wskaźnik do niej. Oto jej deklaracja (również dla utrudnienia, napiszę ją tak, jak "profesjonalni" progamiści to piszą, czyli bez wstawiania spacji wewnątrz nawiasów funkcji, jak to mam w zwyczaju):


void (*signal(int sig, void (*handler)(int)))(int);


No i jak? Podoba się? Tak jest (nawiasem mówiąc) podana ta deklaracja we sławetnej książce Kernighan & Ritche "Język ANSI C" (tzw. "biblii" programistów C). Nikt normalny przecież (a więc również twórcy bibliotek do GNU C) nie wpisałby czegoś takiego do kodu (od samego patrzenia na to można dostać świra :*). Tam deklaracja tej funkcji zawiera pewną deklarację pośrednią (tu pokazałem trochę uproszczoną):


typedef void (*__sighandler_t)( int );


I potem:


__sighandler_t signal( int, __sighandler_t );


I to już jest proste i zrozumiałe (__sighandler_t jest zresztą dalej w tym pliku nagłówkowym używane do jeszcze wielu innych deklaracji).

Gdyby kogoś interesowało, jak wstawiłem tamtą poprzednią deklarację funkcji `signal' do tego tekstu, to wyjaśniam, że napisałem to z pamięci, aczkolwiek nie od początku do końca. Nie skleiłem tego również z przedstawionych deklaracji pośrednich (szczerze powiedziawszy, miałbym z tym problemy, poza tym znalazłem lepsze rozwiązanie). Zanim jednak przedstawię szczegóły, przedstawię źródło całej koncepcji.

Bjarne Stroustrup we wspomnianej już książce wspomina, że pojawiła się koncepcja poprawy konstrukcji deklaratorów, gdzie zamiast operatora przedrostkowego `*' używałoby się przyrostkowego `->' (niestety nie dopracował tego pomysłu i w końcu nie znalazł się on w propozycji do standardu). Choć obowiązkowość tego "typu czołowego" nadal wprowadzałaby pewne zamieszanie, ale deklarator byłby jeszcze jakoś możliwy do odczytania. Na przykład wspomniana funkcja miałaby taki nagłówek:


void signal( int sig, void handler->( int ) )->( int );


Co prawda tutaj `void' jest typem zwracanym funkcji, do której wskaźnik zwraca funkcja `signal' (a nie typem zwracanym przez `signal'), całość bowiem typu zwracanego to jest i to, co jest przed nazwą funkcji, i to co jest za zamykającym nawiasem. Nie jest więc taka deklaracja wystarczająco czytelna, ale już o wiele bardziej, niż ta obowiązująca. Ma zresztą jedną dodatkową zaletę: w odróżnieniu od `*', nie wymaga nigdy nawiasów wymuszających kolejność interpretacji...

Radykalna propozycja, na której się oparłem, polegała dodatkowo na tym, że cały zlepek operatorów przyrostkowych (czyli stojących "za" nazwą) można przenieść przed typ czołowy, ale tylko wtedy, jeśli deklarator zawiera nazwę (bo nie musi; ta pierwsza deklaracja funkcji `signal' nie musiała mieć np. nazwy argumentu `handler'; mogłoby pozostać samo (*) ). Deklaracja taka wyglądałaby wtedy w następujący sposób:


->( int )void signal( int sig, ->( int )void handler );


Czytałoby się to (poczynając od funkcji i kończąc na typie zwracanym): "Funkcja signal, przyjmująca argument `sig' typu `int' oraz argument `handler' typu `wskaźnik do funkcji przyjmującej `int' i zwracającej `void' ', zwracająca wskaźnik do funkcji przyjmującej argument `int' i zwracającej `void'.

To i tak jeszcze mało. Tablica wskaźników do funkcji np. wyglądałaby tak:


[20]->( int )void tab;


Ewentualnie nic nie stoi na przeszkodzie, żeby umieszczać operatory część tu część tam (przy przenoszeniu należy pamiętać, że operatory muszą być ustawione w tej samej kolejności!):


->( int )void tab[20];


Czasem, gdyby zaszła potrzeba stworzenia takiej skomplikowanej deklaracji, zawsze się można posłużyć taką. Konwersja na postać akceptowalną przez kompilator jest banalnie prosta - należy najpierw umieścić wszystkie operatory przyrostkowe tam, gdzie "były", czy powinny być:


void tab[20]->( int );


po czym zamienić wszystkie `->' na `*' w ten sposób: w miejscu, gdzie jest `->' wstawić `)', po czem po tej operacji dodać `(*' przed nazwą. Jeśli wiesz już również, jak konwertować w drugą stronę, to wszystkim wyżej wspomnianym profesorom, doktorom i magistrom możesz pokazać mniej więcej ` _|_ '.



2.7 Niekompatybilności z językiem C

"Powoli i nie bez bólu zgodzono się na to, że nie powinno być niczym nie uzasadnionych niezgodności między językiem C++ a językiem ANSI C, ale też przyjęto, że istnieje coś takiego, jak uzasadniona niezgodność. [...] Później uznano taką zasadę: `język C++: tak bliski języka C, jak tylko możliwe, ale nie bliższy/ [...]. Miarą sukcesu takiej polityki jest to, że każdy przykład w książce Kernighana i Ritchie'ego jest napisany w języku C będącym podzbiorem języka C++."

Bjarne Stroustrup

Wstęp

Wiele z zamieszczonych tu informacji były już po części wspominane wcześniej. Może i nie będą specjalnie przydatne, ale warto się z nimi zapoznać przed zapoznaniem się z elementami biblioteki standardowej C. Nie wszystkie są specjalnie uciążliwe, postanowiłem tutaj jednak zebrać to wszystko "do kupy".

Struktury, wskaźniki, konwersje

W C panowały następujące zasady, które nie przetrwały w C++:

1.       typ strukturalny i wyliczeniowy był poprzedzony słowem kluczowym identyfikującym (struct/union/enum); w konsekwencji nie zajmowało to identyfikatora w bieżącym zasięgu

2.       struktura zagnieżdżona w innej ma w C++ nazwę z podaniem operatora zasięgu, podczas gdy w C była identyczna, jak gdyby była zadeklarowana normalnie w zewnętrznym zasięgu (extern "C" również udostępnia tą właściwość)

3.       wskaźnik void* jest typem uniwersalno-wskaźnikowym; jest on niejawnie konwertowany na każdy typ wskaźnikowy (z zastrzeżeniami co do wariancji); w szczególności zresztą w C można konwertować niejawnie dwa dowolne wskaźniki między sobą i z wartością całkowitą, void* wyróżniłem z uwagi na to, że jest to powszechnie w C stosowane, bo kompilator nie rzuca wtedy ostrzeżeń.

Opuszczanie `int'; styl `K&R' definiowania argumentów funkcji

Jest to składnia (K&R to oczywiście "Kernighan & Ritchie", twórcy języka C; pierwotnie w języku C obowiązywała wyłącznie taka właśnie składnia) o następującej postaci (podam już przykładowo, bez schematów):


int strcpy( dest, source )

        char *source, *dest;

{

        ...

}


Zaletą tego stylu jest to, że można podawać listę zmiennych, tak jak przy deklaracjach zmiennych lokalnych. Nie obowiązuje też oczywiście w takich deklaracjach żadna kolejność.

Posiada również wady. Czytelności tej składni niestety zarzucić nie można. W ogóle nie widać tego, jakie typy argumentów są oczekiwane na konkretnych pozycjach (nawet jeśli programista zachowa kolejność w deklaracjach). Założenia standardu C nie nakazują zachowania zgodności z normalnymi zapowiedziami; niektóre archaiczne kompilatory C nie dopuszczają innych zapowiedzi, niż bez specyfikacji argumentów. Z kolei gdyby używać tych specyfikacji, to ich synchronizacja jest w ten sposób dodatkowo utrudniona.

Ta składnia oczywiście nie zaakceptowała się w C++. Przytaczam to tylko jako ciekawostkę, którą nadal można spotkać w programach, których autorzy na siłę starają się dostosować swoje dzieła do kompilatorów z epoki kamienia łupanego. Nawet sami twórcy C uważają, że jest to anachronizm, który prawdopodobnie zniknie w którymś z następnych standardów.

W C, jak też w pierwotnych wersjach C++, istniała z trudem stłumiona zasada, że w deklaracjach czegokolwiek oprócz zmiennych, można było pominąć nazwę "typu czołowego", jeśli był nim int. W sumie dziś pozostała ona wyłącznie (w C) w formie ostrzeżeń kompilatora (np. jeśli deklaruje się funkcję, lub zmienną ze słowem static, extern czy auto). Proszę sobie wyobrazić, co w efekcie oznaczała ona w połączeniu ze stylem K&R:


fn( a, b, c ) char* b;

{

        ...

}


co odpowiada "normalnemu" nagłówkowi:


int fn( int a, char* b, int c );


extern "C"

Wspominałem już o tym niejednokrotnie, chciałbym tutaj jedynie uściślić kwestię używania extern "C". Dyrektywa ta pozwala na zaimportowanie kodu dostowowanego do C dla C++. Okoliczności, w których konieczne jest jego użycie to przede wszystkim deklaracje funkcji C. Nazwa takiej funkcji nie jest manglowana, a zewnętrznie funkcja jest traktowana jak funkcja C, co oznacza, że nie może być nią operator (tzn. właściwie to może, sprawdziłem to na g++ faktycznie, operator + miał zewnętrzną niezmanglowaną nazwę - __pl!) i nie może być przeciążona (nie znaczy to oczywiście, że nie da się jej przeciążyć, a jedynie, że tylko jedna z przeciążonych funkcji może być extern "C").

Brak manglowania nazwy jest jedną z istotniejszych rzeczy w extern "C". Podczas wiązania poszukje się funkcji o określonej nazwie, a nazwy funkcji z biblioteki standardowej C są przecie zapisane w "gołej" postaci.

Restrykcyjność

Poza tym, co już wymieniłem należy jeszcze pamiętać o następujących rzeczach:

·         w C++ używanie prototypów (zapowiedzi) funkcji jest absolutnie obowiązkowe (g++ w wersji 2.7.2 przyjmował, że niezapowiedziana funkcja ma deklarację "uniwersalną", o czym ostrzegał; aktualne wersje już generują błąd)

·         Pusta lista argumentów w C++ jest synonimem (void), natomiast odpowiednikiem () w C jest (...) w C++ (ma sens tylko z extern "C")

·         Globalne dane w C++ można zadeklarować dokładnie RAZ. Istnieje konkretnie podzial na obiekty o slabym i silnym wiazaniu, przy czym obiekty o slabym wiazaniu to tylko te generowane na potrzeby samego jezyka (czyli np. tablice metod wirtualnych, czy out-line wersje funkcji inline). Obiekty deklarowane jawnie podlegaja zawsze silnemu wiazaniu, wiec proba zlinkowania dwoch plikow deklarujacych symbol o tej samej nazwie zakonczy sie niepowodzeniem

·         Globalne stałe w C++ (const) mają wiązanie plikowe, podczas gdy w C było zewnętrzne. Aby stała w C++ miała wiązanie zewnętrzne, należy dodać modyfikator extern. Stała może być również deklarowana tylko raz, a zaimportowanie różni się od deklaracji tylko tym, że deklarowana jest inicjalizowana

·         Typ wyliczeniowy jest w C++ traktowany jako oddzielny typ, z tym tylko zastrzeżeniem, że konstruktor typu int może przyjmować dowolny enum, jednak niejawne konwersje enumów na int są niedozwolone

·         C++ restrykcyjnie pilnuje kwestii zgodności liczb całkowitych różniących się znakowością (signedness)


 


3. Zaawansowane programowanie w C++


3.1 Wiadomości podstawowe

W tej części zostaną omówione właściwości C++ używane rzadziej, za to udostępniające dość użyteczne możliwości. Do niektórych z nich nawet wszystkie możliwości zastosowania nie zostały jeszcze wymyślone. Zanim jednak przejdę do takowych, podam jeszcze drobne uzupełnienia. Przede wszystkim zaś, dotychczas operowaliśmy obiektami tworzonymi jako zmienne lokalne; tu poznamy bardziej zaawansowane sposoby tworzenia obiektów, jak również dość istotnych reguł, o których należy podczas używania takich obiektów pamiętać.

Jedna z rzeczy, na jaką warto tutaj zwrócić uwagę, to że C++ swoje dość zaawansowane właściwości oparł na tym, co było w języku C. Jednak nie jest to nawet bazowane na teorii języka C, lecz raczej jest to udostępnienie prawie wszystkich możliwości C za pomocą tej samej składni i w ten sam sposób, lecz o zupełnie innych podstawach teoretycznych.

Zestawienie wiadomości o klasach pamięci; obiekty tymczasowe

Poznaliśmy już kilka sposobów tworzenia obiektów. Wiemy zatem, że obiekt może być:

·         statyczny, tzn. jest tworzony na początku uruchomienia programu i usuwany przed samym jego zakończeniem; właściwość tę posiadają statyczne zmienne lokalne oraz zmienne globalne

·         automatyczny, tzn. jest tworzony na początku zawierającego go zasięgu i usuwany przed samym jego końcem; właściwość tę posiadają zwykłe zmienne lokalne

Poza tymi dwiema język C++ udostępnia jeszcze klasę tymczasową i dynamiczną.

Klasa tymczasowa jest to dość niezwykła klasa. Jest ona zdecydowanie najmłodsza w C++. Jakiś czas temu w komitecie standaryzacyjnym X3J16 było wiele burzliwych dyskusji nt. tego, jaką trwałość należy zapewnić obiektom tymczasowym. Co to w ogóle jest?

Wiemy o tym, że obiekt tworzy się przez

<typ> <zmienna>( <argumenty> );.

Jednak obiekt można utworzyć nie nadając mu żadnej nazwy, czyli:

<typ>( <argumenty> )

Co się wtedy dzieje? Otóż obiekt jest tworzony tylko na użytek bieżącej instrukcji. Do czego to zatem służy?

Spróbujmy sobie wyobrazić, że musimy zwrócić jakiś pośredni wynik. Przykładowo mamy takie wyrażenie:


x = flog( add( a, b ) );


Funkcja add musi UTWORZYĆ NOWY obiekt z argumentów a i b. Gdzie go przechowamy? Kiedy ten obiekt musi być usunięty? Musi być ta wyprodukowana wartość gdzieś tymczasowo przechowana i natychmiast usunięta. Jeśli obiekt jest typu int, to jest to żaden problem, ale jeśli jest to macierz, to już nie jest to takie proste - obiekt należy naprawdę utworzyć i zniszczyć w odpowiednim momencie. I te "momenty" trzeba dokładnie określić.

No więc po tych wszystkich burzliwych dyskusjach, z których z trudem wymigano się od implementacji odśmiecacza, ustanowiono że obiekt tymczasowy istnieje do końca instrukcji, która go utworzyła. Jednak jest jeden wyjątek od tej reguły, o którym zaraz powiem.

Mimo wszystko, obiekty tymczasowe, zwłaszcza większych rozmiarów, są plagą w C++. Procedura np. zwracania obiektu przez funkcje zwykle bowiem przebiega w ten sposób, że obiekt jest kopiowany do obiektu tymczasowego, a potem znów kopiowany, żeby został użyty w instrukcji wywołującej funkcję. Pół biedy jeszcze jak obiekt jest przyjmowany przez stałą referencję. Jednak jeśli zrobilibyśmy tak:


x = add( a, b );


to funkcja wyprodukuje obiekt tymczasowy, który następnie zostanie skopiowany do obiektu x przez operator przypisania. Paskudnie. Dużo efektywniej wyjdzie to, jeśli zrobimy tak:


add( a, b, &x );


przekazując x przez wskaznik. Taka postać jest paskudna składniowo i mało czytelna, ale czasem jednak warto.

Jednak jest jeszcze jedna możliwość, która jest właśnie tym wspomnianym wyjątkiem od zasady obsługi obiektów tymczasowych. Mianowicie można użyć zmiennej referencyjnej:


const complex& x = y + z;


Efekt będzie identyczny, jakby w powyższym zapisie nie było znaku &. Jednak tylko efekt zewnętrzny. Wewnętrznie bowiem oszczędzamy jedno kopiowanie. Ten obiekt tymczasowy, który zwróciło dodawanie, zostanie utrzymany przez zmienną referencyjną, która została nim zainicjalizowana. Zatem tak schwytany obiekt tymczasowy będzie miał trwanie równe tej zmiennej referencyjnej. Warto pamiętać o tej właściwości, gdyż jest ona bardzo użyteczna z uwagi na wydajność programu.

Tworzenie obiektów tymczasowych jest niestety dość wrażliwe na błędy, czesto nawet nie wykrywane przez kompilator. Na przykład:


const char* tt = (string( x ) + " było źle!").c_str();


Tutaj będzie tak: utworzy się tymczasowy obiekt z x, który zostanie przekazany jako argument do operatora +. Ten zwróci obiekt tymczasowy. Na rzecz tego obiektu wywołujemy c_str(), żeby uzyskać normalny wskaźnik char*, który jest przypisywany do tt. Tu z kolei instrukcja się kończy i obiekt tymczasowy, którego element przypisaliśmy jest usuwany. O dalszych losach takiego wskaźnika przeczytasz niedługo.

Zunifikowane zarządzanie obiektami; obiekty dynamiczne

Oto druga, nie poznana jeszcze klasa pamięci. Teraz poznamy takie obiekty, których czasem życia musimy zarządzać sami, bądź komuś to zlecić (ta możliwość wyboru jest cechą charakterystyczną C++; ani C ani Java nie dają takiego wyboru: w pierwszym musisz obiektem zarządzać ręcznie, w drugi zaś w ogóle nie masz wolnej ręki w zarządzaniu obiektem; Java o wszystko zadba za Ciebie :*).

Obiekty dynamiczne nie są takoż niczym szczególnym w C++; w C również używało się obiektów dynamicznych, jednak istnieje pewna różnica w zarządzaniu nimi. C++ uściślił bowiem pojęcie zarządzania obiektami (nie tylko dynamicznymi) dzieląc je na następujące etapy:

1.       Przydzielenie pamięci dla obiektu (allocation)

2.       Utworzenie obiektu (construction)

3.       Używanie obiektu (using)

4.       Zniszczenie obiektu (destruction)

5.       Odzyskanie pamięci zajmowanej przez obiekt (recycling?)

W przypadku zmiennych lokalnych, pamięć przydzielana jest z odpowiednio przeznaczonego na to obszaru (zazwyczaj ze stosu; przydział takiej pamięci jest najszybszy; w przypadku typów ścisłych można też przydzielić zmiennej rejestr procesora). Odzysk pamięci następuje zatem przed zrealizowaniem powrotu z funkcji. Co jednak oznaczają te punkty "utworzenie obiektu" i "zniszczenie obiektu"? Dodam dla ciekawostki, że C nie znał takich pojęć... Przypomnę jednak, co pisałem o inicjalizacji zmiennej. Np. deklarujemy sobie zmienną:

int a( 5 );

Taka konstrukcja jest to wywołanie KONSTRUKTORA typu. Czy zatem przy `int a;' się on nie wywołuje? Ależ wywołuje się, tylko że nic nie robi. Gdyby nie było konstruktora bezargumentowego dla typu int, to taka deklaracja byłaby błędna. Tak też w przedstawionej deklaracji używamy KONSTRUKTORA typu int, który przyjmuje argument typu int (dokładnie to const int&, ale nie zagłębiajmy się w szczegóły ;*). Konstruktor jest oczywiście funkcją, a w tym wypadku podejmowaną przez niego akcją jest zapisanie owej zmiennej podaną jako argument wartością.

Konstruktor możemy też wywoływać bezpośrednio i wtedy tworzymy obiekt tymczasowy, np.:

cout << int( 'C' );

Wypisze nam kod ASCII znaku 'C' (wspominałem już operator obleśnego rzutowania; konstruktory są często przez wielu ludzi mylone z operatorem obleśnego rzutowania, czy wręcz nazywane jego `alternatywną formą' - np. Microsoft Visual C++ określa go jako "function-style casting"). Tzn. w niektórych okolicznościach to jest rzutowanie, mianowicie kiedy wykonuje się to dla typów ścisłych; w takim wypadku argumentem konstruktora może być wartość dowolnego typu, z którego można na ten konstruowany rzutować (obleśnie!).

Co to jest zniszczenie obiektu teraz nie będę dokładnie objaśniał, bo nie da się tego już przedstawić bez omawiania wprost właściwości obiektowych C++, dlatego przy tej okazji dopiero zajmę się tym tematem. Jednak owe pojęcia były potrzebne, aby uściślić różnicę pomiędzy zarządzaniem obiektem w C i C++. Język C oczywiście zna pojęcia przydziału i zwalniania pamięci i realizuje sie to odpowiednio funkcjami malloc i free:


Klocek* k = malloc( sizeof (Klocek) );

... // używaj *k

free( k );


I tyle. Niestety istnieją tutaj w C++ dwa problemy. Funkcja `malloc' zwraca typ `void*'. Jak wiemy, typ void* w C++ trzeba by na ten `Klocek*' przekonwertować. To raz. Dwa, że przydział pamięci nie powoduje jeszcze utworzenia obiektu. Zwraca on jedynie wskaźnik do kawałka pamięci o rozmiarze takim jak dany typ (zresztą przecież podaliśmy to jako argument), ale to jeszcze nie znaczy, że wskaźnik, do którego nastąpiło przypisanie, wskazuje na obiekt. Staje się on nim dopiero po odpowiednim jego wypełnieniu (czyli skonstruowaniu, czy - mówiąc inaczej - inicjalizacji).

Podobnie, zanim zwolnimy obiekt, musimy uwolnić go z odpowiednich powiązań z innymi obiektami (jeśli takowe posiada), zwolnić pamięć, która np. została przydzielona na wskaźnik, jakim było jedno z pól tego obiektu i tak dalej... I kiedy to wszystko jest gotowe dopiero wtedy obiekt staje się z powrotem tylko wycinkiem pamięci, którą się następnie zwalnia. Tu też jest podobny problem, jak z funkcją `malloc' (typy).

Tak na marginesie zwracam uwagę, ze C++ jest bodaj jedynym "oryginalnym" językiem, w którym obiekt przestaje być obiektem w momencie WYWOŁANIA DESTRUKTORA, a nie ZWOLNIENIA PAMIĘCI; w pozostałych językach, zwłaszcza z odśmiecaczem, destruktory albo trzeba wywoływać ręcznie (zresztą destruktorem możemy nazwać funkcję tak dla kaprysu), albo mogą się one nie wywołać w ogóle, albo ich po prostu nie ma i za zniszczenie obiektu uważa się zwolnienie po nim pamieci.

W C++ zatem używa się specjalnie do tego przeznaczonych operatorów `new' i `delete'. Użycie operatora new powoduje przydzielenie pamięci i wywołanie konstruktora. Symetrycznie, operator delete wywołuje destruktor dla obiektu, a następnie zwalnia pamięć.


int* ii = new int( 2 );

... // używaj *ii

delete ii;


W C++ panuje nadal - tak jak w C - ręczna gospodarka pamięcią. Inne języki, jak np. Smalltalk i Java, posiadają klasę dynamiczną z automatyczną gospodarką pamięci (w Smalltalku zresztą innej klasy pamięci nie ma), zwana odśmiecaniem (ang. garbage collection). Oznacza to, że obiekt sam się usuwa, kiedy już nic z niego nie korzysta. Zarówno ręczna, jak i automatyczna gospodarka pamięci mają swoje konsekwencje, o których będzie w następnym punkcie.

Oczywiście tworzenie dynamicznych zmiennych typu int nie ma sensu, ale dla większych obiektów jest to bardzo użyteczne. Rzutowanie w przypadku tych operatorów nie jest konieczne; operaror new zwraca wskaźnik do typu, jaki mu się poda jako argument, natomiast operator delete przyjmuje dowolny wskaźnik. Dla tablic mamy również specjalne wersje tych operatorów: new[] i delete[]:


int* tab = new int[20];

...

delete [] tab;


Tu możemy jednak podać (już legalnie ;*) rozmiar tablicy przez zmienną, a więc nie znany w momencie kompilacji. Proszę bezwzględnie pamiętać o używaniu operatora delete[]! Oba te operatory przyjmują wskaźnik, ale operator delete nie musi wiedzieć, czy wskaźnik trzyma pojedynczy obiekt, czy tablicę (tzn. nie ma obowiązku tego sprawdzać!), przez co nie wywoła destruktorów dla wszystkich obiektów, a jedynie dla pierwszego. Operatory new i delete możemy również przeciążać i deklarować ich specjalne wersje, ale o tym w następnym rozdziale.

Ostatnia uwaga co do obiektów dynamicznych: proszę trzymać się wyznaczonej konwencji obsługi tych obiektów. Nie należy mieszać sposobów przydziału i zwalniania obiektów, tzn. obiekty przydzielone przez malloc należy zwalniać przez free, przydzielone przez new zwalniać przez delete, a przydzielone przez new[] zwalniać przez delete[]. Oczywiście, sposób przydzielania pamięci dla tablic typów ścisłych nie ma najmniejszego znaczenia. Niemniej po pierwsze ten sposób jest łatwiejszy i bezpieczniejszy w użyciu, a po drugie lepiej się w ogólnej kwestii przydzielania pamięci trzymać jednej konwencji (w przypadku delete[] chodzi o ilość destruktorów, natomiast jeśli chodzi o malloc -- często operator new ma inną implementację, niż malloc, że nie wspomnę o możliwościach przeciążania tego operatora, no a poza tym najwazniejsza jest tu kwestia konstruktorow i destruktorow).

Przypominam przy okazji, że uniksowa funkcja (nie istnieje w standardzie ANSI) `strdup' przydziela pamięć właśnie funkcją malloc. Gwoli ciekawostki zresztą w uniksowym C istnieją jeszcze różne inne dodatkowe funkcje GNU, które są dość użyteczne, jednak powodują przydział pamięci funkcją malloc, którą trzeba następnie zwolnić przez free. Nie znalazły się w standardzie z przyczyn oczywistych: nie można narzucać użytkownikowi sposobu przydzielania pamięci, że nie wspomne o zasadzie spójności wywołań, która zostaje w takim wypadku naruszona. Poza tym są to w większości wrappery, ułatwiające posługiwanie się często łączonymi wywołaniami, które w C++ i tak można zorganizować w sposób o niebo wygodniejszy.

Przydział pamięci może się oczywiście nie powieść. W C (z braku innego wyboru) po prostu malloc zwraca wskaźnik pusty (zero) i trzeba było go sprawdzić po wykonaniu przydziału. Jednak odradzam zawracanie sobie głowy sprawdzaniem poprawności przydziału pamięci (zwłaszcza, że operat...

Zgłoś jeśli naruszono regulamin