You are on page 1of 47

Dr hab., inż.

Aleksander Timofiejew

Języki i paradygmaty programowania.


Praktyczne programowanie zorientowane obiektowo.
Definicje i przykłady

WARSZAWA 2006
Spis treści

1. Wprowadzenie.................................................................................................................................................................4
2. Zasady programowania zorientowanego obiektowo........................................................................................................4
2.1. Tworzenie programu komputerowego ......................................................................................................................4
2.2. Paradygmaty programowania ..................................................................................................................................6
2.3. Języki programowania zorientowanego obiektowo ..................................................................................................6
2.4. Rozwój języków zorientowanych obiektowo.............................................................................................................7
Język C++...................................................................................................................................................................7
2.5. Zalety i wady programowania zorientowanego obiektowo .......................................................................................7
3. Klasy a obiekty programowe............................................................................................................................................8
3.1. Klasa ........................................................................................................................................................................8
3.2. Obiekty klasy............................................................................................................................................................8
3.3. Dziedziczenie klas....................................................................................................................................................8
3.4. Widoczność komponentów klasy .............................................................................................................................9
3.5. Konstruktor i destruktor klasy ...................................................................................................................................9
3.6. Definicja klasy ..........................................................................................................................................................9
3.7. Definicja obiektu klasy............................................................................................................................................10
3.8. Wewnętrzny wskaźnik na obiekt ............................................................................................................................11
3.9. Tworzenie i usuwanie obiektów..............................................................................................................................11
Alokacja obiektu na stosie ........................................................................................................................................11
Alokacja obiektu na stercie .......................................................................................................................................12
Usuwanie obiektów...................................................................................................................................................12
3.10. Odwołanie do komponentów ................................................................................................................................13
3.12. Dostęp do komponentów obiektu .........................................................................................................................13
3.13. Klasy zaprzyjaźnione............................................................................................................................................14
3.14. Informacja o klasie ...............................................................................................................................................14
3.15. Mechanizm RTTI..................................................................................................................................................14
4. Komponenty klasy .........................................................................................................................................................16
4.1. Komponenty statyczne ...........................................................................................................................................16
4.2. Komponenty stałe ..................................................................................................................................................17
4.3. Metody wstawiane..................................................................................................................................................17
4.4. Właściwości klasy ..................................................................................................................................................18
4.5. Włączenie obiektów klas ........................................................................................................................................19
4.6. Rodzaje konstruktorów...........................................................................................................................................19
4.7. Konstruktor statyczny .............................................................................................................................................20
4.8. Konstruktor kopiujący .............................................................................................................................................20
4.9. Konstruktor z argumentami ....................................................................................................................................20
4.10. Inicjalizacja obiektu ..............................................................................................................................................21
4.11. Konstruktory z listą inicjacyjną .............................................................................................................................21
4.12. Destruktory...........................................................................................................................................................23
5. Dziedziczenie klas .........................................................................................................................................................24
5.1. Deklaracja dziedziczenia........................................................................................................................................24
5.2. Dziedziczenie uprawnień dostępu..........................................................................................................................24
5.3. Interfejs obiektu......................................................................................................................................................25
5.4. Hermetyzacja danych składowych .........................................................................................................................25
5.5. Hierarchia klas .......................................................................................................................................................27
5.6. Dziedziczenie od wielu rodziców............................................................................................................................27
5.7. Przesłonięcie metody .............................................................................................................................................28
5.8. Przeciążenie metody ..............................................................................................................................................28
5.9. Polimorfizm nazw ...................................................................................................................................................29
5.10. Metody wirtualne ..................................................................................................................................................29
5.11. Metody dynamiczne .............................................................................................................................................30
5.12. Klasa polimorficzna i klasa abstrakcyjna ..............................................................................................................30
5.13. Lokalna konwersja typów (rzutowanie typów) ......................................................................................................31
5.14. Kolejność wywoływania konstruktorów klas .........................................................................................................31
5.15. Wywołanie konstruktorów z listy inicjacyjnej ........................................................................................................32
5.16. Destruktory wirtualne............................................................................................................................................32
6. Metody operatorowe......................................................................................................................................................34
6.1. Przeciążanie operatorów........................................................................................................................................34
6.2. Metoda operatorowa klasy .....................................................................................................................................35
6.3. Zaprzyjaźniona metoda operatorowa .....................................................................................................................36
6.4. Operatory przeciążone ...........................................................................................................................................36
6.4.1. Operatory unarne............................................................................................................................................36
6.4.2. Operator =.......................................................................................................................................................36
6.4.3. Operator () ......................................................................................................................................................39
6.4.4. Operator [].......................................................................................................................................................39
6.4.5. Operator -> .....................................................................................................................................................40
6.4.6. Operatory new i delete....................................................................................................................................40
7. Wzorce (szablony) klas i funkcji.....................................................................................................................................43
7.1. Wzorce (szablony) klas .........................................................................................................................................43
7.2. Wzorce (szablony) funkcji .....................................................................................................................................44
7.3. Przykłady definicji wzorców....................................................................................................................................44
Literatura ...........................................................................................................................................................................47
LITERATURA GŁÓWNA ...............................................................................................................................................47
LITERATURA POMOCNICZA ......................................................................................................................................47
1. Wprowadzenie

2. Zasady programowania zorientowanego obiektowo

2.1. Tworzenie programu komputerowego


Pod programowaniem będziemy rozumieć tworzenie programu komputerowego. W czasach
teraźniejszych jest to napisanie tekstu programu w specjalnym języku, tzw. języku programowania
i przekształcenie tekstu do postaci kodu komputerowego za pomocą kompilatora i konsolidatora.
Programowaniem zorientowanym obiektowo będziemy nazywać takie programowanie, kiedy
zmienne i instrukcje należące do jednego obiektu programowego, są skoncentrowane w jednym
miejscu programu w postaci językowych konstrukcji, zwanych zwykłe klasami.
Dwa następujących przykłady w języku C++ pokazują różnicę w strukturze programu, jak
również w rozmiarze tekstu, dla programu z operacjami z licznikiem w dwóch wariantach: „bez
klas” i „z klasami”.
Przykład. Program „bez klas”.
int WinMain(int, int, int*, int)
{
...
int stan; //zmienna „stan licznika”
...
stan=0; //zerowanie licznika
...
stan++; //dodawanie 1
...
if (stan==0) //sprawdzanie stanu licznika
{
...
}
}
Przykład. Program „z klasami”.
//Definicja klasy
class Licznik //początek opisania klasy
{
int stan; //pole danych klasy
public: // kwalifikator dostępu
Licznik(); //deklaracja konstruktora obiektu klasy
void Zero(); //deklaracja metody klasy
int Plus(); //deklaracja metody klasy
bool CzyZero(); //deklaracja metody klasy
};
Licznik::Licznik() //implementacja konstruktora
{
stan=0; //inicjalizacja pola klasy
}
void Licznik::Zero() //implementacja metody
{
stan=0;
}
int Licznik::Plus() //implementacja metody klasy
{
stan++;
return stan;
}
bool Licznik::CzyZero() //implementacja metody klasy
{
return(stan==0);
}
//Program
int WinMain(int, int, int*, int)
{
...
Licznik licznik1; //obiekt licznik1 typu Licznik
...
licznik1.Zero(); //zerowanie licznika
...
licznik1.Plus(); //dodawanie 1
...
if (licznik1.CzyZero()) //sprawdzanie stanu licznika
{
...
}
}

Łączenie zmiennych i instrukcji należących do jednego obiektu programowego potrzebuje


dodatkowej pracy programisty nad definicją klasy, np. Licznik. Dodatkowa praca programisty nad
definicjami klas nie jest mała i prosta. Z tego powodu programowanie zorientowane obiektowo nie
wyparło programowanie „bez klas”, chociaż liczba zwolenników programowania obiektowego ro-
śnie i powstali języki, w których można programować tylko „z klasami”.
Koncentrowanie zmiennych i instrukcji należących do jednego obiektu programowego daje
możliwość programiście budować obiekty programowe jako odpowiedniki obiektów z przestrzeni
przedmiotowej, a to pozwala interpretować współdziałanie obiektów programu jako współdziałanie
obiektów dziedzinowych.
Klasa programowa opisuje strukturę i zachowanie obiektu programowego. Korzystając z na-
zwy klasy można definiować globalne i lokalne obiekty programowe według reguł dla definiowania
„zwykłych” zmiennych.
Obiekt programowy jest interpretowany jako egzemplarz klasy - grupy obiektów z jednako-
wymi właściwościami. Taka interpretacja pomaga przy opracowaniu programu przekształcać klasy
(grupy obiektów) z dziedziny przedmiotowej w klasy programowe. Na przykład liczniki jako grupę
urządzeń liczących można przedstawić w programie klasą programowej z nazwą „Licznik”.
Obiekt, o którym często się mówi w programowaniu zorientowanym obiektowo, jest czymś,
co ma określone granice, stan charakteryzujący jego jednoznaczne i widoczne własne zachowanie
w formie pewnej kolejności stanów.
Programowanie zorientowane obiektowo jest przedłużeniem i ulepszeniem programowania
proceduralnego. Przypomnimy, że program zbudowany na zasadzie metody proceduralnej zawiera
wywołania procedur (funkcji, podprogramów), z których każda wykonuje funkcjonalno ograniczoną
grupę instrukcji. Programowanie obiektowe zmienia strukturę programu proceduralnego. Z powodu
łączenia w klasy zmiennych i instrukcji zmniejsza się objętość tekstu w głównej części programu i
rośnie liczba wywoływanych procedur (funkcji), które powstają zamiast grup instrukcji dotyczących
zmiennych pewnych klas.

2.2. Paradygmaty programowania


Programowanie zorientowane obiektowo wywarło wielki wpływ na metodologie procesu pro-
jektowania. Pojawiło się projektowanie zorientowane obiektowo jako projektowanie, w którego
trakcie buduje się obiektowa dekompozycja przestrzeni przedmiotowej. Program jest rozpatrywany
nie jak łańcuch wykonywanych instrukcji, a jako wspólnota obiektów współpracujących między
sobą.
Przez projektowanie pewnego systemu technicznego będziemy rozumieć przekształcenie
pierwotnego opisu systemu w zadaniu technicznym na jego opis końcowy w postaci dokumentacji
techniczną (tj. dokumentacji, na której podstawie można stworzyć system) poprzez opisy pośred-
nie, wśród których może się znajdować projekt wstępny, projekt szkicowy, projekt techniczny itp.
Obiektowe projektowanie system informatycznych jest często rozdzielane na trzy składniki 1 :
obiektową analizę zagadnienia, obiektową projektowanie klas i programowanie obiektową.
Historycznie najpierw wykrystalizowało się programowanie zorientowane obiektowo, a póź-
nej w wyniku wykorzystania podejścia obiektowego powstali pojęcia „projektowanie obiektowe”,
„obiektowe projektowanie klas” i „analiza obiektowa”.
Wysoka ocena roli projektowania i programowania obiektowych spowodowała to, że zwolen-
nicy podejścia obiektowego do projektowania systemów informatycznych zaliczają zbiory podsta-
wowych pojęć projektowania i programowania obiektowych do paradygmatów.
Przypomnimy 2 , że paradygmat jest zbiorem podstawowych pojęć i twierdzeń, które dostar-
czają modelowych rozwiązań w danej dziedzinie nauki, mogą też pociągać za sobą modelowe
rozwiązania w dziedzinach pokrewnych i stawać się istotnym składnikiem poglądu na świat.
Do paradygmatu programowania obiektowego zaliczamy pojęcia: obiekt, klasa, atrybut (pole
danych), metoda, właściwość itp.

2.3. Języki programowania zorientowanego obiektowo


Pojęcie klasy jako typu złożonego posiadającego własną strukturę i opisującego tak zmien-
ne, jak i ich zachowanie, jest wykorzystane w dużej ilości języków programowania. Przegląd języ-
ków programowania zorientowanych obiektowo znajduje się w [1].
Pierwszy język zorientowany obiektowo, Simula 67 [2], istnieje od 1967 roku i jest rozszerze-
niem słynnego języka ALGOL 60. Autorami języka Simula 67 są Ole-Johan Dahl, Bjorn Myhrhaug
i Kristen Nygaard z Norweskiego Ośrodka Obliczeniowego w Oslo.
Deklaracja klasy w języku Simula 67 ma następującą składnię:

class Nazwa_klasy(Lista_parametrów_formalnych);
value Zbiór_wartości;
Zbiór_specyfikacji_parametrów;
begin
Zbiór_deklaracji_lokalnych;
Lista_instrukcji
end
Grubą czcionką są zaznaczone słowa kluczowe.

1
1) Coad P., Yordan E., 1994: Analiza obiektowa, 2) Coad P., Yordan E., 1994: Projektowanie obiektowe, 3)
Coad P., Nicola J., 1993: Programowanie obiektowe. Warszawa: Oficyna wydawnicza READ ME.
2
PARADYGMAT (nauka) - http://encyklopedia.interia.pl
Paradygmat (nauka) - http://pl/wikipedia.org/wiki/Paradygmat_(nauka)
Opis pewnej klasy „student” wygląda w języku Simula 67 następująco:

class student(imię nazwisko, rok ur, rok bieżący);


value imię nazwisko;
integer rok ur, rok bieżący;
begin
integer wiek;
wiek := rok bieżący - rok ur;
end

W przykładzie zwraca na siebie uwagę identyfikatory argumentów z dwóch słów i spację


między nimi. W późniejszych językach programowania jest niedopuszczalne wykorzystanie spacji
wewnątrz identyfikatora (nazwy).
W języku Simula 67 w pierwszy raz zaistniało dziedziczenie klas pod nazwą „prefiksowanie
klas”.

2.4. Rozwój języków zorientowanych obiektowo


