7. Klasy
Klasa jest kluczow� koncepcj� j�zyka C++, realizuj�c� abstrakcj� danych na bardzo wysokim poziomie. Odpowiednio zdefiniowane klasy stawiaj� do dyspozycji u�ytkownika wszystkie istotne mechanizmy programowania obiektowego: ukrywanie informacji, dziedziczenie, polimorfizm z wi�zaniem p�Ÿnym, a tak�e szablony klas i funkcji. Klasa jest deklarowana i definiowana z jednym z trzech s��w kluczowych: class, struct i union. Chocia� na pierwszy rzut oka mo�e to wygl�da� na rozwlek�oœ�, to jednak, jak poka�emy p�Ÿniej, jest ona zamierzona i celowa.
Elementami sk�adowymi klasy mog� by� struktury danych r�nych typ�w, zar�wno podstawowych, jak i zdefiniowanych przez u�ytkownika, a tak�e funkcje dla operowania na tych strukturach. Dost�p do element�w klasy okreœla zbi�r regu� dost�pu.
7.1. Deklaracja i definicja klasy
Klasa jest typem definiowanym przez u�ytkownika. Deklaracja klasy sk�ada si� z nag��wka, po kt�rym nast�puje cia�o klasy, uj�te w par� nawias�w klamrowych; po zamykaj�cym nawiasie klamrowym musi wyst�pi� œrednik, ewentualnie poprzedzony list� zmiennych. W nag��wku klasy umieszcza si� s�owo kluczowe class (lub struct albo union), a po nim nazw� klasy, kt�ra od tej chwili staje si� nazw� nowego typu.
Klasa mo�e by� deklarowana:
Na zewn�trz wszystkich funkcji programu. Zakres widzialnoœci takiej klasy rozci�ga si� na wszystkie pliki programu.
Wewn�trz definicji funkcji. Klas� tak� nazywa si� lokaln�, poniewa� jej zakres widzialnoœci nie wykracza poza zasi�g funkcji.
Wewn�trz innej klasy. Klas� tak� nazywa si� zagnie�d�on�, poniewa� jej zakres widzialnoœci nie wykracza poza zasi�g klasy zewn�trznej.
Deklaracji klas nie wolno poprzedza� s�owem kluczowym static.
Przyk�adowe deklaracje klas mog� mie� posta�:
class Pusta {};
class Komentarz { /* Komentarz */};
class Niewielka { int n; };
Wyst�pienia klasy deklaruje si� tak samo, jak zmienne innych typ�w, np.
Pusta pusta1, pusta2;
Niewielka nw1, nw2;
Niewielka* wsk = &nw1;
Zmienne pusta1, pusta2, nw1, nw2 nazywaj si obiektami, za wsk jest wskanikiem (zmienn typu Niewielka*) do typu Niewielka, zainicjowanym adresem obiektu nw1. N.b. atwo mona si przekona, e obiekty klasy Pusta maj niezerowy rozmiar.
Klasy w rodzaju Pusta i Komentarz uywa si czsto jako klasy-makiety podczas opracowywania programu.
Jak ju� wspomniano, dopuszczalna jest r�wnie� deklaracja obiekt�w bezpoœrednio po deklaracji klasy, np.
class Gotowa { double db; char znak; } ob1, ob2;
Dost�p do sk�adowej obiektu danej klasy uzyskuje si� za pomoc� operatora dost�pu “.”, np. nw1.n; dla zmiennej wskanikowej uywa si operatora “->”, np. wsk->n, przy czym ostatni zapis jest r�wnowany (*wsk).n.
Przy deklarowaniu sk�adowych obowi�zuj� nast�puj�ce ograniczenia:
deklarowana sk�adowa nie mo�e by� inicjowana w deklaracji klasy;
nazwy sk�adowych nie mog� si� powtarza�;
deklaracje sk�adowych nie mog� zawiera� s��w kluczowych auto, extern i register; natomiast mog� by� poprzedzone s�owem kluczowym static;
Deklaracje element�w sk�adowych klasy mo�na poprzedzi� etykiet� public:, protected:, lub private:. Je�eli sekwencja deklaracji element�w sk�adowych nie jest poprzedzona �adn� z tych etykiet, to kompilator przyjmuje domyœln� etykiet� private:. Oznacza to, �e dana sk�adowa mo�e by� dost�pna jedynie dla funkcji sk�adowych i tzw. funkcji zaprzyjaŸnionych klasy, w kt�rej jest zadeklarowana. Wyst�pienie etykiety public: oznacza, �e wyst�puj�ce po niej nazwy deklarowanych sk�adowych mog� by� u�ywane przez dowolne funkcje, a wi�c r�wnie� takie, kt�re nie s� zwi�zane z deklaracj� danej klasy. Znaczenie etykiety protected: przedstawimy przy omawianiu dziedziczenia.
Ze wzgl�du na sterowanie dost�pem do sk�adowych klasy, s�owa kluczowe public, protected i private nazywa si� specyfikatorami dost�pu.
Przyk�ad 6.1.
#include <iostream.h>
class Niewielka {
public:
int n;
};
void main() {
Niewielka nw1, nw2, *wsk;
nw1.n = 5;
nw2.n = 10;
wsk = &nw1;
cout << nw1.n << endl;
cout << wsk ->n << endl;
wsk = &nw2;
cout << (*wsk).n << endl;
}
Dyskusja. W klasie Niewielka zadeklarowano tylko jedn zmienn skadow n typu int. Poniewa jest to skadowa publiczna, mona w obiektach nw1 i nw2 bezporednio przypisywa jej wartoci 5 i 10. Zwr�my jeszcze uwag na instrukcj deklaracji wskanika wsk do klasy Niewielka: wskanikiem jest identyfikator wsk, a nie *wsk. Jak wida z przykadu, wskanikowi do danej klasy moemy przypisywa kolejno dowolne obiekty tej klasy. Operator dostpu do zmiennej skadowej n dla wskanika wsk jest w programie zapisywany w obu r�wnowanych postaciach: jako wsk->n i jako (*wsk).n. W drugiej postaci konieczne byy nawiasy, poniewa operator dostpu do skadowej “.” ma wyszy priorytet ni operator dostpu poredniego “*”.
Zakres widzialnoœci zmiennych sk�adowych obejmuje ca�y blok deklaracji klasy, bez wzgl�du na to, w kt�rym miejscu bloku znajduje si� ich punkt deklaracji.
Je�eli klasa zawiera funkcje sk�adowe, to ich deklaracje musz� wyst�pi� w deklaracji klasy. Funkcje sk�adowe mog� by� jawnie deklarowane ze s�owami kluczowymi inline, static i virtual; nie mog� by� deklarowane ze s�owem kluczowym extern. W deklaracji klasy mo�na r�wnie� umieszcza� definicje kr�tkich (1-2 instrukcje) funkcji sk�adowych; s� one w�wczas traktowane przez kompilator jako funkcje rozwijalne, tj. tak, jakby by�y poprzedzone s�owem kluczowym inline. W programach jednoplikowych d�u�sze funkcje sk�adowe deklaruje si� w nawiasach klamrowych zawieraj�cych cia�o klasy, zaœ definiuje si� bezpoœrednio po zamykaj�cym nawiasie klamrowym.
Deklaracja klasy wraz z definicjami funkcji sk�adowych (i ewentualnie innymi wymaganymi definicjami) stanowi definicj� klasy.
Funkcje sk�adowe klasy mog� operowa� na wszystkich zmiennych sk�adowych, tak�e prywatnych i chronionych. Mog� one r�wnie� operowa� na zmiennych zewn�trznych w stosunku do definicji klasy.
Przyk�ad 6.2.
#include <iostream.h>
class Punkt {
public:
int x,y;
void init(int a, int b) {x = a; y = b; }
void ustaw(int, int);
};
void Punkt::ustaw(int a, int b)
{ x = x + a; y = y + b; }
int main() {
Punkt punkt1;
Punkt* wsk = &punkt1;
punkt1.init(1,1);
cout << punkt1.x << endl;
cout << wsk -> y << endl;
punkt1.ustaw(10,15);
cout << punkt1.x << endl;
cout << (*wsk).y << endl;
return 0;
}
Dyskusja. W klasie Punkt zar�wno atrybuty x, y, jak i funkcje init() oraz ustaw() s publiczne, a wic dostpne na zewntrz klasy. Funkcja init() suy do zainicjowania obiektu punkt1 klasy Punkt. Jej definicja wewntrz klasy jest r�wnowana definicji poza ciaem klasy o postaci:
inline void Punkt::init(int a, int b) {x = a; y = b; }
Dwuargumentowy (binarny) operator zasi�gu “::” poprzedzony nazw� klasy ustala zasi�g funkcji sk�adowych ustaw() i init(). Napis Punkt:: informuje kompilator, e nastpujca po nim nazwa jest nazw funkcji skadowej klasy Punkt; wywoanie takiej funkcji w programie moe mie miejsce tylko tam, gdzie jest dostpna deklaracja klasy Punkt.
Zwr��my r�wnie� uwag� na spos�b zapisu wywo�ania funkcji sk�adowej: zapis ten ma tak� sam� posta�, jak odwo�anie do atrybutu x, bd y.
7.1.1. Autoreferencja: wskanik this
Funkcje sk�adowe s� zwi�zane z definicj� klasy, a nie z deklaracjami obiekt�w tej klasy. Poci�ga to za sob� nast�puj�ce konsekwencje:
Istnieje tylko jeden “egzemplarz” kodu definicji danej funkcji sk�adowej, b�d�cej “w�asnoœci�” klasy.
Kod ten nie wchodzi w sk�ad �adnego obiektu.
W ka�dym wywo�aniu funkcji sk�adowej klasy musi by� wskazany obiekt wo�aj�cy.
Wskazanie obiektu wo�aj�cego jest realizowane przez przekazanie do funkcji sk�adowej ukrytego argumentu aktualnego, kt�rym jest wskaŸnik do tego obiektu. WskaŸnikiem tym jest inicjowany niejawny argument * wskaŸnik * w definicji funkcji sk�adowej. Do wskaŸnika tego mo�na si� r�wnie� odwo�ywa� jawnie, u�ywaj�c s�owa kluczowego this. Tak wi�c mamy tutaj autorekursj�: poprzez wskaŸnik this odwo�ujemy si� do obiektu, dla kt�rego wywo�ywana jest pewna operacja (wywo�anie funkcji). W podanym ni�ej przyk�adzie pokazano dwie r�wnowa�ne deklaracje, przy czym druga z nich ilustruje mo�liwoœ� jawnego u�ycia wskaŸnika this.
Przyk�ad 6.3.
class Niejawna {
int m;
int funkcja() { return m; }
};
class Jawna {
int m;
int funkcja() { return this->m; }
};
Poniewa� this jest s�owem kluczowym, nie mo�e by� jawnie deklarowany jako zmienna sk�adowa. Mo�na natomiast wydedukowa�, �e w funkcji funkcja() klasy Niejawna wskanik this jest niejawnie zadeklarowany jako
Niejawna* const this;
a inicjowany adresem obiektu, dla kt�rego wo�ana jest funkcja sk�adowa funkcja(). Poniewa wskanik this jest deklarowany jako *const, czyli jako wskanik stay, nie moe on by zmieniany. Mona natomiast wprowadza zmiany we wskazywanym przez niego obiekcie.
Zobaczmy jeszcze, jak wygl�da zastosowanie wskaŸnika this w nieco zmodyfikowanym programie, zawieraj�cym definicj� klasy Punkt.
Przyk�ad 6.4.
#include <iostream.h>
class Punkt {
public:
int init(int a, int b)
{ this->x = a; this->y = b; return x; }
int ustaw(int, int);
private:
int x, y;
};
int Punkt::ustaw(int a, int b) {
this->x = this->x + a; this->y = y + b; return x ;
}
int main() {
Punkt punkt1;
cout << punkt1.init(0,0) << endl;
cout << punkt1.ustaw(10,15) << endl;
return 0;
}
Z pokazanych wy�ej przyk�ad�w wida�, �e jawnych odwo�a� do wskaŸnika this nie op�aca si� u�ywa�, skoro poprawny syntaktycznie jest kr�tszy zapis. Wyj�tkami od tego zalecenia s� sytuacje, gdy jawne odwo�anie do wskaŸnika this jest nie tylko op�acalne, ale i konieczne; s� to definicje funkcji, kt�re operuj� na elementach tzw. list jedno- i dwukierunkowych.
7.1.2. Statyczne elementy klasy
Cia�o klasy mo�e zawiera� deklaracje struktur danych i funkcji poprzedzone s�owem kluczowym static, b�d�cym, podobnie jak auto, register i extern, specyfikatorem klasy pami�ci. S�owo static (a tak�e extern) mo�e poprzedza� jedynie deklaracje zmiennych i funkcji oraz unii anonimowych; nie mo�e poprzedza� deklaracji funkcji wewn�trz bloku ani deklaracji argument�w formalnych funkcji. W og�lnoœci specyfikator static ma dwa znaczenia, kt�re s� cz�sto mylone. Pierwsze z nich m�wi kompilatorowi: przydziel danej wielkoœci ustalony adres w pami�ci i zachowuj go przez ca�y czas trwania programu. Drugie m�wi o zasi�gu: ustal zakres widzialnoœci danej wielkoœci pocz�wszy od miejsca, w kt�rym zosta�a zadeklarowana. Inaczej m�wi�c: wielkoœ� zadeklarowana jako static istnieje przez ca�y czas wykonania programu, ale jej zasi�g zale�y od miejsca deklaracji. Obydwa znaczenia odnosz� si� do statycznych zmiennych sk�adowych, natomiast tylko drugie z nich odnosi si� do statycznych funkcji sk�adowych.
Przyk�ad 6.5.
static int nn;
int main() {
nn = 10;
return 0;
}
Dyskusja. W powy�szym przyk�adzie zmienna globalna nn istnieje przez cay czas wykonania programu, ale jej zakres widzialnoci jest ograniczony do pliku, w kt�rym zostaa zadeklarowana (tutaj plik z funkcj main()). Oznacza to, e jest dopuszczalne uywanie nazwy nn w innych plikach programu i w innym znaczeniu. Zmienna nn ma zasig od punktu deklaracji do koca pliku. Gdybymy umiecili deklaracj zmiennej nn np. po bloku main, to kada pr�ba wykonania na niej operacji wewntrz bloku byaby bdem syntaktycznym. Zmienn nn mona inicjowa w jej deklaracji; poniewa tutaj tego nie uczyniono, kompilator nada jej domyln warto pocztkow 0.
Zmienne sk�adowe klasy s� * u�ywaj�c terminologii j�zyka Smalltalk * zmiennymi wyst�pienia (ang. instance variables); dla ka�dego obiektu danej klasy tworzone s� oddzielne ich kopie. Deklaracje takich zmiennych s� jednoczeœnie ich definicjami; s� to zmienne lokalne o zasi�gu klasy i przypadkowych wartoœciach inicjalnych (deklarowane w bloku klasy zmienne nie mog� by� tam inicjowane).
Je�eli deklaracj� zmiennej sk�adowej poprzedzimy specyfikatorem static, to taka deklaracja staje si� deklaracj� referencyjn�, a wprowadzona ni� zmienna � zmienn� klasy. Konsekwencj� tego jest fakt, �e definicj� takiej zmiennej musimy umieœci� na zewn�trz deklaracji klasy. Statyczna zmienna klasy podlega tym samym regu�om dost�pu (sterowanym etykietami public:, protected: i private:) co zmienna wyst�pienia. Przy definiowaniu zmiennej statycznej mo�na jej nada� wartoœ� pocz�tkow� r�n� od zera; na czas tej operacji ograniczenia dost�pu zostaj� wy��czone. W rezultacie otrzymamy zmienn� o w�asnoœciach obiektu globalnego i zasi�gu pliku, skojarzon� z sam� klas�, a nie z jakimkolwiek jej wyst�pieniem, tj. obiektem. Poniewa� zmienna klasy jest zwi�zana z klas�, a nie z jej obiektami, mo�na na niej wykonywa� operacje w og�le bez tworzenia obiekt�w. W tym sensie jest ona samodzielnym obiektem. Je�eli jednak zadeklarujemy obiekty, to ka�dy z nich b�dzie mia� dost�p do tego samego adresu w pami�ci, kt�ry zosta� przydzielony przez kompilator statycznej zmiennej sk�adowej. Tak wi�c mamy tylko jedn� kopi� zmiennej sk�adowej, dost�pn� bez istnienia obiekt�w, b�dŸ wsp�dzielon� przez zadeklarowane obiekty.
Reasumuj�c: statyczna zmienna klasy jest to zmienna o zasi�gu ograniczonym do klasy, w kt�rej jest zadeklarowana. Wprowadzenie statycznych zmiennych sk�adowych pozwala unikn�� u�ywania zewn�trznych zmiennych globalnych wewn�trz klasy, co cz�sto daje niepo��dane efekty uboczne i narusza zasad� ukrywania informacji.
Sk�adni� deklaracji, definicji i operacji na zmiennej statycznej ilustruje poni�szy przyk�ad.
Przyk�ad 6.6.
#include <iostream.h>
class Test {
public:
int m;
static int n; // Tylko deklaracja
};
// Definicja obiektu statycznego n
int Test::n = 10;
void main() {
Test::n = 25;
cout << *Test::n= * << Test::n << endl;
Test t1,t2;
t1.m = 5; t2.m = 0;
cout << *t1.m= * << t1.m << endl;
cout << *t2.m= * << t2.m << endl;
cout << *t2.n= * << t2.n << endl;
}
Wydruk z programu ma posta�:
Test::n= 25
t1.m= 5
t2.m= 0
t2.n= 25
Dyskusja. W deklaracji klasy Test mamy deklaracj� zmiennej statycznej static int n;. Definicj tej zmiennej, o postaci int Test::n = 10; umieszczono bezporednio po deklaracji klasy. Pierwsz instrukcj w bloku funkcji main() jest instrukcja przypisania Test::n = 25;. Przypisan do zmiennej statycznej warto 25 wyprowadzaj na ekran instrukcje:
cout << *Test::n= * << Test::n << endl;
oraz cout << *t2.n= * << t2.n << endl;
Obiekty t1 i t2 maj swoje prywatne kopie zmiennej m oraz dostp do jednej kopii zmiennej statycznej n.
Uwaga 1. Typ statycznej zmiennej sk�adowej nie obejmuje nazwy jej klasy; tak wi�c typem zmiennej Test::n jest int.
Uwaga 2. Statyczne sk�adowe klasy lokalnej (zadeklarowanej wewn�trz bloku funkcji) nie maj� okreœlonego zakresu widocznoœci i nie mog� by� definiowane na zewn�trz deklaracji klasy. Wynika st�d, �e klasa lokalna nie mo�e mie� zmiennych statycznych.
Uwaga 3. Zauwa�my, �e s�owo static nie jest ani potrzebne, ani dozwolone w definicji statycznej sk�adowej klasy. Gdyby by�o dopuszczalne, to mog�aby powsta� kolizja pomi�dzy znaczeniem static stosowanym do sk�adowych klasy, a znaczeniem, stosowanym do globalnych obiekt�w i funkcji.
S�owo kluczowe static mo�e r�wnie� poprzedza� deklaracj� (nie definicj�!) funkcji sk�adowej klasy. Taka statyczna funkcja sk�adowa mo�e operowa� jedynie na zmiennych statycznych nie ma dost�pu do zmiennych wyst�pienia (zwyk�e, niestatyczne funkcje sk�adowe mog� operowa� zar�wno na zmiennych niestatycznych, jak i statycznych). Wynika to st�d, �e statyczna funkcja sk�adowa nie ma wskaŸnika this, tak �e mo�e ona mie� dost�p do zmiennych niestatycznych swojej klasy jedynie przez zastosowanie operator�w selekcji “.” lub “->” do obiekt�w klasy.
Przyk�ad 6.7.
#include <iostream.h>
class Punkt {
public:
static int init( int ); // Deklaracja init()
private:
static int x; // Deklaracja x
};
int Punkt::x; // Definicja x
// Definicja init()
int Punkt::init( int a) { x = a; return x; }
int main() {
cout << Punkt::init(10) << endl;
return 0;
}
Dyskusja. Przyk�ad pokazuje dowodnie, �e dost�p do zmiennej statycznej i wywo�anie funkcji statycznej danej klasy nie wymagaj� istnienia obiekt�w tej klasy.
7.2. Konstruktory i destruktory
Konstruktory i destruktory nale�� do grupy specjalnych funkcji sk�adowych. Grupa ta obejmuje: konstruktory i destruktory inicjuj�ce, konstruktor kopiuj�cy oraz tzw. funkcje operatorowe.
Konstruktor jest funkcj� sk�adow� o takiej samej nazwie, jak nazwa klasy. Nazw� destruktora jest nazwa klasy, poprzedzona znakiem tyldy (~).
Ka�da klasa zawiera konstruktor i destruktor, nawet gdy nie s� one jawnie zadeklarowane. Zadaniem konstruktora jest konstruowanie obiekt�w swojej klasy; jego wywo�anie w programie poci�ga za sob�:
alokacj� pami�ci dla obiektu;
przypisanie wartoœci do niestatycznych zmiennych sk�adowych;
wykonanie pewnych operacji porz�dkuj�cych (np. konwersji typ�w).
Je�eli w klasie nie zadeklarowano konstruktora i destruktora, to zostan� one wygenerowane przez kompilator i automatycznie wywo�ane podczas tworzenia i destrukcji obiektu.
Przyk�ad 6.8.
#include <iostream.h>
class Punkt {
public:
Punkt(int, int);
void ustaw(int, int);
private:
int x,y;
};
Punkt::Punkt(int a, int b)
{ x = a; y = b; }
void Punkt::ustaw( int c, int d)
{ x = x + c; y = y + d; }
int main() {
Punkt punkt1(3,4);
return 0;
}
Dyskusja. Poniewa� klasa Punkt zawiera dwie prywatne zmienne skadowe: x oraz y, celowym jest zdefiniowanie konstruktora, kt�ry nadaje im wartoci pocztkowe a oraz b. Wartoci te (3 i 4) s przekazywane w wywoaniu konstruktora w bloku funkcji main(). Korzyci z wprowadzenia konstruktora s oczywiste; w poprzednich przykadach, w kt�rych wystpowaa klasa Punkt, musielimy zapisywa w bloku funkcji main() dwie instrukcje: instrukcj deklaracji obiektu, a nastpnie instrukcj wywoania funkcji init() dla przypisania zmiennym danych wartoci. Teraz te dwie operacje wykonuje jedna instrukcja deklaracji
Punkt punkt1(3,4);
Instrukcja ta jest r�wnowa�na instrukcji
Punkt punkt1 = Punkt(3,4);
kt�ra wo�a konstruktor Punkt::Punkt(int, int) dla zainicjowania atrybut�w obiektu punkt1 wartociami 3 i 4.
Poniewa� klasa Punkt nie zawiera destruktora, obiekt punkt1 zostanie zniszczony przez destruktor wywoywany w chwili zakoczenia programu, a wygenerowany w fazie kompilacji.
W nast�pnych definicjach konstruktor�w b�dziemy najcz�œciej wykorzystywa� notacj�, zapo�yczon� przez C++ z j�zyka Simula. Dla zainicjowania zmiennej jednego z typ�w podstawowych wartoœ� inicjaln� mo�emy umieœci� po znaku r�wnoœci, b�dŸ w nawiasach okr�g�ych, np.
int i = 7;
int i(7);
Wykorzystuj�c drug� z r�wnowa�nych notacji mo�emy zapisa� definicj� konstruktora klasy Punkt w postaci:
Punkt::Punkt(int a, int b):x(a),y(b) {}
W powy�szej definicji po nag��wku funkcji mamy list� zmiennych sk�adowych z wartoœciami inicjalnymi w nawiasach okr�g�ych, poprzedzon� pojedynczym dwukropkiem. Poniewa� ten prosty konstruktor nie wykonuje niczego wi�cej poza inicjowaniem zmiennych sk�adowych, jego blok jest pusty.
7.2.1. Wasnoci konstruktor�w i destruktor�w
Konstruktory i destruktory posiadaj� szereg charakterystycznych w�asnoœci i podlegaj� pewnym ograniczeniom. Zacznijmy od ogranicze�.
Deklaracja konstruktora i destruktora nie mo�e zawiera� typu zwracanego (nawet void). W przypadku konstruktora jawna wartoœ� zwracana nie jest wskazana, poniewa� jest on przeznaczony do tworzenia obiekt�w, czyli przekszta�cania pewnego obszaru pami�ci w zorganizowane struktury danych. Gdyby dopuœci� do zwracania jakiejœ wartoœci, by�by to fragment informacji o implementacji obiektu, kt�ra powinna by� niewidoczna dla u�ytkownika. Podobne argumenty dotycz� destruktora.
Nie jest mo�liwy dost�p do adresu konstruktora i destruktora. Zasadniczym powodem jest ukrycie szczeg��w fizycznej alokacji pami�ci, kt�re powinny by� niewidoczne dla u�ytkownika.
Deklaracje konstruktora i destruktora nie mog� by� poprzedzone s�owami kluczowymi const, volatile i static. (Modyfikator volatile wskazuje, �e obiekt mo�e by� w ka�dej chwili zmodyfikowany i to nie tylko instrukcj� programu u�ytkownika, lecz tak�e przez zdarzenia zewn�trzne, np. przez procedur� obs�ugi przerwania). Deklaracja konstruktora nie mo�e by� poprzedzona s�owem kluczowym virtual.
Konstruktory i destruktory nie s� dziedziczone.
Destruktor nie mo�e mie� parametr�w formalnych. Parametrami formalnymi konstruktora nie mog� by� zmienne sk�adowe w�asnej klasy.
Wyliczymy teraz kolejno g��wne w�asnoœci konstruktor�w i destruktor�w.
Konstruktory i destruktory generowane przez kompilator s� public. Konstruktory definiowane przez u�ytkownika r�wnie� powinny by� public, aby istnia�a mo�liwoœ� tworzenia obiekt�w na zewn�trz deklaracji klasy. W pewnych przypadkach szczeg�lnych konstruktory mog� wyst�pi� po etykiecie protected:, a nawet private:.
Konstruktor jest wo�any automatycznie, gdy definiuje si� obiekt; destruktor gdy obiekt jest niszczony.
Konstruktory i destruktory mog� by� wywo�ywane dla obiekt�w const i volatile.
Z bloku konstruktora i destruktora mog� by� wywo�ywane funkcje sk�adowe ich klasy.
Deklaracja destruktora mo�e by� poprzedzona s�owem kluczowym virtual.
Konstruktor mo�e zawiera� referencj� do w�asnej klasy jako argument formalny. W takim przypadku jest on nazywany konstruktorem kopiuj�cym. Deklaracja konstruktora kopiuj�cego dla klasy X moe mie posta X(const X&); lub X(const X&, int=0);. Jest on wywoywany zwykle wtedy, gdy definiuje si obiekt, inicjowany przez wczeniej utworzony obiekt tej samej klasy, np.
X ob2 = ob1;.
Konstruktor jest wo�any wtedy, gdy ma by� utworzony obiekt jego klasy. Obiekt taki mo�e by� tworzony na wiele sposob�w:
jako zmienna globalna;
jako zmienna lokalna;
przez jawne u�ycie operatora new;
jako obiekt tymczasowy;
jako zmienna sk�adowa, zagnie�d�ona w innej klasie.
Wprawdzie konstruktory i destruktory nie mog� by� funkcjami statycznymi, ale mog� operowa� na statycznych zmiennych sk�adowych swojej klasy. W podanym ni�ej przyk�adzie wykorzystano zmienn� statyczn� licznik do rejestrowania istniejcych w danej chwili obiekt�w. Klas Status mona uwaa za fragment podsystemu zarzdzania pamici programu.
Przyk�ad 6.9.
#include <iostream.h>
class Status {
public:
Status() { licznik++; }
~Status() { licznik--}
int odczyt() { return licznik; }
private:
static licznik; //deklaracja
};
int Status::licznik = 0; // Definicja
int main() {
Status ob1,ob2, ob3;
cout << Mamy << ob1.odczyt() << obiekty\n;
Status *wsk;
wsk = new Status; //Alokuj dynamicznie
if (!wsk) {
cout << Nieudana alokacja\n;
return 1; }
cout << Mamy << ob1.odczyt()
<< obiekty po alokacji\n;
// skasuj obiekt
delete wsk;
cout << Mamy << ob1.odczyt()
<< obiekty po destrukcji\n;
return 0;
}
Dyskusja. Wygl�d ekranu po wykonaniu programu b�dzie nast�puj�cy:
Mamy 3 obiekty
Mamy 4 obiekty po alokacji
Mamy 3 obiekty po destrukcji
Obiekty programu s� tworzone przez kolejne wywo�ania konstruktora Status(){licznik++;}. Najpierw s tworzone obiekty ob1, ob2 i ob3, a nastpnie obiekt dynamiczny *wsk. Po destrukcji wskanika wsk mamy trzeci wiersz wydruku. Oczywicie pierwsze trzy obiekty zostan zniszczone przy wyjciu z programu przez trzykrotne wywoanie tego samego co dla wsk destruktora ~Status() { licznik--}.
7.2.2. Przecianie konstruktor�w
Konstruktory, podobnie jak “zwyk�e” funkcje (tak�e funkcje sk�adowe klasy), mog� by� przeci��ane. Najcz�œciej ma to na celu tworzenie obiekt�w inicjowanych zadanymi wartoœciami zmiennych sk�adowych, lub te� obiekt�w bez podanych wartoœci inicjalnych. Dzi�ki temu mo�na zadeklarowa� wi�cej ni� jeden konstruktor dla danej klasy. Wywo�anie konstruktora w deklaracji obiektu pozwala kompilatorowi ustali�, w zale�noœci od liczby i typ�w podanych argument�w, kt�r� wersj� konstruktora nale�y wywo�a�.
W podanym ni�ej przyk�adzie obiektowi punkt1 nadano wartoci pocztkowe (3 i 4), za obiektowi punkt2 nie. Gdyby usun z programu konstruktor domylny Punkt() o pustym wykazie argument�w, program nie zostaby skompilowany, poniewa brakoby konstruktora, kt�ry mogby by dopasowany do wywoania Punkt punkt2.
Przyk�ad 6.10.
#include <iostream.h>
class Punkt {
public:
Punkt() {}
Punkt(int a, int b): x(a), y(b) {}
int x,y;
};
int main() {
Punkt punkt1(3,4);
cout << punkt1.x << endl;
Punkt punkt2;
cout << punkt2.x << endl;
return 0;
}
7.2.3. Konstruktory z argumentami domylnymi
Alternatyw� dla przeci��ania konstruktora jest wyposa�enie go w argumenty domyœlne, podobnie jak czyniliœmy to w stosunku do zwyk�ych funkcji. Przypomnijmy, �e argument domyœlny jest przyjmowany przez kompilator wtedy, gdy w wywo�aniu funkcji nie podano odpowiadaj�cego mu argumentu aktualnego.
Wartoœci domyœlne argument�w formalnych w deklaracji (nie w definicji!) zapisujemy w nast�puj�cy spos�b:
Je�eli w deklaracji konstruktora umieszczono nazwy argument�w formalnych, to po nazwie argumentu piszemy znak r�wnoœci, a po nim odpowiedni� wartoœ�, np. Punkt ( int a = 0, int b = 0 );
Je�eli deklaracja nie zawiera nazw argument�w, a jedynie ich typy, to znak r�wnoœci i nast�puj�c� po nim wartoœ� piszemy po identyfikatorze typu, np. Punkt ( int = 0, int = 0 );
Przyk�ad 6.11.
#include <iostream.h>
class Punkt {
public:
int x,y;
Punkt(int = 0, int = 0);
};
Punkt::Punkt(int a, int b): x(a), y(b)
{
if (a==0 && b==0)
cout << Obiekt bez inicjowania...\n;
else cout << Obiekt z inicjowaniem...\n;
}
int main() {
Punkt punkt1(3,4);
cout << punkt1.x << endl;
Punkt punkt2;
cout << punkt2.x << endl;
return 0;
}
Dyskusja. Powy�szy program zawiera wywo�ania, identyczne jak w przypadku konstruktor�w przeci��onych, ale tylko jedn� definicj� konstruktora. W definicji konstruktora Punkt() z zerowymi wartociami domylnymi umieszczono instrukcj if jedynie w tym celu, aby pokaza, e instrukcja deklaracji Punkt punkt1(3,4); wywouje konstruktor z inicjowaniem, a instrukcja deklaracji Punkt punkt2; wywouje konstruktor bez inicjowania. Wydruk z programu ma posta:
Obiekt z inicjowaniem...
3
Obiekt bez inicjowania...
0
Zauwa�my �e konstruktor Punkt(int = 0, int = 0) nie jest r�wnowa�ny konstruktorowi bez argument�w, np. Punkt(). Prezentowana niej wersja klasy Punkt zawiera taki wanie konstruktor bezargumentowy. Przykad nawizuje take do przeprowadzonej w p. 5.2 dyskusji, w kt�rej zwr�cono uwag na zwizki pomidzy przecianiem funkcji, a uywaniem argument�w domylnych.
Przyk�ad 6.12.
#include <iostream.h>
class Punkt {
public:
Punkt(): x(0), y(0) {}
Punkt(int a, int b): x(a), y(a) {}
int fx() { return x; }
int fy() { return y; }
private:
int x,y;
};
int main() {
Punkt punkt1;
cout << punkt1.x= << punkt1.fx() << \n;
Punkt punkt2(3,2);
cout << punkt2.x= << punkt2.fx() << \n;
Punkt *wsk = new Punkt(2,2);
if (!wsk) return 1;
cout << wsk->fx()= << wsk->fx() << \n;
if (wsk)
{
delete wsk;
wsk = NULL; }
return 0;
}
Dyskusja. W klasie Punkt zmienne skadowe x, y s teraz prywatne, w zwizku z czym zadeklarowano publiczny interfejs w postaci dw�ch funkcji skadowych fx() oraz fy(), kt�re daj dostp do tych zmiennych. Obiekty punkt1 i punkt2 s alokowane na stosie funkcji main(), natomiast obiekt wskazywany przez wsk na kopcu (w pamici swobodnej). Zwr�my uwag na instrukcj if w bloku main(), w kt�rej wystpuje symbol NULL, oznaczajcy wskanik pusty. Zapis if(wsk), r�wnowany if(wsk == NULL) jest testem, kt�ry zapobiega pr�bie niszczenia tego samego obiektu dwa razy (jeeli wsk == NULL, to adna z dw�ch instrukcji w bloku if nie zostanie wykonana).
7.3. Przecianie operator�w
Wœr�d zdefiniowanych w j�zyku C++ operator�w wyst�puj� operatory polimorficzne. Mo�emy tu wyodr�bni� dwa rodzaje polimorfizmu:
Koercj�, gdy dopuszcza si�, �e argumenty operatora mog� by� mieszanych typ�w. Ten rodzaj polimorfizmu jest charakterystyczny dla operator�w arytmetycznych: +, -, *, /; np. operator “+” mo�e s�u�y� do dodania dw�ch liczb ca�kowitych, liczby ca�kowitej do zmiennopozycyjnej, etc.
Przeci��enie operatora, gdy ten sam symbol operatora stosuje si� w operacjach nie zwi�zanych semantycznie. Typowymi przyk�adami mog� by� operatory inicjowania i przypisania, oznaczane tym samym symbolem “=”, lub symbole “>>” oraz “<<”, kt�re, zale�nie od kontekstu, s� bitowymi operatorami przesuni�cia lub operatorami wprowadzania/wyprowadzania.
Wymienione rodzaje wbudowanego polimorfizmu wyst�puj� w wielu nowoczesnych j�zykach programowania. Wsp�ln� dla nich cech� jest to, �e s� one predefiniowane, a ich definicje stanowi� integraln� i niezmienn� cz�œ� definicji j�zyka.
U�ytkownikowi j�zyka C++ dano znacznie wi�ksze mo�liwoœci, spotykane w nielicznych wsp�czesnych j�zykach programowania. Ma on do dyspozycji mechanizm, kt�ry pozwala tworzy� nowe definicje dla istniej�cych operator�w. Dzi�ki temu programista mo�e tak zmodyfikowa� dzia�anie operatora, aby wykonywa� on operacje w narzucony przez niego spos�b. Jest to szczeg�lnie istotne w programach czysto obiektowych. Np. obiekty wielokrotnie redefiniowanej klasy Punkt moemy uwaa za wektory w prostoktnym ukadzie wsp�rzdnych. W geometrii i fizyce dla takich obiekt�w s np. okrelone operacje dodawania i odejmowania. Byoby wskazane zdefiniowa podobne operacje dla deklarowanych przez nas klas. Wykorzystamy do tego celu nastpujc og�ln posta definicji tzw. funkcji operatorowej, tj. funkcji, kt�ra definiuje pewien operator “@”:
typ klasa::operator@(argumenty)
{ // wykonywane operacje }
gdzie s�owo typ oznacza typ zwracany przez funkcj operatorow, sowo klasa jest nazw klasy, w kt�rej funkcja definiujca operator jest funkcj skadow, dwa dwukropki oznaczaj operator zasigu, za symbol “@” bdzie zastpowany w konkretnej definicji przez symbol operatora (np. =, ==, +, ++, new). Nazwa funkcji definiujcej operator skada si ze sowa kluczowego operator i nastpujcego po nim symbolu operatora; np., jeeli jest przeciany operator “+”, to nazw funkcji bdzie operator+.
Przeci��enie operatora przypomina przeci��enie funkcji. I faktycznie jest to przypadek szczeg�lny przeci��enia funkcji. Poni�ej zestawiono wa�niejsze regu�y przeci��ania i w�asnoœci operator�w przeci��anych.
Operator @ jest zawsze przeciany wzgldem klasy, w kt�rej jest zadeklarowana jego funkcja operator@. Zatem w innych kontekstach operator nie traci adnego ze swych oryginalnych znacze, ustalonych w definicji jzyka, natomiast zyskuje znaczenia dodatkowe.
Funkcja definiuj�ca operator musi by� albo funkcj� sk�adow� klasy, albo mie� co najmniej jeden argument b�d�cy obiektem lub referencj� do obiektu klasy (wyj�tkiem s� funkcje, kt�re redefiniuj� znaczenie operator�w new i delete). Ograniczenie to gwarantuje, �e wyst�pienie operatora z argumentami typ�w nie b�d�cych klasami, jest jednoznaczne z wykorzystaniem jego standardowej, wbudowanej w j�zyk definicji. Jego intencj� jest rozszerzanie j�zyka, a nie zmiana wbudowanych definicji.
Nie mog� by� przeci��ane operatory “.”, “.*”, “::”, “?:”, “sizeof” oraz symbole “#” i “##”.
Nie jest mo�liwa zmiana priorytetu, regu� ��cznoœci, ani liczby argument�w operatora.
Funkcje definiuj�ce operatory, za wyj�tkiem funkcji operator=(), s dziedziczone.
Przeci��any operator nie mo�e mie� argument�w domyœlnych.
Funkcje: operator=(), operator[](), operator() i operator->() nie mog by statycznymi funkcjami skadowymi.
Funkcja definiuj�ca operator nie mo�e pos�ugiwa� si� wy��cznie wskaŸnikami.
Operatory: “=” (przypisania), “&” (pobrania adresu) i “,” (przecinkowy) maj� predefiniowane znaczenia w odniesieniu do obiekt�w klas (o ile nie zosta�y na nowo zdefiniowane).
Za wyj�tkiem operator�w wymienionych wy�ej, mo�emy przeci��a� wszystkie operatory wbudowane w j�zyk zar�wno operatory jednoargumentowe (unarne), jak i dwuargumentowe (binarne). Podane ni�ej proste przyk�ady ilustruj� przeci��anie kilku takich operator�w.
Przyk�ad 6.13.
// Operator dodawania +
#include <iostream.h>
class Punkt {
public:
Punkt(): x(0),y(0){}
Punkt(int a, int b): x(a),y(a) {}
int fx() { return x; }
int fy() { return y; }
Punkt operator+(Punkt);
private:
int x,y;
};
Punkt Punkt::operator+(Punkt p)
{
return Punkt(x + p.x, y + p.y);
}
int main() {
Punkt punkt1, punkt2, punkt3;
punkt1 = Punkt(2,2);
punkt2 = Punkt(3,1);
punkt3 = punkt1 + punkt2;
cout << punkt1.x= << punkt1.fx() << endl;
cout << punkt3.x= << punkt3.fx() << endl;
int i = 10, j;
j = i + 15; // Predefiniowany '+'
cout << j= << j << endl;
return 0;
}
Wydruk z programu ma posta�:
punkt1.x= 2
punkt3.x= 5
j= 25
Dyskusja. Instrukcja Punkt punkt1, punkt2, punkt3; wywouje trzykrotnie konstruktor Punkt(). Instrukcja przypisania
punkt1 = Punkt(2,2);
wywo�uje konstruktor Punkt(int,int). Nastpnie kompilator generuje niejawny operator przypisania, kt�ry przypisuje obiektowi punkt1 obiekt, wieo utworzony przez konstruktor Punkt(int,int). Tak samo jest wykonywana druga instrukcja przypisania. Instrukcja dodawania
punkt3 = punkt1 + punkt2;
wo�a najpierw generowany przez kompilator konstruktor kopiuj�cy, kt�ry tworzy kopi� obiektu punkt2 i przekazuje j jako parametr aktualny do funkcji operatorowej Punkt Punkt::operator+(Punkt p). Konieczno wykonania tej operacji byaby bardziej oczywista, gdybymy instrukcj dodawania obiekt�w zapisali w drugiej dopuszczalnej postaci
punkt3 = punkt1.operator+(punkt2);
Nast�pnie jest wywo�ywana funkcja operatorowa “+”, kt�ra z kolei wo�a konstruktor Punkt(int,int), aby utworzy obiekt, bdcy “sum” obiekt�w punkt1 i punkt2. Kocow operacj w tej instrukcji jest wywoanie generowanego przez kompilator operatora przypisania “=”, aby przypisa wynik do obiektu punkt3.
Omawiane niejawne operacje przypisania i kopiowania moglibyœmy przeœledzi�, uzupe�niaj�c definicj� klasy Punkt o wasne konstrukcje programowe. I tak, operator przypisania dla klasy Punkt m�gby mie posta:
Punkt& Punkt::operator=(const Punkt& p)
{ this->x = p.x; this->y = p.y; return *this }
zaœ konstruktor kopiuj�cy:
Punkt::Punkt(const Punkt& p) { x = p.x; y = p.y; }
Dalsze instrukcje s� wykonywane w spos�b standardowy i nie wymagaj� komentarzy. Zauwa�my jedynie, �e predefiniowany operator “+” nie straci� swojego znaczenia, co pokazano na operacjach ze zmiennymi i oraz j.
Przyk�ad 6.14.
// Operator relacyjny ==
#include <iostream.h>
class Punkt {
public:
Punkt(): x(0),y(0) {}
Punkt(int a, int b): x(a),y(a) {}
int fx() { return x; }
int fy() { return y; }
int operator==(Punkt);
private:
int x,y;
};
int Punkt::operator==(Punkt p)
{
if( p.fx() == fx() && p.fy() == fy() )
return (-1);
else return (0);
}
int main() {
Punkt punkt1, punkt2;
punkt1 = Punkt(2,2);
punkt2 = Punkt(3,1);
if ( punkt1 == punkt2 ) cout << Rowne\n;
else cout << Nierowne\n;
int i = 5, j = 6, k;
k = i == j; // predefiniowany '=='
cout << k= << k << endl;
return 0;
}
Dyskusja. Instrukcja deklaracji Punkt punkt1, punkt2; wywouje dwukrotnie bezargumentowy konstruktor Punkt().
Instrukcja punkt1 = Punkt(2,2); najpierw woa konstruktor Punkt::Punkt(int,int), a nastpnie generowany przez kompilator operator przypisania (por�wnaj poprzedni przykad). Podobnie przebiega wykonanie drugiej instrukcji przypisania. Poniewa instrukcj if mona zapisa
w postaci:
if(punkt1.operator==(punkt2))...
zatem, podobnie jak w poprzednim przyk�adzie, wo�any jest niejawny konstruktor kopiuj�cy dla utworzenia kopii obiektu punkt2, a nastpnie funkcja operatorowa “==”, kt�ra wywouje funkcj skadow fx(). Poniewa skadowe x obu obiekt�w s r�ne (2 i 3), test na r�wno na tym si koczy, poniewa pierwszy argument koniunkcji ma warto zero. Podobnie jak w poprzednim przykadzie pokazano, e operator “==” nie straci swojego znaczenia, wbudowanego w jzyk. Wydruk z programu ma posta:
Nierowne
k= 0
Przytoczone przyk�ady ilustrowa�y przeci��anie operator�w binarnych. Dla operator�w unarnych sk�adnia deklaracji i definicji funkcji operatorowej jest taka sama z tym, �e funkcja przeci��aj�ca musi by� bezargumentowa. Ilustracj� tego jest poni�szy przyk�ad.
Przyk�ad 6.15.
// Unarne operatory + i -
#include <iostream.h>
class Firma {
public:
Firma(char* n, char s): nazwa(n),stan(s) {}
void operator+() { stan = '1'; }
void operator-() { stan = '0'; }
void podaj() { cout << stan: << stan << endl;}
private:
char* nazwa;
char stan;
};
int main() {
Firma frm1("firma1", '1');
-frm1;
+frm1;
frm1.podaj();
return 0;
}
Uwaga. Poprawne syntaktycznie stosowanie operator�w przeci��onych mo�e nie mie� sensu nawet w stosunku do obiekt�w tej samej klasy, je�eli obiekt ma by� dobr� abstrakcj� rzeczywistoœci fizycznej. WeŸmy dla przyk�adu dwa obiekty klasy Potrawa; niech ka�dy z nich ma sk�adow� char* smak. Je�eli wartoœ� tej sk�adowej w pierwszym obiekcie jest "s�ony", a w drugim "s�odki", to ma sens por�wnanie (==), ale nie ma sensu np. dodawanie tych dw�ch obiekt�w do siebie. Warto o tym pami�ta� przy tworzeniu system�w obiektowych.
7.3.1. Przecianie operator�w new i delete
Przypomnijmy (nigdy za wiele przypomnie�), �e obiekty j�zyka C++ mog� by� alokowane na trzy sposoby:
na stosie (jest to tzw. pami�� automatyczna, a zmienne w niej lokowane s� nazywane zmiennymi automatycznymi lub lokalnymi),
w pami�ci statycznej pod ustalonym adresem,
w pami�ci swobodnej (dynamicznej), zorganizowanej w postaci listy kom�rek, nazywanej kopcem, stogiem, lub stert�.
Obiektom lokalnym jest przydzielana pami�� na stosie w chwili, gdy wywo�ywana jest funkcja z niepustym wykazem argument�w, lub w chwili, gdy obiekt jest tworzony w bloku funkcji. Pami�� dla obiekt�w statycznych jest przydzielana w fazie konsolidacji (po kompilacji, a przed faz� wykonania). Obiekty dynamiczne s� alokowane w pami�ci przez wywo�anie operatora new. Przy tworzeniu obiekt�w, w kadym z tych przypadk�w najpierw jest przydzielany odpowiedni obszar pamici, a nastpnie jest woany konstruktor, kt�ry inicjuje w tym obszarze obiekt o danych wasnociach. Wyrane rozdzielenie alokacji pamici od inicjowania jest najlepiej widoczne przy alokacji dynamicznej. Np. deklaracja
Test* wsk = new Test(10);
oznacza, �e operator new wywouje pewn (niejawn) procedur alokacji dla uzyskania pamici, a nastpnie woa konstruktor klasy Test z parametrem 10, kt�ry inicjuje t pami. Ta sekwencja operacji jest nazywana tworzeniem obiektu, czyli wystpienia klasy.
W wi�kszoœci przypadk�w u�ytkownik nie musi si� interesowa� wspomnian� procedur� alokacji. Mo�na jednak wymieni� pewne szczeg�lne sytuacje, gdy u�ytkownik powinien sam decydowa� o sposobie przydzia�u pami�ci:
Program tworzy i niszczy bardzo wiele ma�ych obiekt�w (np. w�z�y drzewa, powi�zania listy jednokierunkowej, punkty, linie, komunikaty). Alokacja i dealokacja takich licznych obiekt�w mo�e �atwo zdominowa� czas wykonania programu, a tak�e szybko wyczerpa� zasoby pami�ci. Du�y narzut czasowy jest pochodn� niskiej efektywnoœci standardowego alokatora. Narzut pami�ciowy jest powodowany fragmentacj� pami�ci swobodnej przy alokacji mieszaniny obiekt�w o r�nych rozmiarach.
Program, kt�ry musi dzia�a� nieprzerwanie przez d�ugi czas przy bardzo ograniczonych zasobach. Jest to sytuacja typowa dla system�w czasu rzeczywistego, kt�re wymagaj� gwarantowanego kwantum pami�ci z minimalnym narzutem.
Istniej�ce œrodowisko nie zapewnia procedur zarz�dzania pami�ci� (np. piszemy program, kt�ry ma obs�ugiwa� mikroprocesor bez systemu operacyjnego). W takiej sytuacji programista musi opracowa� procedury niskiego poziomu, odwo�uj�ce si� nawet do adres�w fizycznych kom�rek pami�ci.
W pierwszych dw�ch przypadkach zastosowanie w�asnych mechanizm�w przydzia�u pami�ci mo�e jak pokazuje praktyka przynieœ� popraw� efektywnoœci od dw�ch do nawet dziesi�ciu razy.
Operatory new i delete (podobnie jak pozostae operatory standardowe) mona przecia wzgldem danej klasy. Ich prototypy maj wtedy posta:
class X {
//...
void* operator new(size_t rozmiar);
void operator delete(void* wsk);
void* operator new [] (size_t rozmiar);
void operator delete [] (void* wsk);
};
gdzie typ size_t jest zalenym od implementacji typem cakowitym, uywanym do utrzymywania rozmiar�w obiekt�w; jego deklaracja znajduje si w pliku nag�wkowym <stddef.h>
Pierwsza para operator�w odnosi si� do alokacji pojedynczych obiekt�w, zaœ druga do tablic. Poniewa� funkcja X::operator new() jest wywoywana przed konstruktorem, jej typ zwracany musi by void*, a nie X* (jeszcze nie ma obiektu X). Natomiast destruktor, wywoywany przed funkcj operatorow X::operator delete(), “dekonstruuje” obiekt, pozostawiajc do zwolnienia “niezorganizowan” pami. Dlatego argumentem funkcji X::operator delete() nie jest X*, lecz void*. Ponadto definiowane w klasie X::operator new() i X::operator delete() s statycznymi funkcjami skadowymi, bez wzgldu na to, czy s jawnie zadeklarowane ze sowem kluczowym static, czy te nie. Wasno ta jest konieczna z tych samych powod�w, co wymienione wyej: wywoanie statycznej funkcji skadowej klasy nie wymaga istnienia obiektu tej klasy.
W trzecim z wy�ej przytoczonych przypadk�w programista podaje adres fizyczny lub adres symboliczny alokowanego obiektu. Mo�na si� wtedy pos�u�y� prototypem funkcji operatorowej new() z dwoma argumentami i z pokazanym niej przykadowym blokiem:
void* operator new(size_t, void* wsk) { return wsk; }
gdzie wskaŸnik wsk podaje adres, pod kt�rym jest alokowany dany obiekt.
Poniewa� jest to konstrukcja programowa, kt�ra mo�e si� odwo�a� bezpoœrednio do sprz�tu, ustalono dla niej specjaln� sk�adni� wywo�ania. Je�eli np. adres pami�ci jest podany w postaci:
void* bufor = (void*) 0xF00F;
to wywo�anie alokatora X::operator new() moe mie posta:
X* wskb = new(bufor)X;
Zauwa�my, �e ka�dy operator new() przyjmuje size_t jako sw�j pierwszy argument; zatem w ostatnim wywoaniu rozmiar alokowanego obiektu jest dostarczany niejawnie.
Podany ni�ej przyk�ad ilustruje sk�adni� deklaracji, definicji i wywo�a� przeci��onych operator�w new() i delete(). Jest to jedynie ilustracja, jako e ani nie tworzy si w programie wielu obiekt�w, ani te nie jest to nawet makieta systemu czasu rzeczywistego. W klasie Nowa zadeklarowano zmienn statyczn licznik, kt�ra suy do zliczania obiekt�w po konstrukcji i destrukcji. Doliczanie i odliczanie odbywa si odpowiednio w konstruktorze i destruktorze klasy. W bloku main() wyjcie z ptli do-while nastpuje po wprowadzeniu z klawiatury znaku “q” albo “Q”; niezaleno od maej lub duej litery zapewnia funkcja toupper(char), kt�ra jest zadeklarowana w pliku nag�wkowym ctype.h>.
Przyk�ad 6.16.
# include <iostream.h>
# include < stddef.h>
# include < ctype.h>
class Nowa {
public:
char znak_zliczany;
int liczba_znakow;
Nowa(char znak);
~Nowa() { cout << Destruktor...\n; licznik--; }
void dodaj_znak() { liczba_znakow++; }
void* operator new(size_t rozmiar);
void operator delete(void* wsk);
static licznik;
};
int Nowa::licznik = 0;
Nowa::Nowa(char z)
{
cout << Konstruktor...\n;
znak_zliczany = z;
liczba_znakow = 0;
licznik++;
}
void* Nowa::operator new(size_t rozmiar)
{
cout << Alokacja: new...\n;
void* wsk = new char[rozmiar];
return wsk;
}
void Nowa::operator delete(void* wsk)
{
cout << Dealokacja: delete... ;
delete (void*) wsk;
}
int main() {
char we;
Nowa* wskNowa = new Nowa('x');
cout << Napisz kilka liter 'x'; 'q'-koniec:\n;
do { //wczytujemy iksy
cin >> we;
if(we == wskNowa->znak_zliczany)
wskNowa->dodaj_znak();
} while(toupper(we) != 'Q');
cout << \nLiczba znakow
<< wskNowa->znak_zliczany << : ;
cout << wskNowa->liczba_znakow << endl;
cout << Liczba obiektow: << Nowa::licznik << endl;
delete wskNowa;
return 0;
}
Przyk�adowy wydruk mo�e mie� posta�:
Alokacja: new...
Konstruktor...
Napisz kilka liter 'x'; 'q' - koniec:
x
x
q
Liczba znakow x: 2
Liczba obiektow: 1
Destruktor...
Dealokacja: delete...
7.4. Funkcje i klasy zaprzyjanione
Funkcje sk�adowe klasy mo�na uwa�a� za implementacj� koncepcji, nazywanej w j�zykach obiektowych przesy�aniem komunikat�w. Przy wywo�aniu takiej funkcji obiekt, dla kt�rego jest wywo�ywana, pe�ni rol� odbiorcy komunikatu; wartoœci zmiennych, adresy, czy te� obiekty, przekazywane jako argumenty aktualne funkcji, stanowi� treœ� komunikatu. Np. wywo�anie
punkt1.ustaw(c,d);
mo�na uwa�a� za adresowany do obiektu punkt1 komunikat o nazwie ustaw, kt�rego treci s wartoci dw�ch zmiennych: c oraz d.
Taki styl programowania jest charakterystyczny dla j�zyk�w czysto obiektowych, jak np. Smalltalk. Styl ten zapewnia pe�n� hermetycznoœ� (ukrywanie informacji) klas, kt�rych obiekty s� dost�pne wy��cznie za poœrednictwem funkcji sk�adowych, stanowi�cych ich publiczny interfejs.
Doœ� cz�sto jednak taki sztywny “gorset”, pod kt�rym ukrywa si� informacje prywatne, okazuje si� zbyt ciasny i niewygodny.
J�zyk hybrydowy, jakim jest C++, daje mo�liwoœ� rozluŸnienia tego gorsetu. Pozwala on zwyk�ym funkcjom i operatorom wykonywa� operacje na obiekcie w podobny spos�b, jak na “obiektach” typ�w podstawowych. Przy takim podejœciu moglibyœmy nasz� funkcj� skadow ustaw() uczyni zwyk funkcj, ze zmienn punkt1 jako jednym z argument�w formalnych. Wtedy jej wywoanie miaoby posta:
ustaw(punkt1,c,d);
Tutaj punkt1 jest traktowany na r�wni z pozostaymi argumentami. Zauwamy jednak, e teraz funkcja ustaw() bdzie operowa na kopii argumentu punkt1, a wic nie bdzie moga zmieni wartoci zmiennych skadowych obiektu punkt1. Mona temu atwo zaradzi, przesyajc parametr punkt1 przez referencj, a nie przez warto. Co wicej, funkcj ustaw() mona przecia, podajc r�ne definicje, np. dla ustawienia punktu na jednej z osi ukadu wsp�rzdnych, na paszczynie, czy w przestrzeni tr�jwymiarowej. Mona r�wnie pomyle o rozszerzeniu definicji funkcji ustaw() tak, aby moga oddziaywa na stan kilku obiekt�w jednoczenie; np. wywoanie
ustaw(punkt1, punkt2, c, d);
mog�oby przesy�a� wartoœci c oraz d z obiektu punkt1 do obiektu punkt2.
W podobnych do opisanego wy�ej przypadkach, gdy decydujemy si� ods�oni� cz�œ� informacji ukrytych w deklaracji klasy, mo�emy wykorzysta� mechanizm tzw. funkcji zaprzyjaŸnionych j�zyka C++. Jak sugeruje nazwa, funkcje zaprzyjaŸnione maj� te same przywileje, co funkcje sk�adowe, chocia� same nie s� funkcjami sk�adowymi klasy, w kt�rej zadeklarowano je jako zaprzyjaŸnione. W szczeg�lnoœci maj� one dost�p do tych element�w klasy, kt�rych deklaracje poprzedzaj� etykiety private: i protected:.
Deklaracj� funkcji zaprzyjaŸnionej (a tak�e klasy zaprzyjaŸnionej) poprzedza s�owo kluczowe friend. Przy tym jest oboj�tne, czy tak� deklaracj� umieszcza si� w publicznej, czy w prywatnej cz�œci deklaracji klasy.
Poniewa� funkcja zaprzyjaŸniona nie jest funkcj� sk�adow� klasy, zatem jej lista argument�w nie zawiera ukrytego wskaŸnika this. Zasi�g funkcji zaprzyjaŸnionej jest inny ni� zasi�g funkcji sk�adowych klasy: funkcja zaprzyjaŸniona jest widoczna w zasi�gu zewn�trznym, tj. takim samym zasi�gu, jak klasa, w kt�rej zosta�a zadeklarowana.
Przyk�ad 6.17.
// Funkcja zaprzyjazniona ustaw()
#include <iostream.h>
class Punkt {
public:
Punkt(int, int);
int fx() { return x; }
int fy() { return y; }
friend void ustaw(Punkt&, int,int);
private:
int x,y;
};
Punkt::Punkt(int a, int b): x(a),y(b) {}
void ustaw(Punkt& p, int c, int d)
{ p.x += c; p.y += d; }
int main() {
Punkt punkt1(3,4);
cout << punkt1.x przed ustaw(): << punkt1.fx()
<< endl;
ustaw(punkt1, 5, 5);
cout << punkt1.x po ustaw(): << punkt1.fx()
<< endl;
return 0;
}
Wydruk z programu ma posta�:
punkt1.x przed ustaw(): 3
punkt1.x po ustaw(): 8
Dyskusja. Poniewa� funkcja ustaw() nie jest funkcj skadow, jej definicja nie jest poprzedzona nazw klasy i operatorem zasigu. Zauwamy, e w definicji nie wystpuje specyfikator friend. Zwr�my take uwag na fakt, e ze wzgldu na brak niejawnego wskanika this, funkcja zaprzyjaniona nie moe si odwoywa do zmiennych skadowych bezporednio, lecz poprzez obiekt p klasy Punkt. Argument aktualny punkt1 jest przekazywany przez referencj; dziki temu stan obiektu punkt1 (tj. wartoci jego zmiennych skadowych) moe by modyfikowany przez funkcj ustaw().
Funkcja sk�adowa jednej klasy mo�e by� zaprzyjaŸniona z inn� klas�; ilustracj� tej mo�liwoœci jest poni�szy ci�g deklaracji:
class Pierwsza {
// ...
void f();
};
class Druga {
// ...
friend void Pierwsza::f();
};
Deklaracje funkcji, poprzedzone specyfikatorem friend pozwalaj� tak�e deklarowa� klasy, kt�re odwo�uj� si� do siebie nawzajem. Charakterystycznym przyk�adem mo�e by� deklaracja operatora mno�enia wektora przez macierz, jak pokazano ni�ej na przyk�adowym ci�gu deklaracji. Zauwa�my, �e w tym przypadku konieczne jest u�ycie deklaracji referencyjnej klasy Wektor przed waciwymi deklaracjami klas.
class Wektor;
class Macierz {
// ...
friend Wektor operator*(Macierz&, Wektor&);
};
class Wektor {
// ...
friend Wektor operator*(Macierz&, Wektor&);
};
7.4.1. Zaprzyjaniony operator '<<'
Dotychczas nie pomyœleliœmy o tym, jak u�atwi� sobie wyprowadzanie stanu obiektu. Wprawdzie dla nasze przyk�adowej klasy Punkt zdefiniowalimy funkcje skadowe fx() i fy() dla uzyskania dostpu do zmiennych prywatnych, ale kade wyprowadzenie wartoci x oraz y do strumienia cout wymagao pisania oddzielnych instrukcji z odpowiednimi argumentami dla operatora wstawiania “<<”. Obecnie wykorzystamy moliwo przecienia operatora “<<” dla wyprowadzenia penego stanu obiektu jedn instrukcj.
W pliku nag��wkowym <iostream.h> znajduje si deklaracja klasy strumieni wyjciowych ostream oraz szereg definicji przeciajcych operator “<<”, w kt�rych pierwszym jego argumentem jest obiekt klasy ostream. Definicje te pozwalay nam uywa operatora “<<” do wyprowadzania wartoci r�nych typ�w, np. char, int, long int, double, czy char* (acuch�w). Projektujc wasne klasy, uytkownik moe wprowadza wasne definicje operatora “<<” (w razie potrzeby take “>>”). Maj one nastpujc posta og�ln:
ostream& operator<<(ostream& os, nazwa-klasy& ob)
{
// Ciao funkcji operator<<()
return os;
}
Pierwszym argumentem funkcji operator<<() jest referencja do obiektu typu ostream. Oznacza to, e os musi by strumieniem wyjciowym. Do drugiego argumentu, ob, przesya si w wywoaniu obiekt (adres) typu nazwa-klasy, kt�ry bdzie wyprowadzany na standardowe wyjcie. Zauwamy, e strumie wyjciowy os musi by przekazywany przez referencj, poniewa jego wewntrzny stan bdzie modyfikowany przez operacj wyprowadzania. Funkcja operator<<() zawsze zwraca referencj do swojego pierwszego argumentu, tj. strumienia wyjciowego os; ta wasno oraz fakt, e operator “<<” wie od lewej do prawej, pozwala uywa go wielokrotnie w tej samej instrukcji wyprowadzania. Np. w instrukcji
cout << ob1 << \n;
wyra�enie jest wartoœciowane tak, jak gdyby by�o zapisane w postaci:
( cout << ob1 ) << \n
Wartoœciowanie wyra�enia w nawiasach okr�g�ych wstawia ob1 do cout i zwraca referencj do cout; ta referencja staje si pierwszym argumentem dla drugiego operatora “<<”. Tak wic drugi operator “<<” jest przykadany tak, jak gdyby napisano:
cout << \n;
Wyra�enie cout << \n r�wnie zwraca referencj do cout, a wic moe po nim wystpi nastpny operator” “<<”, i.t.d.
Funkcja operator<<() nie powinna by funkcj skadow klasy, na kt�rej obiektach ma operowa. Wynika to std, e gdyby bya funkcj skadow, to jej pierwszym z lewej argumentem, przekazywanym niejawnie poprzez wskanik this, byby obiekt, kt�ry generuje wywoanie tej funkcji. Tymczasem w naszej definicji pierwszym z lewej argumentem musi by strumie klasy ostream, natomiast prawy operand jest obiektem, kt�ry chcemy wyprowadzi na standardowe wyjcie (kolejnoci tej nie mona zmieni, poniewa tak kolejno argument�w narzucaj definicje w <iostream.h>). Wobec tego funkcj operator<<() musimy zadeklarowa jako funkcj zaprzyjanion klasy, na kt�rej obiektach ma operowa. Ilustruje to pokazany niej prosty przykad.
Przyk�ad 6.18.
// Zaprzyjazniony operator <<
#include <iostream.h>
class Punkt {
public:
Punkt(int, int);
friend ostream& operator<<(ostream&, Punkt&);
private:
int x,y;
};
Punkt::Punkt(int a, int b): x(a),y(b) {}
ostream& operator<<(ostream& os, Punkt& ob)
{
os << ob.x << , << ob.y << \n;
return os;
}
int main() {
Punkt punkt1(3,4), punkt2(10,15);
cout << punkt1 << punkt2;
return 0;
}
Wydruk z programu b�dzie mia� posta�:
3, 4
10, 15
Dyskusja. Mo�e si� wydawa� dziwnym, �e w definicji operatora << uywa si tego samego symbolu. Zauwamy jednak, e operatory << uywane w definicji wyprowadzaj liczby cakowite i acuchy; zatem wywouj one ju istniejce w pliku <iostream.h> definicje, w kt�rych drugim operandem jest liczba typu int lub acuch (typu char*). Drugim godnym uwagi faktem jest to, e instrukcja wyprowadzania w definicji operatora << wysya wartoci x oraz y do zupenie dowolnego strumienia klasy ostream, przekazywanego do funkcji operator<<() jako parametr aktualny. W wywoaniu operatora << w bloku main() uylimy cout jako parametru aktualnego. R�wnie dobrze moglibymy jednak skierowa wyjcie naszego programu do pliku, zamiast na konsol doczon do cout. W takim przypadku naleaoby wykorzysta definicje, zawarte w pliku nag�wkowym <fstream.h>.
7.4.2. Klasy zaprzyjanione
Ka�da klasa mo�e mie� wiele funkcji zaprzyjaŸnionych; jest zatem naturalne, aby ca�� klas� uczyni� zaprzyjaŸnion� z inn� klas�. Je�eli klasa A ma by� zaprzyjaŸniona z klas� B, to deklaracja klasy A musi poprzedza� deklaracj� klasy B. Schemat deklaracji ilustruje poni�szy przyk�ad: ka�da funkcja sk�adowa klasy Pierwsza staje si funkcj zaprzyjanion klasy Druga. W rezultacie wszystkie skadowe prywatne, publiczne, i chronione klasy Druga staj si dostpne dla klasy zaprzyjanionej Pierwsza.
class Pierwsza {
// ...
};
class Druga {
public:
Druga(int i = 0, int j = 0): x(i),y(j) {}
friend class Pierwsza;
private:
int x, y;
};
7.5. Obiekty i funkcje
Prawie wszystkie prezentowane dot�d przyk�ady klas zawiera�y dwa rodzaje funkcji sk�adowych:
Funkcje, kt�re mog�y zmienia� wartoœci zmiennych sk�adowych, tj. stan obiektu. Funkcje takie w j�zykach obiektowych nazywa si� operacjami mutacji (ang. mutator operations).
Funkcje, kt�re jedynie podawa�y aktualne wartoœci zmiennych sk�adowych, tj. bie��cy stan obiektu. Funkcje takie w j�zykach obiektowych nazywa si� operacjami dost�pu (ang. accessor operations, lub field accessors); w j�zyku C++ nazywamy je funkcjami sta�ymi.
Obecnie zajmiemy si� bardziej szczeg�owo zagadnieniem zmian i zachowania stanu obiektu w aspekcie obu rodzaj�w funkcji.
7.5.1. Obiekty i funkcje stae
Obiekty klas, podobnie jak obiekty typ�w wbudowanych, mo�na deklarowa� jako sta�e symboliczne, poprzedzaj�c deklaracj� s�owem kluczowym const.
Np. dla klasy Punkt z konstruktorem domylnym Punkt() moemy utworzy obiekt stay punkt1 instrukcj deklaracji:
const Punkt punkt1;
Do tego samego celu mo�na wykorzysta� konstruktor z parametrami:
const Punkt punkt2(3,2);
W obu przypadkach kompilator C++ zaakceptuje definicje zmiennych, kt�re zostan� odpowiednio zainicjowane przez konstruktory. Natomiast kompilator odrzuci ka�d� pr�b� zmiany stanu obiekt�w punkt1 i punkt2 zar�wno przez bezporednie przypisanie, jak i przez przypisanie za pomoc wskanik�w, np.
Punkt p1;
punkt1 = p1;
Punkt* wsk = &punkt1;
Gdyby jednak zadeklarowano wskaŸnik sta�y const Punkt* wsk; to oczywiœcie mo�na mu przypisa� adres obiektu sta�ego punkt1
wsk = &punkt1;
Pozostaje jednak otwarte pytanie: czy wolno dla obiektu sta�ego wywo�ywa� funkcje (np. funkcj� ustaw()), kt�re mog zmieni jego stan? Logika m�wi, e takie operacje nie powinny by dopuszczalne.
Dla tak prostej klasy jak Punkt, kompilator m�gby prawdopodobnie odr�ni funkcje, kt�re zmieniaj wartoci zmiennych skadowych x oraz y od funkcji, kt�re pozostawiaj je bez zmian. W og�lnoci jednak nie jest to moliwe. Tak wic w praktyce programista musi pom�c kompilatorowi przez odpowiednie zadeklarowanie i zdefiniowanie tych funkcji, kt�re zachowuj stan obiektu. Wyr�nienie takich funkcji jest moliwe przez dodanie sowa kluczowego const do ich deklaracji i definicji. Zasig tych funkcji bdzie si pokrywa z zasigiem klasy, a ich deklaracje maj posta:
typ-zwracany nazwa-funkcji(parametry) const;
S�owo kluczowe const musi r�wnie� pojawi� si� po nag��wku w definicji funkcji. Np. definicja funkcji sta�ej fx(), umieszczona wewn�trz deklaracji klasy Punkt bdzie mie posta:
int fx() const { return x; }
Podany ni�ej przyk�ad ilustruje przeprowadzon� dyskusj�.
Przyk�ad 6.19.
#include <iostream.h>
class Punkt {
public:
Punkt(): x(0),y(0) {}
Punkt( int a, int b ): x(a),y(a) {}
void ustaw(int c, int d)
{ x = x + c; y = y + d; }
int fx() const { return x; }
int fy() const { return y; }
private:
int x,y;
};
int main() {
const Punkt p0;
cout << p0.x= << p0.fx() << \n;
cout << p0.y= << p0.fy() << \n;
Punkt p1;
// p0 = p1; Niedopuszczalne
// Punkt* wsk = &p0; Niedopuszczalne
// p0.ustaw(3,4); Niedopuszczalne
return 0;
}
Analiza programu. Niedopuszczalnoœ� przypisania p0 = p1 oraz wywoania p0.ustaw(3,4); jest oczywista, poniewa obiekt p0 jest obiektem staym i jego skadowe nie mog by zmieniane w programie przez adn operacj. W drugiej bdnej instrukcji, Punkt* wsk = &p0; pr�buje si wskanikowi przypisa adres obiektu staego, co, podobnie jak dla staych typ�w wbudowanych, jest niedopuszczalne.
Uwaga 1. Niekt�re kompilatory (np. Borland C++,v.3.1) akceptuj� kod Ÿr�d�owy, w kt�rym funkcje, nie wyr�nione s�owem kluczowym const, mog� by� wywo�ywane dla obiektu sta�ego. Kompilatory te wprawdzie ostrzegaj� u�ytkownika, ale i tak produkuj� kod wykonalny, kt�ry zmienia stan obiekt�w sta�ych!
Przypomnijmy jeszcze, �e ka�da niestatyczna funkcja sk�adowa zawiera niejawny argument this, kt�ry (domylnie) wystpuje jako pierwszy z lewej w wykazie argument�w. Domylna deklaracja tego wskanika dla kadej funkcji skadowej pewnej klasy X ma posta: X *const this;. Dla staych funkcji skadowych domylna deklaracja bdzie: const X *const this;.
7.5.2. Kopiowanie obiekt�w
W zasadzie istniej� dwa przypadki, w kt�rych wyst�puje potrzeba kopiowania obiekt�w:
gdy deklarowany obiekt pewnej klasy inicjuje si� innym, wczeœniej utworzonym obiektem;
gdy obiektowi przypisuje si� inny obiekt w instrukcji przypisania.
Obiekt jest r�wnie� kopiowany wtedy, gdy jest przekazywany przez wartoœ� jako parametr aktualny funkcji oraz gdy jest wynikiem zwracanym przez funkcj�.
Kopiowanie wyst�pie� (“obiekt�w”) typ�w wbudowanych, np. char, int, etc. oznacza po prostu kopiowanie ich wartoœci. Przyk�adowo mo�emy napisa�:
int i(10); // to samo co int i = 10;
int j = i;
W obu przypadkach operator “=” jest operatorem inicjowania, a nie przypisania.
Dla obiekt�w klasy, w kt�rej nie zdefiniowano specyficznych dla niej operacji, kopiowanie i przypisywanie obiekt�w tej klasy b�dzie wykonywane za pomoc� generowanego przez kompilator konstruktora kopiuj�cego i generowanego operatora przypisania. Np. dla klasy X bd generowane automatycznie: konstruktor kopiuj�cy X::X(const X&) oraz operator przypisania
X& operator=(const X&).
Funkcje te wykonuj� kopiowanie obiekt�w sk�adowa po sk�adowej. Np. dla klasy:
class Punkt { int x,y; public: Punkt(int, int); };
mo�emy zadeklarowa� obiekt:
Punkt p1(3,4);
a nast�pnie obiekt p2, kt�rego pola x, y bd inicjowane wartociami p�l obiektu p1:
Punkt p2 = p1;
Zwr��my uwag� na nast�puj�cy fakt: gdyby deklaracja obiektu p2 miaa posta: Punkt p2;, to deklaracj klasy Punkt musielibymy rozszerzy o konstruktor domylny, np. Punkt() { x = 0; y = 0; }. Tymczasem poprawna deklaracja Punkt p2 = p1; nie wymaga istnienia konstruktora domylnego. Wniosek std taki, e obiekt p2, inicjowany w deklaracji obiektem p1, nie jest tworzony przez aden zadeklarowany konstruktor klasy! I tak jest istotnie: obiekt p2 jest tworzony, skadowa po skadowej, przez generowany konstruktor kopiujcy Punkt::Punkt(const Punkt& p1). Konstruktor ten jest wywoywany niejawnie przez kompilator, przy czym obiekt p1 przechodzi przez referencj jako parametr aktualny. Zauwamy te, e wprawdzie konstruktor kopiujcy operuje bezporednio na obiekcie p1, a nie na jego kopii, to jednak obiekt p1 nie ulegnie adnym zmianom, poniewa argument formalny konstruktora kopiujcego jest poprzedzony sowem kluczowym const.
Kopiowanie obiekt�w przez niejawne wywo�anie automatycznie generowanego konstruktora kopiuj�cego podlega, niestety, bardzo istotnemu ograniczeniu. Operacja ta sprawdza si� jedynie dla obiekt�w, kt�re nie zawieraj� wskaza� na inne obiekty. Je�eli obiekt jest wyst�pieniem klasy, w kt�rej zadeklarowano wskaŸnik do jej w�asnego obiektu lub obiektu innej klasy, to przy wy�ej opisanej procedurze zostan� wprawdzie skopiowane wskaŸniki, ale nie b�d� skopiowane obiekty wskazywane. Dlatego te� kopiowanie z niejawnym wywo�aniem generowanego przez kompilator konstruktora kopiuj�cego okreœla si� jako kopiowanie p�ytkie (ang. shallow copy). P�ytkie kopiowanie ma jeszcze jedn� wad�: je�eli w klasie zdefiniowano destruktor, to po ka�dej operacji kopiowania zmiennych wskaŸnikowych b�dziemy mie� po dwa wskazania na ten sam adres. W�wczas wywo�ywany przed zako�czeniem programu (lub funkcji) destruktor b�dzie dwukrotnie niszczy� ten sam obiekt! Pokazany ni�ej przyk�ad ilustruje taki w�aœnie przypadek.
Przyk�ad 6.20.
#include <iostream.h>
class Niepoprawna {
public:
Niepoprawna() { wsk = new int(10); }
~Niepoprawna() { delete wsk; }
private:
int* wsk;
};
int main() {
Niepoprawna z1;
Niepoprawna z2 = z1;
return 0;
}
Dyskusja. Druga instrukcja deklaracji w bloku main() wywouje generowany konstruktor kopiujcy, inicjujc obiekt z2 obiektem z1. Wskanik wsk typu int* obiektu z2 zostaje zainicjowany kopi wskanika wsk obiektu z1. W rezultacie wsk obu obiekt�w wskazuj na ten sam obiekt typu int o wartoci 10. Przed zakoczeniem programu wykonywany jest dwukrotnie destruktor, odpowiednio dla obiekt�w z1 i z2. Za kadym razem niszczony jest ten sam obiekt typu int*, co moe przynie niepodane konsekwencje. Podobna sytuacja powstaje w przypadku kopiowania obiekt�w przez przypisanie.
Dla wi�kszoœci klas rozwi�zaniem ukazanego problemu jest zdefiniowanie w�asnego konstruktora kopiuj�cego i operatora przypisania. W�aœciwe zdefiniowanie tych funkcji pozwoli nam na tzw. kopiowanie g��bokie (ang. deep copy), przy kt�rym b�d� kopiowane nie tylko wskaŸniki, ale i obiekty przez nie wskazywane. Prawid�owo b�dzie te� przebiega� destrukcja obiekt�w.
Przyk�ad 6.21.
#include <iostream.h>
class Poprawna {
public:
Poprawna(): wsk(new int(10)) {}
~Poprawna() { delete wsk; }
Poprawna(const Poprawna& nz)
{ wsk = new int(*nz.wsk); }
Poprawna& operator=(const Poprawna& nz)
{
if(this != &nz)
{
delete wsk;
wsk = new int(*nz.wsk);
}
return *this;
}
private:
int* wsk;
};
int main() {
Poprawna z1;
Poprawna z2 = z1;
return 0;
}
Dyskusja. W programie zdefiniowano w�asny konstruktor kopiuj�cy i w�asny operator przypisania (nie u�ywany). Pierwsza instrukcja w bloku main() wywouje konstruktor Poprawna() { wsk = new int(10); }, kt�ry tworzy obiekt z1, a w nim podobiekt typu int, na kt�ry wskazuje wsk. Druga instrukcja wywouje konstruktor kopiujcy Poprawna(const Poprawna&) z parametrem aktualnym z1; konstruktor ten tworzy nowy podobiekt typu int i umieszcza go pod innym adresem ni pierwszy (oczywicie oba podobiekty maj t sam warto 10). Dziki temu wywoywany dwukrotnie przed zakoczeniem programu destruktor niszczy za kadym razem inny obiekt.
Uwaga. Instrukcj� Poprawna z2 = z1; mo�na tak�e zapisa� w postaci Poprawna z2(z1);, pokazuj�cej wyraŸnie, �e obiekt z2 jest inicjowany obiektem z1.
7.5.3. Przekazywanie obiekt�w do/z funkcji
Syntaktyka i semantyka przekazywania obiekt�w do funkcji jest identyczna dla obiekt�w typ�w predefiniowanych (np. int, double), jak i obiekt�w klas definiowanych przez u�ytkownika. Dla typu zdefiniowanego przez u�ytkownika parametr formalny funkcji b�dzie klas�, wskaŸnikiem, lub referencj� do klasy. Funkcj� tak� wywo�ujemy z parametrem aktualnym, b�d�cym odpowiednio obiektem danej klasy, adresem obiektu, lub zmienn� referencyjn�. Podobnie jak dla typ�w wbudowanych, obiekty klas s� domyœlnie przekazywane przez wartoœ�, a semantyka przekazywania jest taka sama, jak semantyka inicjowania.
Obiekty przekazywane do funkcji tworzone s� jako automatyczne, tzn. takie, kt�re tworzy si� za ka�dym razem gdy jest wykonywana ich instrukcja deklaracji, i niszczy za ka�dym razem, gdy sterowanie opuszcza blok zawieraj�cy deklaracj�. Je�eli funkcja jest wywo�ywana wiele razy (co cz�sto si� zdarza), to tyle samo razy jest wykonywane tworzenie i niszczenie obiekt�w, a wi�c za ka�dym razem mamy nowy obiekt, z nowymi inicjalnymi wartoœciami zmiennych sk�adowych. Pokazany ni�ej przyk�ad ilustruje przekazywanie obiektu do funkcji przez wartoœ�.
Przyk�ad 6.22.
#include <iostream.h>
class Test {
public:
Test(int a): x(a) { cout << Konstrukcja...\n; }
Test(const Test& t)
{ this->x = t.x; cout << Konstrukcja kopii...\n; }
~Test() { cout << Destrukcja...\n; }
int podaj() { return x; }
void ustaw(int i) { x = i; }
private:
int x;
};
void f(Test arg) {
arg.ustaw(50);
cout << Funkcja f: ;
cout << t1.x == << arg.podaj() << '\n';
}
int main() {
Test t1(10);
cout << t1.x == << t1.podaj() << '\n';
f(t1);
cout << t1.x == << t1.podaj() <<'\n';
return 0;
}
Wydruk z programu ma posta�:
Konstrukcja...
t1.x == 10
Konstrukcja kopii...
Funkcja f: t1.x == 50
Destrukcja...
t1.x == 10
Destrukcja...
Analiza programu. Wykonanie instrukcji deklaracji Test t1(10); tworzy obiekt t1 za pomoc konstruktora Test(int), od kt�rego pochodzi pierwszy wiersz wydruku. W instrukcji cout wywouje si funkcj skadow podaj(), kt�ra wywietla drugi wiersz wydruku. Instrukcja wywoania funkcji f(t1), z argumentem przekazywanym przez warto, wywouje konstruktor kopiujcy
Test(const Test&), kt�ry generuje trzeci wiersz wydruku. Czwarty wiersz jest generowany przez funkcj f(); wypisuje ona warto pola x lokalnej kopii argumentu, tj. obiektu utworzonego przez konstruktor kopiujcy. Zauwamy, e warto x w kopii obiektu zostaa zmieniona na 50 funkcj skadow ustaw(), wywoan z bloku funkcji f(), co pokazuje czwarty wiersz wydruku. Po wydrukowaniu czwartego wiersza sterowanie opuszcza blok funkcji f(), wywoujc destruktor ~Test(), kt�ry niszczy obiekt, utworzony przez konstruktor kopiujcy (pity wiersz z tekstem Destrukcja...). Po opr�nieniu stosu funkcji f() sterowanie wraca do bloku main(), wywoujc w instrukcji cout funkcj skadow podaj() obiektu t1. Wykonanie tej instrukcji pokazuje, e warto pola x pozostaa bez zmiany, jako e zmiana x
na 50 by�a wykonywana przez funkcj� f() na kopii obiektu t1, a nie na samym obiekcie. Ostatni wiersz wydruku sygnalizuje destrukcj obiektu t1, gdy sterowanie opuszcza blok funkcji main().
W powy�szym przyk�adzie warto zwr�ci� uwag� na dwa momenty. Gdy jest tworzona kopia obiektu przekazywanego do argumentu formalnego funkcji, nie wywo�uje si� konstruktora obiektu, lecz konstruktor kopiuj�cy. Pow�d jest oczywisty: poniewa� konstruktor jest w og�lnoœci u�ywany do inicjowania obiektu (np. nadania wartoœci pocz�tkowych zmiennym sk�adowym), to nie mo�e by� wo�any gdy wykonuje si� kopi� ju� istniej�cego obiektu. Je�eli kopia ma zosta� przes�ana do funkcji, to zale�y nam przecie� na aktualnym stanie obiektu, a nie na jego stanie pocz�tkowym. Natomiast jest celowe i konieczne wywo�anie destruktora, gdy funkcja ko�czy dzia�anie. Jest to konieczne, poniewa� obiekt w bloku funkcji m�g�by by� u�yty do jakiejœ operacji, kt�ra musi zosta� uniewa�niona, gdy obiekt wychodzi poza sw�j zasi�g. Przyk�adem mo�e by� alokacja pami�ci dla kopii; pami�� ta musi by� zwolniona po wyjœciu z bloku funkcji.
Destrukcja kopii obiektu u�ywanej do wywo�ania funkcji mo�e by� Ÿr�d�em pewnych k�opot�w, szczeg�lnie w przypadku dynamicznej alokacji pami�ci. Je�eli np. obiekt u�yty jako argument alokuje pami�� na kopcu (ang. heap) i zwalnia j� po destrukcji, to i jego kopia b�dzie zwalnia� t� sam� pami��, gdy zostanie wywo�any jej destruktor. Jednym ze sposob�w na unikni�cie tego rodzaju “niespodzianek” jest przekazywanie do funkcji adresu obiektu zamiast samego obiektu. Wtedy, co jest oczywiste, nie b�dzie tworzony �aden nowy obiekt, i nie b�dzie wo�any �aden destruktor przy wyjœciu z bloku funkcji. Adresy obiekt�w mo�na przesy�a� do funkcji albo za pomoc� wskaŸnik�w, albo referencji. Przy tym, je�eli chcemy unikn�� zmiany argumentu aktualnego przez operacje wykonywane w bloku funkcji, to wystarczy w definicji funkcji zadeklarowa� argument formalny ze s�owem kluczowym const. Podany ni�ej przyk�ad ilustruje wariant ze wskaŸnikiem i z referencj�.
Przyk�ad 6.23.
#include <iostream.h>
class Test {
public:
Test(int a): x(a){ cout << Konstrukcja...\n; }
Test(const Test& t)
{ this->x=t.x; cout<<Konstrukcja kopii...\n; }
~Test() { cout << Destrukcja...\n; }
int podaj() { return x; }
void ustaw(int i) { x = i; }
private:
int x;
};
void fwsk(Test* arg)
{ arg->ustaw(50); cout << Funkcja fwsk: ;
cout << arg.x == << arg->podaj() << '\n';
}
void fref(Test& arg)
{ arg.ustaw(60); cout << Funkcja fref: ;
cout << arg.x == << arg.podaj() << '\n';
}
int main() {
Test t1(10);
cout << t1.x == << t1.podaj() << '\n';
fwsk(&t1);
// fref(t1);
cout << t1.x == << t1.podaj() << '\n';
return 0;
}
Wydruk z programu ma posta�:
Konstrukcja...
t1.x == 10
Funkcja fwsk: arg.x == 50
t1.x == 50
Destrukcja...
Dyskusja. Jak pokazuje wydruk, tworzony i niszczony jest tylko jeden obiekt. Argumentem funkcji fwsk() jest wskanik do obiektu klasy Test; wobec tego jej argument aktualny w wywoaniu musi by adresem obiektu tej klasy. Poniewa w bloku funkcji fwsk() jest wywoywana funkcja skadowa ustaw() zmieniajca warto x, to po wykonaniu funkcji fwsk() warto ta (50) zostaa wywietlona przez bezporednie wywoanie funkcji podaj() dla obiektu t1. Gdyby w funkcjach fwsk() i fref() wyeliminowa wywoanie funkcji, zmieniajcej stan obiektu, to ich prototypy mogyby mie posta:
void fwsk(const Test* arg);
i
void fref(const Test& arg);
Uwaga 1. W podanych wy�ej przyk�adach konstruktory kopiuj�ce i destruktory wprowadzono dla lepszego zobrazowania wydruk�w (obiekty nie zawieraj� wskaŸnik�w do innych obiekt�w). Gdyby ich nie zadeklarowano, kopiowanie i destrukcj� wykona�yby funkcje, generowane przez kompilator.
Uwaga 2. W dobrze skonstruowanym programie ma�e obiekty mog� by� przesy�ane przez wartoœ�, a du�e przez wskaŸniki lub referencje.
Podobnie jak dla typ�w wbudowanych, wynikiem prowadzonych w bloku funkcji oblicze� mo�e by� obiekt klasy zdefiniowanej przez u�ytkownika. Typowa definicja takiej funkcji mo�e mie� jedn� z postaci:
klasa nazwa-funkcji(klasa obiekt)
{
//...
return obiekt;
}
lub
klasa& nazwa-funkcji(klasa& obiekt)
{
//...
return obiekt;
}
W pierwszym przypadku, zar�wno przy wywo�aniu funkcji, jak i powrocie z funkcji, b�dzie wywo�ywany konstruktor kopiuj�cy (domyœlny lub zdefiniowany w klasie). W drugim przypadku obiekt b�dzie przes�any do funkcji przez referencj� (lub sta�� referencj�), i w taki sam spos�b przekazany z funkcji. Stosuj�c technik� referencji nale�y pami�ta� o niebezpiecze�stwie przekazania referencji do obiektu, kt�ry przestaje istnie� po wyjœciu z bloku funkcji, np.
Test& g() { Test ts; return ts; }
Tutaj zmienna lokalna ts przestaje istnie po wykonaniu funkcji, a wic funkcja zwraca “wiszc referencj” referencj do nieistniejcego obiektu.
Przyk�ad 6.24.
#include <iostream.h>
class Test {
public:
Test(int a): x(a) { cout << Konstrukcja...\n; }
Test(const Test&)
{ this->x = t.x; cout << Konstrukcja kopii...\n; }
~Test() { cout << DESTRUKCJA...\n; }
Test& operator=(const Test& t)
{ this->x = t.x;
cout << Przypisanie...\n;
return *this;
}
int podaj() { return x; }
private:
int x;
};
Test g(Test arg)
{
cout << Funkcja g: ;
cout << arg.x == << arg.podaj() << '\n';
return arg;
}
int main() {
Test t1(10), t2(20);
t1 = g(t2);
cout << t1.x == << t1.podaj() << '\n';
return 0;
}
Wydruk z programu ma posta�:
Konstrukcja...
Konstrukcja...
Konstrukcja kopii...
Funkcja g: arg.x == 20
Konstrukcja kopii...
DESTRUKCJA...
Przypisanie...
DESTRUKCJA...
t1.x == 20
DESTRUKCJA...
DESTRUKCJA...
Analiza programu. Pierwsze dwa wiersze wydruku to (jedyne) dwa wywo�ania konstruktora Test(int). Wykonanie instrukcji t1 = g(t2); pociga za sob nastpujc sekwencj czynnoci:
wywo�anie konstruktora kopiuj�cego (Konstrukcja kopii...), kt�ry tworzy obiekt tymczasowy dla parametru arg
wywo�anie funkcji g() z argumentem utworzonym przez konstruktor kopiujcy
wyœwietlenie napisu: Funkcja g: arg.x == 20, w kt�rym liczba 20 jest wynikiem wywoania funkcji skadowej podaj()
wywo�anie konstruktora kopiuj�cego dla utworzenia obiektu tymczasowego (wyra�enia, przekazywanego przez instrukcj� return)
destrukcj� pierwszego obiektu tymczasowego
przypisanie wartoœci funkcji g() do t1 wykonywane przez wywoanie przecionego operatora przypisania
destrukcj� drugiego obiektu tymczasowego
wyœwietlenie odpowiedzi z wywo�ania funkcji sk�adowej podaj()
destrukcj� obiekt�w t1 i t2.
7.5.4. Konwersje obiekt�w
Kompilacja i wykonanie program�w w j�zyku C++ prawie zawsze wymaga wykonania wielu konwersji typ�w. Zdecydowana wi�kszoœ� tych konwersji wykonywana jest niejawnie, bez udzia�u programisty. Typowym przyk�adem mo�e by� proces dopasowania argument�w funkcji, w szczeg�lnoœci argument�w funkcji przeci��onych. Oczywiœcie programista mo�e dokonywa� konwersji jawnych za pomoc� operatora konwersji “()”, ale nale�y to czyni� raczej oszcz�dnie i tylko w przypadkach naprawd� koniecznych.
Natomiast w dotychczasowej dyskusji o klasach i obiektach klas w zasadzie nie zwracaliœmy uwagi na fakt, �e konwersja towarzyszy kreowaniu obiekt�w. WeŸmy najbli�szy przyk�ad z p. 6.5.3. Utworzenie obiektu klasy Test wymagao uycia konstruktora Test::Test(int y), kt�ry zmienn y typu int przeksztaca w obiekt typu Test. Przykad ten mona uog�lni: jednoargumentowy konstruktor klasy X mona traktowa jako przepis, kt�ry z argumentu konstruktora tworzy obiekt klasy X. Zauwamy, e argument konstruktora nie musi by typu wbudowanego; moe nim by zmienna innej klasy, o ile tylko potrafimy zdefiniowa metod przeksztacenia obiektu danej klasy w obiekt innej klasy.
Wykorzystanie konstruktora do konwersji nie jest mo�liwe, gdy chcemy dokona� konwersji w drug� stron�, tj. przekszta�ci� obiekt klasy do typu wbudowanego. W takich przypadkach definiujemy specjaln� funkcj� sk�adow� konwersji. Og�lna posta� funkcji konwersji dla klasy X jest nast�puj�ca:
X::operator T() { return w; }
gdzie T jest typem, do kt�rego dokonujemy konwersji, za w wyraeniem, kt�rego argumentami musz by skadowe klasy, poniewa funkcja konwersji operuje na obiekcie, dla kt�rego jest woana.
Zauwa�my, �e w definicji funkcji konwersji nie podaje si� ani typu zwracanego, ani argument�w.
Przyk�ad 6.25.
#include <iostream.h>
class Test {
public:
Test(int i): x(i) {}
operator int() { return x*x; };
private:
int x;
};
int main() {
int num1 = 2, num2 = 3;
Test t1(num1), t2(num2);
int ii;
ii = t1;
cout << ii << endl;
ii = 10 + t2;
cout << ii << endl;
return 0;
}
Wydruk z programu ma posta�:
4
19
Dyskusja. W przyk�adzie mamy konwersje w obie strony: jednoparametrowy konstruktor Test(int i) przeksztaca argument i typu int w obiekt klasy Test, a funkcja konwersji operator int() { return x*x; }; pozwala uywa obiekty typu Test w taki sam spos�b, jak obiekty typu int.
Przyk�ad 6.26.
#include <iostream.h>
class Boolean {
public:
enum logika { false = 0, true = 1 };
//konstruktory
Boolean(): z(true) {}
Boolean(int num): z(num != 0) { }
Boolean(double d) { z = (d != 0); }
Boolean (void* wsk): z(wsk != 0) { }
//Konwersja
operator int() const { return z; }
//Negacja
Boolean operator!() const { return !z; }
private:
char z;
};
Boolean pierwiastki(double a, double b, double c)
{
int pr = b*b >= 4*a*c;
cout << pr << endl;
return pr;
}
int main() {
Boolean b1(Boolean::true);
Boolean b2(5);
int ii = !b1 || b2;
cout << ii = << ii << endl;
int* wsk = new int(10);
Boolean b3(wsk);
Boolean b4(3.5);
double a = 1, b = 4, c = 3;
if(pierwiastki(a,b,c))
cout << Dwa << endl;
return 0;
}
Wydruk z programu ma posta�:
ii = 1
1
Dwa
Dyskusja. Klasa Boolean imituje predefiniowany typ bool, kt�ry dopiero ostatnio wprowadzono do standardu jzyka C++. W klasie zdefiniowano cztery konstruktory, kt�re mog tworzy obiekty typu Boolean. Pierwszy z nich, domylny konstruktor bezparametrowy, wychodzi poza opisan wczeniej konwencj, tym niemniej bywa uyteczny, np. przy tworzeniu tablic wartoci logicznych. W definicji konstruktora z parametrem typu int, Boolean(int num): z(num != 0) { } uyto czciej obecnie stosowanej notacji, ni starsza, w kt�rej napisalibymy:
Boolean(int num) { z = num != 0; }
Wartoœci logiczne prawda i fa�sz zosta�y zdefiniowane jako typ wyliczeniowy logika z jawnie zainicjowanymi staymi true i false. Zdefiniowano take przeciony operator negacji logicznej. Funkcja konwersji do typu int jest funkcj sta, podobnie jak funkcja operator!(). Obiekty klasy Boolean s wykorzystywane w instrukcji przypisania int ii = !b1 || b2;
Wykonanie tej instrukcji przebiega nast�puj�co:
Dla obiektu b1 zostaje wywoana funkcja operator!(); powr�t z tej funkcji wymaga utworzenia obiektu tymczasowego. Obiekt taki jest tworzony przez wywoanie konstruktora Boolean(int).
Po wykonaniu negacji zostaje dwukrotnie wywo�ana funkcja konwersji operator int() dla obiekt�w !b1 oraz b2, co pozwala obliczy warto alternatywy logicznej.
Wartoœ� alternatywy logicznej zostaje przypisana do ii.
Obiekt b3(wsk) jest tworzony przez wywoanie konstruktora Boolean(void* wsk) { z = wsk != 0; }. Wykorzystuje si tutaj wasno typu void*, do kt�rego moe by automatycznie (niejawnie) przeksztacony wskanik dowolnego typu.
W instrukcji if wywo�ywana jest funkcja pierwiastki(). Poniewa funkcja jest typu Boolean, przy powrocie tworzony jest obiekt tymczasowy wywoaniem konstruktora Boolean(int), a nastpnie zostaje wywoana dla tego obiektu funkcja konwersji, jako e wyraenie w instrukcji if musi dawa warto liczbow.
7.5.5. Klasa String
Konwersje typ�w okazuj� si� szczeg�lnie przydatne w operacjach na �a�cuchach znak�w i dlatego poœwi�cimy temu tematowi osobny podrozdzia�.
Przypomnijmy, �e podstawowe operacje dla typu char* (obliczanie d�ugoœci �a�cucha, kopiowanie, konkatenacja �a�cuch�w, etc.) s� zadeklarowane w pliku nag��wkowym string.h, za kilka operacji konwersji acuch�w na liczby zadeklarowano w stdlib.h. Operacje te s zapoyczone z ANSI C. Zadeklarowane w pliku string.h bardzo uyteczne funkcje operuj na zakoczonych znakiem '\0' acuchach znak�w jzyka C. Korzystanie z nich bywa jednak do uciliwe, szczeg�lnie w odniesieniu do zarzdzania pamici. Wemy dla przykadu funkcj, kt�ra bierze dwa acuchy jako argumenty i scala je w jeden, pozostawiajc spacj pomidzy acuchami wejciowymi:
char* SPkonkat(const char* wyraz1, const char* wyraz2)
{
unsigned int rozmiar=strlen(wyraz1)+strlen(wyraz2)+1;
char* wynik = new char[rozmiar];
return
strcat(strcat(strcpy(wynik,wyraz1), ),wyraz2);
}
Pierwsza instrukcja w bloku funkcji oblicza d�ugoœ� wynikowego �a�cucha, uwzgl�dniaj�c rozdzielaj�c� wyrazy spacj� i terminalny znak zerowy ('\0'), a druga alokuje niezb�dn� pami��. Po przydzieleniu pami�ci trzecia instrukcja wykorzystuje dwie funkcje biblioteczne ( ze string.h): najpierw kopiuje acuch wyraz1 do zmiennej wynik, docza rozdzielajc spacj, i na koniec docza drugi acuch wyraz2. Kada z funkcji bibliotecznych zwraca wskanik do swojego pierwszego argumentu (tj. acucha docelowego), dziki czemu moglimy ustawi w sekwencj wywoania kolejnych funkcji. Funkcja woajca jest “odpowiedzialna” za usunicie wynikowego acucha:
char* wsk = SPkonkat(Jan, Kowalski);
// ...
delete wsk;
Jak wida� z powy�szego przyk�adu, celowym by�oby zdefiniowa� klas�, kt�ra stanowi�aby swego rodzaju “opakowanie” dla istniej�cych funkcji j�zyka C, u�atwiaj�c u�ytkownikowi operowanie na �a�cuchach znak�w za pomoc� kilku prostych operator�w.
Termin “opakowanie” odpowiada prawie dok�adnie temu, co okreœliliœmy wczeœniej jako hermetyzacj� albo ukrywanie informacji. Tutaj nasz� intencj� jest taka abstrakcja danych, aby szczeg�y reprezentacji �a�cuch�w j�zyka C, tj. typu char*, zosta�y ukryte przed u�ytkownikiem. Zamiast nich u�ytkownik powinien mie� do dyspozycji dobrze zaprojektowany interfejs (cz�œ� publiczn�) do klasy, kt�rej obiekty zachowywa�yby si� podobnie, jak �a�cuchy j�zyka C.
Podane ni�ej deklaracje, definicje i komentarze mo�na traktowa� jako prost� implementacj� takiej klasy.
Przyk�ad 6.27.
class String {
public:
//Publiczny interfejs uzytkownika:
//Redefinicja + dla konkatenacji - trzy przypadki:
String operator+(const String&) const;
friend String operator+(const char*, const String&);
friend String operator+(const String&, const char*);
int length() const; // Length of string in chars
String();
String(const char*);
String(const String&);
~String();
String& operator=(const String&);
operator const char*() const;
friend ostream& operator<<(ostream&, const String&);
char& operator [] (int);
private:
char* dane;
};
Dyskusja. Pierwsze spostrze�enie: zmienna char* dane, reprezentujca acuch znak�w jzyka C, jest ukryta w czci prywatnej klasy String i jest dostpna jedynie dla jej funkcji skadowych i operator�w.
Klasa ma trzy konstruktory.
Konstruktor domyœlny String::String();
String::String()
{ dane = new char[1]; dane[0] = '\0'; }
kt�ry tworzy �a�cuch pusty. Konstruktor ten b�dzie wo�any np. przy tworzeniu wektora obiekt�w klasy String: String wektor[10];
Konstruktor String::String(const char*);
String::String(const char* st) {
int rozmiar = ::strlen(st) + 1;
dane = new char[rozmiar];
::strcpy(dane, s); }
kt�ry dokonuje wspomnianej uprzednio konwersji acucha st w obiekt klasy String. W tej i w nastpnych definicjach bdziemy uywa unarnego operatora zasigu “::” przy odwoaniach do zmiennych i funkcji globalnych (z pliku string.h), aby unikn konfliktu nazw. Rozmiar tworzonego dynamicznie podobiektu dane jest o 1 wikszy od dugoci acucha st, aby zmieci terminalny znak '\0'.
Konstruktor kopiuj�cy String::String(const String&);
String::String(const String& st); {
dane = new char[st.length() + 1];
// kopiuj stare dane na nowe
::strcpy(dane, st.dane); }
kt�ry jest wywo�ywany przy przesy�aniu parametr�w przez wartoœ� i niekiedy przy powrocie z funkcji.
Destruktor:
String::~String() { delete [] dane; }
Jego zadaniem jest zwolnienie wszystkich zasob�w, kt�re pozyska� obiekt dzi�ki wykonaniu konstruktor�w lub funkcji sk�adowych.
Operatorowa funkcja przypisania:
String& String::operator=(const String& st)
{
if(dane != st.dane) {
delete [] dane;
int rozmiar = st.length() + 1;
dane = new char[rozmiar];
::strcpy(dane, st.dane); }
return *this; // referencja do obiektu
}
Funkcja zwraca referencj� do klasy String, a wic nie jest tworzony aden obiekt tymczasowy. W bloku funkcji najpierw sprawdza si, czy nie jest to pr�ba przypisania obiektu do samego siebie; jeeli nie, to usuwa si stare dane. Kolejna instrukcja tworzy obiekt w pamici swobodnej, a nastpna kopiuje zawarto pola st.dane argumentu funkcji do nowego obiektu dane. Wynik operacji, *this, jest referencj do klasy String.
Funkcja konwersji z typu String do typu char*:
String::operator const char*() const { return dane; }
pozwala u�ywa� obiekty klasy String w tych samych kontekstach, co zwyke acuchy znak�w. Zwr�my uwag na dwukrotne wystpienie modyfikatora const. Pierwszy z lewej ustala, e zawarto obszaru pamici wskazywanego przez warto do kt�rej nastpuje konwersja (typu char*) nie moe by zmieniona przez adn operacj zewntrzn w stosunku do klasy. Drugi const oznacza, e zdefiniowana wyej funkcja skadowa operator const char*() const nie zmienia stanu obiektu klasy String, na kt�rym operuje.
Definicje przeci��onych operator�w '<<' i '[]' oraz funkcji przekazuj�cej d�ugoœ� �a�cucha s� typowe i nie wymagaj� komentarzy:
ostream& operator<<(ostream& os, const String& cs)
{ return os << cs.dane; }
char& String::operator [] (int indeks)
{ return dane[indeks]; }
int String::length() const
{ return ::strlen(dane); }
Natomiast szerszego komentarza wymagaj� operatory konkatenacji.
Operator konkatenacji dla klasy
String String::operator+(const String& st) const
{
char* buf = new char[st.length() + length() + 1];
::strcpy(buf, dane);
::strcat(buf, st.dane);
String retval(buf); //Obiekt tymczasowy
delete [] buf;
return retval;
}
Pierwsza instrukcja alokuje pami�� na wynikowy �a�cuch; druga kopiuje dane do tego �a�cucha, a trzecia scala te dane z danymi przekazanymi przez argument st. Nastpnie ze zmiennej buf jest tworzony nowy, lokalny obiekt retval klasy String, usuwany ju niepotrzebny buf, a przy powrocie funkcja przekazuje kopi retval, tworzon przez konstruktor kopiujcy. Korzysta si z tej definicji w nastpujcy spos�b: jeeli s1 i s2 s obiektami klasy String, to moemy je “doda” do siebie, piszc: s1 + s2; lub s1.operator+(s2);
ZaprzyjaŸnione operatory konkatenacji
W klasie String zadeklarowano dwa operatory konkatenacji, aby byo moliwe “dodawanie” obiekt�w klasy String do zwykych acuch�w znak�w, z obiektami klasy String zar�wno po prawej, jak i po lewej stronie operatora “+”.
String operator+(const char* sc, const String& st)
{
String retval;
retval.dane = new char[::strlen(sc) + st.length()];
::strcpy(retval.dane, sc);
::strcat(retval.dane, st.dane);
return retval;
}
String operator+(const String& st, const char* sc)
{
String retval;
retval.dane = new char[::strlen(sc) + st.length()];
::strcpy(retval.dane, st.dane);
::strcat(retval.dane, sc);
return retval;
}
Jak ju� wspomniano, musimy mie� dwie funkcje operatorowe dla zapewnienia symetrii. Dzi�ki tym definicjom mog� zosta� wykonane instrukcje:
String s1;
abcd + s1;
s1 + "abcd";
Symetri� mo�na te� zapewni� dla obiekt�w klasy, definiuj�c dodatkowy konstruktor dwuargumentowy:
String::String(const String& st1, const String& st2):
dane(strcat(strcpy(new char[st1.strlen() + st2.strlen()+1],st1.dane),st2.dane)) { }
i redefiniuj�c funkcj� sk�adow� operator+():
String operator+(const String& st1, const String& st2)
{ return String( st1, st2); }
Konstruktor bierze dwa obiekty klasy String, alokuje pami dla poczonego acucha, kopiuje pierwszy argument do przydzielonego obszaru pamici i na koniec dokonuje konkatenacji drugiego argumentu z poprzednim wynikiem. Taki specjalizowany konstruktor bywa nazywany konstruktorem operatorowym. Definiuje si go jedynie w celu implementacji danego operatora. Symetryczny operator “+” po prostu wywo�uje ten konstruktor dla wykonania konkatenacji swoich argument�w.
Podsumowanie. Zaprezentowana wy�ej klasa String nie moe pretendowa do przyjcia jej jako standardowej klasy bibliotecznej dla acuch�w jzyka C++, poniewa przyjlimy zbyt wiele zaoe upraszczajcych, np. nie sprawdzalimy powodzenia alokacji dynamicznej, etc. Pominlimy r�wnie wiele moliwych do wprowadzenia uytecznych funkcji, np. operatory por�wnania acuch�w, kopiowania fragment�w acuch�w, wyszukiwania znak�w i cig�w znak�w w acuchach, etc. Tym niemniej struktura tej klasy powinna uatwi uytkownikowi zrozumienie podobnych konstrukcji bibliotecznych.
W charakterze wst�pnego treningu mo�na sprawdzi� dzia�anie podanego ni�ej programu, lokuj�c przedtem deklaracj� klasy String wraz z definicjami funkcji skadowych i zaprzyjanionych w pliku nag�wkowym wpstring.h (lub w dw�ch plikach: w jednym, wpstring.h, deklaracj klasy, a w drugim, np. wpstring.cc albo wpstring.cpp, definicje funkcji).
#include <iostream.h>
#include wpstring.h
int main() {
String s1(ABC);
String s2 = s1;
String s3;// pusty
s3 = s2;
String s4;//pusty
s4 = s2 + s3;
// lub s4 = s2.operator+(s3);
"DEF" + s1;
s1 + ghi;
cout << s1[1] << endl;
cout << s1 + ghi << endl;
return 0;
}
Wydruk ma posta�:
B
ABCghi
7.6. Tablice obiekt�w
Kilkakrotnie ju� zwracaliœmy uwag� na fakt, �e obiekty klas s� zmiennymi typ�w definiowanych przez u�ytkownika i maj� analogiczne w�asnoœci, jak zmienne typ�w wbudowanych. Tak wi�c nic nie stoi na przeszkodzie, aby umieszcza� obiekty w tablicy. Deklaracja tablicy obiekt�w jest w pe�ni analogiczna do deklaracji tablicy zmiennych innych typ�w. Tak�e spos�b dost�pu do element�w takiej tablicy niczym si� nie r�ni od poznanych ju� zasad. Jedyn� istotn� cech� wyr�niaj�c� tablic� obiekt�w od innych tablic jest spos�b ich inicjowania. Tablic� obiekt�w danej klasy inicjuje si� przez jawne, b�dŸ niejawne wywo�ywanie konstruktora klasy tyle razy, ile element�w zawiera tablica. Je�eli w klasie zdefiniowano konstruktory, to mo�na je u�y� w wywo�aniu jawnym w taki sam spos�b, jak dla indywidualnych obiekt�w; parametry aktualne wywo�a� konstruktora b�d� wartoœciami inicjalnymi dla zmiennych sk�adowych ka�dego obiektu tablicy. Dla konstruktor�w domyœlnych, tj. z pustym wykazem argument�w, wartoœci inicjalne zmiennych sk�adowych b�d� zale�ne od tego, czy w bloku konstruktora podano wartoœci domyœlne: je�eli tak, to wartoœci inicjalne b�d� r�wne wartoœciom domyœlnym; je�eli nie, to b�d� przypadkowe.
Innym wygodnym sposobem jest zainicjowanie ka�dego elementu tablicy wczeœniej utworzonym obiektem, lub przypisanie ka�demu elementowi tablicy wczeœniej utworzonego obiektu. W pierwszym przypadku wywo�ywany jest (niejawnie) konstruktor kopiuj�cy generowany przez kompilator lub (jawnie) konstruktor kopiuj�cy, zdefiniowany w klasie. W drugim przypadku b�dzie to generowany lub zdefiniowany operator przypisania. Ponadto jednowymiarowe tablice obiekt�w, kt�rych konstruktor zawiera tylko jeden parametr, mo�na inicjowa� dok�adnie tak samo, jak tablice, kt�rych elementami s� zmienne typ�w podstawowych, podaj�c wartoœci inicjalne w nawiasach klamrowych.
Dla wprowadzenia w zagadnienie pos�u�ymy si� obiektami wielokrotnie ju� eksploatowanej klasy Punkt.
Przyk�ad 6.28.
#include <iostream.h>
class Punkt {
public:
Punkt() {}
Punkt(int a): x(a) {}
int fx() { return x; }
private:
int x;
};
int main() {
Punkt p1[] = { Punkt(10), Punkt(20), Punkt(30) };
for (int i = 0; i < 3; i++)
cout << p1[ << i << ].x =
<< p1[i].fx() << '\t';
cout << '\n';
Punkt p2[] = { 15, 25, 35 };
Punkt p3[] = { Punkt(), Punkt(), Punkt() };
for (i = 0; i < 3; i++)
cout << p3[ << i << ].x =
<< p3[i].fx() << '\t';
cout << '\n';
Punkt p4[] = { Punkt(), Punkt(40), Punkt() };
for (i = 0; i < 3; i++)
cout << p4[ << i << ].x =
<< p4[i].fx() << '\t';
cout << '\n';
return 0;
}
Dyskusja. Klasa Punkt ma dwa konstruktory: bezparametrowy konstruktor domylny Punkt() oraz konstruktor z jednym parametrem Punkt(int a). Pierwsza instrukcja deklaracji tworzy tablic zoon z trzech obiekt�w, wywoujc trzykrotnie konstruktor z parametrem. W instrukcji:
Punkt p2[] = { 15, 25, 35 };
mamy uproszczon� posta� zapisu wywo�a� konstruktora Punkt(15), Punkt(25) i Punkt(35). Instrukcja, tworzca tablic p3[] wywouje trzykrotnie konstruktor domylny, za instrukcja definiujca tablic p4[] wywouje dwukrotnie konstruktor domylny i jeden raz konstruktor z parametrem. Poniewa wszystkie tablice byy inicjowane dan liczb obiekt�w, mona byo opuci w deklaracji wymiary tablic.
Wydruk z przedstawionego programu mo�e mie� posta�:
p1[0].x = 10 p1[1].x = 20 p1[2].x = 30
p3[0].x = 1160 p3[1].x = -14 p3[2].x = 8607
p4[0].x = 12950 p4[1].x = 40 p4[2].x = 0
(U�yto tutaj s�owa “mo�e”, poniewa� wartoœci inicjalne, uzyskiwane przez wywo�anie konstruktora domyœlnego, b�d� przypadkowe).
Przyk�ad 6.29.
#include <iostream.h>
class Punkt {
public:
Punkt(): x(0) {}
Punkt( int a ): x(a) {}
Punkt(const Punkt& p) { x = p.x; }
Punkt& operator=(const Punkt& p)
{ this->x = p.x; return *this; }
int fx() { return x; }
private:
int x;
};
int main() {
Punkt p0(7);
Punkt p1[] = { p0, p0, p0 };
Punkt p2[3];
for (int i = 0; i < 3; i++)
p2[i] = p1[i];
return 0;
}
Dyskusja. W programie wykorzystano trzy sposoby inicjowania tablicy obiekt�w. Instrukcja Punkt p1[]={p0,p0,p0}; inicjuje zmienn skadow x kadego obiektu trzyelementowej tablicy p1[] wartoci 7, skopiowan z obiektu p0. Kopiowanie kadego obiektu tablicy odbywa si przez wywoanie konstruktora kopiujcego Punkt(const Punkt& p). Zauwamy, e parametr aktualny (argument) dla konstruktora kopiujcego musi by stay i przekazywany przez referencj (gdyby przyj przekazywanie przez warto, to mielibymy wywoanie konstruktora kopiujcego, kt�ry wanie definiujemy).
Z kolei ka�dy obiekt tablicy p2[] jest inicjowany wartoci zero przez konstruktor domylny Punkt(){x=0;}. Nastpnie w ptli for kolejnym obiektom tablicy p2[] jest przypisywany obiekt p1[i] uprzednio zainicjowanej tablicy p1[]. W tym przypadku za kadym razem jest wywoywany przeciony operator przypisania
Punkt& Punkt::operator=(const Punkt&)
Jak dla ka�dego operatora sk�adowego klasy, pierwszym argumentem funkcji operator=() jest wskanik do obiektu, dla kt�rego jest wywoywana (zamiast x = p.x mona napisa this->x = p.x). Drugi argument jest przekazywany przez referencj i stay, co gwarantuje jego niezmienno. Funkcja operator=() zwraca referencj do obiektu klasy Punkt, wobec czego w instrukcji return wystpuje jawnie nazwa tego obiektu, *this.
Konstruktor kopiuj�cy i przeci��ony operator przypisania zosta�y u�yte w tym przyk�adzie g��wnie dla cel�w dydaktycznych. Gdyby zabrak�o ich definicji, odpowiednie operacje zosta�yby wykonane przez operatory generowane. Faktyczna potrzeba takich definicji pojawia si� dopiero wtedy, gdy obiekt zawiera wskaŸniki do innych obiekt�w. W�wczas kopiowanie sk�adowych, wykonywane przez operatory generowane, przestaje by� w og�lnoœci wystarczaj�ce, poniewa� kopiuje si� wskaŸniki, a nie obiekty, do kt�rych wskaŸniki si� odwo�uj�.
Przyk�ad 6.30.
#include <iostream.h>
class Punkt {
public:
Punkt(): x(0) {}
Punkt( int a ): x(a){}
void ustaw(int b) { x = b; }
int fx() { return x; }
private:
int x;
};
int main() {
int i,j;
Punkt p1[2][3];
for (i = 0; i < 2; i++)
{
for (j = 0; j < 3; j++)
p1[i][j].ustaw(10 + j);
}
for(i = 0; i < 2; i++)
{
for (j = 0; j < 3; j++)
{
cout << p1[ << i << ][ << j << ].x = ;
cout << p1[i][j].fx() << '\n';
}
}
cout << endl;
return 0;
}
Dyskusja. W programie zadeklarowano tablic� p[2][3] o dw�ch wierszach i trzech kolumnach. Instrukcja deklaracji: Punkt p1[2][3]; wywouje szeciokrotnie konstruktor domylny Punkt() { x = 0; }, kt�ry ustawia zmienn skadow x na warto zero. Tworzona w ten spos�b tablica jest inicjowana wierszami. W pierwszej ptli for dla kadego obiektu tablicy jest wywoywana funkcja skadowa Punkt::ustaw(), kt�ra ustawia zmienne Punkt::x w kolejnych obiektach na wartoci 10+j. Wydruk z programu ma posta:
p[0][0].x = 10
p[0][1].x = 11
p[0][2].x = 12
p[1][0].x = 10
p[1][1].x = 11
p[1][2].x = 12
Zauwa�my, �e i w tym przypadku moglibyœmy ka�demu obiektowi tablicy p1[2][3] przypisa wczeniej utworzony obiekt. Np. tworzc obiekt p0(7) instrukcj deklaracji Punkt p0(7); tj. wywoujc konstruktor z parametrem, moemy zamiast instrukcji
p1[i][j].ustaw(10 + j);
u�y� w p�tli for instrukcj
p1[i][j] = p0;
W tym przypadku dla ka�dego i,j b�dzie wo�any generowany przez kompilator operator przypisania dla obiekt�w klasy Punkt.
Innym mo�liwym wariantem omawianej instrukcji przypisania mog�aby by� instrukcja:
p1[i][j] = p0(i);
W tym przypadku dla ka�dego obiektu tablicy p1[][] bd wykonywane kolejno dwie operacje:
Wywo�anie konstruktora Punkt::Punkt( int i ) { x = i; }
Wywo�anie generowanego przez kompilator domyœlnego operatora przypisania Punkt& Punkt::operator=(const Punkt&).
Operator przypisania, podobnie jak zdefiniowany w poprzednim przyk�adzie, przypisuje sk�adowej x obiektu p[i][j] warto skadowej x obiektu p0.
Przyk�ad 6.31.
// Alokacja dynamiczna - 1
#include <iostream.h>
class Punkt {
public:
Punkt() {};
void ustaw(int c, int d)
{ x = c; y = d; }
int fx() { return x; }
int fy() { return y; }
private:
int x,y;
};
int main() {
Punkt* wsk;
wsk = new Punkt [3];
if (!wsk)
{
cerr << Nieudana alokacja\n;
return 1;
}
for (int i = 0; i < 3; i++)
wsk[i].ustaw(i,i);
delete [] wsk;
return 0;
}
Dyskusja. Program tworzy tablic� trzech obiekt�w typu Punkt w pamici swobodnej. Poniewa dla tablicy dynamicznej nie mona poda wartoci inicjujcych w jej deklaracji, w klasie Punkt zdefiniowano tylko konstruktor domylny. Inicjowanie tablicy odbywa si w ptli for, za pomoc funkcji skadowej Punkt::ustaw(). Poniewa w klasie Punkt nie zdefiniowano destruktora, zastosowano skadni operatora delete bez symbolu “[]”.
Przyk�ad 6.32.
// Alokacja dynamiczna - 2
#include <iostream.h>
class Punkt {
public:
Punkt(): x(0),y(0) {};
~Punkt() { cout << Destrukcja...\n; }
int fx() { return x; }
int fy() { return y; }
private:
int x,y;
};
int main() {
Punkt* wsk;
wsk = new Punkt [3];
if (!wsk)
{
cerr << Nieudana alokacja\n;
return 1;
}
for (int i = 0; i < 3; i++)
cout << wsk[i].fx() << '\t'
<< wsk[i].fy() << endl;
delete [] wsk;
return 0;
}
Dyskusja. W tym przyk�adzie klasa Punkt zawiera definicj destruktora; teraz odzyskiwanie pamici przydzielonej dla wsk jest nastpuje przez wywoanie destruktora dla kadego obiektu skadowego tablicy. Ilustruje to wydruk z programu:
0 0
0 0
0 0
Destrukcja...
Destrukcja...
Destrukcja...
7.7. Wskaniki do element�w klasy
Poznane dot�d konwencje notacyjne nie dawa�y nam mo�liwoœci wyra�enia w spos�b jawny wskaŸnika do elementu sk�adowego klasy. Dla zmiennej sk�adowej klasy mo�liwoœ� tak� stwarza deklaracja o postaci:
klasa::*wsk = &klasa::zmienna;
gdzie: klasa jest nazw klasy, w kt�rej jest zadeklarowana zmienna o nazwie zmienna, za wsk jest wskanikiem do tej zmiennej. Wskanik jest inicjowany wyraeniem z prawej strony operatora “=”.
Dla funkcji sk�adowej klasy stosuje si� nast�puj�c� posta� deklaracji wskaŸnika:
typ (klasa::*wskf)(arg) = &klasa::funkcja;
gdzie: typ jest typem wartoci obliczonej przez funkcj funkcja, wskf jest wskanikiem do tej funkcji, za arg jest wykazem argument�w (sygnatur), z opcjonalnymi identyfikatorami. Wskanik jest inicjowany wyraeniem z prawej strony operatora “=”.
Zauwa�my, �e w powy�szych deklaracjach u�ywa si� binarnego operatora “::*”, kt�ry informuje kompilator, i� wskaŸniki wsk i wskf maj zasig klasy. Wskaniki s inicjowane adresami wskazywanych skadowych klasy. Przypomnijmy, e dla dostpu bezporedniego do element�w klasy uywalimy operatora “::”.
Inicjowanie wskaŸnik�w adresami element�w sk�adowych bezpoœrednio w ich deklaracji nie jest, rzecz jasna, obowi�zkowe. W instrukcji deklaracji wskaŸnik mo�na zainicjowa� zerem (0 lub NULL), lub te� mo�na mu przypisa� odpowiedni adres w instrukcji przypisania. Tak wi�c mo�emy np. zadeklarowa�:
klasa::*wsk1 = 0; // lub NULL
klasa::*wsk2 = &klasa::zmienna;
wsk1 = wsk2;
WskaŸniki do zmiennych sk�adowych klasy okaza�y si� u�yteczn� metod� wyra�ania “topografii” klasy, tj. wzgl�dnego po�o�enia element�w w klasie w spos�b niezale�ny od implementacji. Podany ni�ej przyk�ad ilustruje deklaracj� wskaŸnika oraz spos�b dost�pu do zmiennej sk�adowej x klasy Punkt.
Przyk�ad 6.33.
#include <iostream.h>
class Punkt {
public:
int x;
};
int Punkt::*wskx = &Punkt::x;
int main() {
Punkt punkt1;
punkt1.*wskx = 10;
cout << punkt1.x << endl;
return 0;
}
Dyskusja. Zauwa�my, �e odwo�anie do zmiennej sk�adowej Punkt::x jest moliwe tylko poprzez wystpienie tej klasy, tj. obiekt punkt1. Posta odwoania poredniego (.*) r�ni si od bezporedniego (.) dodaniem symbolu “*”, za typem wskanika wskx jest Punkt::*. Zwr�my te uwag na fakt, e zmienna skadowa x jest publiczna w klasie. Gdyby zmienna skadowa bya prywatna, to pr�ba dostpu do adresu tej skadowej byaby zasygnalizowana przez kompilator jako bd. W takim przypadku naleaoby rozszerzy deklaracj klasy o odpowiedni funkcj zaprzyjanion, doda w klasie Punkt definicj funkcji, przekazujcej warto x:
friend int f(Punkt& p)
{
int Punkt::*wskx = &Punkt::x;
p.*wskx = 10;
return p.x;
}
i wywo�a� j� dla obiektu punkt1, np w instrukcji:
cout << f(punkt1);
Przy deklarowaniu wskaŸnika do statycznej zmiennej (a tak�e funkcji) sk�adowej klasy nie mo�na mu nada� typu klasa::*, jak dla skadowej niestatycznej, poniewa kompilator nie przydziela pamici dla skadowej statycznej w adnym obiekcie danej klasy. Tak wic w tym przypadku mamy zwyky wskanik.
Przyk�ad 6.34.
#include <iostream.h>
class Punkt {
public:
static int x;
};
int Punkt::x = 10;
int* wskx = &Punkt::x;
int main() {
cout << *wskx << endl;
return 0;
}
Dyskusja. Zgodnie z obowi�zuj�cymi zasadami sk�adowa statyczna x zostaa zainicjowana poza blokiem klasy Punkt wartoci 10. W deklaracji wskanika do tej skadowej, wskx, nie wystpi operator “::*”, lecz tylko “*”. Poniewa x jest samodzielnym obiektem typu int, zatem wskx jest typu int*, a odwoanie do wartoci zmiennej x ma posta *wskx.
Znacznie wi�cej korzyœci daje zadeklarowanie wskaŸnika do funkcji sk�adowej klasy. Jest to jeden ze sposob�w wprowadzenia zmiany zachowania si� funkcji w fazie wykonania. WskaŸnik do niestatycznej funkcji sk�adowej mo�emy wprowadzi� w deklaracji klasy i u�ywa� go do wywo�a� funkcji po uprzednim w�aœciwym zainicjowaniu. W deklaracji wskaŸnika nale�y poda� zar�wno typ klasy zawieraj�cej wskazywan� funkcj� sk�adow�, jak i sygnatur� tej funkcji. Podanie tych informacji jest wymagane przez kompilator, jako �e C++ jest j�zykiem o silnej typizacji i sprawdza nawet wskaŸniki do funkcji.
Maj�c zadeklarowany wskaŸnik mo�emy mu przypisywa� adresy innych funkcji sk�adowych pod warunkiem, �e funkcje te maj� t� sam� liczb� i typy parametr�w oraz typ obliczanej wartoœci. Podany ni�ej przyk�ad ilustruje u�ywan� w tym przypadku notacj�.
Przyk�ad 6.35.
#include <iostream.h>
class Firma {
public:
char* nazwa;
Firma(char* z): nazwa(z) {}; // Konstruktor
~Firma() {}; // Destruktor
void podaj(int x)
{
cout << nazwa << << x << '\n';
}
};
typedef void(Firma::*WSKFI) (int);
int main() {
Firma frm1(firma1);
Firma* wsk = &frm1;
WSKFI wskf = &Firma::podaj;
frm1.podaj(1);
(frm1.*wskf)(2);
(wsk->*wskf)(3);
return 0;
}
Wydruk z programu ma posta�:
firma1 1
firma1 2
firma1 3
Dyskusja. Deklaracja typedef void(Firma::*WSKFI) (int);
wprowadza nazw� WSKFI dla wskanika do funkcji typu void o zasigu klasy Firma i jednym argumencie typu int. Identyfikator WSKFI uywa si nastpnie dla zadeklarowania wskanika wskf, zainicjowanego adresem funkcji skadowej podaj() klasy Firma. W programie pokazano trzy sposoby drukowania zawartoci pola Firma::nazwa obiektu frm1:
instrukcja frm1.podaj(1);
korzysta z operatora “.” dost�pu bezpoœredniego;
instrukcja (frm1.*wskf)(2);
korzysta z operatora “.*” dost�pu poœredniego;
instrukcja (wsk->*wskf)(3);
korzysta z operatora “->*”, kt�ry jest operatorem dost�pu poœredniego dla wskaŸnika wsk do obiektu frm1.
Operatory .* i ->* wi��� wskaŸniki z konkretnym obiektem, daj�c w wyniku funkcj�, kt�ra mo�e by� u�yta w wywo�aniu. Binarny operator .* wi��e sw�j prawy operand, kt�ry musi by� typu "wskaŸnik do sk�adowej klasy T" z lewym operandem, kt�ry musi by� typu "klasy T". Wynikiem jest funkcja typu okreœlonego przez drugi operand. Analogicznie ->*. Priorytet () jest wy�szy ni� .* i ->*, tak �e nawiasy s� konieczne. Gdyby pomin�� nawiasy, to np. wyra�enie:
frm1.*wskf(2);
by�oby potraktowane jako
frm1.*(wskf(2))
czyli jako wartoœ� sk�adowej obiektu frm1, na kt�r wskazuje warto zwracana przez zwyk funkcj wskf().
Uwaga. Wartoœciowanie wyra�enia z operatorem '.*' lub '->*' daje l-wartoœ�, je�eli prawy operand jest l-wartoœci�.
Kolejny przyk�ad mo�na potraktowa� jako fragment oprogramowania systemu nadzorowania obiektu, w kt�rym sygna� przychodz�cy do miejsca oznaczonego jako przycisk wyzwala odpowiedni reakcj systemu. Sygna 0 oznacza stan normalny, sygna 1 ewentualne zagroenie (alert), za sygna 2 alarm. Stany te mog by wywietlane na ekranie (jak w programie), lub powodowa inn reakcj (np. odpowiednie sygnay dwikowe).
Przyk�ad 6.36.
#include <iostream.h>
class Ochrona {
public:
char* nazwa;
Ochrona(char* z): nazwa(z) {}; // Konstruktor
~Ochrona() {}; // Destruktor
void (Ochrona::*przycisk) (int j); // Wskaznik
void kontrola(int);
void norma(int x)
{ cout << STAN NORMALNY << endl; }
void alert(int y) { cout << POGOTOWIE << endl; }
void alarm(int z) { cout << ALARM!!! << endl; }
};
void Ochrona::kontrola(int w)
{
switch (w) {
case 0: przycisk = &Ochrona::norma; norma(w);
break;
case 1: przycisk = &Ochrona::alert; alert(w);
break;
case 2: przycisk = &Ochrona::alarm; alarm(w);
break;
}
}
int main() {
Ochrona ob(System1);
typedef void (Ochrona::*WSKFI) (int);
WSKFI wskf = &Ochrona::kontrola;
int ii;
cin >> ii;
(ob.*wskf)(ii);
Ochrona* wsk = &ob;
(wsk->*wskf)(ii);
(ob.*(ob.przycisk))(ii);
return 0;
}
Przyk�adowy wydruk z programu dla ii==1, ma posta�:
ALARM!!!
ALARM!!!
ALARM!!!
Dyskusja. Podobnie jak w poprzednim przyk�adzie u�yto deklaracji typedef dla �atwiejszego odwo�ywania si� do wskaŸnik�w funkcji. W klasie Ochrona zadeklarowano “funkcj skadow” przycisk, kt�ra posuya w ostatniej przed return instrukcji programu do sprawdzenia, jaki sygna zosta przekazany do systemu nadzorowania. Termin “funkcja skadowa” ujlimy w znaki cudzysowu, poniewa przycisk nie jest funkcj, lecz wskanikiem funkcji, ustawianym w bloku funkcji kontrola na adres funkcji norma(), alert(), lub alarm(). W instrukcji switch jest podejmowana decyzja o tym, kt�r z funkcji skadowych naley wywoa dla zadanego parametru aktualnego ii. Wykonanie programu przebiega nastpujco:
Pierwsza instrukcja w bloku main() woa konstruktor Ochrona(char*) i tworzy obiekt ob. W instrukcji WSKFI wskf = &Ochrona::kontrola; wskanik wskf jest inicjowany na adres funkcji skadowej kontrola(). Po wczytaniu wartoci ii (np. 2), wykonywana jest instrukcja (ob.*wskf)(ii);, tzn. przez wskanik wskf zostaje wywoana funkcja skadowa kontrola(2). W funkcji kontrola() wskanikowi przycisk zostaje przypisany adres funkcji skadowej alarm(), po czym zostaje wywoana funkcja alarm() z parametrem aktualnym 2. Po wykonaniu funkcji alarm(), a nastpnie instrukcji break; program opuszcza blok funkcji kontrola(). W programie zadeklarowano r�wnie wskanik do obiektu klasy Ochrona i zainicjowano go adresem obiektu ob. Posuyo to do wywoania funkcji kontrola() z instrukcji (wsk->*wskf)(ii), w kt�rej wskanik wsk odwouje si do wskanika wskf, a ten wywouje funkcj kontrola(2) i dalej proces biegnie jak poprzednio. Ostatnia instrukcja, (ob.*(ob.przycisk))(ii) odwouje si bezporednio do funkcji alarm(), poniewa wskanik przycisk zosta ju wczeniej ustawiony na adres tej funkcji. Program koczy wykonanie wywoaniem destruktora ~Ochrona().
7.8. Klasy w programach wieloplikowych
Przyjtym standardem dla program�w wieloplikowych jest umieszczanie deklaracji klas w pliku (plikach) nag�wkowym. Definicje funkcji skadowych oraz definicje inicjujce zmienne statyczne klas s umieszczane w pliku (plikach) r�dowym. Moliwa jest wtedy oddzielna kompilacja plik�w r�dowych, kt�ra oszczdza czas, poniewa w przypadku zmian w programie powt�rna kompilacja jest wymagana tylko dla plik�w, kt�re zostay zmienione. Ponadto wiele implementacji zawiera program o nazwie make, kt�ry zarzdza ca kompilacj i konsolidacj dla projektu programistycznego; program make rekompiluje tylko te pliki, kt�re zostay zmienione od czasu ostatniej kompilacji. Zauwamy te, e rozdzielna kompilacja zachca programist�w do tworzenia og�lnie uytecznych plik�w tymczasowych (pliki z rozszerzeniem nazwy .o pod systemem Unix, lub .obj pod systemem MS-DOS) i bibliotek, kt�re mog wielokrotnie wykorzystywa w swoich wasnych programach i dzieli je z innymi uytkownikami.
Podany niej przykad ilustruje wykorzystanie w programie wieloplikowym tzw. bezpiecznych tablic. Przymiotnika “bezpieczny” uywamy tutaj nie bez powodu. Dotychczas pomijalimy milczeniem fakt, e jzyk C++ nie ma wbudowanego mechanizmu kontroli zakresu tablic. Wskutek tego jest moliwe np. wpisywanie wartoci do element�w tablicy poza zakresem wczeniej zadeklarowanych indeks�w. Ten niepodany efekt odnosi si zar�wno do przekroczenia zakresu od dou (ang. underflow), jak i od g�ry (ang. overflow), i to w r�wnym stopniu do tablic alokowanych w pamici statycznej, na stosie funkcji, czy te w przypadku alokacji dynamicznej w pamici swobodnej. I tutaj, jak w wielu innych przypadkach, przychodzi nam z pomoc mechanizm klas. W podanym niej przykadzie zdefiniowano trzy klasy tablic z elementami typu int, double oraz char*. Wszystkie trzy klasy zawieraj dwa rodzaje funkcji skadowych pierwsza, wstaw(), suy do zapisywania informacji w tablicy, za druga, pobierz(), suy do wyszukiwania informacji w tablicy. Te dwie funkcje s w stanie sprawdza w fazie wykonania programu, czy zakresy tablicy nie zostay przekroczone.
Przykad 6.37.
// Plik TABLICA.H pod DOS, tablica.h pod Unix
// Bezpieczna tablica: elementy typu int
class tabint {
public:
tabint(int liczba);
int& wstaw(int i);
int pobierz(int i);
private:
int rozmiar, *wsk;
};
// Bezpieczna tablica: elementy typu double
class tabdouble {
double* wsk;
public:
tabdouble(int liczba);
double& wstaw(int i);
double pobierz(int i);
private:
int rozmiar;
};
// Bezpieczna tablica: elementy typu char
class tabchar {
public:
tabchar(int liczba);
char& wstaw(int i);
char pobierz(int i);
private:
int rozmiar;
char* wsk;
};
// Plik Tablica.CPP pod DOS, tablica.c pod Unix
// Definicje funkcji klas tabint, tabdouble, tabchar
#include tablica.h
#include <iostream.h>
#include <stdlib.h>
// Funkcje klasy tabint
// Konstruktor
tabint::tabint(int liczba) {
wsk = new int [liczba];
if (!wsk) {
cout << Brak alokacji\n;
exit (1); }
rozmiar = liczba; }
// Wstaw element do tablicy.
int& tabint:: wstaw (int i) {
if(i < 0 || i >= rozmiar) {
cout << Przekroczenie zakresu!!!\n;
exit (1);
}
return wsk[i]; // zwraca referencje do wsk[i]
}
// Pobierz element z tablicy.
int tabint::pobierz(int i) {
if (i < 0 || i > rozmiar)
{
cout << Przekroczenie zakresu!!!\n;
exit(1);
}
return wsk[i]; // zwraca element
}
// Funkcje klasy tabdouble
tabdouble::tabdouble(int liczba)
{
wsk = new double [liczba];
if (!wsk)
{
cout << Brak alokacji\n;
exit (1);
}
rozmiar = liczba;
}
double& tabdouble:: wstaw (int i)
{
if(i < 0 || i >= rozmiar)
{
cout << Przekroczenie zakresu!!!\n;
exit (1);
}
return wsk[i]; // zwraca referencje do wsk[i]
}
double tabdouble::pobierz(int i)
{
if (i < 0 || i > rozmiar)
{
cout << Przekroczenie zakresu!!!\n;
exit(1);
}
return wsk[i]; // zwraca element
}
// Funkcje klasy tabchar
tabchar::tabchar(int liczba) {
wsk = new char [liczba];
if (!wsk) {
cout << Brak alokacji\n;
exit (1); }
rozmiar = liczba;
}
char& tabchar:: wstaw (int i) {
if(i < 0 || i >= rozmiar) {
cout << Przekroczenie zakresu!!!\n;
exit (1); }
return wsk[i]; // zwraca referencje do wsk[i]
}
char tabchar::pobierz(int i)
{
if (i < 0 || i > rozmiar)
{
cout << "Przekroczenie zakresu!!!\n";
exit(1);
}
return wsk[i]; // zwraca element
}
// Plik MAINTAB.CPP pod DOS, maintab.c pod Unix
#include tablica.h
#include <iostream.h>
int main() {
tabint tab(5);
tab.wstaw(3) = 13;
tab.wstaw(2) = 12;
cout << tab.pobierz(3) << endl
<< tab.pobierz(2) << endl;
// Teraz przekroczenie zakresu
tab.wstaw(6) = '!';
return 0;
}
7.9. Szablony klas i funkcji
Szablony lub wzorce (ang. template), nazywane r�wnie typami parametryzowanymi, pozwalaj definiowa wzorce dla tworzenia klas i funkcji. Dla lepszego zrozumienia podstawowych koncepcji posuymy si analogi z matematyki.
Matematyka operuje czsto pojciem r�wnania parametrycznego, kt�re pozwala generowa jedno- lub wieloparametrowe rodziny krzywych i prostych. W takich r�wnaniach wystpuj og�lne wyraenia matematyczne, kt�rych wartoci s zalene od zmiennych lub staych, nazywanych parametrami. Tak wic parametr mona okreli jako zmienn lub sta, kt�ra wyr�nia przypadki szczeg�lne og�lnego wyraenia. Np. og�lna posta r�wnania prostej
y = mx + b
gdzie m jest sta, pozwala generowa rodzin prostych r�wnolegych o nachyleniu m. Podobnie r�wnanie
(x-a)2+(y-b)2=r2
przy ustalonej wartoci r, suy do generacji rodziny okrg�w o pooeniu rodka zalenym od parametr�w a i b.
Koncepcja klasy nawizuje w pewnym stopniu do tych idei. Klas mona traktowa jako generator rodziny obiekt�w, w kt�rej kademu obiektowi przydziela si niejawny parametr, ustalajcy jego tosamo. Ponadto rodzin obiekt�w mona parametryzowa przez nadawanie r�nych wartoci ich zmiennym skadowym. Mona wtedy wyr�ni dwa charakterystyczne przypadki.
Obiekt jest tworzony za pomoc konstruktora generowanego przez kompilator.
Obiekt jest tworzony za pomoc konstruktora zdefiniowanego przez programist.
W pierwszym przypadku zmiennym skadowym r�nych obiekt�w mona nadawa r�ne wartoci po uprzednim utworzeniu obiekt�w z wartociami domylnymi. W przypadku drugim uytkownik ma wicej moliwoci: moe on np. zdefiniowa konstruktor z pust list argument�w, z argumentami domylnymi, bd definiowa konstruktory przecione.
Wszystko to jednak dzieje si na poziomie jednej rodziny obiekt�w, w ramach jednej definicji klasy. Nasuwa si w zwizku z tym pytanie: czy mona zmieni deklaracj klasy w taki spos�b, aby stworzy sobie moliwo generowania wielu rozcznych rodzin obiekt�w?
Skoro zaczlimy od analogii matematycznej dla schematu klasa-rodzina obiekt�w, spr�bujmy poszuka nastpnej analogii. W geometrii analitycznej rozwaa si m.in. przekroje stoka koowego paszczyznami. W zalenoci od nachylenia paszczyzny tncej otrzymuje si szereg rodzin parametryzowanych krzywych: rodzin okrg�w, rodzin elips, rodzin parabol i rodzin hiperbol.
By moe, i takie wanie analogie nasuway si tw�rcom obowizujcej obecnie wersji 4.0 kompilatora jzyka C++. Wprowadzona w tej wersji deklaracja szablonu ma nastpujc posta og�ln:
template<argumenty> deklaracja
gdzie:
sowo kluczowe (specyfikator) template sygnalizuje kompilatorowi wystpienie deklaracji szablonu,
sowo argumenty oznacza list argument�w (parametr�w) szablonu,
sowo deklaracja oznacza deklaracj klasy lub funkcji.
Lista argument�w szablonu moe si skada z rozdzielonych przecinkami argument�w, przy czym kady argument musi by postaci class nazwa, lub deklaracj argumentu. Wynika std, e nazwy argument�w mog si odnosi zar�wno do klas, jak i do typ�w wbudowanych, bd zdefiniowanych przez uytkownika.
Deklaracja szablonu moe wystpowa jedynie jako deklaracja globalna, a nazwy uywane w szablonie stosuj si do wszystkich regu dostpu i kontroli.
7.9.1. Szablony klas
Deklaracj szablonu mona wykorzystywa do definiowania klas wzorcowych. W takim przypadku po zamykajcym nawiasie ktowym umieszcza si definicj odpowiedniej klasy, np.
template <class T> class stream { /* ... */ };
Wystpujca we wzorcu nazwa klasy stream moe by nastpnie uywana wszdzie tam, gdzie dopuszcza si uywanie nazwy klasy, np. w bloku funkcji moemy zadeklarowa wskaniki do tej klasy:
class stream<char> *firststream, *secondstream;
Przykad 6.38.
#include <iostream.h>
template<class Typ>
class Tablica {
// Klasa parametryzowana typem element�w tablicy
public:
Tablica(int); // Deklaracja konstruktora
~Tablica() { delete [] tab; } // Destruktor
Typ& operator [] (int j) { return tab[j]; }
private:
Typ* tab; // Wskaz do tablicy
int rozmiar;
};
template<class Typ>
Tablica<Typ>::Tablica(int n) // Konstruktor
{ tab = new Typ[n]; rozmiar = n; }
int main() {
// Tablica 10-elementowa. Elementy typu int
Tablica<int> x(10);
for(int i = 0; i < 10; ++i) x[i] = i;
for(i = 0; i < 10; ++i) cout << x[i] << ' ';
cout << endl;
// Tablica 5-elementowa. Elementy typu double
Tablica<double> y(5);
// Tablica 6-elementowa. Elementy typu char
Tablica<char> z(6);
return 0;
}
Dyskusja. Kade wystpienie nazwy klasy Tablica wewntrz deklaracji klasy jest skr�tem zapisu Tablica<Typ>, np. gdyby deklaracj konstruktora zastpi definicj, to miaaby ona posta:
Tablica<int n> {tab = new Typ[n]; rozmiar = n; }
Deklaracja Tablica<int> x(10); wywouje konstruktor z parametrem 10 przekazywanym przez warto. Konstruktor tworzy 10-elementow tablic dynamiczn o elementach typu int. Deklarator tablicy, [], zosta zdefiniowany w treci klasy jako przeciony operator[]. Dziki temu obiekty klasy Tablica<Typ> mona indeksowa tak samo, jak zwyke tablice. Warto r�wnie zwr�ci uwag na zwizo zapisu: deklaracje tablic typu double oraz char korzystaj z tego samego szablonu, co tablica typu int adna z nich nie wymaga uprzedniej definicji.
Gdyby pisanie nazwy klasy wzorcowej z obowizujcymi nawiasami ktowymi okazao si nuce dla uytkownika, mona uy konstrukcji z typedef dla wprowadzenia synonim�w. Mona np. wprowadzi zastpcze nazwy deklaracjami
typedef Tablica<int> tabint;
typedef Tablica<char> tabchar;
i stosowa te nazwy dla tworzenia odpowiednich tablic
tabint x(10);
tabchar z(6);
7.9.2. Szablony funkcji
Szablon funkcji okrela spos�b konstrukcji poszczeg�lnych funkcji. Np. rodzina funkcji sortujcych moe by zadeklarowana w postaci:
template <class Typ> void sort(Tablica<Typ>);
Szablon funkcji wyznacza de facto nieograniczony zbi�r (przecionych) funkcji. Generowan z szablonu funkcj nazywa si funkcj wzorcow. Przy wywoaniach funkcji wzorcowych nie podaje si jawnie argument�w wzorca. Zamiast tego uywa si mechanizmu rozpoznawania (ang. resolution) wywoania. Dopuszcza si przy tym przecianie funkcji wzorcowej przez zwyke funkcje o takiej samej nazwie, lub przez inne funkcje wzorcowe o takiej samej nazwie. W omawianych przypadkach stosowany jest nastpujcy algorytm rozpoznawania.
Sprawd, czy istnieje dokadne dopasowanie wywoania do prototypu funkcji. Jeeli tak, to wywoaj funkcj.
Poszukaj szablonu funkcji, z kt�rego moe by wygenerowana funkcja dokadnie dopasowana do wywoania. Jeeli znajdziesz taki szablon, wywoaj go.
Wypr�buj zwyke metody dopasowania argument�w (promocja,konwersja) dla funkcji przecionych. Jeeli znajdziesz odpowiedni funkcj, wywoaj j.
Taki sam proces rozpoznawania jest stosowany dla wskanik�w do funkcji.
Podany niej elementarny przykad jest ilustracj kroku 2 algorytmu. Jedyn, jak si wydaje, korzyci z zastosowanego tutaj szablonu jest kr�tki kod r�dowy. Gdybymy zastosowali poznany wczeniej mechanizm przeciania funkcji, to byoby konieczne podanie trzech oddzielnych definicji odpowiednio dla typ�w int, double oraz char.
Przykad 6.39.
#include <iostream.h>
// Szablon
template <class T>
T max( T a, T b ) { return a > b ? a : b; }
int main() {
int i = 2, j = max(i,0);
cout << Max integer: << j << endl;
double db = 1.7, dc = max(db,3.14);
cout << Max double: << dc << endl;
char z1 = 'A', z2 = max(z1,'B');
cout << Max char: << z2 << endl;
return 0;
}
Interesujcym przykadem zastosowania szablon�w moe by definicja funkcji, kt�rej zadaniem jest kopiowanie sekwencyjnych struktur danych. Definicj t zastosujemy do kopiowania sekwencji znak�w.
Przykad 6.40.
#include <iostream.h>
template < class We, class Wy >
Wy copy ( We start, We end, Wy cel )
{
while ( start != end )
*cel++=*start++;
return cel;
}
void main () {
char* hello = Hello ;
char* world = world;
char komunikat[15];
char* wsk = komunikat;
wsk = copy ( hello, hello+6, wsk );
wsk = copy ( world, world+5, wsk );
*wsk = '\0';
cout << komunikat << endl;
}
Analiza programu. Pierwsze wywoanie funkcji copy kopiuje warto Hello" do pierwszych szeciu znak�w (zwr�my uwag na spacj po znaku 'o') tablicy komunikat, za druga kopiuje warto "world" do nastpnych piciu znak�w tablicy. Po tych operacjach zmienna p wskazuje na znak (nieokrelony, poniewa tablica komunikat nie zostaa zainicjowana) wystpujcy bezporednio po literze 'd' sowa "world". Nastpnie do wskazania *p przypisujemy znak zerowy '\0', aby otrzyma normaln posta acucha stosowan w jzyku C++. Ostatni instrukcj wstawiamy zmienn komunikat do strumienia cout.
7.10. Klasy zagniedone
Klasa moe by deklarowana wewntrz innej klasy. Jeeli klas B zadeklarujemy wewntrz klasy A, to klas B nazywamy zagniedon w klasie A. Klasa zagniedona jest widoczna tylko w zasigu zwizanym z deklaracj klasy wewntrznej. Miejscem deklaracji klasy zagniedonej moe by cz publiczna, prywatna, lub chroniona klasy otaczajcej. Zauwamy, e zagniedenie jest przykadem asocjacji: klasa wewntrzna jest deklarowana jedynie w tym celu, aby umoliwi wykorzystanie jej zmiennych i funkcji skadowych klasie zewntrznej. Typowym przykadem moe tu by asocjacja grupujca; np. w klasie Komputer moemy zagniedzi klasy Procesor i Pami. Jeeli pami potraktujemy jako tablic jednowymiarow, to wykonanie operacji dodaj liczb a do liczby b bdzie wymaga zadeklarowania obiektu klasy Procesor z funkcjami pobierz() i wykonaj() oraz z licznikiem rozkaz�w, kt�ry bdzie wskazywa kolejne adresy pamici.
Przykad 6.41.
#include <iostream.h>
class Otacza {
public:
class Zawarta {
public:
Zawarta(int i): y(i) {} //Konstruktor
int podajy() { return y; }
private:
int y;
};
Otacza(int j): x(j){} //Konstruktor
int podajx() { return x; }
private:
int x;
};
int main() {
Otacza zewn(7);
int m,n;
m = zewn.podajx();
cout << m << endl;
Otacza::Zawarta wewn(9);
n = wewn.podajy();
cout << n << endl;
return 0;
}
Dyskusja. Instrukcja deklaracji Otacza zewn(7); wywouje konstruktor klasy zewntrznej Otacza(int j){x=j;} z parametrem aktualnym j=7, inicjujc x na warto 7. Instrukcja m=zewn.podajx(); wywouje funkcj skadow podajx() klasy zewntrznej. Obiekt wewn klasy Zawarta jest tworzony instrukcj deklaracji Otacza::Zawarta wewn(9);.
7.11. Struktury i unie jako klasy
W jzyku C++ struktury i unie s w zasadzie “penoprawnymi” klasami. Poniej wyliczono te cechy struktur i unii, kt�re wyr�niaj je w stosunku do klas, deklarowanych ze sowem kluczowym class.
Struktura jest klas, deklarowan ze sowem kluczowym struct; elementy skadowe struktury oraz jej klasy bazowe s domylnie publiczne.
Unia jest klas, deklarowan ze sowem kluczowym union; elementy skadowe unii s domylnie publiczne; unia utrzymuje w danym momencie czasu tylko jedn skadow.
Struktura moe zawiera funkcje skadowe, wcznie z konstruktorami i destruktorami.
Unia nie moe dziedziczy adnej klasy, ani nie moe by klas bazow dla klasy pochodnej.
Unia nie moe zawiera adnej skadowej statycznej, tj. deklarowanej ze sowem kluczowym static.
Unia nie moe zawiera obiektu, kt�ry ma konstruktor lub destruktor. Tym niemniej unia moe mie konstruktor i destruktor.
Niekt�re kompilatory nie akceptuj prywatnych skadowych unii.
Tak wic struktura r�ni si od klasy, deklarowanej ze sowem kluczowym class jedynie tym, e jeeli jej skadowe nie s poprzedzone etykiet private:, to s one publiczne. Oznacza to, e wszystkie zmienne i funkcje skadowe s dostpne za pomoc operatora selekcji '.' lub w przypadku wskanika do obiektu operatora “->”. Wobec” tego deklaracje:
class Punkt { public: int x,y; };
oraz
struct Punkt { int x,y; };
s r�wnowane. Podobnie r�wnowane bd deklaracje:
class Punkt { int x,y; };
oraz
struct Punkt { private: int x,y; };
Pokazany niej przykad jest nieznaczn modyfikacj poprzedniego; w programie wyeliminowano funkcje skadowe podajx() oraz podajy(), poniewa skadowe x oraz y s teraz publiczne, a wic dostpne bezporednio.
Przykad 6.42.
#include <iostream.h>
struct Otacza {
int x;
struct Zawarta
{
int y;
Zawarta(int i): y(i) {} //Konstruktor
};
Otacza(int j): x(j) {} //Konstruktor
};
int main() {
Otacza zewn(7);
int m,n;
cout << zewn.x << endl;
Otacza::Zawarta wewn(9);
cout << wewn.y << endl;
return 0;
}
Chocia struktury maj w zasadzie te same moliwoci co klasy, wikszo programist�w stosuje struktury bez funkcji skadowych. S one wtedy odpowiednikami struktur jzyka C, bd rekord�w, definiowanych w jzykach Pascal, czy Modula-2. Zalet takiego stylu programowania jest wiksza czytelno program�w: tam, gdzie nie jest wymagane ukrywanie informacji, uywa si struktur w przeciwnym przypadku klas.
Pamitamy z wczeniejszego wprowadzenia, e wprawdzie unia moe grupowa skadowe r�nych typ�w, ale tylko jedna ze skadowych moe by “aktywna” w danym momencie. Jest to cecha, istotna w aspekcie ukrywania informacji: uywajc unii moemy tworzy klasy, w kt�rych wszystkie dane wsp�dziel ten sam obszar w pamici. Jest to co, czego nie da si zrobi, uywajc klas, deklarowanych ze sowem kluczowym class.
Przykad 6.43.
#include <iostream.h>
union bity {
bity (int n); //Deklaracja konstruktora
void podajbity();
int d;
unsigned char z[sizeof(int)];
};
bity::bity(int n): d(n) {} //Definicja konstruktora
void bity::podajbity()
{
int i,j;
for (j = sizeof(int)-1; j >= 0; j--)
{
cout << Wzorzec bitowy w bajcie << j << : ;
for (i = 128; i; i >>= 1)
if (i & z[j]) cout << 1;
else cout << 0;
cout << \n;
}
}
int main() {
bity obiekt(255);
obiekt.podajbity();
return 0;
}
Dyskusja. Przykad prezentuje wykorzystanie unii do wywietlenia ukadu bit�w w kolejnych bajtach liczby (255), podanej w wywoaniu konstruktora. Wykonanie programu powinno da wydruk o postaci:
Wzorzec bitowy w bajcie 1: 00000000
Wzorzec bitowy w bajcie 0: 11111111