LEKCJA 36
FUNKCJE WIRTUALNE i KLASY ABSTRAKCYJNE.
W trakcie tej lekcji dowiesz się, co mawia żona programisty, gdy nie chce być obiektem klasy abstrakcyjnej.
FUNKCJE W PEŁNI WIRTUALNE (PURE VIRTUAL).
W skrajnych przypadkach wolno nam umieścić funkcję wirtualną w klasie bazowej nie definiując jej wcale. W klasie bazowej umieszczamy wtedy tylko deklarację-prototyp funkcji. W następnych pokoleniach klas pochodnych mamy wtedy pełną swobodę i możemy zdefiniować funkcję wirtualną w dowolny sposób - adekwatny dla potrzeb danej klasy pochodnej. Możemy np. do klasy bazowej (ang. generic class) dodać prototyp funkcji wirtualnej funkcja_eksperymentalna() nie definiując jej w (ani wobec) klasie bazowej. Sens umieszczenia takiej funkcji w klasie bazowej polege na uzyskaniu pewności, iż wszystkie klasy pochodne odziedziczą funkcję funkcja_eksperymentalna(), ale każda z klas pochodnych wyposaży tę funkcję we własną definicję. Takie postępowanie może okazać się szczególnie uzasadnione przy tworzeniu biblioteki klas (class library) przeznaczonej dla innych użytkowników. C++ w wersji instalacyjnej posiada już kilka gotowych bibliotek klas. Funkcje wirtuale, które nie zostają zdefiniowane - nie posiadają zatem ciała funkcji - nazywane są funkcjami w pełni wirtualnymi (ang. pure virtual function).
O KLASACH ABSTRAKCYJNYCH.
Jeśli zadeklarujemy funkcję CZwierzak::Oddychaj() jako funkcję w pełni wirtualną, oprócz słowa kluczowego virtual, trzeba tę informację w jakiś sposób przekazać kompilatorowi C++. Aby C++ wiedział, że naszą intencją jest funkcja w pełni wirtalna, nie możemy zadeklarować jej tak:
class CZwierzak
{
...
public:
virtual void Oddychaj();
...
};
a następnie pominąć definicję (ciało) funkcji. Takie postępowanie C++ uznałby za błąd, a funkcję - za zwykłą funkcję wirtualną, tyle, że "niedorobioną" przez programistę. Naszą intencję musimy zaznaczyć już w definicji klasy w taki sposób:
class CZwierzak
{
...
public:
virtual void Oddychaj() = 0;
...
};
Informacją dla kompilatora, że chodzi nam o funkcję w pełni wirtualną, jest dodanie po prototypie funkcji "= 0". Definiując klasę pochodną możemy rozbudować funkcję wirtualną np.:
class CZwierzak
{
...
public:
virtual void Oddychaj() = 0;
...
};
class CPiesek : public CZwierzak
{
...
public:
void Oddychaj() { cout << "Oddycham..."; }
...
};
Przykładem takiej funkcji jest funkcja Mów() z przedstawionego poniżej programu. Zostawiamy ją w pełni wirtualną, ponieważ różne obiekty klasy CZLOWIEK i klas pochodnych
class CZLOWIEK
{
public:
void Jedz(void);
virtual void Mow(void) = 0; //funkcja WIRTUALNA
};
class NIEMOWLE : public CZLOWIEK
{
public:
void Mow(void); // Tym razem BEZ slowa virtual
};
/* Tu definiujemy metodę wirtualną: -------------------- */
void NIEMOWLE::Mow(void) { cout << "Nie Umiem Mowic! \n"; };
mogą mówić na różne sposoby... Obiekt Niemowle, dla przykładu, nie chce mówić wcale, ale z innymi obiektami może być inaczej. Wyobraź sobie np. obiekt klasy Żona (żona to przecież też człowiek !).
class Zona : public CZLOWIEK
{
public:
void Mow(void);
}
W tym pokoleniu definicja wirtualnej metody Mow() mogłaby wyglądać np. tak:
void Zona::Mow(void)
{
cout << "JA NIE MAM CO NA SIEBIE WLOZYC !!! ";
cout << "DLACZEGO KOWALSKI ZARABIA ZAWSZE WIECEJ NIZ TY ?!!!";
//... itd., itd., itd...
}
[P128.CPP]
#include "iostream.h"
class CZLOWIEK
{
public:
void Jedz(void);
virtual void Mow(void) = 0;
};
void CZLOWIEK::Jedz(void) { cout << "MNIAM, MNIAM..."; };
class Zona : public CZLOWIEK
{
public:
void Mow(void); //Zona mowi swoje
}; //bez wzgledu na argumenty (typ void)
void Zona::Mow(void)
{
cout << "JA NIE MAM CO NA SIEBIE WLOZYC !!!";
cout << "DLACZEGO KOWALSKI ZARABIA ZAWSZE WIECEJ NIZ TY ?!!!";
}
class NIEMOWLE : public CZLOWIEK
{
public:
void Mow(void);
};
void NIEMOWLE::Mow(void) { cout << "Nie Umiem Mowic! \n"; };
main()
{
NIEMOWLE Dziecko;
Zona Moja_Zona;
Dziecko.Jedz();
Dziecko.Mow();
Moja_Zona.Mow()
return 0;
}
Przykładowa klasa CZŁOWIEK jest klasą ABSTRAKCYJNĄ. Jeśli spróbujesz dodać do powyższego programu np.:
CZLOWIEK Facet;
Facet.Jedz();
uzyskasz komunikat o błędzie: Cannot create a variable for abstract class "CZLOWIEK" (Nie mogę utworzyć zmiennych dla klasy abstrakcyjnej "CZLOWIEK"
[???] KLASY ABSTRAKCYJNE.
* Po klasach abstrakcyjnych MOŻNA dziedziczyć!
* Obiektów klas abstrakcyjnych NIE MOŻNA stosować bezpośrednio!
Ponieważ wyjaśniliśmy, dlaczego klasy są nowymi typami danych, ięc logika (i sens) innej rozpowszechnionej nazwy klas abstrakcyjnych - ADT - Abstract Data Type (Abstrakcyjne Typy Danych) jest chyba zrozumiała i oczywista.
ZAGNIEŻDŻANIE KLAS I OBIEKTÓW.
Może się np. zdarzyć, że klasa stanie się wewnętrznym elementem (ang. member) innej klasy i odpowiednio - obiekt - elementem innego obiektu. Nazywa się to fachowo "zagnieżdżaniem" (ang. nesting). Jeśli, dla przykładu klasa CB będzie zawierać obiekt klasy CA:
class CA
{
int liczba;
public:
CA() { liczba = 0; } //Konstruktor domyslny
CA(int x) { liczba = x; }
void operator=(int n) { liczba = n }
};
class CB
{
CA obiekt;
public:
CB() { obiekt = 1; }
};
Nasze klasy wyposażyliśmy w konstruktory i od razu poddaliśmy overloadingowi operator przypisania = . Aby prześledzić kolejność wywoływania funkcji i sposób przekazywania parametrów pomiędzy tak powiązanymi obiektami rozbudujemy każdą funkcję o zgłoszenie na ekranie.
class CA
{
int liczba;
public:
CA() { liczba = 0; cout << "-> CA(), CA_O::liczba = 0 "; }
CA(int x) { liczba = x; cout << "->CA(int) "; }
void operator=(int n) { liczba = n; cout << "->operator "; }
};
class CB
{
CA obiekt;
public:
CB() { obiekt = 1; cout << "->Konstruktor CB() "; }
};
Możemy teraz sprawdzić, co stanie się w programie po zadeklarowaniu obiektu klasy CB:
[P129.CPP]
# include "iostream.h"
class CA
{
int liczba;
public:
CA() { liczba = 0; cout << "-> CA(), CA_O::liczba = 0 "; }
CA(int x) { liczba = x; cout << "->CA(int) "; }
void operator=(int n) { liczba = n; cout << "->operator "; }
};
class CB
{
CA obiekt;
public:
CB() { obiekt = 1; cout << "->Konstruktor CB() "; }
};
main()
{
CB Obiekt;
return 0;
}
Po uruchomieniu programu możesz przekonać się, że kolejność działań będzie następująca:
C:\>program
-> CA(), CA_O::liczba = 0 ->operator ->Konstruktor CB()
Skoro oprócz zainicjowania obiektu klasy pochodnej nie robimy w programie dokładnie nic, nie dziwmy się ostrzeżeniu
Warning: Obiekt is never used...
Jest to sytuacja trochę podobna do komunikacji pomiędzy konstruktorami klas bazowych i pochodnych. Jeśli zaprojektujemy prostą strukturę klas:
class CBazowa
{
private:
int liczba;
public:
CBazowa() { liczba = 0}
CBazowa(int n) { liczba = n; }
};
class CPochodna : public CBazowa
{
public:
CPochodna() { liczba = 0; }
CPochodna(int x) { liczba = x; }
};
problem przekazywania parametrów między konstruktorami klas możemy w C++ rozstrzygnąć i tak:
class CPochodna : public CBazowa
{
public:
CPochodna() : CBazowa(0) { liczba = 0; }
CPochodna(int x) { liczba = x; }
};
Będzie to w praktyce oznaczać wywołanie konstruktora klasy bazowej z przekazanym mu argumentem 0. Podobnie możemy postąpić w stosunku do klas zagnieżdżonych:
[P130.CPP]
#include "iostream.h"
class CA
{
int liczba;
public:
CA() { liczba = 0; cout << "-> CA(), CA_O::liczba = 0 "; }
CA(int x) { liczba = x; cout << "->CA(int) "; }
void operator=(int n) { liczba = n; cout << "->operator "; }
};
class CB
{
CA obiekt;
public:
CB() : CA(1) {}
};
main()
{
CB Obiekt;
return 0;
}
Eksperymentując z dwoma powyższymi programami możesz przekonać się, jak przebiega przekazywanie parametrów pomiędzy konstruktorami i obiektami klas bazowych i pochodnych.
JESZCZE RAZ O WSKAŹNIKU *this.
Szczególnie ważnym wskaźnikiem przy tworzeniu klas pochodnych i funkcji operatorowych może okazać się pointer *this. Oto przykład listy.
[P131.CPP]
# include "string.h"
# include "iostream.h"
class CLista
{
private:
char *poz_listy;
CLista *poprzednia;
public:
CLista(char*);
CLista* Poprzednia() { return (poprzednia); };
void Pokazuj() { cout << '\n' << poz_listy; }
void Dodaj(CLista&);
~CLista() { delete poz_listy; }
};
CLista::CLista(char *s)
{
poz_listy = new char[strlen(s)+1];
strcpy(poz_listy, s);
poprzednia = NULL;
}
void CLista::Dodaj(CLista& obiekt)
{
obiekt.poprzednia = this;
}
main()
{
CLista *ostatni = NULL;
cout << '\n' << "Wpisanie kropki [.]+[Enter] = Quit \n";
for(;;)
{
cout << "\n Wpisz nazwe (bez spacji): ";
char TAB[70];
cin >> TAB;
if (strncmp(TAB, ".", 1) == 0) break;
CLista *lista = new CLista(TAB);
if (ostatni != NULL)
ostatni->Dodaj(*lista);
ostatni = lista;
}
for(; ostatni != NULL;)
{
ostatni->Pokazuj();
CLista *temp = ostatni;
ostatni = ostatni->Poprzednia();
delete (temp);
}
return 0;
}
Z reguły to kompilator nadaje wartość wskaźnikowi this i to on automatycznie dba o przyporządkowanie pamięci obiektom. Pointer this jest zwykle inicjowany w trakcie działania konstruktora obiektu.
4