Język C++
Język C++ stworzył Bjarn Stroustrup, który nazywa ten język „C z klasami”, ponieważ w języ-
ku jest wykorzystana notacja języka C i dodane są nowe konstrukcje - klasy. Język C zaofiarował
Denis M. Ritchie.
Autor języka C++ Bjarn Stroustrup zapożyczył koncepcję klasy z języka Simula 67. Analiza
języka C++ ujawnia również wpływ na ten język języka Algol 68.
Istnieją liczne kompilatory języka C++ znanych firm Borland, Microsoft, Watcom. Rozszerzo-
ne wersji języka C++ są włączone w szeroko znane systemy oprogramowania (środowiska) Bor-
land C++ Builder oraz Visual C++ (część Visual Studio firmy Microsoft).
Język C++ jest standaryzowany od 1994 komisją ISO/ANSI, standard z ostateczną korektą -
standard ANSI 14882 2003 roku.
Język C++ jest najszerzej używanym językiem. Język C++ wywarł wielki wpływ na inne języki
zorientowane obiektowo.

2.5. Zalety i wady programowania zorientowanego obiektowo


Źródłem zalet programowania zorientowanego obiektowo jest ulepszenie jasności programu.
Korzystanie z klas oraz nierzadko z hierarchii klas, nieuchronnie doprowadzi do zaistnienia w pro-
gramie strukturalnych komponentów (modułów itp.) zbliżonych do obiektów dziedziny przedmioto-
wej. Podobieństwo obiektów realnych i programowych pomaga programiście lepiej rozumieć swój
program, a to zmniejsza liczbę błędów i skraca czas projektowania.
Ważną zaletą programowania zorientowanego obiektowo jest to, że klasy można wykorzy-
stać ponownie w innych projektach, co też wpływa pozytywne na czas projektowania.
Autonomiczność klasy doprowadzi do tego, że przy modernizacji programu, aby na przykład
zwiększyć szybkość działania, nierzadko można ograniczać się do miejscowej korekty kluczowych
metod klasy (procedur) i uniknąć ogólnych zmian niebezpiecznych dla projektu.
W literaturze o programowaniu zorientowanym obiektowo rzadko pisze się o jego wadach.
Programowanie zorientowane obiektowo ma jednak wady i w niektórych przypadkach mogą one
wykluczyć zastosowanie programowania zorientowanego obiektowo.
Główną wadą programowania zorientowanego obiektowo jest zmniejszenie szybkości pro-
gramu.
Należy zaznaczyć, że większość języków zorientowanych obiektowo mają konstrukcje skła-
dniowe zezwalające programować „nie obiektowo” tj. „bez klas”.
Prawda, w językach Smalltalk, Java, Visual Basic .NET nie ma możliwości programować
„bez klas”, ponieważ nie ma innych typów danych niż klasy.
3. Klasy a obiekty programowe

3.1. Klasa
Klasa jest konstrukcją składniową języka zorientowanego obiektowo, która zawiera opis
komponentów klasy i implementacje metod klasy.
Komponenty klasy można podzielić na
• pola składowe, zwane także „atrybutami”, „danymi składowymi”, ”zmiennymi klasy”,
• metody klasy, zwane również ”funkcjami składowymi”, ”funkcjami klasy”, ”procedurami klasy”.
Pola składowe (atrybuty) są to zmienne ze wskazanym typem.
Metody klasy są procedurami (funkcjami) zawierającymi ciągi operatorów języka programo-
wania.
W niektórych językach do pól i metod mogą być dołączone jeszcze definicje typów z zakre-
som działania w granicach klasy.
Pola i metody klasy mogą tworzyć trwałe grupy. W środowiskach Delphi, C++ Builder i w ję-
zykach Visual Basic .NET, Visual C# .NET taką grupą nazywana jest właściwością klasy (ang.
class property).
W językach C++, Java i Visual C# struktury programowe są równe klasom. Struktury pro-
gramowe są interpretowane jako klasy z dostępem publicznym do pól i metod, tj. z możliwością
odwołania do pól i metod z każdego miejsca programu.
W języku Java oprócz pól i metod klasa może zawierać jeszcze segment stałego kodu wy-
konywanego przy tworzeniu obiektu danej klasy.

3.2. Obiekty klasy


Deklaracja klasy jest opisem złożonego typu danych i wykorzystana jest jako typ obiektu
programowego.
Pod obiektem programowym rozumiemy dokładnie określony obszar pamięci, którego roz-
miar zależy wprost od typu.
Translator wyznacza każdemu obiektowi programu, posiadającemu typ klasy, miejsce w pa-
mięci, którego rozmiar jest sumą rozmiarów pamięci potrzebnych do rozmieszczenia pól danych
(atrybutów) klasy.
Rozmiar pamięci, która jest potrzebna do rozmieszczenia każdego pola klasy (atrybutu), liczy
się zgodnie z typem każdego z pół, ale może być zwiększony, żeby rozmiar pasował do kroku
rozmieszczenia zmiennych. Krok rozmieszczenia zależy od ustawień (opcji) kompilatora. W celu
zmiany kroku rozmieszczenia można wykorzystać też instrukcje przekompilacji w programie.
Kod komputerowy metod klasy jest umieszczany przez kompilator tylko w jednym miejscu
pamięci, niezależne od liczby obiektów danej klasy. Adres metody kompilator oblicza i wpisuje w
czasie kompilacji lub przygotuje fragment kodu dla dynamicznego obliczania adresu (tj. obliczania
w czasie wykonania programu).
Dynamiczne obliczenie adresu ma miejsce przy tzw. przesłonięciu (ang. overriding) metody.
Do obszaru pamięci zajmowanego metodami kompilator może dołączyć komórki z informacją
„służbową” (typ klasy, znaczniki dostępu itp.).

3.3. Dziedziczenie klas


Dziedziczenie klas jest cechą charakterystyczną dla programowania zorientowanego obiek-
towo. Dziedziczenie klas daje możliwość wykorzystania w obiekcie klasy dziedziczonej (tzw. klasy
pochodnej) już istniejącego kodu dla tej klasy, od której dziedziczy ta klasa.
We wszystkich językach zorientowanych obiektowo można przy definicji klasy wskazać klasę
macierzyńską (tzw. klasę podstawową).
Klasa pochodna jest nazywana też klasą potomnej, dziedziczy komponenty klasy podstawo-
wej nazywaną też klasą bazową.
Klasa pochodna dziedziczy prawie wszystkie komponenty klasy bazowej oprócz konstrukto-
rów i destruktorów.
Obiekt klasy pochodnej zawiera obszar pamięci odpowiadający polom klasy bazowej. Ten
obszar pamięci jest inicjalizowany wartościami równymi wartościom pól klasy bazowej.
Do klasy pochodnej można dodać nowe komponenty, tj. nowe pola danych (atrybuty) lub
nowe metody.
W przypadku dodania nowych pól danych (atrybutów) obiekt klasy bazowej zawiera obszar
pamięci odpowiadający polom klasy bazowej i dodatkowy obszar pamięci odpowiadający dodat-
kowym polom klasy pochodnej.
W przypadku dodania nowych metod rośnie kod maszynowy odpowiadający metodom klasy.
Dowolna metoda klasy bazowej może być zamieniona w klasie pochodnej. Zamiana metody
nazywa się przesłonięciem (ang. overriding) metody
Obszar pamięci zajmowany obiektem klasy pochodnej może tylko rosnąć w stosunku do kla-
sy bazowej. Nie ma możliwości zmniejszenia liczby komponentów przez wprowadzenie klasy po-
chodnej.
Jeżeli w procesie programowania powstaje taka potrzeba, to należy rozpatrzyć wariant za-
miany miejscami klas bazowej i pochodnej w hierarchii klas.

3.4. Widoczność komponentów klasy


We wszystkich językach programowania zorientowanego obiektowo jest możliwość definio-
wania widoczności komponentów klasy przez określenie uprawnień dostępu.
Są trzy rodzaje widoczności komponentu:
• widoczność z każdego miejsca programu nazywana dostępem publicznym;
• widoczność w granicach klasy, klasy pochodnej i tzw. klas zaprzyjaźnionych, nazywana dostę-
pem chronionym;
• widoczność w granicach klasy i tzw. klas zaprzyjaźnionych, nazywana dostępem prywatnym.
Do wskazania uprawnień dostępu (widoczności) wykorzystują się tzw. kwalifikatory dostępu
do komponentów klasy.

3.5. Konstruktor i destruktor klasy


Dwie metody klasy - konstruktor klasy i destruktor klasy są konieczne. Jeżeli konstruktor lub
destruktor nie zdefiniowane są wewnątrz definicji klasy, to kompilator korzysta z domyślnego kon-
struktora lub destruktora.
Konstruktor opisuje inicjalizację pól danych klasy.
W destruktorze programista zwalnia miejsce przydzielone dynamicznym zmiennym klasy.
Jeżeli takich zmiennych w klasie nie ma, można korzystać z domyślnego destruktora. Domyślny
destruktor kompilatora jest pustą procedurą.
Metody „konstruktor” i „destruktor” nie zwracają wartości i nie posiadają typów.
Konstruktor i destruktor są opisywane różnie w językach programowania.
W językach C++, Java, C# konstruktor i destruktor klasy muszą mieć nazwę powtarzającą
nazwę klasy, przy czym w destruktorze przed nazwą powinien być symbol „~” (tylda).
W językach Smalltalk, Visual Basic .NET konstruktor i destruktor mają przedefiniowane na-
zwy (np. Sub New - konstruktor, Sub Dispose - destruktor w języku Visual Basic .NET).
W języku Object Pascal konstruktor jest zaznaczany słowem kluczowym constructor.

3.6. Definicja klasy


Język C++
W języku C++ definicja klasy rozpoczyna się od słowa kluczowego class. Klasa Licznik
może mieć definicję podaną w przykładzie:
class Licznik //początek opisania klasy
{
int liczba; //opis pola klasy
public:
Licznik(); //opis konstruktora klasy
int Plus(); //opis metody klasy
};
Licznik::Licznik() //konstruktor klasy
{
liczba=0; //inicjalizacja pola klasy
}
int Licznik::Plus() //metoda klasy
{
liczba++; if (liczba>=100) liczba=0;
return (int)liczba;
}

W przykładzie w bloku ze słowem class i nazwą klasy Licznik są opisane komponenty kla-
sy: pole danych liczba, dwie metody Licznik i Plus. Metoda Licznik jest konstruktorem. Poza
opisem klasy umieszczone są implementacje dwóch metod. Do nazw metod jest dodana nazwa
klasy, tj. Licznik::.

3.7. Definicja obiektu klasy


Definicja obiektu programowego typu klasy jest podobna do definicji każdego innego obiektu
programu.
Na przykład obiekt programowy liczn typu Licznik definiuje się następująco:

w językach C++, Java:

Licznik liczn;

O rozmiarze pamięci zajmowanej przez obiekt można dowiedzieć się za pomocą funkcji si-
zeof w języku C++. Do funkcji można przekazać nazwę obiektu lub nazwę klasy.

Alokacja pamięci dla obiektu typu klasa może odbywać się na stosie lub na stercie w zależ-
ności od instrukcji programu. Zwykle obiekty są zaalokowane na stercie, a do odwołania do obiektu
jest wykorzystany wskaźnik lub referencja.

Referencję należy interpretować jako obiekt prezentowany wskaźnikiem zamaskowanym


(niewidocznym) dla wygody programisty. Programista pisze tekst programu korzystając ze zmien-
nej referencyjnej, a kompilator przekształca wszystkie operacje z tej zmiennej na operacji ze
wskaźnikiem na obiekt (tj. z adresem obiektu). Każdej referencji odpowiada tymczasowy obiekt,
którym „zarządza” kompilator.
Na przykład definicja zmiennej referencyjnej „ref” w języku C (C++) wygląda następująco:

int& ref;

Operację dodawania jedynki programista opisuje przez instrukcję „ref++;” wykorzystując


zmienną „ref” jako „zwykłą” zmienną, a kompilator realizuje tą instrukcję następująco:

(*pref)++;

gdzie „pref” jest niewidoczny wskaźnik na zmienną (obiekt) „ref”, a znak „*” jest operatorem
wyłuskania (instrukcją pobrania wartości zmiennej z pod adresu).
W języku C (C++) wartość niewidocznego wskaźnika „pref” można wydobyć za pomocą in-
strukcji z operatorem pobrania adresu „&”:

int* p = &ref;
ponieważ operator wyłuskania „*” i operator pobrania adresu „&” niszczą jeden drugiego:

p = &ref = &(*pref) = &*pref = pref

W językach programowania Ada, Visual Basic .NET, Visual C# .NET odwołanie do obiektu
może być tylko przez referencję, a bezpośredni operacje nad wskaźnikami są niemożliwe.

3.8. Wewnętrzny wskaźnik na obiekt


Dla obiektów dynamicznych adres, z którego system operacyjny umieszcza nowy obiekt, jest
znany tylko w czasie stworzenia obiektu przy wykonywaniu programu. Ponieważ ten adres nie mo-
że być znany na etapie programowania, to powstaje problem jak zapisać w programie operacji z
adresem obiektu. Do rozwiązania tego problemu w jezykach programowania obiektowego do słów
kluczowych dodane jest słowo:
this w językach C++, Java, Visual C# .NET,
self w językach Smalltalk, Object Pascal,
Me w języku Visual Baisc .NET.
Słowo kluczowe this (self, Me) nazywa się wewnętrznym wskaźnikiem na obiekt.
Wewnętrzny wskaźnik może być wykorzystany bezpośrednio tylko w metodach klasy. Ale
wewnętrzny wskaźnik można zwrócić jako zwykły adres przez metodę (podprogram) i wykorzystać
do celów operacji na polach obiektu, na przykład do zapisu ich wartości na dysk.
Z wewnętrznego wskaźnika należy korzystać także w sytuacjach, kiedy występują jednako-
we nazwy formalnych parametrów metody i pól klasy.
Na przykład w następującym tekście w języku C++ wewnętrzny wskaźnik this pomaga od-
dzielić argument „liczba” od pola danych „liczba” (w implementacji metody „Dodaj”).

class Licznik //początek opisania klasy


{
int liczba; //opis pola klasy
public:
Licznik(); //opis konstruktora klasy
int Dodaj(int liczba); //opis metody klasy
};
Licznik::Licznik() //konstruktor klasy
{
liczba=0; //inicjalizacja pola klasy
}
int Licznik::Dodaj(int liczba) //metoda klasy
{
this->liczba+=liczba;
if (this->liczba>=100) this->liczba=0;
return this->liczba;
}

3.9. Tworzenie i usuwanie obiektów


W programach w języku zorientowanym obiektowo do tworzenia i usuwania obiektów wyko-
rzystuje się dynamiczne zarządzanie pamięcią. Obiekty można alokować na stosie i na stercie w
zależności od miejsca i formatu definicji obiektu.
Alokacja obiektu na stosie
„Zwykła” definicja obiektu wewnątrz bloku powoduje alokację obiektu na stosie systemowym.
Przykłady:

- w języku C++:

void proc()
{
Licznik liczn;
...
}
W językach Smalltalk, Java obiekty alokują się tylko na stercie.

Alokacja obiektu na stercie


Do alokacji obiektu na stercie służą specjalne operatory i funkcji.
Język C++
W języku C++ operatorem alokacji jest operator new, ktory zwraca wskaźnik na obiekt. Ope-
rator new może mieć jedną z trzech postaci:

new nazwa_klasy dla alokacji jednego obiektu,


new nazwa_klasy (wartość_pola) dla alokacji i inicjacji obiektu,
new nazwa_klasy [liczba_obiektów] dla alokacji tablicy obiektów.
W ostatnim wariancie operator new zwraca wskaźnik na pierwszy obiekt tablicy.
Przykłady zapisywania operatora new:

cA* p=new cA;


cA* p=new cA(10);//inicjacja obiektu *p liczbą 10
cA* p=new cA[12];//alokacja 12 obiektów

Usuwanie obiektów
W programowaniu zorientowanym obiektowo trzeba zwracać uwagę na usuwanie obiektów
dynamicznych.
System operacyjny typu Windows dla każdego wskaźnika na dynamicznie zaalokowany
obiekt wydziela dodatkową pamięć do rozmieszczenia „stancji wskaźnika”. Stancja wskaźnika za-
wiera fizyczny adres komórki pamięci z obiektem oraz znaczniki systemowe. Stancja wskaźnika
jest potrzebna Windows z powodu dynamicznego przemieszczenia obszarów pamięci zajmowa-
nych aplikacją, w tym z pamięci operacyjnej na dysk i z powrotem w inne miejsce. Typowy rozmiar
stancji wskaźnika wynosi 16 bajtów. Ta pamięć oraz pamięć, którą zajmuje obiekt, zostaje zazna-
czona jako zajęta także po zakończeniu aplikacji. Dlatego aplikacja powinna oczyszczać po sobie
pamięć.
Nie jest potrzebne programowe czyszczenie pamięci w przypadku korzystania z wirtualnych
maszyn Java, Smalltalk i w aplikacjach w językach Visual Basic.NET, Visual C#.NET, które mają
śmieciarza do czyszczenia sterty pamięci z niepotrzebnych obiektów.
Śmieciarz od czasu do czasu sprawdza widoczność każdego referencyjnego obiektu pro-
gramu. Jeżeli nie ma odwołań do obiektu referencyjnego (np. z powodu wyjścia programu poza
granicy widoczności referencji), to śmieciarz zwalnia pamięć, którą zajmuje tymczasowy obiekt
referencyjny. Z wewnątrz programu jest niewiadomo, kiedy będzie działał śmieciarz i kiedy fak-
tyczne będzie miał miejsce proces zwalniania.
Do zwalniania pamięci przydzielonej alokowanemu obiektowi służą specjalne operatory lub
specjalne podprogramy.
Język C++
W języku C++ istnieje specjalny operator delete. Operator delete może mieć postać:

delete wskaźnik_na_obiekt

dla zwalniania pamięci przydzielonej jednemu obiektowi lub

delete [ ] wskaźnik_na_obiekt

dla zwalniania pamięci przydzielonej tablicy obiektów:

Przykłady zapisu operatora delete:


delete p;//zwalnianie pamięci przydzielonej obiektowi
delete [] p;//zwalnianie pamięci przydzielonej tablicy

3.10. Odwołanie do komponentów


Konstrukcja odwołania do komponentu obiektu zależy od tego, czy chcemy wykorzystać na-
zwę obiektu, czy wskaźnik na obiekt. W przypadku korzystania z nazwy obiektu składnia konstruk-
cji odwołania się do komponentu jest podobna w różnych językach zorientowanych obiektowo:
przed nazwą komponentu musi być nazwa obiektu i symbol „.” (kropka):

nazwa_obiektu.komponent_obiektu

W przypadku korzystania ze wskaźnika, konstrukcja syntaktyczna ma następującą składnię:


w języku C++:

wskaźnik_na_obiekt->komponent_obiektu;

3.12. Dostęp do komponentów obiektu


W języku C++ kwalifikatorami dostępu do komponent klasy służą słowa kluczowe:
o public dla wskazania na dostęp publiczny;
o protected dla wskazania na dostęp chroniony;
o private dla wskazania na dostęp prywatny.
Słowa te muszą być napisane razem z symbolem „:” (dwukropek) przed grupą komponentów
klasy. Zakres działania kwalifikatorów dostępu rozciąga się do następnego kwalifikatora.
Na początku definicji klasy można wskazać komponenty mające prywatny dostęp bez słowa
kluczowego private, ponieważ w języku C++ komponenty klasy mają domyślnie dostęp prywat-
ny.
Na przykład uprawnienia dostępu w klasie „Licznik” można określić w języku C++ w sposób
przedstawiony w przykładzie.

class Licznik //początek opisania klasy


{
public:
Licznik(); //opis konstruktora klasy
protected:
int Plus(); // podprogram
private:
int liczba; //opisanie pola klasy
};
Licznik::Licznik() //konstruktor klasy
{
liczba=0; //inicjalizacja pola klasy
}
int Licznik::Plus() //metoda klasy
{
liczba++; if (liczba>=100) liczba=0;
return liczba;
}

W środowisku C++ Builder występuje dodatkowy kwalifikator __published. Nagłówek


sekcji __published wskazuje kompilatorowi, że komponent ma uprawnienia dostępu publicznego
oraz że trzeba zapamiętać dodatkową informację o typie komponentu, żeby przy wykonywaniu
programu mógł działać mechanizm RTTI (Runtime type identification) dla tego komponentu. Słowo
kluczowe __published może być użyte tylko w klasach pochodnych od głównej klasy TObject.
3.13. Klasy zaprzyjaźnione
W definicji klasy mogą być oznaczone klasy zaprzyjaźnione. Obiekty typu klasy zaprzyjaź-
nionej mają uprawnienia dostępu do chronionych i prywatnych komponentów klasy.
W języku C++ klasa zaprzyjaźniona jest oznaczana za pomocą słowa kluczowego friend.
Na przykład klasa „Procesor” zaprzyjaźniona z klasą „Licznik” może być wskazana w języku
C++ następująco:

class Licznik //początek opisania klasy


{
friend class Procesor; //opis klasy zaprzyjaźnionej
...
};

3.14. Informacja o klasie


Niektóre kompilatory języków zorientowanych obiektowo dają możliwość wykorzystania w
programie informacji o klasie.
W środowisku Borland C++ Builder informację o klasie można wydobyć za pomocą słowa
kluczowego __classid.
Słowo kluczowe __classid jest używane w konstrukcji składowej __classid(nazwa
klasa). W procesie kompilacji w miejscu tej konstrukcji kompilator wpisuje adres obiektu typu
TMetaClass. Obiekt typu TMetaClass kompilator tworzy w czasie rejestracji klasy w swojej we-
wnętrznej tablicy klas, którą konsolidator dołącza do modułu wywoławczego.
Przykład instrukcji kreacji formularza klasy TForm1:

Application->CreateForm(__classid(TForm1), &Form1);

W przykładzie zmienna Form1 jest wskaźnikiem na formularz.

Główna bazowa klasa TObject, od ktorej dziedziczą wszystkie klasy C++ Buildera, zawiera
metody ClassInfo, ClassName, ClassType, ClassParent, które dają możliwość otrzymania infor-
mację o klasie i jej rodzicu. Na przykład następujace instrukcje demonstrują, jak wydobyć nazwę
klasy formantu (komponentu) pierwszego na formularzu

TObject* pob=(TObject* )Components[0];


TMetaClass* pmeta=pob->ClassType();
if (pmeta!=NULL)
AnsiString as=AnsiString(pmeta->ClassName());

3.15. Mechanizm RTTI


W czasie wykonywania programu może być potrzebne ustalenie typu obiektu. Na przykład
informacja o typie obiektu może być potrzebna wewnątrz podprogramu operujacego ze wskaźni-
kiem p na obiekt, jeżeli wskaźnik p jako argument podprogramu może wskazywać na obiekty róż-
nego typu.
Pomaga w ustaleniu typu obiektu mechanizm RTTI (Runtime type identification).
Mechanizm RTTI można włączyć/wyłączyć za pomocą opcji kompilatora. Jeżeli mechanizm
RTTI jest włączony to kompilator zapisuje w miejscu położenia obiektów identyfikator typu klasy.
W środowisku C++ Builder można ustalić typ obiektu w czasie wykonywania programu za
pomocą funkcji typeid. Funkcja typeid może mieć jedną z postaci:

typeid(operator_ wyłuskania) , tj. typeid(*p), lub


typeid(referencja), lub
typeid(nazwa_typu)

i zwraca referencję klasy type_info.


Najczęściej są wykorzystywane cztery metody klasy type_info:
const char* name() const;
int before(const type_info&);
int operator==(const type_info &) const;
int operator!=(const type_info &) const;

Metoda name zwraca wskaźnik na wiersz z nazwą klasy. Metoda before porównuje nazwy
klas w schowanej tablicy typów uporządkowanej alfabetyczne i zwraca 1 jeżeli nazwa klasy obiektu
poprzedza nazwę klasy argumentu, lub 0 w przeciwnym przypadku. Metody operatorowe opera-
tor== i operator!= można wykorzystać do porównania typów.
W przykładzie pokazano korzystanie z mechanizmu RTTI w środowisku Borland C++ Buil-
der.

static const char *fNameOfType(TObject *pOb)


{
return typeid(*pOb).name();
}
//--------------------------------------------------
class TForm1 : public TForm
{
__published:
TButton *Button1;
void __fastcall Button1Click(TObject *Sender);
public:
__fastcall TForm1(TComponent* Owner);
};
void __fastcall TForm1::Button1Click(TObject *Sender)
{
if (typeid(*Sender)==typeid(TForm))
Button1->Caption = fNameOfType(Button1);
}
Procedura fNameOfType w przykładzie jest globalną procedurą aplikacji i może być wywo-
ływana wielokrotne z różnymi faktycznymi argumentami. Wskaźnik Button1 jest wskaźnikiem na
obiekt graficzny - klawisz typu TButton. Po naciśnięciu myszą na klawisz graficzny na nim pojawi
się napis „TButton”.
W przykładzie należy zwrócić uwagę na to, że formalny argument podprogramu fNameO-
fType ma typ „TObject*” a faktyczny argument jest typu „TButton*”. W równaniu „type-
id(*Sender) == typeid(TForm)” argumentem pierwszego typeid jest referencja, a argu-
mentem drugiego typeid jest nazwa klasy.
4. Komponenty klasy

4.1. Komponenty statyczne


Komponent klasy może być oznaczony jako statyczny. Statyczny komponent klasy jest
wspólny dla wszystkich obiektów (egzemplarzy klasy).
Statyczne pole klasy pełnia role zmiennej globalnej połączonej z klasą dla wygody programi-
sty.
Z powodu rozmieszczenia komponentów statycznych przed alokacją obiektów klasy metoda
statyczna może być wywoływana w sytuacji, kiedy jeszcze nie istnieją obiekty danej klasy.
W metodzie statycznej można wywoływać statyczne pola klasy, ale jest niedozwolone korzy-
stanie z nie statycznych pól. Metoda statyczna może komunikować się z otoczeniem tylko przez
argumenty metody.
Statycznych komponentów (pól lub metod) klasy bazowej nie można zmieniać w klasach po-
chodnych. Metod statycznych nie można przeciążać.
W języku C++ na statyczny komponent wskazuje kwalifikator static. Statyczne pole klasy
musi być zdefiniowane i inicjalizowane na zewnątrz definicji klasy. Zapis statycznego pola we-
wnątrz konstrukcji „class {...}” ma charakter tylko informujący.
W następującym przykładzie w języku C++ pokazano zastosowanie pola statycznego
maxNumber jako stałej i pola statycznego numbObjects do obliczania ilości obiektów danej klasy.

class Licznik //początek opisania klasy


{
int liczba; //opis pola klasy
public:
static int maxNumber; //pole statyczne
static int numbObjects; //pole statyczne
Licznik(); //opis konstruktora klasy
int Plus(); //opis metody klasy
};
Licznik::Licznik() //konstruktor klasy
{
liczba=0; //inicjalizacja pola
}
int Licznik::Plus() //metoda klasy
{
liczba++; if (liczba>=maxNumber) liczba=0;
return liczba;
}
int Licznik::maxNumber=100; //inicjalizacja pola
int Licznik::numbObjects=0;//inicjalizacja pola
...
Licznik *p=new Licznik; // generacja obiektu
p->numbObjects++; // liczenie obiektów
lub
(*p).numbObjects++; // liczenie obiektów
...
delete p; // niszczenie obiektu
Licznik::numbObjects--; // liczenie obiektów
W języku C++ wywołanie komponentu statycznego może być zapisane w którymkolwiek wa-
riancie prefiksu: prefiksem może być obiekt (np. (*p).numbObjects++;), wskaźnik na obiekt (np. p-
>numbObjects++;) lub nazwa klasy (np. Licznik::numbObjects--;).
W przypadku wywołania komponentu statycznego przez wskaźnik obiekt tego typu może
jeszcze nie istnieć, dlatego lepiej korzystać z nazwy klasy. Oprócz tego, że jest to bezpiecznie,,
wywołanie komponentu statycznego za pomocą nazwy klasy łatwo odróżnić od wywołania kompo-
nentu nie statycznego.

4.2. Komponenty stałe


Komponent klasy może być oznaczony jako stały w celu „ochrony” go przed próbami zmiany.
Pole stałe służy dla definicji stałej.
Zaznaczenie metody klasy jako stałej powoduje, że kompilator kontroluje, czy metoda nie
modyfikuje komponentów klasy. Stała metoda zwykle służy do odczytywania wartości pól klasy.
W języku C++ stałość określa atrybut const. Przykład pokazuje jak korzystać ze stałych
pól i metod w języku C++:

class Licznik
{
static const int maxNumber; //pole stale
public:
Licznik();
int Plus();
bool IsLicznikZero() const; // metoda stala
};
Licznik::Licznik() //konstruktor klasy
{
liczba=0; //inicjalizacja pola
}
int Licznik::Plus() //metoda klasy
{
liczba++; if (liczba>=maxNumber) liczba=0;
return liczba;
}
bool Licznik::IsLicznikZero() const // metoda stala
{
return(liczba==0);
}
const int Licznik::maxNumber=100; //inicjalizacja pola

4.3. Metody wstawiane


Metody wstawiane (ang. inline methods) są metodami, których implementacje (kody) kompi-
lator układa w to miejsce, z którego są wywoływane metody. Oczywiście rośnie rozmiar kodu pro-
gramu, ale rośnie i szybkość opracowania danych.
Język C++ pozwala zaznaczyć metodę wstawianą słowom kluczowym inline. Na przykład:

class Licznik
{
int liczba;
public:
Licznik();
inline int Plus(); //opis wstawianej metody klasy
};
Licznik::Licznik() //konstruktor klasy
{
liczba=0; //inicjalizacja pola klasy
}
inline int Licznik::Plus() //metoda klasy
{
liczba++; if (liczba>=100) liczba=0;
return liczba;
}
W środowisku C++ Builder umieszczenie tekstu metody bezpośrednio za opisem metody
klasy wewnątrz konstrukcji „class {...}” powoduje interpretację tej metody jako metody wstawianą.
W przykładzie metoda Plus klasy Licznik jest metodą wstawianej, chociaż przed nazwą metody nie
jest napisane słowo inline.

class Licznik //początek opisu klasy


{
int liczba; //opis pola klasy
public:
Licznik(); //opis konstruktora klasy
int Plus() //wstawiana metoda
{
liczba++; if (liczba>=100) liczba=0; return liczba;
}
};
Licznik::Licznik() //konstruktor klasy
{
liczba=0; //inicjalizacja pola klasy
}
Polecanym stylem programowania jest styl, w którym implementacja metody wstawianej jest
umieszczona osobno poza definicją klasy, a deklaracja metody jest zapisana z kluczowym słowem
„inline”.

4.4. Właściwości klasy


W środowiskach C++ Builder, Delphi, Visual Basic .NET, C# .NET klasa może zawierać tzw.
właściwość (ang. property). Właściwość jest trwałą grupą zawierającą pole i metody do czytania i
zapisu tego pola. Wartość pola właściwości jest wyświetlana w Inspektorze Obiektów środowiska
programowania.
W środowisku C++ Builder w deklaracji właściwości jest wykorzystane słowo kluczowe
__property. Deklaracja właściwości ma następującą składnię:

__property Typ_właściwości Nazwa_właściwości = (read = nazwa_pola_lub_metody,


write = nazwa_pola_lub_metody);

Typem właściwości Typ_właściwości może być każdy z typów jak również typ klasy.
Jeżeli w deklaracji właściwości nie ma opcji write, to właściwość staje się właściwością
„tylko do odczytu”. Argument Nazwa_pola_lub_metody w opcjach read i write może być po-
lem lub metodą klasy.
Jeżeli argument w opcjach read i write jest polem, to operacje czytania i zapisywania war-
tości pola wlasciwości są realizowane przez operatory przepisania. Jeżeli argument w opcjach
read i write jest metodą, to do czytania lub zapisu wartości pola kompilator wywołuje wskazaną
metodę.
Niezależnie od rodzaju argumentu w opcjach read i write w tekście programu stosuje się
„prosty” operator przepisania wartości zmiennej typu właściwości. Na przykład:

...
Licznik *pOb=new Licznik;
int liczba=pOb->stan;
liczba++;
pOb->stan=liczba;
...
gdzie właściwość stan jest zdefiniowana następująco:

class Licznik //klasa z właściwością (argument-pole)


{
int liczba;
public:
__property int stan(read = liczba, write = liczba);
};
Właściwość stan może być zdefiniowana z argumentem - metodą:

class Licznik //klasa z właściwością (argument-metoda)


{
int liczba; //opis pola klasy
int GetNumber(); //opis metody do czytania
void SetNumber(int numb);//metoda do zapisu
public:
__property int stan(read = GetNumber, write = SetNumber);
};
int Licznik::GetNumber() //metoda do czytania
{
return liczba;
}
void Licznik::SetNumber(int numb)//metoda do zapisu
{
liczba=numb;
}
W ostatnim przypadku zaletą korzystania z właściwości jest nie tylko korzystanie z „prostych”
operatorów przepisania, ale i to, że procedury do czytania lub zapisu wartości pola mogą być
schowane, tj. mogą być metodami prywatnymi.
Jeżeli nie korzystać z właściwości, to w programie należy wywoływać procedury, które mu-
szą być publiczne (nie chronione), a ich nazwy programista musi pamiętać:

...
//bez deklaracji właściwości:
Licznik *pOb=new Licznik;
int liczba=pOb->GetNumber();
liczba++;
pOb->SetNumber(liczba);
...

4.5. Włączenie obiektów klas


Pola klasy mogą być każdego typu, a wśród nich mogą być obiekty lub wskaźniki na obiekty
innej klasy.
W języku C++ włączenie do klasy obiektu mającego typ innej klasy jest pokazane w następ-
nym przykładzie:

class Procesor //początek opisania klasy


{
Licznik liczn; //obiekt innej klasy
Licznik* pliczn; //wskaźnik na obiekt innej klasy
...
};

4.6. Rodzaje konstruktorów


Konstruktor klasy zawiera instrukcje inicjalizacji pól. Konstruktor jako metoda klasy może wy-
stępować bez parametrów (konstruktor bezparametrowy) lub z formalnymi parametrami (konstruk-
tor wieloargumentowy).
Mimo że konstruktor jest podobny do metody (procedurę, funkcji), konstruktor nie jest pod-
programem. Konstruktor nie ma typu i nie ma adresu. Tekst konstruktora informuje kompilator o
dodatkowych działaniach w celu inicjalizacji tego obszaru pamięci, w którym kompilator umieścił
obiekt.
Pojęcie „wywoływanie konstruktora” obejmuje rozmieszczenie (zaalokowanie) obiektu w pa-
mięci, wyzerowanie tego obszaru pamięci i inicjalizację tych pól, które są obecne w ciele konstruk-
tora.
W przypadku braku konstruktora w definicji klasy kompilator wywołuje „konstruktor domyślny
(ang. default constructor)” tj. tylko rozmieszcza obiekt w pamięci i zeruje ten obszar pamięci.
4.7. Konstruktor statyczny

4.8. Konstruktor kopiujący


Konstruktorem kopiującym nazywamy konstruktor, którego wywołanie oznacza alokację
obiektu (rozmieszczenie obiektu w pamięci) i kopiowanie do tego obiektu wartości pól obiektu -
argumentu w trybie „pole do pola”.
Konstruktor kopiujący jest wywoływany kompilatorem przy realizacji operatorów przepisania
oraz przy przekazywaniu obiektu do podprogramu.
Kompilator języka C++ w przypadku braku konstruktora w definicji klasy wywołuje domyślny
konstruktor kopiujący dla operatorów przepisania i przy przekazywaniu obiektu do podprogramu.
Operatorami przepisania w języku C++ są operatory:

=, *=, /=, %=, +=, -=, <<=, >>=, &=, ^=, |=.

Na przykład przy definicji dwóch obiektów ob1 i ob2 typu klasy cKlass bez konstruktora zdefi-
niowanego jawnie kompilator wywołuje konstruktor domyślny dla obiektu ob1 i konstruktor kopiują-
cy dla obiektu ob2:

cKlass ob1;
cKlass ob2=ob1; lub cKlass ob2(ob1);

Konstruktor kopiujący ma jeden argument typu tej samej klasy przekazywany przez referen-
cję:

cKlasa(const cKlasa& obiekt);

Przypomnimy, że słowo const podkreśla, że obiekt - argument nie będzie zmieniony we-
wnątrz konstruktora.
Kompilator języka Object Pascal interpretuje każdą zmienną typu klasa jako referencję. W
związku z tym operator przepisania ob2:=ob1 nie powoduje w języku Object Pascal kopiowania
obiektu ob1:

var
ob1,ob2: cKlass;
begin
ob1:=cKlass.Create;
ob2:=ob1
end;

W przykładzie w wyniku działania operatora przepisania referencje będą równe między sobą,
tj. niewidoczny wskaźnik referencji ob2 będzie wskazywał na ten sam obiekt, co i niewidoczny
wskaźnik referencji ob1.
Wskazana właściwość języka Object Pascal powoduje ograniczenia przy korzystaniu z bi-
blioteki klas VCL (Visual Component Library) w środowisku C++ Builder, ponieważ biblioteka
klas VCL była napisana dla środowiska Delphi w języku Object Pascal.
Przy programowaniu w środowisku Borland C++ Builder można polecić:
1) konstruowanie obiektów związanych z biblioteką klas VCL zawsze za pomocą operatora new,
2) zakaz wykorzystania operatorów przepisania dla obiektów związanych z biblioteką klas VCL.
W razie potrzeby w kopiowaniu obiektów klas VCL lub klas pochodnych od klas VCL należy
napisać tekst konstruktora kopiującego pole po polu. Dla klas pochodnych od klasy TPersistent
można ponowne zdefiniować metodę Assign służącą do kopiowania pól z jednego obiektu do dru-
giego.

4.9. Konstruktor z argumentami


W językach C++, Object Pascal, Visual Basic .NET, Visual C# .NET konstruktor może
mieć argumenty.
W języku C++ w definicji konstruktora można korzystać z argumentów, którym przepisane
są wartości domyślne. W następującym przykładzie klasa cPoint posiada konstruktor z trzema
argumentami domyślnymi:

class cPoint
{
int x,y,z;
cPoint(int ix=-1,int iy=-1,int iz=-1);
}
cPoint::cPoint(int ix=-1,int iy=-1,int iz=-1)
{
x=ix; y=iy; z=iz;
}

Definicja obiektu w postaci:

cPoint point;

tworzy obiekt point z polami point.x, point.y, point.z równymi -1.

4.10. Inicjalizacja obiektu


Język zorientowany obiektowo z reguły daje możliwość podania początkowych wartości pól
razem z definicją obiektu.
W języku C++ obiekt ob klasy Licznik z poprzedniego przykładu może być inicjalizowany
w następujący sposób:

Licznik ob(1);

W języku C++ przy definicji obiektu z jednoargumentowym konstruktorem klasy (nie pochod-
nej) można określić początkowe wartości pól przez listę inicjacyjną obiektu podobnie jak dla struk-
tury.
Na przykład można napisać

Licznik ob={5};

lub

Licznik ob=5;

dla obiektu klasy Licznik z przykładu


Za pomocą słowa kluczowego explicit przed konstruktorem można zakazać kompilatoro-
wi przyjmowania definicji obiektu w formacie z listą inicjacyjną. Korzystanie ze słowa kluczowego
explicit pokazano w przykładzie:

class Licznik
{
int liczba; //opis pola klasy
public:
explicit Licznik(int value); //konstruktor klasy
};
Licznik::Licznik(int value) //konstruktor klasy
{
liczba=value; //inicjalizacja pola klasy
}

4.11. Konstruktory z listą inicjacyjną


W języku C++ konstruktor klasy może mieć tzw. listę inicjacyjną konstruktora, w której jest
zapisana inicjalizacja pól.
Lista inicjacyjna rozmieszcza się przed implementacją konstruktora i musi być poprzedzona
dwukropkiem. Elementami listy inicjacyjnej są nazwy pól lub nazwy klas bazowych z wyrażeniami
w okrągłych nawiasach. Wyrażenia mogą zawierać stałe, parametry formalne konstruktora, już
zainicjowane pola oraz proste funkcje od pól i parametrów.
Jedynie przez listę inicjacyjną można inicjalizować pola, które są obiektami innych klas, oraz
pola - referencje. Jeszcze jedną zaletą listy inicjacyjnej konstruktora jest możliwość wskazania w
klasie pochodnej konkretnego z wielu konstruktorów klasy bazowej.
Konstruktory z listą inicjacyjną są ukazane w przykładzie:

class cClass1
{
int a, b, c;
public:
cClass1: a(0),b(1),c(2){};
};
//-----------------------------
class cClass2
{
int a, b, &c; //c jest referencją
public:
cClass2(int ia, int ib): a(ia),b(ia+ib),c(a){};
};
//-----------------------------
class cClass3
{
int a;
public:
cClass3(int i) { a=i; };
};

class cClass4
{
int b;
public:
cClass4(int i) : b(i) {};
};
class cClass5 : public cClass3, public cClass4
{
int a, b;
public:
cClass5(int i,int j);
};
cClass5::cClass5(int i,int j):
cClass3(i), cClass4(j+i), a(i)
{
b=j;
}
//-----------------------------
class cClass6
{
cClass2 cc; //cc jest klasą
int x, y;
public:
cClass6(int ix, int iy):
x(ix), y(ix+iy), cc(ix,x) {};
};

Przykład pokazuje, że sposób inicjalizacji może być mieszany: przez listę inicjacyjną i przez
implementację konstruktora.
4.12. Destruktory
Destruktor tak samo jak konstruktor jest podobny do metody (procedurę, funkcji), ale nim nie
jest. Destruktor też nie ma ani typu ani adresu.
Pojęcie „wywołanie destruktora” obejmuje wykonanie instrukcji destruktora i niszczenie
obiektu, tj. uwolnienie obszaru pamięci, w którym kompilator umieścił obiekt przy wywoływaniu
konstruktora.
W przypadku braku destruktora w definicji klasy kompilator wywołuje destruktor domyślny
(ang. default destructor), a mianowicie tylko usuwa obiekt z pamięci.
Przy wielokrotnym dziedziczeniu destruktory są wywoływane w odwrotnej kolejności niż kon-
struktory, tj. wywoływany jest najpierw destruktor danej klasy, a późnej destruktory klas bazowych
w kolejności odwrotnej od tej, w której są one zapisane w definicji klasy.
Jeżeli wśród pól klasy znajdują się wskaźniki na obiekty, trzeba pamiętać o dodawaniu do
destruktora operatorów usuwania tych obiektów. Można polecić napisanie tekstu destruktora bez-
pośrednio po napisaniu tekstu konstruktora.
W języku C++ operator usuwania obiektu, na który wskazuje jakikolwiek wskaźnik pointer,
ma następującą zalecaną postać:

if (pointer!=NULL) delete pointer;

Porównanie zmiennej pointer z wartością NULL zapobiega usuwaniu nieistniejącego


obiektu.
5. Dziedziczenie klas

5.1. Deklaracja dziedziczenia


Klasa pochodna nie dziedziczy dosłownie wszystkiego od klasy podstawowej. Klasa po-
chodna nie może dziedziczyć konstruktora ani destruktora oraz niektórych typów metod, np. meto-
dy typu „operator =” i „operator ()”.
Jeżeli jest potrzebne pełne dziedziczenie, to dziedziczenie trzeba zamienić na włączenia kla-
sy, która występowała jako klasa podstawowa.
Konstrukcja składniowa dziedziczenia zależy zasadniczo od języka programowania.
W języku C++ klasa bazowa musi być wskazana przez symbol „:” (dwukropek) poza nazwą
klasy. Na przykład następująca zapis w języku C++ opisuje dziedziczenie klasy pochodnej
„TextLicznik” od klasy bazowej „Licznik”:

class TextLicznik : Licznik //początek opisu klasy


{
...
};

5.2. Dziedziczenie uprawnień dostępu


Dziedziczenie jako cecha jezyka zorientowanego obiektowo odkrywa perspektywę budowa-
nia hierarchii klas. W trakcie budowy hierarchii klas jest możliwość zmiany widoczności komponen-
tów klas podstawowych (bazowych) ze strony klas pochodnych za pomocą modyfikatorów dostę-
pu.
W celu ograniczenia widoczności komponentów w definicji klasy pochodnej przed nazwą kla-
sy bazowej można użyć jednego z modyfikatorów dostępu.
We wszystkich językach zorientowanego obiektowo obiekty klasy pochodnej nie mają dostę-
pu do prywatnych komponentów klasy bazowej.
Dziedziczenie uprawnień dostępu w językach C++, Object Pascal do komponentów obiektu
danej klasy jest przedstawione w tab. 5.1.
Tabela 5.1.

Zmiany dostępu do komponentów klasy bazowej

Dostęp do komponentów w klasie bazowej


Typ dziedziczenia
Publiczny Chroniony Prywatny
Publiczny Publiczny
Chroniony
Chroniony Chroniony Prywatny
Prywatny Prywatny

Z tab. 5.1 wynika, że widoczność komponentów klasy bazowej może być tylko zmniejszona.
Dziedziczenie publiczne nie zmienia widoczności każdego z komponentów klasy bazowej.
W przypadku dziedziczenia chronionego publiczne komponenty klasy bazowej są interpreto-
wane w klasie pochodnej jako komponenty chronione (w klasie bazowej), dlatego one są dostępne
w metodach klasy pochodnej i niedostępne dla obiektów klasy pochodnej.
Dziedziczenie prywatne powoduje interpretowanie publicznych oraz chronionych komponen-
tów klasy bazowej jako prywatnych. Z tego powodu metody klasy pochodnej już nie mają dostępu
do publicznych oraz chronionych komponentów klasy bazowej, a dla obiektów klasy pochodnej
zamyka się dostęp do publicznych komponentów klasy bazowej.
W następnym przykładzie w języku C++ klasa B ma typ dziedziczenia chronionego, a klasa
cC - typ dziedziczenia prywatnego od klasy cA. Publiczna metoda plus klasy cA może być wy-
wolywana dla obiektów klas cA, ale nie dla obiektów klasy cB lub cC:

class cA
{
int liczba;
public:
cA(int pocz);
int plus();
};
class cB : protected cA
{
public:
cB(int pocz);
};
class cC : private cA
{
public:
cC(int pocz);
};
cA::cA(int pocz) { liczba=pocz; }
int cA::plus() { liczba++; return liczba;}
cB::cB(int pocz) : cA(pocz){}
cC::cC(int pocz): cA(pocz){}

cA obA(5); cB obB(10); cC obC(15);


int nA=obA.plus();
int nB=obB.plus();//?? kompilator informuje o błędzie
int nC=obC.plus();//?? kompilator informuje o błędzie

5.3. Interfejs obiektu


Publiczna część komponentów klasy może być interpretowana jako interfejsna część obiek-
tu, tj. jako część, przez którą obiekt programu współdziała z otoczeniem.
Ograniczenie dostępu do komponentów daje programiście możliwość skupienia się na inter-
fejsowych częściach obiektów programu. Wyodrębnienie interfejsowych części obiektów jest bar-
dzo przydatne przy pracę grupy nad dużym projektem oraz przy korzystaniu z gotowych bibliotek
klas.

5.4. Hermetyzacja danych składowych


Całkowite ograniczenie bezpośredniego dostępu z zewnątrz do danych składowych jest na-
zywane hermetyzacją danych składowych. W przypadku hermetyzacji danych składowych dostęp
do danych składowych jest możliwy tylko przez publiczne metody.
Zwolennicy programowania zorientowanego obiektowo uważają, że hermetyzacja danych
składowych jest obowiązkowa.
Główną zaletą hermetyzacji danych składowych jest możliwość zmiany realizacji metod klasy
bez zmiany interfejsu klasy.
Na przykład, niech klasa Stos, opisująca stos do całkowitych liczb, ma trzy metody publicz-
ne: metodę push do położenia (zapisywania) na stos, metodę pop do zdejmowania ze stosu i me-
todę CzyStosPusty do sprawdzania czy stos jest pusty. Jest zupełne możliwie, by nie zmieniając
opisu tych metod, tj. nie zmieniając interfejsu klasy, zmienić dane hermetyzowane, na przykład
zrezygnować z korzystania z tablicy liczb całkowitych i przejść do struktury listowej.
Niżej w przykładach są pokazane warianty realizacji klasy Stos.
Przykład. Realizacja stosu na bazie tablicy liczb całkowitych
class Stos //opis klasy
{
int array[1000];
int index;
public:
Stos();
bool CzyStosPusty();
void push(int elem);
int pop();
};
Stos::Stos() { index=0; }
bool Stos::CzyStosPusty() { return(index==0); }
void Stos::push(int elem)
{
array[index]=elem;
index++; if (index>=1000) index=0;
}
int Stos::pop()
{
index--; if (index<0) index=999;
return(array[index]);
}

Wadą tej realizacji jest ograniczenie rozmiaru stosu liczbą 1000.


Przykład. Realizacja stosu na bazie struktury listowej
struct list //struktura listowa
{
int elem;
list *pNext;
};
class Stos //opis klasy
{
list *pList;
public:
Stos();
~Stos(); //destruktor
bool CzyStosPusty();
void push(int elem);
int pop();
};
Stos::Stos() { pList=NULL; }
Stos::~Stos() //destruktor
{
while (pList!=NULL)
{
list *pTemp=pList;
pList=pList->pNext; // usuwanie “z przodu”
delete [] pTemp; //zwolnienie pamięci
}
}
bool Stos::CzyStosPusty() { return(pList==NULL); }
void Stos::push(int elem)
{
list *pTemp=pList;
pList=new list;
pList->elem=elem;
pList->pNext=pTemp; // dodawanie do listy “z przodu”
}
int Stos::pop()
{
if (pList==NULL) return 0;
int value=pList->elem;
list *pTemp=pList;
pList=pList->pNext; // usuwanie listy “z przodu”
delete [] pTemp; //zwolnienie pamięci
return(value);
}
W nowej realizacji stosu już nie ma ograniczenia na rozmiar stosu, ale interfejs klasy nie
zmienił się.

5.5. Hierarchia klas


Dziedziczenie pozwala na budowanie hierarchii klas.
We wszystkich językach jest dozwolone wielodzietne dziedziczenie od jednej klasy oraz
dziedziczenie sekwencyjne (wielostopniowe) bez ograniczeń na liczbę klas w hierarchii.
Na kształt hierarchii należy nakładać ograniczenia. Kompilatory nie kontrolują kształtu hierar-
chii i ten obowiązek jest nakładany na programistę. Programista musi wyeliminować sytuacje, kie-
dy klasa dziedziczy sama od siebie przez łańcuch klas pochodnych.

5.6. Dziedziczenie od wielu rodziców


Dziedziczenie od wielu rodziców (ang. multiple inheritance) jest dziedziczeniem od dwóch i
więcej klas. Tylko język C++ daje możliwość dziedziczenia od wiele rodziców.
Dziedziczenie od wiele rodziców może spowodować sytuację powtórzenia komponentów ba-
zowej klasy w klasie pochodnej. Przykład hierarchii z niebezpiecznym dziedziczeniem jest pokaza-
ny na rys. 5.1. W przypadku standardowego dziedziczenia klasa cE będzie miała podwójne kom-
ponenty dziedziczone od klas cA i cB, z których jeden komplet komponentów będzie dziedziczony
przez klasę cC, a drugi komplet - przez klasę cD.
Żeby zapobiec powstaniu hierarchii z niebezpiecznym dziedziczeniem, w języku C++ jest
wprowadzony rodzaj „wirtualnej klasy bazowej”. Informacja o tym, że klasa jest wirtualną bazową
zmusza kompilator do analizy struktury hierarchii. Dla zaznaczenia wirtualnej klasy bazowej trzeba
skorzystać z kluczowego słowa virtual, ale atrybut virtual musi być zapisany nie w deklara-
cji wirtualnej klasy bazowej, lecz w deklaracjach wszystkich klas dziedziczonych od klasy bazowej
przed nazwą klasy bazowej.

Rys. 5.1. Przykład hierarchii klas

Na przykład w przypadku hierarchii klas ze strukturą rys. 5.1 dwie klasy cA i cB muszą być
klasami wirtualnymi bazowymi i dlatego klasy należy zadeklarować następująco:

class cA {...};
class cB {...};
class cC: virtual public cA, virtual public cB {...};
class cD: virtual public cA, virtual public cB {...};
class cE: public cC, public cD {...};

5.7. Przesłonięcie metody


Przesłonięciem (ang. overriding) pola, metody lub właściwości nazywa się powtórna deklara-
cja pola, metody lub właściwości w klasie pochodnej. Jasne, że w przypadku przesłonięcia metody
lub właściwości musi być zamieniona ich treść (implementacja). W przypadku przesłonięcia pola
danych mogą być zmienione prawa dostępu do pola.
Można przesłaniać komponenty klasy przodka tylko z dostępem publicznym i chronionym, tj.
możliwa jest zamiana tylko publicznej lub chronionej metody klasy bazowej.
W środowisku Borland C++ Builder przesłonięcie komponentu klasy daje możliwość wpro-
wadzenia zmian dla właściwości klas z graficznej biblioteki VCL (Visual Component Library).

5.8. Przeciążenie metody


Przeciążeniem (ang. overloading) metody nazywa się dodawanie do metod klasy nowej me-
tody, która ma nazwę już istniejącej metody, ale odróżniającej się zbiorem parametrów formalnych.
Kompilator wykonuje dodatkową pracę po identyfikacji potrzebnego egzemplarza metody w zależ-
ności od zbioru parametrów faktycznych.
Przeciążenie metod daje programiście udogodnienie przez to, że metody, które nieznaczne
się różnią, można nazywać jednakowo.
Przeciążenie metod można łączyć z dziedziczeniem, a mianowicie metoda przeciążająca
może znajdować się w klasie pochodnej. Jasne, że w takim przypadku przeciążenie metod jest
mniej widoczne.
Zwykle metody podobne po nazwie znajdują się w tej samej klasie. Rzadko ma miejsce
przeciążenie metody w klasie pochodnej w stosunku do klasy bazowej.
W przykładzie przeciążenia w języku C++ w klasie Licznik są dwie metody Set; jedna z
nich przyjmuje początkową wartość w formie łańcucha tekstowego, a druga - w formie liczby cał-
kowitej:
class Licznik //początek opisania klasy
{
int liczba; //opis pola klasy
public:
Licznik(); //opis konstruktora klasy
void Set(int value); //metoda przeciążona
void Set(char *pText); //metoda przeciążająca
. . .
};
Licznik::Licznik() //konstruktor klasy
{
liczba=0; //inicjalizacja pola klasy
}
void Licznik::Set(int value) //metoda przeciążona
{
if (value>=0 && value<100) liczba=value;
}
void Licznik::Set(char *pText) //metoda przeciążająca
{
int temp;
sscanf(pText,”%d”,&temp);
Set(temp);
}

5.9. Polimorfizm nazw


Przeciążanie i przesłonięcie metod jest wykazaniem tzw. polimorfizmu nazw.
W programowaniu zorientowanym obiektowo polimorfizm nazw jest właściwością, która ze-
zwala nazywać jednakowo podobne działania.
W przypadku przeciążenia metody kompilator wybiera jeden z wariantów w zależności od
liczby i typów faktycznych argumentów metody.
W przypadku przesłonięcia komponentu kompilator przygotowuje dodatkowy fragment kodu
w celu wyboru wariantu w czasie wykonywania programu w zależności od faktycznego typu obiek-
tu.

5.10. Metody wirtualne


Odwołanie do metody przesłoniętej w każdej klasie łańcucha klas może sprawić problem w
przypadku przekazywania do podprogramu wskaźnika na obiekt, jeżeli obiekt może być typu każ-
dej z klas łańcucha. Oczywiście można zdefiniować grupę podprogramów z trochę zmienionymi
nazwami w ilości, ile może być typów obiektów (wskaźników).
Lepszym rozwiązaniem problemu jest zaznaczenie metody jako wirtualnej w pierwszej klasie
łańcucha. Pośród metod przesłoniętych w łańcuchu klas będzie wybrana ta, która odpowiada typu
obiektu (typu wskaźnika na obiekt). Żeby realizować dynamiczną obliczenie adresu faktycznej me-
tody w czasie wykonania programu kompilator dodaje w miejscu wywołania fragment kodu z adre-
sem tablicy wskaźników na metody, tzw. tablicy vtable.
W klasie bazowej metoda wirtualna może być zarówno pusta jak i niepusta.
Ponowna definicja metody wirtualnej w klasach pochodnych nie jest konieczna. W przypadku
ponownej definicji metody wirtualnej w klasach pochodnych liczba i typy argumentów oraz typ me-
tody powinny zostawać bez zmiany. Jasne, że przy każdej nowej definicji istota metody z reguły
powinna być inna.
Metoda wirtualna nie może być metodą stałą. Metodą wirtualną może być metoda zaprzyjaź-
niona z inną klasą.

W języku C++ metoda wirtualna jest oznaczana słowem kluczowym virtual.


Na przykład:

virtual int func(void); // metoda wirtualna


W języku C++ występuje wyjątkowa sytuacja, kiedy typ metody wirtualnej może być zmienio-
ny. Warunkami tego są: po pierwsze, typ metody w klasie bazowej musi być referencją lub wskaź-
nikiem na typ bazowy, po drugie, typ metody w klasie pochodnej musi być referencją lub wskaźni-
kiem na typ klasy pochodnej.

5.11. Metody dynamiczne


Dla metod wirtualnych kompilator buduje w każdej klasie tablicę wskaźników na metody, tzw.
tablicę vtable. Kopiowanie vtable - tablic od klasy do klasy w łańcuchu klas pochodnych powoduje
szybkie znalezienie metody w czasie działania aplikacji.
Żeby zmniejszyć miejsce, które zajmują vtable - tablicy w środowisku Borland C++ Builder
dla klas dziedziczonych od klasy TObject na topie hierarchii, są wprowadzone tzw. metody dyna-
miczne. Do definicji metody dynamicznej służy atrybut __declspec(dynamic). Kompilator zapi-
suje wskaźnik na metodę dynamiczną jeden raz w tablicy tej klasy, w której jest zdefiniowana me-
toda. Wadą takiego rozwiązania problemu zmniejszenia miejsca jest to, że metody są szukane
metodą kolejnego przeglądu vtable - tablic klas bazowych, co wydłuża czas wywołania metody.

5.12. Klasa polimorficzna i klasa abstrakcyjna


Metoda wirtualna nazywa się metodą czysto wirtualnej, jeżeli wskaźnik na nią jest równy ze-
ru, innymi słowy metoda czysto wirtualna jest metodą z nieistniejącym kodem.
Klasa nazywa się klasą polimorficzną, jeżeli zawiera metodę wirtualną lub czysto wirtualną
lub dziedziczy od klasy zawierającej metodę wirtualną lub czysto wirtualną.
Budując hierarchie klas programista może zdecydować na wprowadzenie na topie hierarchii
tzw. klasy abstrakcyjnej. Klasa nazywa się klasą abstrakcyjnej, jeżeli zawiera przynajmniej jedną
metodę czysto wirtualną.
Klasę abstrakcyjną można wykorzystać tylko jako klasę bazową. Nie można definiować
obiekty typu klasy abstrakcyjnej. Typy podprogramów i ich argumenty nie mogą być typu klasy
abstrakcyjnej.
Klasa, która dziedziczy od klasy abstrakcyjnej, staje się też klasą abstrakcyjną, jeżeli nie
wszystkie metody czysto wirtualne będą zdefiniowane w klasie pochodnej.
W języku C++ klasa, która zawiera przynajmniej jedną metodę czysto wirtualną, jest klasą
abstrakcyjną. Metoda czysto wirtualna jest metodą z domyślną wartością równej 0. Na przykład:

virtual int func(void)=0; // metoda czysto wirtualna

W języku C++ można definiować wskaźnik na klasę abstrakcyjną. Z tego powodu jest
dozwolona referencja typu klasy abstrakcyjnej (ale w tym przypadku kompilator nie alokuje tymcza-
sowego obiektu typu klasy abstrakcyjnej).
Przykład z poprawnymi i niepoprawnymi definicjami obiektów:

class cA
{ // klasa abstrakcyjna.
int paramA;
public:
get() { return paramA; }
set(int p) { paramA = p; fA2(); }
virtual void fA1(int)=0;//metoda czysto wirtualna
virtual void fA2()=0; //metoda czysto wirtualna
}
cA x;//błąd
cA* sptr;// poprawnie
cA f(); // błąd
int g(cA s);//błąd
cA& h(cA&);// poprawnie

W następującym przykładzie klasa cB dziedziczona od klasy cA staje się klasą abstrakcyjną,


ponieważ nie wszystkie metody czysto wirtualne zdefiniowane w pochodnej klasie cB:
class cB : public cA
{// klasa jest pochodna od klasy abstrakcyjnej
int paramB;
public:
void fA1(int) { } //zdefiniowano pustą metodę
//nie ma definicji metody fA2
}

5.13. Lokalna konwersja typów (rzutowanie typów)


W językach ze wskaźnikami i referencjami często pojawia się potrzeba lokalnej konwersji
(zamiany) typów obiektów. W programowaniu obiektowym konwersja typów obiektów nazywana
też rzutowaniem typów jest pożyteczna przy dziedziczeniu klas.
Kompilator często konwertuje typy wskaźników na obiekty automatycznie, a w przypadku
niejasnych sytuacji podaje odpowiednie komunikaty.
W języku C++ występuje specjalne słowo kluczowe dynamic_cast dla konwersji w obrę-
bie hierarchii klas pochodnych. Aby można było korzystać z dynamic_cast, klasa bazowa po-
winna być polimorficzna.
Składnia konwersji ma postać:
dynamic_cast < typ_nowy >(argument)
Element składni argument musi być wskaźnikiem lub referencją, a element składni typ_nowy
- wskaźnikiem lub referencją, albo wskaźnikiem amorficznym void*.
Konwersja za pomocą dynamic_cast przydaje się do konwersji typu wskaźnika na obiekt z
typu klasy bazowej na typ klasy pochodnej. Na przykład za pomocą dynamic_cast można wy-
słać do provedury wskaźnik na obiekt klasy bazowej, podczas gdy argument procedury posiada
typ wskaźnika na klasę pochodną. Korzystanie z dynamic_cast pokazane jest w przykładzie:

class сB
{
public:
virtual void f(void) {}; // Metoda wirtualna
};
class cD : public cB { };
cD d; cD* pd=NULL;
cB* pb=&d; //automatyczna konwersja typu wskaźnika
pd=dynamic_cast<cD*>(pb); //konwersja typu wskaźnika

5.14. Kolejność wywoływania konstruktorów klas


Wywoływanie konstruktora następuje w momencie, kiedy program wchodzi w zakres wi-
doczności obiektu programowego. Konstruktory globalnych obiektów są wywoływane przed wywo-
łaniem głównej procedury (main, WinMain itp.).
W języku C++ w implementacji klasy pochodnej należy wskazywać konstruktor klasy bazo-
wej, jeżeli klasa bazowa ma więcej niż jeden konstruktor. Przykład:

class cA
{
int liczba;
public:
cA() {liczba=0;}
cA(int licz) {liczba=licz;}
...
};
class cB : public cA
{
cB(int licz); ...
};
cB::cB(int licz):cA(licz)
{
...
}
Dla obiektu klasy pochodnej wywoływane są najpierw konstruktory klas bazowych w tej ko-
lejności, w której są zapisane w definicji klasy, a na końcu jest wywoływany konstruktor danej kla-
sy.
Omówiona kolejność może być zmieniona, jeżeli przed bazową klasą będzie napisane słowo
virtual. Klasy z prefiksem virtual mają priorytet przed innymi klasami bazowymi, tj. ich kon-
struktory są wywoływane w pierwszej kolejności.

5.15. Wywołanie konstruktorów z listy inicjacyjnej


W języku C++ dla klasy pochodnej lista inicjacyjna nie zmienia ogólnej kolejności inicjalizacji:
najpierw wywołanie konstruktorów i inicjalizacja pól klas bazowych, a na końcu inicjalizacja pól
klasy pochodnej. Dlatego kompilator najpierw wybiera z listy inicjacyjnej elementy z klasami
bazowymi w kolejności definicji tych klas w klasie pochodnej, potem inicjalizuje pola klasy
pochodnej wskazane na liście inicjacyjnej, a po zakończeniu tej listy inicjalizacja odbywa się
według kolejności operatorów w ciele konstruktora. Z tego wynika, że jeżeli w liście inicjacyjnej
konstruktora klasy pochodnej znajduje się inicjalizacja pola klasy bazowej nową wartością pola
klasy pochodnej, to ta inicjalizacja może nie mieć miejsca.
Następny przykład ilustruje ten szczególny błąd w liście inicjacyjnej:

class cA
{
protected:
int a;
public:
cA(int k);
};
cA::cA(int k) { a=k; }
class cB: private cA
{
int b;
public:
cB(int i): b(2*i),cA(b){};
};
main()
{
cB obB(10);
}

Wynikiem inicjalizacji obiektu obB będzie wartość 20 pola b, a dla pola a - wartość przypad-
kowa, a nie 20, ponieważ kompilator najpierw weźmie element cA(b) listy inicjacyjnej i umieści w
polu a liczbę równą przypadkowej wartości zmiennej b, i tylko w następnym kroku kompilator
weźmie element b(2*i) tej listy.

5.16. Destruktory wirtualne


W hierarchii klas z sekwencyjnym dziedziczeniem występuje problem z wywołaniem po-
prawnego łańcucha destruktorów, jeżeli wskaźnik na obiekt jest deklarowany jako wskaźnik na
klasę bazową a obiekt ma typ klasy pochodnej. W języku C++ do rozwiązania tego problemu sto-
suje się destruktory wirtualne (ang. virtual destructors). Destruktor wirtualny klasy bazowej musi
być oznaczony przez atrybut virtual, na przykład:

virtual ~cKlasa();

W takim przypadku destruktory we wszystkich klasach pochodnych od danej klasy bazowej


będą też wirtualne, niezależne od tego, czy jest napisane przed nimi słowo virtual, czy nie.
Kolejność usuwania obiektów przy korzystaniu z wirtualnych destruktorów pokazuje przykład:
char wynik[]=”oooooooooo”;
int index=0;
class cA
{
public:
virtual ~cA() // destruktor wirtualny
{
wynik[index++]=’A’;
}
};
class cB : public cA
{
public:
~cB() // ten destruktor też jest wirtualny
{
wynik[index++]=’B’;
}
};
class cC : public cB
{
public:
~cC() // ten destruktor też jest wirtualny
{
wynik[index++]=’C’;
}
};
int main()
{
cA *p[3]; //wskaźniki na obiekty klasy cA
p[0] = new cB;//alokacja obiektu klasy cB
p[1] = new cC;//alokacja obiektu klasy cC
p[2] = new cA;//alokacja obiektu klasy cA
delete p[0]; //łańcuch z destruktorów cB i cA
delete p[1]; //łańcuch z destruktorów cC, cB i cA
delete p[2]; //łańcuch z destruktora cA
return 0; // tutaj ustawić breakpoint
// oraz sprawdzić że wynik[]=”BACBAAoooo”;
}

Po wykonaniu programu z przykładu w tablicy wynik będzie się znajdował tekst BACBA-
Aoooo, co świadczy o poprawnej kolejności wywoływania destruktorów.
6. Metody operatorowe

6.1. Przeciążanie operatorów


Przeciążanie operatorów języka programowania zorientowanego obiektowo wykorzystuje się
bardzo często. Za pomocą prostych jedno-, dwu-, trzy - symbolowych operatorów można wywołać
skomplikowane algorytmy opracowywania złożonych struktur danych, dzięki czemu tekst programu
staje się przejrzysty.
Na przykład porównamy realizacje funkcji sumy dla liczb zespolonych przez podprogram i
przez przeciążony operator +.
W pierwszym wariancie będzie wywoływany podprogram SumaZespolonych z trzema ar-
gumentami (dwa składniki i wynik):

SumaZespolonych(a,b,c);

przed którymi musi być następująca definicja:

typedef struct {double re, im;} s_compl;


void SumaZespolonych(s_compl &a, s_compl &b, s_compl &c)
{
c.re=a.re+b.re; c.im=a.im+b.im;
}
s_compl a={0.0,1.0},b={1.0,2.0},c;

W drugim wariancie (z przeciążonym operatorem +) fragment tekstu zawierający sumowanie


wygląda następująco:

с=a+b;

ale przedtem trzeba zdefiniować:

class s_compl
{
double re, im;
public:
s_compl(double re, double im);
s_compl& operator+(s_compl& arg);
};
s_compl::s_compl(double re, double im)
{
this->re=re; this->im=im;
};
s_compl& s_compl::operator+(s_compl& arg)
{
re+=arg.re; im+=arg.im;
};
s_compl a(0.0,1.0),b(1.0,2.0),c;

W tym przykładzie dobrze widać wady i zalety programowania zorientowanego obiektowo: w


miejscu gdzie korzystamy z klas, tekst c=a+b ma bardzo przejrzystą postać, ale tekst definicji kla-
sy jest trudny do napisania. Prawda, w środowiskach do programowania obiektowego są przygo-
towane tysiące klas.
Należy zwrócić uwagę, że o przeciążaniu operatorów mówimy nie tylko w sytuacji, kiedy w
klasie są zdefiniowane dwa lub więcej operatorów, ale nawet, kiedy w klasie jest zdefiniowany tyl-
ko jeden operator. Przyczyna tego faktu tkwi w tym, że każdy operator języka programowania ma
w każdej klasie niejawnie dodaną definicję.
Jest dopuszczalne dziedziczenie wszystkich przeciążonych operatorów oprócz przeciążone-
go operatora „=”.

6.2. Metoda operatorowa klasy


Metoda operatorowa zawiera słowo kluczowe operator i ma następującą syntaktyczną
konstrukcję:

operator symbol_operatorowy ( _argumenty )


{
_działania
}

Liczba argumentów metody operatorowej jest z reguły o jeden mniejsza niż liczba argumen-
tów samego operatora. Za dodatkowy argument operatora służy obiekt, na którego rzecz jest wy-
woływana metoda. W treści _działania metody operatorowej ten argument jest reprezentowany
przez obiekt (*this) lub wewnętrzny wskaźnik this.
Dla jednoargumentowych (unarnych) operatorów argument jest reprezentowany przez
obiekt, na którego rzecz jest wywoływana metoda, a metoda operatorowa jest metodą bezargu-
mentową.
Który z argumentów dwuargumentowego operatora jest reprezentowany przez obiekt, na
rzecz którego jest wywoływana metoda, a który jest argumentem, zależy od kierunku wykonywania
lańcucha podobnych operatorów.
Dla kierunku „lewy” obiektem jest prawy argument operatora, a argumentem - lewy argument
operatora.
Dla kierunku „prawy” obiektem jest lewy argument operatora, a argumentem - prawy argu-
ment operatora.
A więc, argumentem jest zawsze pierwszy dla wskazanego kierunku argument operatora.
Jeżeli wynik wykonania operatora nie jest obiektem, na którego rzecz jest wywoływana me-
toda, to liczba argumentów metody operatorowej musi być równa liczbie argumentów samego ope-
ratora.
W tabeli 6.1 są wskazane kierunek wykonywania operatorów i ich starszeństwo międzygru-
powe w języku C++. Operatory pierwszego wierszu tabelę są wykonywane w pierwszej kolejności.
W zakresie jednej grupy starszeństwo operatorów jest jednakowe i takie operatory są wykonywane
w kierunku analogicznym do kierunku wykonywania osobnego operatora tej grupy.
Można zauważyć, że w tabeli 6.1 powtarzają się operatory, które mogą być i jednoargumen-
towe (unarne) i dwuargumentowe (np. „+”, „-”). Pierwsze od początku tabeli wystąpienie operatora
jest jednoargumentowe (unarne).
Tabela 6.1
Kierunek wykonywania i starszeństwo operatorów

Operatory Kierunek Starszeństwo


() [] -> :: . lewy 1 (pierwszy)
! ~ + - ++ -- & * prawy 2
sizeof new delete
.* ->* lewy 3
* / % lewy 4
+ - lewy 5
<< >> lewy 6
< <= > >= lewy 7
== != lewy 8
& lewy 9
^ lewy 10
| lewy 11
&& lewy 12
|| prawy 13
?: lewy 14
= *= /= %= += -= &= prawy 15
^= |= <<= >>=
, lewy 16 (ostatni)

Kompilator języka C++ zabrania przeciążania operatorów „.”, ”.*”, „::”, „?:”.

6.3. Zaprzyjaźniona metoda operatorowa


Przeciążona metoda operatorowa może występować z atrybutem friend, pokazującym, że
operator języka programowania jest zaprzyjaźniony z daną klasą.
Zaprzyjaźniona metoda operatorowa jest wykorzystywana w przypadku, kiedy pierwszy ar-
gument operatora nie jest obiektem, na którego rzecz jest wywoływana metoda. W związku z tym
zaprzyjaźniona metoda operatorowa zawsze ma liczbę argumentów, która równa się liczbie argu-
mentów samego operatora.

6.4. Operatory przeciążone


6.4.1. Operatory unarne

W języku C++ operatory :


! negacja bitowa, * operator wyłuskania,
++ dodawanie 1, ~ dopełnienie bitowe,
-- odejmowanie 1, - negacja,
+ plus unarny,
należą do operatorów unarnych.
Operatory „++” i „--” mogą być przedrostkowe i przyrostkowe. W środowisku C++ Builder wy-
stępuje ograniczenie w przeciążaniu operatorów przyrostkowych: drugi argument może być tylko
typu int.
Zamiast zwykłej formy zapisu któregokolwiek unarnego operatora O można korzystać z nie-
zwykłej, ale poprawnej formy

a.operatorO()

jeżeli a jest obiektem klasy, w której jest zdefiniowany przeciążony operator O.


6.4.2. Operator =
W środowisku C++ Builder niejawnie dodany operator „=” (operator przepisania) kopiuje pola
klasy-źródła (drugiego argumentu operatora). Niejawnie dodany operator „=” dowolnej klasy cA ma
definicję:
cA& cA::operator=(const cA& b)
{
kopiowanie pól referencji b
}

Jeśli klasa ma konstruktor alokujący pamięć dla swoich pól typu wskaźnikowego, to z reguły
klasa powinna mieć przeciążony operator „=”. W przeciwnym razie dla konstrukcji a=b niejawnie
dodany operator kopiuje z b na a tylko pola-wskaźniki bez kopiowania tych obiektów, na które
wskazują.
Dziedziczenie przeciążonego operatora „=” nie jest dopuszczalne. Wywołanie przeciążonego
operatora „=”, np. a=b, jest interpretowane przez kompilator jako

a.operator=(b)

W swoim programie można korzystać z takiej niezwykłej formy zapisu operatora „=”.
W przykładzie 6.1 jest pokazana klasa typu „tablica dynamiczna” z przeciążonym operatorem
„=”.
Przykład 6.1. Klasa z przeciążonymi operatorami
class dynInt
{
protected:
int *m_pData; // wskaźnik do tablicy
int m_nSize; // rozmiar aktualny
int m_nMaxSize; // rozmiar zaalokowany
int m_nGrowBy; // krok przy zwiększeniu
public:
dynInt();
dynInt(dynInt& ob); //konstruktor kopiujący
dynInt(int size,int growBy);
~dynInt();
int fGetSize();
void fSetSize(int newsize, int nGrowBy = -1)
void fSetAtGrow(int nIndex, int newElement);
dynInt& operator=(const dynInt &C);
int& operator[](int nIndex);
const int& operator[](int nIndex) const;
int operator()(int nBegIndex,int nNumber);
};
dynInt::dynInt()
{
m_pData = NULL;
m_nSize = m_nMaxSize = m_nGrowBy = 0;
}
dynInt::dynInt(dynInt& ob)
{//konstruktor kopiujący
m_nSize=m_nMaxSize=ob.fGetSize();
m_pData=(int*) new char[m_nSize*sizeof(int)];
for (int i=0;i<m_nSize;i++)
m_pData[i]=ob[i];
}
dynInt::dynInt(int size,int growBy)
{
m_nSize = m_nMaxSize = size;
m_nGrowBy = growBy;
m_pData=(int*) new char[m_nMaxSize*sizeof(int)];
memset(m_pData,0,size*sizeof(int)); //zerowanie
}
dynInt::~dynInt()
{
delete [] (char*)m_pData;
}
int dynInt::fGetSize()
{
return m_nSize;
}
void dynInt::fSetSize(int newsize,int nGrowBy = -1 )
{
if (nGrowBy != -1)
m_nGrowBy = nGrowBy;
if (newsize<=0)
{// zerować obiekt
delete [] (char*)m_pData;
m_pData = NULL;
m_nSize = m_nMaxSize = 0;
}
else
if (m_pData == NULL)
{ // zaalokować pierwszy raz
m_pData = (int*) new char[newsize*sizeof(int)];
memset(m_pData,0,newsize*sizeof(int)); //zerowanie
m_nSize = m_nMaxSize = newsize;
}
else
if (newsize <= m_nMaxSize)
{// alokacja jest niepotrzebna
if (newsize > m_nSize)
{//inicjalizacja dodatkowa
memset(&m_pData[m_nSize],0,(newsize-m_nSize) * sizeof(int));
}
m_nSize = newsize;
}
else
{// alokacja jest potrzebna
int nNewMax;
if (newsize<m_nMaxSize+m_nGrowBy)
nNewMax=m_nMaxSize+m_nGrowBy; // plus krok
else
nNewMax=newsize;
int *pNew=(int*) new char[nNewMax*sizeof(int)];
// kopiowanie danych dawnych
memcpy(pNew,m_pData,m_nSize*sizeof(int));
// budowanie elementów dodatkowych
if (newsize > m_nSize)
{
memset(&pNewData[m_nSize],0,(newsize-m_nSize) * sizeof(int));
// niszczenie alokacji poprzedniej
delete [] (char*)m_pData;
m_pData = pNewData;
m_nSize = newsize;
m_nMaxSize = nNewMax;
}
}
}
void dynInt::fSetAtGrow(int nIndex, int newElement)
{
if (nIndex>=0)
{
if (nIndex>=m_nSize)
fSetSize(nIndex+1,4);
m_pData[nIndex]=newElement;
}
}
dynInt& dynInt::operator=(const dynInt& ob)
{
if (&ob==this) return *this; //sytuacja “A=A;”
if (m_pData) delete [] (char *)m_pData;
m_nSize=m_nMaxSize=ob.fGetSize();
m_pData=(int*) new char[m_nSize*sizeof(int)];
for (int i=0;i<m_nSize;i++)
m_pData[i]=ob[i];
return *this;
};
int& dynInt::operator[](int nIndex)
{
if (nIndex >= 0 && nIndex < m_nSize)
return m_pData[nIndex];
else
return 0;
}
const int& dynInt::operator[](int nIndex) const
{
if (nIndex >= 0 && nIndex < m_nSize)
return m_pData[nIndex];
else
return 0;
}
int dynInt::operator()(int nBegIndex,int nNumber)
{
if (nBegIndex<0) nBegIndex=0;
int nEnd=nBegIndex+nNumber;
if (nEnd>m_nSize) nEnd=m_nSize;
int suma=0;
for (int i=nBegIndex;i<nEnd;i++)
suma+=m_pData[i];
return suma;
}
6.4.3. Operator ()
Przeciążony operator „()” jest podobny do globalnego operatora wywołania funkcji. Zasadni-
cza różnica między nimi polega na tym, że lewym argumentem operatora wywołania funkcji jest
nazwa funkcji lub wskaźnik na funkcję, podczas gdy lewym argumentom przeciążonego operatora
„()” jest obiekt.
Za pomocą przeciążonego operatora „()” można zbudować własną konstrukcję podobną do
konstrukcji wywołania funkcji.
Na przykład dla obiektów klas typu „tablica dynamiczna” można realizować którąkolwiek
funkcję typową, na przykład funkcję sumowania, przez przeciążony operator „()”. W przykładzie 6.1
pokazana definicja dynamicznej klasy dynInt z przeciążonym operatorem „()”.
Po definicji klasy dynInt można skorzystać z wygodnego zapisu dla funkcji sumowania:

dynInt oB(100,10);//definicja obiektu oB na 100 elementów


. . . //działania z obiektom oB
int Sum=oB(5,25);//sumowanie 25 elementów

Wywołanie oB(5,25) jest interpretowane przez kompilator jako


oB.operator()(5,25) .
6.4.4. Operator []
Przeciążony operator „[ ]” jest podobny do globalnego operatora indeksowania. Zasadnicza
różnica między nimi polega na tym, że lewym argumentem operatora indeksowania jest wskaźnik
na tablicę, podczas gdy lewym argumentem przeciążonego operatora „[ ]” jest obiekt.
Dwie definicje przeciążonego operatora „[ ]” podaje przykład 6.1. Druga definicja operatora „[
]” dla obiektów stałych jest potrzebna do operatorów przepisania i konstruktorów kopiujących. Ko-
rzystanie z wariantów przeciążonego operatora indeksowania pokazuje następujący tekst:
dynInt oB(100,10);//definicja obiektu oB na 100 elementów
... //działania z obiektom oB
int Elem5=oB[5];//pobranie elementu w wariancie „stałym”
oB[10]=7;//zapis elementu w wariancie „zmiennym”

Wywołanie oB[5] jest interpretowane przez kompilator jako


oB.operator[](5) .
6.4.5. Operator ->
Przeciążony operator „->” jest podobny do globalnego operatora pośredniego wyboru kom-
ponentu. Zasadnicza różnica między nimi polega na tym że lewym argumentem operatora pośred-
niego wyboru komponentu jest wskaźnik na obiekt, podczas gdy lewym argumentem przeciążone-
go operatora „->” jest obiekt.
Za pomocą przeciążonego operatora „->” można zbudować w swoim programie własny sys-
tem oznaczeń do wyboru następnego obiektu. Coś podobnego do:
obiekt1 -> obiekt2 -> obiekt3 -> obiekt4.
Wynikiem przeciążonego operatora „->” musi być wskaźnik na obiekt, lub obiekt, ale inny niż
ten, na rzecz którego jest wywołany operator.
Metoda operatorowa „operator->” musi być bezargumentowa, z jednym niejawnym we-
wnętrznym argumentem w postaci obiektu.
Operator A->B z tekstu programu, gdzie A jest obiektem, a B jest polem innego obiektu X,
jest wykonywany przez kompilator w dwóch krokach, a mianowicie:
Krok 1. Wykonanie przeciążonego operatora „->” z klasy obiektu A, w wyniku którego musi
powstać wskaźnik pX na obiekt X albo inny obiekt Y.
Krok 2. Wykonanie globalnego operatora pośredniego wyboru komponentu pX->B dla pola
B obiektu X.
Jeśli wynikiem kroku 1 będzie nie wskaźnik, a obiekt Y, to kompilator powtarza krok 1 dla
przeciążonego operatora „->” z klasy obiektu Y, aż wynikiem nie będzie wskaźnik. Ostatni wskaź-
nik nie powinien być wskaźnikiem na obiekt A. Jeżeli ten wskaźnik będzie wskaźnikiem na obiekt
A, to kompilator tego nie zauważy, a program wejdzie w nieskończoną pętlę.
Następna definicja w klasie Z przeciążonym operatorem „->”:

Z* operator->() { return this; }

daje niezwykły wynik:


wyrażenie Z->a, w którym Z jest obiektem, a nie wskaźnikiem jak zwykle, staje się równoznaczne
wyrażeniu Z.a.
6.4.6. Operatory new i delete
W definicji klasy operatory new, new[], delete lub delete[] mogą być przeciążone.
Składnie tych operatorów wyglądają dokładne tak jak konstrukcje globalnych wariantów tych ope-
ratorów, na przykład:

cA* pA=new cA;


cB* pB=new cB[10];
. . .
delete pA;
delete [] pB;

Kompilator odróżnia operator przeciążony od globalnego w momencie, kiedy wywołuje kon-


struktor (w przypadku new) lub destruktor (w przypadku delete) wskazanej klasy. Jeżeli w tej
klasie lub w klasie, od której ta klasa jest pochodną, występuje operator przeciążony, to kompilator
korzysta z operatora przeciążonego, a nie z globalnego. Wewnątrz podprogramów operatorów
przeciążonych można używać innych operatorów new, delete dlatego że one powinny dotyczyć
innych klas (żeby nie powstała nieskończona pętla).
Jeżeli w danej klasie znajduje się przeciążony operator new lub delete, a trzeba alokować
obiekt tej klasy za pomocą globalnego operatora, to należy użyć kwalifikatora globalności, tj. użyć:
::new, ::new[], ::delete lub ::delete[].
Za pomocą przeciążonego operatora new lub new[] można połączyć alokację z inicjalizacją
albo przekazać funkcji new argumenty dodatkowe. Przeciążenie operatorów delete lub dele-
te[] służy warunkowemu usuwaniu obiektów.
Korzystanie z przeciążonego operatora new do łączenia alokacji z inicjalizacją pokazuje
przykład 6.2. Przy alokacji nowego obiektu klasy cA bufor (pole buffer) będzie zapełniony sym-
bolami „.” (kropka). Usuwanie tego bufora przez operator delete jest uzależnione od wartości
flagi (pole flaga).
Należy zwrócić uwagę, że argument size typu size_t w deklaracji operatora new jest wy-
syłany przez kompilator i jego wartość równa się rozmiarowi pól klasy w bajtach.
Debugowanie przykładu 6.2 pokazuje, że kompilator rozmieszcza instrukcje w następującej
kolejności: instrukcje operatora new, po nich instrukcje konstruktora klasy, następnie instrukcje
destruktora klasy, a po nich instrukcje operatora delete.
Przykład 6.2. Łączenie alokacji z inicjalizacją
class cA
{
char * buffer;
public:
cA();
~cA();
void* operator new(size_t size);
void operator delete(void* ptr);
bool flaga;
};
cA::cA()
{
flaga=false;
}
cA::~cA()
{
}
void* cA::operator new(size_t size)
{
cA *ptr=(cA *)::new char[size];//operator globalny
ptr->buffer=::new char[1024];//operator globalny
for (int i=0;i<1024;i++) ptr->buffer[i]='.';
return ptr;
}
void cA::operator delete(void* ptr)
{
if (((cA *)ptr)->flaga)
{
::delete [] ((cA *)ptr)->buffer;//operator globalny
((cA *)ptr)->buffer=NULL;
::delete ptr;//operator globalny
}
}
//działania sprawdzić w trybie debugowania klawiszem F7
cA *pobA=new cA;
delete pobA;
pobA->flaga=true;
delete pobA;

Korzystanie z przeciążonego operatora new w celu przekazania operatowi argumentów jest


pokazane w przykładzie 6.3. Przy alokacji nowego obiektu klasy cB bufor (pole buffer) będzie
miał wskazany rozmiar (argument volumn) i będzie zapełniony symbolami według argumentu
symbol.
Przykład 6.3. Przekazywanie argumentów operatowi new
class cB
{
char * buffer;
public:
~cB();
void* operator new(size_t size,int volumn,int symbol);
};
cB::~cB()
{
delete [] buffer; // operator globalny
buffer=NULL;
}
void* cB::operator new(size_t size,int volumn,int symbol)
{
cB *ptr=(cB *)new char[size]; // operator globalny
ptr->buffer=new char[volumn]; // operator globalny
for (int i=0;i<volumn;i++)
ptr->buffer[i]=(char)symbol;
return ptr;
}
//działania sprawdzić w trybie debugowania klawiszem F7
cB *pobB=new(1024,’.’) cB;
delete pobB;

Należy zwrócić uwagę, że w deklaracjach operatorów są wskazane ich typy: void* dla new
oraz void dla delete.

Chociaż jest możliwe przeciążenie operatora referencji „&” i operatora wyłuskania „*” [Kisile-
wicz], nie ma powodów, żeby przeciągać wskazane operatory.
7. Wzorce (szablony) klas i funkcji

7.1. Wzorce (szablony) klas

Wzorce (szablony) klas odwzorowują grupę klas podobnych strukturalnie i odróżniających


się tylko typem danych podlegających opracowywaniu. Znane są przykłady bardzo skutecznego
korzystania ze wzorców klas dla tablic z różnymi typami elementów i dla strumieni z różnymi typa-
mi danych.
Wzorce (szablony) klas lub funkcji mogą nazywać się parametryzowanymi klasami i funkcji.
Nie wszystkie języki zorientowane obiektowo mają możliwość stosowania wzorców. Wzorce
klas i funkcji można definiować w języku C++.
Definicja wzorca klasy składa się ze słowa kluczowego template, listy parametrów formal-
nych w nawiasach „<”, „>” i definicji klasy, w tekście której można korzystać z parametrów formal-
nych. Jeżeli parametr formalny jest klasą, to musi być przed nim napisane słowo kluczowe class, w
innych przypadkach musi być napisane słowo kluczowe typename.
Struktura wzorca klasy (np. cW) ma następującą postać:

template <class typ_formalny> class cW


{
public:
cW(){};
cW(typ_formalny *p, . . .){. . .};
~cW(){. . .};
protected:
. . .
private:
. . .
};
Na końcu definicji wzorca klasy musi znajdować się symbol „;”.
W implementacji metody wzorca klas w tym miejscu, gdzie powinna być podana nazwa kla-
sy, trzeba napisać nazwę wzorca klasy, a w nawiasach „<”, „>” listę parametrów jako listę parame-
trów faktycznych, tj. bez słów kluczowych class lub typename.
Implementacja metody wzorca klas może być napisana w jednym z dwóch wariantów:
1) jako „wzorcowa” metoda, tj. jako metoda, która opisuje podobne działania dla grupy me-
tod,
2) jako metoda dla konkretnego typu danych.
Struktura „wzorcowej” metody wzorca klasy (np. met) ma następującą postać:

template <class typ_formalny> cW<typ_formalny>::met(. . .)


{
. . .
}

W środowisku C++ Builder implementacja „wzorcowej” metody wzorca klas umieszczona w


pliku „.cpp” nie jest widoczna z innych plików „.cpp” (jest to błąd kompilatora). Dlatego należy im-
plementację „wzorcowej” metody umieszczać wewnątrz definicji klasy (tzn. w pliku „.h”). Można też
umieścić implementację „wzorcowej” metody w tym pliku „.cpp”, w którym są zdefiniowane obiekty
wzorca, ale takie rozwiązanie problemu nie jest dobrym.
Jeżeli działania wewnątrz metody wzorca klas zależą od typu danych, to zamiast „wzorco-
wej” metody należy napisać grupę metod dla każdego typu danych. Struktura każdej z takich im-
plementacji metod (np. met2) wzorca klas ma następującą postać:
cW<typ_faktyczny>::met2(. . .)
{
. . . //działania, które zależą od typu „typ_faktyczny”
}
Definicja obiektu
Przy definicji obiektu typu wzorca klas trzeba podawać parametry faktyczne. Definicja obiek-
tu klasy opisanej przez wzorzec (np. cW) ma następującą postać:

cW<typ_faktyczny> ob;

7.2. Wzorce (szablony) funkcji


Wzorce (szablony) funkcji odwzorowują grupę funkcji podobnych strukturalnie i odróżniają-
cych się tylko typem danych podlegających opracowywaniu. Typ danych może być klasą.
Wzorce (szablony) funkcji mogą nazywać się parametryzowanymi funkcjami.
Definicja wzorca (szablona) funkcji składa się ze słowa kluczowego template, listy parame-
trów formalnych w nawiasach „<”, „>” i definicji funkcji, w której można korzystać z parametrów
formalnych. Jeżeli parametr formalny jest klasą, to przed nim musi być napisane słowo kluczowe
class, w innych przypadkach musi być napisane słowo kluczowe typename.
Struktura wzorca funkcji (np. funk) ma następującą postać:

template <class typ_formalny1, typename typ_formalny2> funk(. . .)


{
. . .
};
Na końcu wzorca funkcji trzeba umieścić symbol „;”.
Wywołanie funkcji
Przy wywołaniu funkcji opisanej przez wzorzec trzeba podawać parametry faktyczne.
Wywołanie funkcji (np. funk) opisanej przez wzorzec ma następującą postać:

funk<typ_faktyczny>(...);

7.3. Przykłady definicji wzorców


W przykładach 7.1 i 7.2 pokazane są definicje wzorców klas. Przykłady są zapożyczone ze
standardowej biblioteki C++ środowiska C++ Builder.
Przykład. Definicja wzorca klasy
// Klasa basic_iostream
template<class charT, class traits>
class basic_iostream :
public basic_istream<charT, traits>,
public basic_ostream<charT, traits>
{
public:
explicit basic_iostream(basic_streambuf<charT, traits> *sb);
virtual ~basic_iostream();
protected:
explicit basic_iostream();
};
Przykład. Definicje wzorca i metody wzorca
// Klasa basic_ios
template<class charT, class traits>
class basic_ios : public ios_base
{
public:
// Typy:
typedef typename traits::char_type char_type;
typedef typename traits::int_type int_type;
typedef typename traits::pos_type pos_type;
typedef typename traits::off_type off_type;
typedef traits traits_type;
typedef basic_ios<charT,traits> ios_type;
typedef basic_streambuf<charT,traits> streambuf_type;
typedef basic_ostream<charT,traits> ostream_type;
inline operator void*() const;
inline bool operator! () const;
inline iostate rdstate() const;
inline void clear(iostate state = goodbit);
inline void setstate(iostate state);
inline bool good() const;
inline bool eof() const;
inline bool fail() const;
inline bool bad() const;
inline iostate exceptions() const;
inline void exceptions(iostate excpt);

explicit basic_ios(basic_streambuf<charT, traits> *sb_arg);


virtual ~basic_ios();
inline ostream_type *tie() const;
ostream_type *tie(ostream_type *tie_arg);
inline streambuf_type *rdbuf() const;
streambuf_type *rdbuf( streambuf_type *sb);
ios_type& copyfmt(const ios_type& rhs);
char_type fill() const
{
return __fillch;
}
char_type fill(char_type ch)
{
char_type temp=__fillch;
__fillch=ch;
return temp;
}
locale imbue(const locale& loc);
inline char narrow(charT, char) const;
inline charT widen(char) const;
protected:
basic_ios();
basic_ios(int):ios_base(1){};//konstruktor strumienia
void init(basic_streambuf<charT, traits> *sb);
inline void _RW_UNSAFE_clear(iostate state = goodbit);
private:
basic_ios(const basic_ios& );
basic_ios& operator=(const basic_ios&);
streambuf_type *__sb;
ostream_type *__tiestr;
char_type __fillch;
iostate __state;
iostate _Except;
};//koniec template . . . class basic_ios . . .
// metoda rdbuf()
template<class charT, class traits>
inline basic_streambuf<charT, traits> *
basic_ios<charT, traits>::rdbuf() const
{
return __sb;
}
W przykładzie pierwszym klasa basic_iostream jest klasą pochodną od dwóch klas. Pa-
rametry formalne charT i traits są wykorzystane tak wewnątrz klasy jak i w definicjach klas
bazowych.
W przykładzie drugim należy zwrócić uwagę na dużą liczbę definicji typów, utworzonych za
pomocą słowa kluczowego typedef. Celem tych definicji jest tylko zmniejszenie długości tekstu,
które zajmują konstrukcje z template oraz konstrukcje z nazwami klas.
Słowo kluczowe typename jest wykorzystane w przykładzie 7.2 w drugim z dwóch możli-
wych wariantów korzystania z niego (pierwszy wariant - w konstrukcji template), a mianowicie
jako definicja typu, który nie był jeszcze określony.
Literatura
1. Porebski W., 1993: Języki obiektowe. Gdańsk: Wydawnictwo PG.
2. Oktaba H., Ratajczak W., 1980: Simula 67. Warszawa: WNT.
3. Goldberg, Adele, and David Robson, 1989: Smalltalk-80: The Language. Addison-Wesley.
4. Morgan M., 1999: Poznaj język JAVA 1.2. Warszawa: Wydawnictwo MIKOM.
5. Porebski W., 2003: ADA 95. Michałowice: Komputerowa Oficyna Wydawnicza „HELP”.
6. Struzińska-Walczak A., Walczak K., 2001: Nauka programowania w systemie Delphi. Wydawnictwo W&W. Warszawa.

LITERATURA GŁÓWNA

Kisilewicz J., Język C w środowisku Borland C++. Oficyna Wydawnicza Politechniki Wrocławskiej. Wrocław,
2000
Kisilewicz J., Język C++: programowanie obiektowe, Wrocław: Oficyna Wydawnicza Politechniki Wrocław-
skej, 2002
C++ Builder 5. Vademecum profesjonalisty. Tom 1,2, Wydawnictwo HELION 2001
Porebski W., Języki obiektowe, Gdańsk: Wydawnictwo PG 1993
Youdon E., Argila C., Analiza obiektowa i projektowanie. Przykłady zastosowań, Warszawa 2000
Booch G., UML- przewodnik użytkownika, WNT 2001

LITERATURA POMOCNICZA
Coad P., Nicola J., Programowanie obiektowe, Warszawa 1993.
Deitel H.M., Deitel P.J., Arkana C++. Programowanie, Warszawa 1998.
Grębosz J., Symfonia C++. Programowanie w języku C++ orientowane obiektowo, Kraków 1999.
Kliszewski M., Inżynieria oprogramowania obiektowego, Tomaszów Maz. 1994.
Ledgard H.F., Mała księga programowania obiektowego, Warszawa 1998.
Struzińska-Walczak A., Walczak K., Nauka programowania w języku C++. Wydawnictwo W&W. Warszawa.
2001

You might also like