5536


Rozdział 14.
Polimorfizm

Z rozdziału 12. dowiedziałeś się, jak pisać funkcje wirtualne w klasach wyprowadzonych. Jest to jedna z podstawowych umiejętności potrzebnych przy posługiwaniu się polimorfizmem, czyli możliwością przypisywania — już podczas działania programu — specyficznych obiektów klas pochodnych do wskaźników wskazujących na obiekty klasy bazowej.

Z tego rozdziału dowiesz się:

Problemy z pojedynczym dziedziczeniem

Przypuśćmy, że od pewnego czasu pracujemy z naszymi klasami zwierząt i że podzieliliśmy hierarchię klas na ptaki (Bird) i ssaki (Mammal). Klasa Bird posiada funkcję składową Fly() (latanie). Klasa Mammal została podzielona na różne rodzaje ssaków, między innymi na klasę Horse (koń). Klasa Horse posiada funkcje składowe Whinny() (rżenie) oraz Gallop() (galopowanie).

Nagle okazuje się, że potrzebujemy obiektu pegaza (Pegasus): skrzyżowania konia z ptakiem. Pegasus może latać (metoda Fly()), ale także może rżeć (Whinny()) i galopować (Gallop()). Przy dziedziczeniu pojedynczym okazuje się, że jesteśmy w kropce.

Możemy uczynić z pegaza obiekt klasy Bird, ale wtedy nie będzie mógł rżeć ani galopować. Możemy zrobić z niego obiekt Horse, ale wtedy nie będzie mógł latać.

Pierwszą próbą rozwiązania tego problemu może być skopiowanie metody Fly() do klasy Pegasus i wyprowadzenie tej klasy z klasy Horse. Będzie to prawidłowa operacja, przeprowadzona jednak kosztem posiadania metody Fly() w dwóch miejscach (w klasach Bird i Pegasus). Gdy zmienisz ją w jednym miejscu, musisz pamiętać o wprowadzeniu modyfikacji także w drugim. Oczywiście, programista, który kilka miesięcy czy lat później spróbuje zmodyfikować taki kod, także musi wiedzieć o obu miejscach.

Wkrótce jednak pojawia się nowy problem. Chcemy stworzyć listę obiektów typu Horse oraz listę obiektów typu Bird. Chcielibyśmy dodać obiekt klasy Pegasus do dowolnej z tych list, ale gdyby Pegasus został wyprowadzony z klasy Horse, nie moglibyśmy go dodać do listy obiektów klasy Bird.

Istnieje kilka rozwiązań tego problemu. Możemy zmienić nazwę metody Gallop() na Move() (ruch), a następnie przesłonić metodę Move() w klasie Pegasus tak, aby wykonywała pracę metody Fly(). Następnie przesłonilibyśmy metodę Move() innych koni tak, aby wykonywała pracę metody Gallop(). Być może pegaz byłby inteligentny na tyle, by galopować na krótkich dystansach, a latać tylko na dłuższych:

Pegasus::Move(long distance)

{

if (distance > veryFar)

Fly(distance);

else

Gallop(distance);

}

To rozwiązanie posiada jednak pewne ograniczenia. Być może któregoś dnia pegaz zechce latać na krótkich dystansach lub galopować na dłuższych. Następnym rozwiązaniem mogłoby być przeniesienie metody Fly() w górę, do klasy Horse, co zostało pokazane na listingu 14.1. Problem jednak polega na tym, iż zwykłe konie nie potrafią latać, więc w przypadku koni innych niż pegaz, ta metoda nie będzie nic robić.

Listing 14.1. Gdyby konie umiały latać...

0: // Listing 14.1. Gdyby konie umiały latać...

1: // Przeniesienie metody Fly() do klasy Horse

2:

3: #include <iostream>

4: using namespace std;

5:

6: class Horse

7: {

8: public:

9: void Gallop(){ cout << "Galopuje...\n"; }

10: virtual void Fly() { cout << "Konie nie potrafia latac.\n" ; }

11: private:

12: int itsAge;

13: };

14:

15: class Pegasus : public Horse

16: {

17: public:

18: virtual void Fly() {cout<<"Moge latac! Moge latac! Moge latac!\n";}

19: };

20:

21: const int NumberHorses = 5;

22: int main()

23: {

24: Horse* Ranch[NumberHorses];

25: Horse* pHorse;

26: int choice,i;

27: for (i=0; i<NumberHorses; i++)

28: {

29: cout << "(1)Horse (2)Pegasus: ";

30: cin >> choice;

31: if (choice == 2)

32: pHorse = new Pegasus;

33: else

34: pHorse = new Horse;

35: Ranch[i] = pHorse;

36: }

37: cout << "\n";

38: for (i=0; i<NumberHorses; i++)

39: {

40: Ranch[i]->Fly();

41: delete Ranch[i];

42: }

43: return 0;

44: }

Wynik

(1)Horse (2)Pegasus: 1

(1)Horse (2)Pegasus: 2

(1)Horse (2)Pegasus: 1

(1)Horse (2)Pegasus: 2

(1)Horse (2)Pegasus: 1

Konie nie potrafia latac.

Moge latac! Moge latac! Moge latac!

Konie nie potrafia latac.

Moge latac! Moge latac! Moge latac!

Konie nie potrafia latac.

Analiza

Ten program oczywiście działa, ale kosztem posiadania przez klasę Horse metody Fly(). Metoda Fly() dla klasy Horse jest zdefiniowana w linii 10. W rzeczywistej klasie mogłaby po prostu wyświetlać komunikat błędu lub po cichu zakończyć działanie. W linii 18. klasa Pegasus przesłania metodę Fly() tak, aby wykonywała właściwą pracę, w tym przypadku polegającą na wypisywaniu radosnego komunikatu.

Tablica wskaźników do klasy Horse, zadeklarowana w linii 24., służy do zademonstrowania,[Author ID1: at Wed Oct 31 13:55:00 2001 ] że właściwa metoda Fly()zostaje wywołana w zależności od tego, czy został stworzony obiekt klasy Horse lub klasy Pegasus.

UWAGA Pokazany tutaj przykład został bardzo okrojony, do elementów niezbędych dla zrozumienia zasad jego działania. Konstruktory, wirtualne destruktory i tak dalej, zostały usunięte w celu ułatwienia analizy kodu.

Przenoszenie w górę

Przenoszenie pożądanej funkcji w górę hierarchii klas jest powszechnym rozwiązaniem tego typu problemów; powoduje jednak, że w klasie bazowej występuje wiele funkcji „nadmiarowych”. Istnieje niebezpieczeństwo, że klasa bazowa stanie się globalną przestrzenią nazw dla wszystkich funkcji, które mogłyby być użyte w klasach potomnych. Może to znacznie wpłynąć na efektywność zarządzania typami w C++ i powodować zbytni rozrost i skomplikowanie klas bazowych.

Chcemy przenieść funkcjonalność w górę hierarchii, ale bez równoczesnego przenoszenia interfejsu każdej z klas. Oznacza to, że jeśli dwie klasy posiadają wspólną klasę bazową (na przykład klasy Horse i Bird pochodzą od klasy Animal) i posiadają wspólną funkcję (zarówno konie, jak i ptaki odżywiają się), powinniśmy przenieść tę cechę w górę, do klasy bazowej i stworzyć z niej funkcję wirtualną.

Powinniśmy unikać przy tym przenoszenia interfejsu (tak, jak przeniesienie metody Fly() tam, gdzie nie powinno jej być) tylko w celu wywoływania danej funkcji w niektórych z klas wyprowadzonych.

Rzutowanie w dół

Alternatywą dla przedstawionego wcześniej rozwiązania (nie wykluczającą korzystania z pojedynczego dziedziczenia), jest zatrzymanie metody Fly() wewnątrz klasy Pegasus i wywoływanie jej tylko wtedy, gdy wskaźnik do obiektu rzeczywiście wskazuje obiekt klasy Pegasus. Aby sposób ten mógł działać, musimy mieć możliwość zapytania wskaźnika, jaki typ faktycznie wskazuje. Nazywa się to identyfikacją typów podczas wykonywania programu (RTTI, Run Time Type Identification). Korzystanie z RTTI stało się oficjalnym elementem języka C++ dopiero od niedawna.

Jeśli kompilator nie obsługuje RTTI, możemy symulować tę obsługę, umieszczając w każdej z klas metodę zwracającą jedną z wyliczeniowych stałych. Możemy następnie sprawdzać typ podczas działania programu i wywoływać metodę Fly() tylko wtedy, gdy ta metoda zwróci stałą dla typu Pegasus.

UWAGA Bądź ostrożny z RTTI. Korzystanie z tego mechanizmu może być oznaką słabości projektu programu. Zamiast tego użyj funkcji wirtualnych, wzorców lub wielokrotnego dziedziczenia.

Aby móc wywołać metodę Fly(), musimy dokonać rzutowania wskaźnika, informując kompilator, że wskazywany obiekt jest obiektem typu Pegasus, a nie obiektem typu Horse. Nazywa się to rzutowaniem w dół, gdyż obiekt Horse rzutujemy w dół hierarchii, do typu bardziej wyprowadzonego.

Dziś C++ już oficjalnie, choć dość niechętnie, obsługuje rzutowanie w dół za pomocą nowego operatora dynamic_cast. Oto sposób jego działania:

Jeśli mamy wskaźnik do klasy bazowej, takiej jak Horse, i przypiszemy mu adres obiektu klasy wyprowadzonej, takiej jak Pegasus, możemy używać wskaźnika do klasy Horse polimorficznie. Jeśli chcemy następnie odwołać się do obiektu klasy Pegasus, tworzymy wskaźnik do tej klasy i w celu dokonania konwersji używamy operatora dynamic_cast.

W czasie działania programu nastąpi sprawdzenie wskaźnika do klasy bazowej. Jeśli konwersja będzie właściwa, nowy wskaźnik do klasy Pegasus będzie poprawny. Jeśli konwersja będzie niewłaściwa (nie będzie to wskaźnik do klasy Pegasus), nowy wskaźnik będzie pusty (null). Ilustruje to listing 14.2.

Listing 14.2. Rzutowanie w dół

0: // Listing 14.2 Użycie operatora dynamic_cast.

1: // Using rtti

2:

3: #include <iostream>

4: using namespace std;

5:

6: enum TYPE { HORSE, PEGASUS };

7:

8: class Horse

9: {

10: public:

11: virtual void Gallop(){ cout << "Galopuje...\n"; }

12:

13: private:

14: int itsAge;

15: };

16:

17: class Pegasus : public Horse

18: {

19: public:

20:

21: virtual void Fly() {cout<<"Moge latac! Moge latac! Moge latac!\n";}

22: };

23:

24: const int NumberHorses = 5;

25: int main()

26: {

27: Horse* Ranch[NumberHorses];

28: Horse* pHorse;

29: int choice,i;

30: for (i=0; i<NumberHorses; i++)

31: {

32: cout << "(1)Horse (2)Pegasus: ";

33: cin >> choice;

34: if (choice == 2)

35: pHorse = new Pegasus;

36: else

37: pHorse = new Horse;

38: Ranch[i] = pHorse;

39: }

40: cout << "\n";

41: for (i=0; i<NumberHorses; i++)

42: {

43: Pegasus *pPeg = dynamic_cast< Pegasus *> (Ranch[i]);

44: if (pPeg)

45: pPeg->Fly();

46: else

47: cout << "Po prostu kon\n";

48:

49: delete Ranch[i];

50: }

51: return 0;

52: }

Wynik

(1)Horse (2)Pegasus: 1

(1)Horse (2)Pegasus: 2

(1)Horse (2)Pegasus: 1

(1)Horse (2)Pegasus: 2

(1)Horse (2)Pegasus: 1

Po prostu kon

Moge latac! Moge latac! Moge latac!

Po prostu kon

Moge latac! Moge latac! Moge latac!

Po prostu kon

Analiza

Ten sposób również okazał się dobry. Metoda Fly() została utrzymana poza klasą Horse i nie jest wywoływana dla obiektów typu Horse. Jednak w przypadku wywoływania jej dla obiektów klasy Pegasus, musi być stosowane rzutowanie jawne; obiekty klasy Horse nie posiadają metody Fly(), więc musimy poinformować kompilator, że wskaźnik wskazuje na klasę Pegasus.

Potrzeba rzutowania obiektu klasy Pegasus jest ostrzeżeniem, że program może być źle zaprojektowany. Taki program znacznie obniża użyteczność polimorfizmu funkcji wirtualnych, gdyż podczas działania jest zależny od rzutowania obiektu do jego rzeczywistego typu.

Często zadawane pytanie

Podczas kompilacji za pomocą kompilatora Visual C++ Microsoftu otrzymują ostrzeżenie: „warning C4541: 'dynamic_cast' used on polymorphic type 'class Horse' with /GR-; unpredictable behavior may result”. Co powinienem zrobić?

Odpowiedź: Jest to jeden z najbardziej kłopotliwych komunikatów o [Author ID1: at Wed Oct 31 13:56:00 2001 ]błędach[Author ID1: at Wed Oct 31 13:56:00 2001 ]ów[Author ID1: at Wed Oct 31 13:56:00 2001 ]. Aby się go pozbyć, wykonaj następujące kroki:

1. W swoim projekcie wybierz polecenie Project | Settings.

2. Przejdź na zakładkę C++.

3. Z listy rozwijanej wybierz pozycję C++ Language.

4. Włącz opcję Enable Runtime Type Information (RTTI).

5. Zbuduj cały projekt ponownie.

Połączenie dwóch list

Inny problem z tymi[Author ID1: at Wed Oct 31 13:56:00 2001 ]podanymi wyżej[Author ID1: at Wed Oct 31 13:56:00 2001 ] rozwiązaniami polega na tym, że zadeklarowaliśmy obiekt Pegasus jako obiekt typu Horse, więc nie możemy dodać obiektu Pegasus do listy obiektów Bird. Straciliśmy albo poprzez [Author ID1: at Wed Oct 31 13:57:00 2001 ]przeniesienie[Author ID1: at Wed Oct 31 13:57:00 2001 ]osząc[Author ID1: at Wed Oct 31 13:57:00 2001 ] metody[Author ID1: at Wed Oct 31 13:57:00 2001 ]ę[Author ID1: at Wed Oct 31 13:57:00 2001 ] Fly() w górę albo poprzez [Author ID1: at Wed Oct 31 13:58:00 2001 ]rzutowanie[Author ID1: at Wed Oct 31 13:58:00 2001 ]ując[Author ID1: at Wed Oct 31 13:58:00 2001 ] wskaźnika[Author ID1: at Wed Oct 31 13:58:00 2001 ] w dół, a mimo to wciąż nie osiągnęliśmy pełnej funkcjonalności.

Pojawia się jeszcze jedno, ostatnie rozwiązanie, [Author ID1: at Wed Oct 31 13:58:00 2001 ]również wykorzystujące pojedyncze dziedziczenie. Możemy umieścić metody Fly(), Whinny() oraz Gallop() w klasie bazowej wspólnej zarówno dla klas Bird, jak i Horse: w klasie Animal. Teraz, zamiast osobnej listy ptaków i listy koni, możemy mieć jedną, zunifikowaną listę zwierząt. Sposób ten działa, ale powoduje jeszcze większe przeniesienie specjalnych funkcji w górę do klas bazowych.

Możemy również pozostawić te metody tam, gdzie są i rzutować w dół obiekty klas Horse, Bird i Pegasus, ale to jeszcze gorsze rozwiązanie!

TAK

NIE

Przenoś funkcjonalność w górę hierarchii dziedziczenia.

Unikaj przełączania na podstawie typu obiektu sprawdzanego podczas działania programu — używaj metod wirtualnych, wzorców oraz wielokrotnego dziedziczenia.

Nie przenoś interfejsów w górę hierarchii dziedziczenia.

Nie rzutuj wskaźników do obiektów bazowych w dół, do obiektów wyprowadzonych.

Dziedziczenie Wielokrotne

Istnieje możliwość wyprowadzenia nowej klasy z więcej niż jednej klasy bazowej. Nazywa się to dziedziczeniem wielokrotnym. Aby wyprowadzić klasę z więcej niż jednej klasy bazowej, w nagłówku klasy musimy oddzielić każdą z klas bazowych przecinkami. Listing 14.3 pokazuje sposób zadeklarowania klasy Pegasus jako pochodzącej zarówno od klasy Horse, jak i klasy Bird. Następnie program dodaje obiekty klasy Pegasus do list obu typów.

Listing 14.3. Dziedziczenie wielokrotne

0: // Listing 14.3. Dziedziczenie wielokrotne.

1: // Dziedziczenie wielokrotne

2:

3: #include <iostream>

4: using std::cout;

5: using std::cin;

6:

7: class Horse

8: {

9: public:

10: Horse() { cout << "Konstruktor klasy Horse... "; }

11: virtual ~Horse() { cout << "Destruktor klasy Horse... "; }

12: virtual void Whinny() const { cout << "Ihaaa!... "; }

13: private:

14: int itsAge;

15: };

16:

17: class Bird

18: {

19: public:

20: Bird() { cout << "Konstruktor klasy Bird... "; }

21: virtual ~Bird() { cout << "Destruktor klasy Bird... "; }

22: virtual void Chirp() const { cout << "Cwir, cwir... "; }

23: virtual void Fly() const

24: {

25: cout << "Moge latac! Moge latac! Moge latac! ";

26: }

27: private:

28: int itsWeight;

29: };

30:

31: class Pegasus : public Horse, public Bird

32: {

33: public:

34: void Chirp() const { Whinny(); }

35: Pegasus() { cout << "Konstruktor klasy Pegasus... "; }

36: ~Pegasus() { cout << "Destruktor klasy Pegasus... "; }

37: };

38:

39: const int MagicNumber = 2;

40: int main()

41: {

42: Horse* Ranch[MagicNumber];

43: Bird* Aviary[MagicNumber];

44: Horse * pHorse;

45: Bird * pBird;

46: int choice,i;

47: for (i=0; i<MagicNumber; i++)

48: {

49: cout << "\n(1)Horse (2)Pegasus: ";

50: cin >> choice;

51: if (choice == 2)

52: pHorse = new Pegasus;

53: else

54: pHorse = new Horse;

55: Ranch[i] = pHorse;

56: }

57: for (i=0; i<MagicNumber; i++)

58: {

59: cout << "\n(1)Bird (2)Pegasus: ";

60: cin >> choice;

61: if (choice == 2)

62: pBird = new Pegasus;

63: else

64: pBird = new Bird;

65: Aviary[i] = pBird;

66: }

67:

68: cout << "\n";

69: for (i=0; i<MagicNumber; i++)

70: {

71: cout << "\nRanch[" << i << "]: " ;

72: Ranch[i]->Whinny();

73: delete Ranch[i];

74: }

75:

76: for (i=0; i<MagicNumber; i++)

77: {

78: cout << "\nAviary[" << i << "]: " ;

79: Aviary[i]->Chirp();

80: Aviary[i]->Fly();

81: delete Aviary[i];

82: }

83: return 0;

84: }

Wynik

(1)Horse (2)Pegasus: 1

Konstruktor klasy Horse...

(1)Horse (2)Pegasus: 2

Konstruktor klasy Horse... Konstruktor klasy Bird... Konstruktor klasy Pegasus...

(1)Bird (2)Pegasus: 1

Konstruktor klasy Bird...

(1)Bird (2)Pegasus: 2

Konstruktor klasy Horse... Konstruktor klasy Bird... Konstruktor klasy Pegasus...

Ranch[0]: Ihaaa!... Destruktor klasy Horse...

Ranch[1]: Ihaaa!... Destruktor klasy Pegasus... Destruktor klasy Bird... Destruktor klasy Horse...

Aviary[0]: Cwir, cwir... Moge latac! Moge latac! Moge latac! Destruktor klasy Bird...

Aviary[1]: Ihaaa!... Moge latac! Moge latac! Moge latac! Destruktor klasy Pegasus... Destruktor klasy Bird... Destruktor klasy Horse...

Analiza

W liniach od 7. do 15. została zadeklarowana klasa Horse. Jej konstruktor i destruktor wypisuje komunikat, zaś metoda Whinny() wypisuje komunikat Ihaaa!.

W liniach od 17. do 29. została zadeklarowana klasa Bird. Oprócz konstruktora i destruktora, ta klasa posiada dwie metody: Chirp() (ćwierkanie) oraz Fly(). Obie te metody wypisują odpowiednie komunikaty. W rzeczywistym programie mogłyby na przykład uaktywniać głośnik lub wyświetlać animowane sekwencje.

Na zakończenie, w liniach od 31. do 37. została zadeklarowana klasa Pegasus. Dziedziczy ona zarówno po klasie Horse, jak i klasie Bird. Klasa Pegasus przesłania metodę Chirp() tak, aby została wywołana metoda Whinny(), odziedziczona po klasie Horse.

Tworzone są dwie tablice: w linii 42. tablica Ranch (ranczo) ze wskaźnikami do klasy Horse oraz w linii 43. tablica Aviary (ptaszarnia) ze wskaźnikami do klasy Bird. W liniach od 47. do 56. do tablicy Ranch są dodawane obiekty klas Horse i Pegasus. W liniach od 57. do 66. do tablicy Aviary są dodawane obiekty klas Bird i Pegasus.

Wywołania metod wirtualnych zarówno dla wskaźników do obiektów klasy Bird, jak i obiektów klasy Horse, działają poprawnie także dla obiektów klasy Pegasus. Na przykład, w linii 79., elementy tablicy Aviary są używane do wywołania metody Chirp() wskazywanych przez nie obiektów. Klasa Bird deklaruje tę metodę jako wirtualną, więc dla każdego obiektu wywoływana jest właściwa funkcja.

Zwróć uwagę, że za każdym razem, gdy tworzony jest obiekt Pegasus, wyniki odzwierciedlają, że tworzone są także części tego obiektu należące tak [Author ID1: at Wed Oct 31 13:59:00 2001 ]do klasy Bird, oraz[Author ID1: at Wed Oct 31 13:59:00 2001 ]jak i [Author ID1: at Wed Oct 31 13:59:00 2001 ]Horse. Gdy obiekt Pegasus jest niszczony, niszczone są także części obiektu należące do klas Bird oraz Horse, a to dzięki temu, że destruktor także został zamieniony na wirtualny.

Deklarowanie dziedziczenia wielokrotnego

Deklarowanie obiektu dziedziczącego z więcej niż jednej klasy bazowej polega na umieszczeniu po nazwie tworzonej klasy dwukropka i rozdzielonej przecinkami listy klas bazowych.

Przykład 1

class Pegasus : public Horse, public Bird

Przykład 2

class Schnoodle : Public Schnauzer, public Poodle

Części obiektu z dziedziczeniem wielokrotnym

Gdy w pamięci tworzony jest obiekt Pegasus, na część tego obiektu składają się obie klasy bazowe, co ilustruje rysunek 14.1.

Rys. 14.1. Obiekt klasy z wielokrotnym dziedziczeniem

0x01 graphic

W przypadku obiektów posiadających kilka klas bazowych, pojawia się kilka zagadnień. Na przykład, co się stanie, gdy dwie klasy bazowe mają dane lub funkcje wirtualne o tych samych nazwach? Jak są inicjalizowane konstruktory klas bazowych? Co się dzieje, gdy różne klasy bazowe dziedziczą z tej samej klasy? Na te pytania odpowiemy w następnych podrozdziałach i pokażemy także, jak dziedziczenie wielokrotne można wykorzystać do pracy.

Konstruktory w obiektach dziedziczonych wielokrotnie

Jeśli klasa Pegasus jest wyprowadzona z klas Horse oraz Bird, a każda z nich posiada konstruktory wymagające parametrów, klasa Pegasus inicjalizuje te konstruktory po kolei. Ilustruje to listing 14.4.

Listing 14.4. Wywoływanie wielu konstruktorów

0: // Listing 14.4

1: // Wywoływanie wielu konstruktorów

2:

3: #include <iostream>

4: using namespace std;

5:

6: typedef int HANDS;

7: enum COLOR { Red, Green, Blue, Yellow, White, Black, Brown } ;

8:

9: class Horse

10: {

11: public:

12: Horse(COLOR color, HANDS height);

13: virtual ~Horse() { cout << "Destruktor klasy Horse...\n"; }

14: virtual void Whinny()const { cout << "Ihaaa!... "; }

15: virtual HANDS GetHeight() const { return itsHeight; }

16: virtual COLOR GetColor() const { return itsColor; }

17: private:

18: HANDS itsHeight;

19: COLOR itsColor;

20: };

21:

22: Horse::Horse(COLOR color, HANDS height):

23: itsColor(color),itsHeight(height)

24: {

25: cout << "Konstruktor klasy Horse...\n";

26: }

27:

28: class Bird

29: {

30: public:

31: Bird(COLOR color, bool migrates);

32: virtual ~Bird() {cout << "Destruktor klasy Bird...\n"; }

33: virtual void Chirp()const { cout << "Cwir, cwir... "; }

34: virtual void Fly()const

35: {

36: cout << "Moge latac! Moge latac! Moge latac! ";

37: }

38: virtual COLOR GetColor()const { return itsColor; }

39: virtual bool GetMigration() const { return itsMigration; }

40:

41: private:

42: COLOR itsColor;

43: bool itsMigration;

44: };

45:

46: Bird::Bird(COLOR color, bool migrates):

47: itsColor(color), itsMigration(migrates)

48: {

49: cout << "Konstruktor klasy Bird...\n";

50: }

51:

52: class Pegasus : public Horse, public Bird

53: {

54: public:

55: void Chirp()const { Whinny(); }

56: Pegasus(COLOR, HANDS, bool,long);

57: ~Pegasus() {cout << "Destruktor klasy Pegasus...\n";}

58: virtual long GetNumberBelievers() const

59: {

60: return itsNumberBelievers;

61: }

62:

63: private:

64: long itsNumberBelievers;

65: };

66:

67: Pegasus::Pegasus(

68: COLOR aColor,

69: HANDS height,

70: bool migrates,

71: long NumBelieve):

72: Horse(aColor, height),

73: Bird(aColor, migrates),

74: itsNumberBelievers(NumBelieve)

75: {

76: cout << "Konstruktor klasy Pegasus...\n";

77: }

78:

79: int main()

80: {

81: Pegasus *pPeg = new Pegasus(Red, 5, true, 10);

82: pPeg->Fly();

83: pPeg->Whinny();

84: cout << "\nTwoj Pegaz ma " << pPeg->GetHeight();

85: cout << " dloni wysokosci i ";

86: if (pPeg->GetMigration())

87: cout << "migruje.";

88: else

89: cout << "nie migruje.";

90: cout << "\nLacznie " << pPeg->GetNumberBelievers();

91: cout << " osob wierzy ze on istnieje.\n";

92: delete pPeg;

93: return 0;

94: }

Wynik

Konstruktor klasy Horse...

Konstruktor klasy Bird...

Konstruktor klasy Pegasus...

Moge latac! Moge latac! Moge latac! Ihaaa!...

Twoj Pegaz ma 5 dloni wysokosci i migruje.

Lacznie 10 osob wierzy ze on istnieje.

Destruktor klasy Pegasus...

Destruktor klasy Bird...

Destruktor klasy Horse...

Analiza

W liniach od 9. do 20. została zadeklarowana klasa Horse. Jej konstruktor wymaga dwóch parametrów: jednym z nich jest stała wyliczeniowa zadeklarowana w linii 7., a drugim typ zadeklarowany w linii 6. Implementacja konstruktora zawarta w liniach od 22. do 26. po prostu inicjalizuje zmienne składowe i wypisuje komunikat.

W liniach od 28. do 44. jest deklarowana klasa Bird, zaś implementacja jej konstruktora znajduje się w liniach od 46. do 50. Także konstruktor tej klasy wymaga podania dwóch parametrów. Co ciekawe, konstruktor klasy Horse wymaga podania koloru (dzięki czemu możemy reprezentować konie o różnych kolorach), tak jak wymaga go konstruktor klasy Bird (co pozwala mu na reprezentowanie ptaków o różnym kolorze upierzenia). W momencie zapytania pegaza o jego kolor powstaje problem, co zresztą zobaczymy w następnym przykładzie.

Sama klasa Pegasus została zadeklarowana w liniach od 52. do 56., zaś jej konstruktor znajduje się w liniach od 67. do 77. Inicjalizacja obiektu tej klasy składa się z trzech instrukcji. Po pierwsze, konstruktor klasy Horse jest inicjalizowany kolorem i wysokością. Po drugie, konstruktor klasy Bird jest inicjalizowany kolorem i wartością logiczną. Na koniec inicjalizowana jest zmienna składowa klasy Pegasus, itsNumberBelievers (ilość wierzących). Gdy proces ten zostanie zakończony, wywoływane jest ciał konstruktora obiektu Pegasus.

W funkcji main() tworzony jest wskaźnik do obiektu klasy Pegasus i zostaje on użyty do wywołania funkcji składowych klas bazowych tego obiektu.

Eliminowanie niejednoznaczności

Na listingu 14.4 zarówno klasa Horse, jak i klasa Bird posiadały metodę GetColor() (pobierz kolor). Możemy poprosić obiekt Pegasus o podanie swojego koloru, ale będziemy mieli wtedy problem — klasa Pegasus dziedziczy zarówno po klasie Bird, jak i klasie Horse. Obie te klasy posiadają kolor, zaś metody odczytywania koloru tych klas mają te same nazwy i sygnatury. To powoduje niejednoznaczność, niezrozumiałą dla kompilatora. Spróbujemy rozwiązać ten problem.

Jeśli napiszemy po prostu:

COLOR currentColor = pPeg->GetColor();

otrzymamy błąd kompilatora:

Member is ambiguous: 'Horse::GetColor' and 'Bird::GetColor'

(składowa jest niejednoznaczna: 'Horse::GetColor' i 'Bird::GetColor')

Możemy zlikwidować tę niejednoznaczność przez jawne wywołanie funkcji, której chcemy użyć:

COLOR currentColor = pPeg->Horse::GetColor();

Za każdym razem, gdy chcemy określić klasę używanej funkcji lub danej składow[Author ID1: at Wed Oct 31 14:00:00 2001 ]ej, możemy użyć pełnej kwalifikowanej nazwy, poprzedzając nazwę składowej nazwą klasy bazowej.

Zwróć uwagę, że gdyby klasa Pegasus przesłoniła tę funkcję, problem[Author ID1: at Wed Oct 31 14:00:00 2001 ] [Author ID1: at Wed Oct 31 14:00:00 2001 ]pytanie o kolor [Author ID1: at Wed Oct 31 14:00:00 2001 ]zostało[Author ID1: at Wed Oct 31 14:00:00 2001 ]by przesunięte[Author ID1: at Wed Oct 31 14:00:00 2001 ]y[Author ID1: at Wed Oct 31 14:00:00 2001 ] tak, jak powinno, do funkcji składowej klasy Pegasus:

virtual COLOR GetColor()const { return Horse::GetColor(); }

To powoduje ukrycie problemu przed klientami klasy Pegasus i ukrywa wewnątrz tej klasy wiedzę o tym, z której klasy bazowej obiekt chce odczytać swój kolor. Klient jednak w dalszym ciągu może wymusić odczytanie koloru z żądanej klasy, pisząc:

COLOR currentColor = pPeg->Bird::GetColor();

Dziedziczenie ze wspólnej klasy bazowej

Co się stanie, gdy zarówno klasa Bird, jak i klasa Horse dziedziczy ze wspólnej klasy bazowej, takiej jak klasa Animal? Sytuacja wygląda podobnie do przedstawionej na rysunku 14.2.

Rys. 14.2. Wspólne klasy bazowe

0x01 graphic

Jak widać na rysunku 14.2, istnieją dwa obiekty klasy bazowej. Gdy następuje odwołanie do danej lub metody we wspólnej klasie bazowej, powstaje kolejna niejednoznaczność. Na przykład, jeśli klasa Animal deklaruje itsAge jako swoją zmienną składową i GetAge() jako swoją funkcję składową, po czym[Author ID1: at Wed Oct 31 14:01:00 2001 ]i jeśli[Author ID1: at Wed Oct 31 14:01:00 2001 ] wywołamy pPeg->GetAge(), to czy chodzi nam o wywołanie funkcji GetAge() odziedziczonej od klasy Animal poprzez klasę Horse, czy poprzez klasę Bird? Tę niejednoznaczność także musimy usunąć, tak jak ilustruje listing 14.5.

Listing 14.5. Wspólne klasy bazowe

0: // Listing 14.5

1: // Wspólne klasy bazowe

2:

3: #include <iostream>

4: using namespace std;

5:

6: typedef int HANDS;

7: enum COLOR { Red, Green, Blue, Yellow, White, Black, Brown } ;

8:

9: class Animal // wspólna klasa dla klasy Bird i Horse

10: {

11: public:

12: Animal(int);

13: virtual ~Animal() { cout << "Destruktor klasy Animal...\n"; }

14: virtual int GetAge() const { return itsAge; }

15: virtual void SetAge(int age) { itsAge = age; }

16: private:

17: int itsAge;

18: };

19:

20: Animal::Animal(int age):

21: itsAge(age)

22: {

23: cout << "Konstruktor klasy Animal...\n";

24: }

25:

26: class Horse : public Animal

27: {

28: public:

29: Horse(COLOR color, HANDS height, int age);

30: virtual ~Horse() { cout << "Destruktor klasy Horse...\n"; }

31: virtual void Whinny()const { cout << "Ihaaa!... "; }

32: virtual HANDS GetHeight() const { return itsHeight; }

33: virtual COLOR GetColor() const { return itsColor; }

34: protected:

35: HANDS itsHeight;

36: COLOR itsColor;

37: };

38:

39: Horse::Horse(COLOR color, HANDS height, int age):

40: Animal(age),

41: itsColor(color),itsHeight(height)

42: {

43: cout << "Konstruktor klasy Horse...\n";

44: }

45:

46: class Bird : public Animal

47: {

48: public:

49: Bird(COLOR color, bool migrates, int age);

50: virtual ~Bird() {cout << "Destruktor klasy Bird...\n"; }

51: virtual void Chirp()const { cout << "Cwir, cwir... "; }

52: virtual void Fly()const

53: { cout << "Moge latac! Moge latac! Moge latac! "; }

54: virtual COLOR GetColor()const { return itsColor; }

55: virtual bool GetMigration() const { return itsMigration; }

56: protected:

57: COLOR itsColor;

58: bool itsMigration;

59: };

60:

61: Bird::Bird(COLOR color, bool migrates, int age):

62: Animal(age),

63: itsColor(color), itsMigration(migrates)

64: {

65: cout << "Konstruktor klasy Bird...\n";

66: }

67:

68: class Pegasus : public Horse, public Bird

69: {

70: public:

71: void Chirp()const { Whinny(); }

72: Pegasus(COLOR, HANDS, bool, long, int);

73: virtual ~Pegasus() {cout << "Destruktor klasy Pegasus...\n";}

74: virtual long GetNumberBelievers() const

75: { return itsNumberBelievers; }

76: virtual COLOR GetColor()const { return Horse::itsColor; }

77: virtual int GetAge() const { return Horse::GetAge(); }

78: private:

79: long itsNumberBelievers;

80: };

81:

82: Pegasus::Pegasus(

83: COLOR aColor,

84: HANDS height,

85: bool migrates,

86: long NumBelieve,

87: int age):

88: Horse(aColor, height,age),

89: Bird(aColor, migrates,age),

90: itsNumberBelievers(NumBelieve)

91: {

92: cout << "Konstruktor klasy Pegasus...\n";

93: }

94:

95: int main()

96: {

97: Pegasus *pPeg = new Pegasus(Red, 5, true, 10, 2);

98: int age = pPeg->GetAge();

99: cout << "Ten pegaz ma " << age << " lat.\n";

100: delete pPeg;

101: return 0;

102: }

Wynik

Konstruktor klasy Animal...

Konstruktor klasy Horse...

Konstruktor klasy Animal...

Konstruktor klasy Bird...

Konstruktor klasy Pegasus...

Ten pegaz ma 2 lat.

Destruktor klasy Pegasus...

Destruktor klasy Bird...

Destruktor klasy Animal...

Destruktor klasy Horse...

Destruktor klasy Animal...

Analiza

Na tym listingu występuje kilka ciekawych elementów. W liniach od 9. do 18. została zadeklarowana klasa Animal. Klasa ta posiada własną zmienną składową, itsAge, oraz dwa akcesory przeznaczone do jej odczytywania i ustawiania: GetAge() i SetAge().

W linii 26. rozpoczyna się deklaracja klasy Horse, wyprowadzonej z klasy Animal. Obecnie konstruktor klasy Horse posiada trzeci parametr, age (wiek), który przekazuje do swojej klasy bazowej. Zwróćmy uwagę, że klasa Horse nie przesłania metody GetAge(), lecz po prostu ją dziedziczy.

W linii 46. rozpoczyna się deklaracja klasy Bird, także wyprowadzonej z klasy Animal. Jej konstruktor także otrzymuje wiek i używa go do inicjalizacji swojej klasy bazowej. Oprócz tego, także w tej klasie nie jest przesłaniana metoda GetAge().

Klasa Pegasus dziedziczy po klasach Bird i Horse, więc w swoim łańcuchu dziedziczenia posiada dwie klasy Animal. Gdybyśmy zechcieli wywołać funkcję GetAge() dla obiektu klasy Pegasus, musielibyśmy usunąć niejednoznaczność, czyli użyć pełnej kwalifikowanej nazwy metody, której chcielibyśmy użyć.

Problem ten zostaje rozwiązany w linii 77., w której klasa Pegasus przesłania metodę GetAge() tak, aby nie robiła niczego poza przekazaniem wywołania w górę, do którejś z metod w klasach bazowych.

Przekazywanie w górę odbywa się z dwóch powodów: albo w celu rozwiązania niejednoznaczności wywołania metody klasy bazowej, tak jak w tym przypadku, albo w celu wykonania pewnej pracy, a następnie pozwolenia, by klasa bazowa wykonała dodatkową pracę. Czasem zechcemy wykonać pracę i dopiero potem wywołać metodę klasy bazowej, a czasem zechcemy najpierw wywołać metodę klasy bazowej, a dopiero po powrocie z niej wykonać swoją pracę.

Konstruktor klasy Pegasus posiada pięć parametrów: jego kolor, wysokość (w DŁONIACH), zmienną określającą, czy migruje, ilość wierzących w niego osób oraz wiek. Konstruktor inicjalizuje część Horse obiektu kolorem, wysokością i wiekiem (w linii 89.). Część Bird inicjalizowana jest kolorem, określeniem, czy migruje oraz wiekiem (w linii 88.). Na zakończenie, w linii 90., konstruktor inicjalizuje swoją składową itsNumberBelievers.

Wywołanie konstruktora klasy Horse w linii 88. powoduje wywołanie jego implementacji zawartej w linii 39. Konstruktor klasy Horse używa parametru age do zainicjalizowania części Animal obiektu klasy Pagasus. Następnie inicjalizuje dwie składowe klasy HorseitsColor oraz itsHeight.

Wywołanie konstruktora klasy Bird w linii 89. wywołuje jego implementację zawartą w linii 61. Także w tym przypadku parametr age jest używany do zainicjalizowania części Animal obiektu.

Zwróć uwagę, że parametr koloru przekazany konstruktorowi klasy Pegasus jest używany do zainicjalizowania zmiennych składowych zarówno w klasie Bird, jak i klasie Horse. Zauważ także. że parametr age jest używany do zainicjalizowania wieku (zmiennej itsAge) w klasie Animal, otrzymanej od klasy Bird oraz w klasie Animal, otrzymanej od klasy Horse.

Dziedziczenie wirtualne

Na listingu 14.5 pokazano, jak klasa Pegasus musiała usuwać pewne niejednoznaczności związane z tym, która z bazowych klas Animal miała być użyta. W większości przypadków taka decyzja może być dowolna — w końcu klasy Horse i Bird mają tę samą klasę bazową.

Istnieje możliwość poinformowania C++, że nie chcemy mieć dwóch kopii wspólnej klasy bazowej, tak jak widzieliśmy na rysunku 14.2, ale że chcemy mieć pojedynczą wspólną klasę bazową, tak jak pokazano na rysunku 14.3.

Rys. 14.3. Dziedziczenie wirtualne

0x01 graphic

Osiągniemy to, czyniąc z klasy Animal wirtualną klasę bazową zarówno dla klasy Horse, jak i klasy Bird. Klasa Animal nie ulega żadnej zmianie. W deklaracjach klas Horse i Bird przed nazwą klasy Animal dodawane jest jedynie słowo kluczowe virtual. Jednak klasa Pegasus podlega już większym modyfikacjom.

Normalnie konstruktor klasy inicjalizuje tylko swoje własne zmienne i swoją klasę bazową. Klasy bazowe dziedziczone wirtualnie są jednak wyjątkiem. Są one inicjalizowane przez swoje najbardziej wyprowadzone klasy. Tak więc klasa Animal nie będzie inicjalizowana przez klasy Horse i Bird, ale przez klasę Pegasus. Klasy Horse i Bird muszą w swoich konstruktorach inicjalizować klasę Animal, ale w przypadku tworzenia obiektu klasy Pegasus te inicjalizacje są ignorowane.

Listing 14.6 zawiera zmodyfikowaną wersję listingu 14.5, wykorzystującą zalety dziedziczenia wirtualnego.

Listing 14.6. Przykład użycia dziedziczenia wirtualnego

0: // Listing 14.6

1: // Dziedziczenie wirtualne

2: #include <iostream>

3: using namespace std;

4:

5: typedef int HANDS;

6: enum COLOR { Red, Green, Blue, Yellow, White, Black, Brown } ;

7:

8: class Animal // wspólna klasa dla klasy Bird i Horse

9: {

10: public:

11: Animal(int);

12: virtual ~Animal() { cout << "Destruktor klasy Animal...\n"; }

13: virtual int GetAge() const { return itsAge; }

14: virtual void SetAge(int age) { itsAge = age; }

15: private:

16: int itsAge;

17: };

18:

19: Animal::Animal(int age):

20: itsAge(age)

21: {

22: cout << "Konstruktor klasy Animal...\n";

23: }

24:

25: class Horse : virtual public Animal

26: {

27: public:

28: Horse(COLOR color, HANDS height, int age);

29: virtual ~Horse() { cout << "Destruktor klasy Horse...\n"; }

30: virtual void Whinny()const { cout << "Ihaaa!... "; }

31: virtual HANDS GetHeight() const { return itsHeight; }

32: virtual COLOR GetColor() const { return itsColor; }

33: protected:

34: HANDS itsHeight;

35: COLOR itsColor;

36: };

37:

38: Horse::Horse(COLOR color, HANDS height, int age):

39: Animal(age),

40: itsColor(color),itsHeight(height)

41: {

42: cout << "Konstruktor klasy Horse...\n";

43: }

44:

45: class Bird : virtual public Animal

46: {

47: public:

48: Bird(COLOR color, bool migrates, int age);

49: virtual ~Bird() {cout << "Destruktor klasy Bird...\n"; }

50: virtual void Chirp()const { cout << "Cwir, cwir... "; }

51: virtual void Fly()const

52: { cout << "Moge latac! Moge latac! Moge latac! "; }

53: virtual COLOR GetColor()const { return itsColor; }

54: virtual bool GetMigration() const { return itsMigration; }

55: protected:

56: COLOR itsColor;

57: bool itsMigration;

58: };

59:

60: Bird::Bird(COLOR color, bool migrates, int age):

61: Animal(age),

62: itsColor(color), itsMigration(migrates)

63: {

64: cout << "Konstruktor klasy Bird...\n";

65: }

66:

67: class Pegasus : public Horse, public Bird

68: {

69: public:

70: void Chirp()const { Whinny(); }

71: Pegasus(COLOR, HANDS, bool, long, int);

72: virtual ~Pegasus() {cout << "Destruktor klasy Pegasus...\n";}

73: virtual long GetNumberBelievers() const

74: { return itsNumberBelievers; }

75: virtual COLOR GetColor()const { return Horse::itsColor; }

76: private:

77: long itsNumberBelievers;

78: };

79:

80: Pegasus::Pegasus(

81: COLOR aColor,

82: HANDS height,

83: bool migrates,

84: long NumBelieve,

85: int age):

86: Horse(aColor, height,age),

87: Bird(aColor, migrates,age),

88: Animal(age*2),

89: itsNumberBelievers(NumBelieve)

90: {

91: cout << "Konstruktor klasy Pegasus...\n";

92: }

93:

94: int main()

95: {

96: Pegasus *pPeg = new Pegasus(Red, 5, true, 10, 2);

97: int age = pPeg->GetAge();

98: cout << "Ten pegaz ma " << age << " lat.\n";

99: delete pPeg;

100: return 0;

101: }

Wynik

Konstruktor klasy Animal...

Konstruktor klasy Horse...

Konstruktor klasy Bird...

Konstruktor klasy Pegasus...

Ten pegaz ma 4 lat.

Destruktor klasy Pegasus...

Destruktor klasy Bird...

Destruktor klasy Horse...

Destruktor klasy Animal...

Analiza

W linii 25. klasa Horse deklaruje, że dziedziczy wirtualnie po klasie Animal.[Author ID1: at Wed Oct 31 14:02:00 2001 ],[Author ID1: at Wed Oct 31 14:02:00 2001 ] zaś w linii 45 [Author ID1: at Wed Oct 31 14:04:00 2001 ]klasa [Author ID1: at Wed Oct 31 14:03:00 2001 ]Bird[Author ID1: at Wed Oct 31 14:03:00 2001 ] zgłasza t[Author ID1: at Wed Oct 31 14:03:00 2001 ]T[Author ID1: at Wed Oct 31 14:03:00 2001 ]ę samą deklarację zgłasza klasa [Author ID1: at Wed Oct 31 14:03:00 2001 ]Bird [Author ID1: at Wed Oct 31 14:03:00 2001 ]w linii [Author ID1: at Wed Oct 31 14:04:00 2001 ]45[Author ID1: at Wed Oct 31 14:04:00 2001 ]. Zauważ, że konstruktory obu tych klas nadal inicjalizują obiekt Animal.

Klasa Pegasus dziedziczy po klasach Bird i Horse, zaś[Author ID1: at Wed Oct 31 14:05:00 2001 ]i z tej racji[Author ID1: at Wed Oct 31 14:05:00 2001 ], jako najbardziej wyprowadzona klasa, musi inicjalizować klasę Animal. Podczas inicjalizacji obiektu klasy Pegasus, wywołania konstruktora[Author ID1: at Wed Oct 31 14:05:00 2001 ]ów[Author ID1: at Wed Oct 31 14:05:00 2001 ] klasy [Author ID1: at Wed Oct 31 14:05:00 2001 ]Animal[Author ID1: at Wed Oct 31 14:05:00 2001 ] w konstruktorach [Author ID1: at Wed Oct 31 14:05:00 2001 ]klas Bird i Horse są ignorowane. Możemy to zobaczyć, gdyż do konstruktora przekazywana jest wartość 2, która w klasach Horse i Bird jest[Author ID1: at Wed Oct 31 14:07:00 2001 ]byłaby[Author ID1: at Wed Oct 31 14:07:00 2001 ] przekazywana dalej (do [Author ID1: at Wed Oct 31 14:07:00 2001 ]Animal[Author ID1: at Wed Oct 31 14:07:00 2001 ])[Author ID1: at Wed Oct 31 14:07:00 2001 ], natomiast w klasie Pegasus jest podwajana. Rezultat, wartość 4, został odzwierciedlony w komunikacie wypisywanym w linii 98. i widocznym w wynikach.

Klasa Pegasus nie musi już usuwać niejednoznaczności wywołania metody GetAge() i może po prostu odziedziczyć tę funkcję od klasy Animal. Zauważ jednak, że klasa Pegasus musi w dalszym ciągu usuwać niejednoznaczność wywołania metody GetColor(), gdyż ta funkcja występuje w obu klasach bazowych, a nie w klasie Animal.

Deklarowanie klas dla dziedziczenia wirtualnego

Aby zapewnić, że wyprowadzone klasy mają tylko jeden egzemplarz wspólnej klasy bazowej, zadeklaruj klasy pośrednie tak, aby wirtualnie [Author ID1: at Wed Oct 31 14:07:00 2001 ]dziedziczyły wirtualnie [Author ID1: at Wed Oct 31 14:07:00 2001 ]po klasie bazowej.

Przykład 1

class Horse : virtual public Animal

class Bird : virtual public Animal

class Pegasus : public Horse, public Bird

Przykład 2

class Schnauzer : virtual public Dog

class Poodle : virtual public Dog

class Schnoodle : public Schnauzer, public Poodle

Problemy z dziedziczeniem wielokrotnym

Choć dziedziczenie wielokrotne w porówniu do dziedziczenia pojedynczego posiada kilka zalet, jednak wielu programistów C++ korzysta z niego niechętnie. Tłumaczą to faktem, iż wiele kompilatorów jeszcze go nie obsługuje, że utrudnia ono debuggowanie oraz że prawie wszystko to, co można uzyskać dzięki dziedziczeniu wielokrotnemu, da się uzyskać także bez niego.

Są to ważkie powody i sam powinieneś uważać, by nie komplikować niepotrzebnie swoich programów. Niektóre debuggery nie radzą sobie z dziedziczeniem wielokrotnym; zdarza się też, że wprowadzenie wielokrotnego dziedziczenia niepotrzebnie komplikuje projekt programu.

TAK

NIE

Używaj dziedziczenia wielokrotnego, jeśli nowa klasa wymaga funkcji i cech pochodzących z więcej niż jednej klasy bazowej.

Używaj dziedziczenia wirtualnego wtedy, gdy najbardziej wyprowadzona klasa musi posiadać tylko jeden egzemplarz wspólnej klasy bazowej.

Używając wirtualnych klas bazowych, inicjalizuj wspólną klasę bazową w klasie [Author ID1: at Wed Oct 31 14:08:00 2001 ]najbardziej wyprowadzonej.[Author ID1: at Wed Oct 31 14:08:00 2001 ] z klas.[Author ID1: at Wed Oct 31 14:08:00 2001 ]

Nie używaj dziedziczenia wielokrotnego tam, gdzie wystarczy dziedziczenie pojedyncze.

Mixiny i klasy metod

Jednym ze sposobów uzyskania efektu pośredniego pomiędzy dziedziczeniem wielokrotnym a pojedynczym jest użycie tak zwanych mixinów. Możemy więc wyprowadzić klasę Horse z klasy Animal oraz z klasy Displayable (możliwy do wyświetlenia). Klasa Displayable będzie posiadała jedynie kilka metod służących do wyświetlenia dowolnego obiektu na ekranie.

Mixin, czyli klasa metod, jest klasą dodającą[Author ID1: at Wed Oct 31 14:08:00 2001 ]zwiększającą[Author ID1: at Wed Oct 31 14:08:00 2001 ] funkcjonalność, ale nie posiadającą własnych danych lub posiadającą ich bardzo niewiele.

Klasy metod są łączone (miksowane — stąd nazwa) z klasami wyprowadzanymi w taki sam sposób, jak wszystkie inne klasy: przez zadeklarowanie klasy wyprowadzonej jako klasy publicznie po nich dziedziczącej. Jedyną[Author ID1: at Wed Oct 31 14:09:00 2001 ]a[Author ID1: at Wed Oct 31 14:09:00 2001 ] różnicą[Author ID1: at Wed Oct 31 14:09:00 2001 ]a[Author ID1: at Wed Oct 31 14:09:00 2001 ] po[Author ID1: at Wed Oct 31 14:09:00 2001 ]między klasą metod a inną klasą jest to, że klasa metod zwykle nie zawiera danych. Jest to oczywiście rozróżnienie dość dowolne, które przypomina jedynie, że czasem jedyną rzeczą, jakiej potrzebujemy, jest dołączenie pewnych dodatkowych możliwości bez komplikowania klasy wyprowadzonej.

W przypadku pewnych debuggerów łatwiej jest pracować z mixinami niż z bardziej złożonymi obiektami dziedziczonymi wielokrotnie. Oprócz tego prawdopodobieństwo wystąpienia niejednoznaczności przy dostępie do danych w innych podstawowych klasach bazowych jest mniejsze.

Na przykład, gdyby klasa Horse dziedziczyła po klasach Animal i Displayable, wtedy klasa Displayable nie zawierałaby żadnych danych. Klasa Animal wyglądałaby tak jak zwykle, więc wszystkie dane w klasie Horse pochodziłyby z klasy Animal, zaś funkcje składowe pochodziłyby z obu klas bazowych.

Określenie mixin narodziło się w pewnej lodziarni w Sommerville w stanie Massachusetts, w której z podstawowymi smakami lodów miksowano różne ciastka i słodycze. Dla odwiedzających lodziarnię programistów zorientowanych obiektowo stanowiło to dobrą metaforę, szczególnie wtedy, gdy pracowali nad obiektowo zorientowanym językiem SCOOPS.

Abstrakcyjne typy danych

Często hierarchie klas tworzone są wspólnie. Na przykład, możemy stworzyć klasę Shape (kształt), a z niej wyprowadzić klasy Rectangle (prostokąt) i Circle (okrąg). Z klasy Rectangle możemy wyprowadzić klasę Square (kwadrat), stanowiącą specjalny przypadek prostokąta.

W każdej z wyprowadzonych klas zostanie przesłonięta metoda Draw() (rysuj), GetArea() (pobierz obszar), i tak dalej. Listing 14.7 ilustruje podstawowy szkielet implementacji klasy Shape i wyprowadzonych z niej klas Circle i Rectangle.

Listing 14.7. Klasy kształtów

0: //Listing 14.7. Klasy kształtów

1:

2: #include <iostream>

3: using std::cout;

4: using std::cin;

5: using std::endl;

6:

7: class Shape

8: {

9: public:

10: Shape(){}

11: virtual ~Shape(){}

12: virtual long GetArea() { return -1; } // błąd

13: virtual long GetPerim() { return -1; }

14: virtual void Draw() {}

15: private:

16: };

17:

18: class Circle : public Shape

19: {

20: public:

21: Circle(int radius):itsRadius(radius){}

22: ~Circle(){}

23: long GetArea() { return 3 * itsRadius * itsRadius; }

24: long GetPerim() { return 6 * itsRadius; }

25: void Draw();

26: private:

27: int itsRadius;

28: int itsCircumference;

29: };

30:

31: void Circle::Draw()

32: {

33: cout << "Procedura rysowania okregu!\n";

34: }

35:

36:

37: class Rectangle : public Shape

38: {

39: public:

40: Rectangle(int len, int width):

41: itsLength(len), itsWidth(width){}

42: virtual ~Rectangle(){}

43: virtual long GetArea() { return itsLength * itsWidth; }

44: virtual long GetPerim() {return 2*itsLength + 2*itsWidth; }

45: virtual int GetLength() { return itsLength; }

46: virtual int GetWidth() { return itsWidth; }

47: virtual void Draw();

48: private:

49: int itsWidth;

50: int itsLength;

51: };

52:

53: void Rectangle::Draw()

54: {

55: for (int i = 0; i<itsLength; i++)

56: {

57: for (int j = 0; j<itsWidth; j++)

58: cout << "x ";

59:

60: cout << "\n";

61: }

62: }

63:

64: class Square : public Rectangle

65: {

66: public:

67: Square(int len);

68: Square(int len, int width);

69: ~Square(){}

70: long GetPerim() {return 4 * GetLength();}

71: };

72:

73: Square::Square(int len):

74: Rectangle(len,len)

75: {}

76:

77: Square::Square(int len, int width):

78: Rectangle(len,width)

79: {

80: if (GetLength() != GetWidth())

81: cout << "Blad, nie Square... Moze Rectangle??\n";

82: }

83:

84: int main()

85: {

86: int choice;

87: bool fQuit = false;

88: Shape * sp;

89:

90: while ( !fQuit )

91: {

92: cout << "(1)Circle (2)Rectangle (3)Square (0)Wyjscie: ";

93: cin >> choice;

94:

95: switch (choice)

96: {

97: case 0: fQuit = true;

98: break;

99: case 1: sp = new Circle(5);

100: break;

101: case 2: sp = new Rectangle(4,6);

102: break;

103: case 3: sp = new Square(5);

104: break;

105: default: cout<<"Wpisz liczbe pomiedzy 0 a 3"<<endl;

106: continue;

107: break;

108: }

109: if( !fQuit )

110: sp->Draw();

111: delete sp;

112: sp = 0;

113: cout << "\n";

114: }

115: return 0;

116: }

Wynik

(1)Circle (2)Rectangle (3)Square (0)Wyjscie: 2

x x x x x x

x x x x x x

x x x x x x

x x x x x x

(1)Circle (2)Rectangle (3)Square (0)Wyjscie: 3

x x x x x

x x x x x

x x x x x

x x x x x

x x x x x

(1)Circle (2)Rectangle (3)Square (0)Wyjscie: 0

Analiza

W liniach od 7. do 16. tworzona jest klasa Shape. Metody GetArea() i GetPerim() (pobierz obwód) zwracają wartość, będącą kodem [Author ID1: at Wed Oct 31 14:10:00 2001 ]błędu, zaś metoda Draw() nie robi niczego. Właściwie, co to znaczy: narysować „kształt”? Można rysować jedynie rodzaje kształtów (okręgi, prostokąty, itd.); kształt jako taki jest abstrakcją, której nie można narysować.

Klasa Circle jest wyprowadzona z klasy Shape i prze[Author ID1: at Wed Oct 31 14:10:00 2001 ]y[Author ID1: at Wed Oct 31 14:10:00 2001 ]słania jej trzy metody wirtualne. Zauważ, że nie ma powodu do stosowania słowa kluczowego virtual, gdyż jest to część ich dziedziczenia. Nie zaszkodzi jednak tego uczynić, tak jak pokazano w klasie Rectangle w liniach 43., 44. oraz 47. Dobrym nawykiem jest stosowanie słowa kluczowego virtual w celu przypomnienia (jako formy dokumentacji).

Klasa Square jest wyprowadzona z klasy Rectangle; została w niej przesłonięta metoda GetPerim(), zaś inne metody zostały odziedziczone od klasy Rectangle.

Kłopoty mogą pojawić się, gdy klient spróbuje stworzyć egzemplarz obiektu klasy Shape, należy mu to uniemożliwić. Klasa Shape istnieje tylko po to, by dostarczać interfejsu dla wyprowadzonych z niej klas; jako taka jest abstrakcyjnym typem danych, czyli ADT (abstract data type).

Abstrakcyjny typ danych reprezentuje koncepcję (taką jak kształt), a nie sam obiekt (na przykład okrąg). W C++ ADT jest zawsze klasą bazową innych klas; tworzenie egzemplarzy obiektów klas abstrakcyjnych nie jest możliwe.

Czyste funkcje wirtualne

C++ obsługuje tworzenie abstrakcyjnych typów danych poprzez czyste funkcje wirtualne. Funkcja wirtualna staje się „czysta”, gdy zainicjalizujemy ją wartością zero, tak jak:

virtual void Draw() = 0;

Każda klasa, zawierająca jedną lub więcej czystych funkcji wirtualnych, staje się abstrakcyjnym typem danych i nie jest możliwe tworzenie jej obiektów. Próba stworzenia obiektu klasy abstrakcyjnej powoduje błąd kompilacji. Umieszczenie w klasie czystej funkcji wirtualnej sygnalizuje aplikacjom-[Author ID1: at Wed Oct 31 14:10:00 2001 ]klientom tej klasy dwie rzeczy:

Każda klasa wyprowadzona z klasy abstrakcyjnej dziedziczy czystą funkcję wirtualną jako czystą, dlatego, jeśli chce[Author ID1: at Wed Oct 31 14:11:00 2001 ]aby[Author ID1: at Wed Oct 31 14:11:00 2001 ] móc tworzyć egzemplarze obiektów, musi przesłonić każdą czystą funkcję wirtualną. Zatem, gdyby klasa Rectangle dziedziczyła po klasie Shape i klasa Shape miała trzy czyste funkcje wirtualne, wtedy klasa Rectangle musiałaby prze[Author ID1: at Wed Oct 31 14:11:00 2001 ]y[Author ID1: at Wed Oct 31 14:11:00 2001 ]słonić wszystkie trzy funkcje, gdyż w przeciwnym razie sama stałaby się klasą abstrakcyjną. Listing 14.8 został przepisany tak, aby klasa Shape stała się klasą abstrakcyjną. Aby zaoszczędzić miejsca, nie została tu pokazana pozostała część listingu 14.7. Zamień deklarację klasy Shape na listingu 14.7, linie od 7. do 16., deklaracją klasy Shape z listingu 14.8, po czym uruchom program ponownie.

Listing 14.8. Abstrakcyjne typy danych

0: //Listing 14.8 Abstrakcyjne typy danych

1:

2: class Shape

3: {

4: public:

5: Shape(){}

6: ~Shape(){}

7: virtual long GetArea() = 0; // błąd[Author ID1: at Wed Oct 31 14:11:00 2001 ]

8: virtual long GetPerim()= 0;

9: virtual void Draw() = 0;

10: private:

11: };

Wynik

(1)Circle (2)Rectangle (3)Square (0)Wyjscie: 2

x x x x x x

x x x x x x

x x x x x x

x x x x x x

(1)Circle (2)Rectangle (3)Square (0)Wyjscie: 3

x x x x x

x x x x x

x x x x x

x x x x x

x x x x x

(1)Circle (2)Rectangle (3)Square (0)Wyjscie: 0

Analiza

Jak widać, działanie programu nie zmieniło się. Jedyną różnicą jest to, że teraz nie jest możliwe stworzenie obiektu klasy Shape.

Abstrakcyjne typy danych

Klasa abstrakcyjna powstaje wtedy, gdy w jej deklaracji znajdzie się jedna lub więcej czystych funkcji wirtualnych. Funkcja wirtualna staje się „czysta”, gdy do jej deklaracji dopiszemy = 0.

Przykład

class Shape

{

virtual void Draw() = 0; // czysta funkcja wirtualna

};

Implementowanie czystych funkcji wirtualnych

Zwykle czyste funkcje wirtualne nie są implementowane w abstrakcyjnej klasie bazowej. Ponieważ nigdy nie są tworzone obiekty tego typu, nie ma powodu do dostarczania implementacji; klasa abstrakcyjna funkcjonuje wyłącznie jako definicja interfejsu dla obiektów z niej wyprowadzanych.

Czyste funkcje wirtualne mogą być jednak implementowane. Taka funkcja może być wywoływana przez klasy wyprowadzone z klasy abstrakcyjnej, na przykład w celu zapewnienia wspólne[Author ID1: at Wed Oct 31 14:13:00 2001 ]j funkcjoalności wszystkich funkcji [Author ID1: at Wed Oct 31 14:13:00 2001 ]przesłoniętych funkcji[Author ID1: at Wed Oct 31 14:13:00 2001 ]. Listing 14.9 stanowi reprodukcję listingu 14.7, w którym klasa Shape jest tym razem klasą abstrakcyjną, zawierającą implementację czystej funkcji wirtualnej Draw(). Klasa Circle przesłania funkcję Draw(), gdyż musi, ale potem [Author ID1: at Wed Oct 31 14:13:00 2001 ]przekazuje to [Author ID1: at Wed Oct 31 14:13:00 2001 ]wywołanie do klasy bazowej, w celu skorzystania z dodatkowej funkcjonalności.

W tym przykładzie dodatkowa funkcjonalność polega po prostu na wypisaniu dodatkowego komunikatu, ale można sobie wyobrazić, że klasa bazowa mogłaby zawierać wspólny mechanizm rysowania, który mógłby pomóc w przygotowaniu okna używanego przez wszystkie wyprowadzone klasy.

Listing 14.9. Implementowanie czystych funkcji wirtualnych

0: //Listing 14.9 Implementowanie czystych funkcji wirtualnych

1:

2: #include <iostream>

3: using namespace std;

4:

5: class Shape

6: {

7: public:

8: Shape(){}

9: virtual ~Shape(){}

10: virtual long GetArea() = 0; // błąd

11: virtual long GetPerim()= 0;

12: virtual void Draw() = 0;

13: private:

14: };

15:

16: void Shape::Draw()

17: {

18: cout << "Abstrakcyjny mechanizm rysowania!\n";

19: }

20:

21: class Circle : public Shape

22: {

23: public:

24: Circle(int radius):itsRadius(radius){}

25: virtual ~Circle(){}

26: long GetArea() { return 3 * itsRadius * itsRadius; }

27: long GetPerim() { return 9 * itsRadius; }

28: void Draw();

29: private:

30: int itsRadius;

31: int itsCircumference;

32: };

33:

34: void Circle::Draw()

35: {

36: cout << "Procedura rysowania okregu!\n";

37: Shape::Draw();

38: }

39:

40:

41: class Rectangle : public Shape

42: {

43: public:

44: Rectangle(int len, int width):

45: itsLength(len), itsWidth(width){}

46: virtual ~Rectangle(){}

47: long GetArea() { return itsLength * itsWidth; }

48: long GetPerim() {return 2*itsLength + 2*itsWidth; }

49: virtual int GetLength() { return itsLength; }

50: virtual int GetWidth() { return itsWidth; }

51: void Draw();

52: private:

53: int itsWidth;

54: int itsLength;

55: };

56:

57: void Rectangle::Draw()

58: {

59: for (int i = 0; i<itsLength; i++)

60: {

61: for (int j = 0; j<itsWidth; j++)

62: cout << "x ";

63:

64: cout << "\n";

65: }

66: Shape::Draw();

67: }

68:

69:

70: class Square : public Rectangle

71: {

72: public:

73: Square(int len);

74: Square(int len, int width);

75: virtual ~Square(){}

76: long GetPerim() {return 4 * GetLength();}

77: };

78:

79: Square::Square(int len):

80: Rectangle(len,len)

81: {}

82:

83: Square::Square(int len, int width):

84: Rectangle(len,width)

85:

86: {

87: if (GetLength() != GetWidth())

88: cout << "Blad, nie Square... moze Rectangle??\n";

89: }

90:

91: int main()

92: {

93: int choice;

94: bool fQuit = false;

95: Shape * sp;

96:

97: while (1)

98: {

99: cout << "(1)Circle (2)Rectangle (3)Square (0)Wyjscie: ";

100: cin >> choice;

101:

102: switch (choice)

103: {

104: case 1: sp = new Circle(5);

105: break;

106: case 2: sp = new Rectangle(4,6);

107: break;

108: case 3: sp = new Square (5);

109: break;

110: default: fQuit = true;

111: break;

112: }

113: if (fQuit)

114: break;

115:

116: sp->Draw();

117: delete sp;

118: cout << "\n";

119: }

120: return 0;

121: }

Wynik

(1)Circle (2)Rectangle (3)Square (0)Wyjscie: 2

x x x x x x

x x x x x x

x x x x x x

x x x x x x

Abstrakcyjny mechanizm rysowania!

(1)Circle (2)Rectangle (3)Square (0)Wyjscie: 3

x x x x x

x x x x x

x x x x x

x x x x x

x x x x x

Abstrakcyjny mechanizm rysowania!

(1)Circle (2)Rectangle (3)Square (0)Wyjscie: 0

Analiza

W liniach od 5. do 14. została zadeklarowana abstrakcyjna klasa Shape, której wszystkie trzy metody użytkowe zostały zadeklarowane jako czyste funkcje wirtualne. Zwróć uwagę, że nie jest to konieczne. Gdyby którakolwiek z tych metod zostałaby zadeklarowana jako czysta, i tak cała klasa byłaby traktowana jako abstrakcyjna.

Metody GetArea() oraz GetPerim() nie zostały zaimplementowane (w przeciwieństwie do metody Draw()). W klasach Circle i Rectangle metoda Draw() została przesłonięta, jednakże[Author ID1: at Wed Oct 31 14:14:00 2001 ] i [Author ID1: at Wed Oct 31 14:14:00 2001 ]w obu przypadkach,[Author ID1: at Wed Oct 31 14:15:00 2001 ] w przesłoniętych wersjach jest wywoływana także[Author ID1: at Wed Oct 31 14:15:00 2001 ] metoda klasy bazowej, w celu dodatkowego [Author ID1: at Wed Oct 31 14:15:00 2001 ]skorzystania ze wspólnej funkcjonalności.

Złożone hierarchie abstrakcji

Czasem zdarza się, że wyprowadzamy klasy abstrakcyjne[Author ID1: at Wed Oct 31 14:16:00 2001 ]y[Author ID1: at Wed Oct 31 14:16:00 2001 ] z innych klas abstrakcyjnych. Być może zechcemy zmienić niektóre z odziedziczonych czystych funkcji wirtualnych w zwykłe funkcje, zaś inne pozostawić jako czyste.

Jeśli stworzymy klasę Animal, to wszystkie [Author ID1: at Wed Oct 31 14:16:00 2001 ]metody typu Eat() (jedzenie), Sleep() (spanie), Move() (poruszanie się) i Reproduce() (rozmnażanie) możemy wszystkie [Author ID1: at Wed Oct 31 14:17:00 2001 ]zmienić na czyste funkcje wirtualne. Być może, [Author ID1: at Wed Oct 31 14:18:00 2001 ]zechcesz N[Author ID1: at Wed Oct 31 14:18:00 2001 ]n[Author ID1: at Wed Oct 31 14:18:00 2001 ]astępnie wyprowadzić[Author ID1: at Wed Oct 31 14:18:00 2001 ] z klasy Animal możemy [Author ID1: at Wed Oct 31 14:18:00 2001 ]na przykład wyprowadzić [Author ID1: at Wed Oct 31 14:18:00 2001 ]klasę[Author ID1: at Wed Oct 31 14:19:00 2001 ]y[Author ID1: at Wed Oct 31 14:19:00 2001 ] Mammal (ssak) lub Fish (ryba).

Po dłuższej analizie hierarchii, możemy zdecydować, że każdy ssak będzie rozmnażał się w ten sam sposób, więc metodę Mammal::Reproduce() zamienimy w zwykłą funkcję, pozostawiając metody Eat(), Sleep() i Move() w postaci czystych funkcji wirtualnych.

Z klasy Mammal wyprowadzimy klasę Dog (pies), w której musimy przesłonić i zaimplementować trzy pozostałe czyste funkcje wirtualne tak, aby móc tworzyć egzemplarze obiektów tej klasy.

Jako projektanci klasy stwierdzamy fakt, że nie można tworzyć egzemplarzy klas Animal i Mammal, i że wszystkie obiekty klasy Mammal mogą dziedziczyć dostarczoną metodę Reproduce() bez konieczności jej przesłaniania.

Technikę tę ilustruje listing 14.10; zastosowano w nim jedynie szkieletową implementację omawianych klas.

Listing 14.10. Wyprowadzanie klas abstrakcyjnych z innych klas abstrakcyjnych

0: // Listing 14.10

1: // Wyprowadzanie klas abstrakcyjnych z innych klas abstrakcyjnych

2: #include <iostream>

3: using namespace std;

4:

5: enum COLOR { Red, Green, Blue, Yellow, White, Black, Brown } ;

6:

7: class Animal // wspólna klasa bazowa dla klas Mammal i Fish

8: {

9: public:

10: Animal(int);

11: virtual ~Animal() { cout << "Destruktor klasy Animal...\n"; }

12: virtual int GetAge() const { return itsAge; }

13: virtual void SetAge(int age) { itsAge = age; }

14: virtual void Sleep() const = 0;

15: virtual void Eat() const = 0;

16: virtual void Reproduce() const = 0;

17: virtual void Move() const = 0;

18: virtual void Speak() const = 0;

19: private:

20: int itsAge;

21: };

22:

23: Animal::Animal(int age):

24: itsAge(age)

25: {

26: cout << "Konstruktor klasy Animal...\n";

27: }

28:

29: class Mammal : public Animal

30: {

31: public:

32: Mammal(int age):Animal(age)

33: { cout << "Konstruktor klasy Mammal...\n";}

34: virtual ~Mammal() { cout << "Destruktor klasy Mammal...\n";}

35: virtual void Reproduce() const

36: { cout << "Rozmnazanie dla klasy Mammal...\n"; }

37: };

38:

39: class Fish : public Animal

40: {

41: public:

42: Fish(int age):Animal(age)

43: { cout << "Konstruktor klasy Fish...\n";}

44: virtual ~Fish() {cout << "Destruktor klasy Fish...\n"; }

45: virtual void Sleep() const { cout << "Ryba spi...\n"; }

46: virtual void Eat() const { cout << "Ryba zeruje...\n"; }

47: virtual void Reproduce() const

48: { cout << "Ryba sklada jaja...\n"; }

49: virtual void Move() const

50: { cout << "Ryba plywa...\n"; }

51: virtual void Speak() const { }

52: };

53:

54: class Horse : public Mammal

55: {

56: public:

57: Horse(int age, COLOR color ):

58: Mammal(age), itsColor(color)

59: { cout << "Konstruktor klasy Horse...\n"; }

60: virtual ~Horse() { cout << "Destruktor klasy Horse...\n"; }

61: virtual void Speak()const { cout << "Ihaaa!... \n"; }

62: virtual COLOR GetItsColor() const { return itsColor; }

63: virtual void Sleep() const

64: { cout << "Kon spi...\n"; }

65: virtual void Eat() const { cout << "Kon sie pasie...\n"; }

66: virtual void Move() const { cout << "Kon biegnie...\n";}

67:

68: protected:

69: COLOR itsColor;

70: };

71:

72: class Dog : public Mammal

73: {

74: public:

75: Dog(int age, COLOR color ):

76: Mammal(age), itsColor(color)

77: { cout << "Konstruktor klasy Dog...\n"; }

78: virtual ~Dog() { cout << "Destruktor klasy Dog...\n"; }

79: virtual void Speak()const { cout << "Hau, hau!... \n"; }

80: virtual void Sleep() const { cout << "Pies chrapie...\n"; }

81: virtual void Eat() const { cout << "Pies je...\n"; }

82: virtual void Move() const { cout << "Pies biegnie...\n"; }

83: virtual void Reproduce() const

84: { cout << "Pies sie rozmnaza...\n"; }

85:

86: protected:

87: COLOR itsColor;

88: };

89:

90: int main()

91: {

92: Animal *pAnimal=0;

93: int choice;

94: bool fQuit = false;

95:

96: while (1)

97: {

98: cout << "(1)Dog (2)Horse (3)Fish (0)Quit: ";

99: cin >> choice;

100:

101: switch (choice)

102: {

103: case 1: pAnimal = new Dog(5,Brown);

104: break;

105: case 2: pAnimal = new Horse(4,Black);

106: break;

107: case 3: pAnimal = new Fish (5);

108: break;

109: default: fQuit = true;

110: break;

111: }

112: if (fQuit)

113: break;

114:

115: pAnimal->Speak();

116: pAnimal->Eat();

117: pAnimal->Reproduce();

118: pAnimal->Move();

119: pAnimal->Sleep();

120: delete pAnimal;

121: cout << "\n";

122: }

123: return 0;

124: }

Wynik

(1)Dog (2)Horse (3)Fish (0)Quit: 1

Konstruktor klasy Animal...

Konstruktor klasy Mammal...

Konstruktor klasy Dog...

Hau, hau!...

Pies je...

Pies sie rozmnaza...

Pies biegnie...

Pies chrapie...

Destruktor klasy Dog...

Destruktor klasy Mammal...

Destruktor klasy Animal...

(1)Dog (2)Horse (3)Fish (0)Quit: 0

Analiza

W liniach od 7. do 21. została zadeklarowana abstrakcyjna klasa Animal. Klasa ta posiada zwykłe[Author ID1: at Wed Oct 31 14:20:00 2001 ]y[Author ID1: at Wed Oct 31 14:20:00 2001 ] wirtualne[Author ID1: at Wed Oct 31 14:20:00 2001 ]y[Author ID1: at Wed Oct 31 14:20:00 2001 ] akcesory[Author ID1: at Wed Oct 31 14:20:00 2001 ] do[Author ID1: at Wed Oct 31 14:20:00 2001 ]la[Author ID1: at Wed Oct 31 14:20:00 2001 ] swojej składowej itsAge, współuzytkowanej[Author ID1: at Wed Oct 31 14:20:00 2001 ] przez wszystkie obiekty tej klasy. Oprócz tego posiada pięć czystych funkcji [Author ID1: at Wed Oct 31 14:20:00 2001 ]wirtualnych funkcji[Author ID1: at Wed Oct 31 14:20:00 2001 ]: Sleep(), Eat(), Reproduce(), Move() i Speak() (mówienie).

Klasa Mammal, zadeklarowana w liniach od 29. do 37., została wyprowadzona z klasy Animal. Nie posiada ona żadnych własnych danych. Przesłania jednak funkcję Reproduce(), zapewniając wspólną formę rozmnażania dla wszystkich ssaków. W klasie Fish funkcja Reproduce() musi zostać przesłonięta, gdyż ta klasa musi [Author ID1: at Wed Oct 31 14:21:00 2001 ]dziedziczyć[Author ID1: at Wed Oct 31 14:21:00 2001 ] bezpośrednio po klasie Animal i nie może skorzystać z rozmnażania na wzór [Author ID1: at Wed Oct 31 14:21:00 2001 ]ssaków (i dobrze!).

Klasy ssaków nie muszą już przesłaniać funkcji Reproduce(), ale jeśli chcą, mogą to zrobić, na przykład tak, jak klasa Dog w linii 83. Klasy Fish, Horse i Dog wszystkie [Author ID1: at Wed Oct 31 14:22:00 2001 ]przesłaniają pozostałe czyste funkcje wirtualne, dzięki czemu można tworzyć specyficzne dla nich [Author ID1: at Wed Oct 31 14:22:00 2001 ]egzemplarze ich [Author ID1: at Wed Oct 31 14:22:00 2001 ]obiektów.

Znajdujący się w ciele programu wskaźnik do klasy Animal jest używany do kolejnego [Author ID1: at Wed Oct 31 14:23:00 2001 ]wskazywania obiektów różnych wyprowadzonych klas. Wywoływane są metody wirtualne i, w zależności od powiązań tworzonych dynamicznie, z [Author ID1: at Wed Oct 31 14:23:00 2001 ]właściwych klas pochodnych wywoływane są właściwe funkcje.

Próba stworzenia egzemplarza klasy Animal lub Mammal spowodowałaby błąd kompilacji, gdyż obie te klasy są klasami abstrakcyjnymi.

Które typy są abstrakcyjne?

Klasa Animal jest abstrakcyjna w jednym programie, a w innym nie. Co sprawia[Author ID1: at Wed Oct 31 14:23:00 2001 ]decyduje o tym,[Author ID1: at Wed Oct 31 14:23:00 2001 ] że jakąś [Author ID1: at Wed Oct 31 14:24:00 2001 ]klasę[Author ID1: at Wed Oct 31 14:24:00 2001 ]a[Author ID1: at Wed Oct 31 14:24:00 2001 ] jest[Author ID1: at Wed Oct 31 14:24:00 2001 ]czynimy[Author ID1: at Wed Oct 31 14:24:00 2001 ] abstrakcyjną[Author ID1: at Wed Oct 31 14:24:00 2001 ]a[Author ID1: at Wed Oct 31 14:24:00 2001 ]?

Odpowiedź na to pytanie nie zależy od żadnego określonego[Author ID1: at Wed Oct 31 14:24:00 2001 ]czynnika zewnętrznego[Author ID1: at Wed Oct 31 14:24:00 2001 ], ale od tego, co ma sens dla danego programu. Jeśli piszesz program opisujący farmę lub ogród zoologiczny, możesz zdecydować, by klasa Animal była klasą abstrakcyjną, a klasa Dog była klasą, której obiekty mógłbyś samodzielnie tworzyć.

Z drugiej strony, gdybyś tworzył animowane schronisko dla psów, mógłbyś zdecydować, by klasa Dog była abstrakcyjnym typem danych i tworzyć jedynie egzemplarze ras psów: jamniki, teriery i tak dalej. Poziom abstrakcji zależy od tego, jak precyzyjnie chcesz rozróżniać swoje typy.

TAK

NIE

W celu zapewnienia wspólnej funkcjonalności licznym powiązanym ze sobą klasom, używaj typów abstrakcyjnych.

Przesłaniaj wszystkie czyste funkcje wirtualne.

Jeśli funkcja musi zostać przesłonięta, zamień ją w czystą funkcję wirtualną.

Nie próbuj tworzyć egzemplarzy obiektów klas abstrakcyjnych.

Program podsumowujący wiadomości

{uwaga korekta: to jest zawartość rozdziału „Week 2 In Review” }

W tym programie zebrano w w całość wiele omawianych w poprzednich rozdziałach zagadnień.

Przedstawiona poniżej demonstracja połączonych list wykorzystuje funkcje wirtualne, czyste funkcje wirtualne, przesłanianie funkcji, polimorfizm, dziedziczenie publiczne, przeciążanie funkcji, pętle nieskończone, wskaźniki, referencje i inne elementy. Należy zwrócić uwagę, że ta lista połączona różni się od listy opisywanej wcześniej; w C++ zwykle ten sam efekt można uzyskać na kilka sposobów.

Celem programu jest utworzenie listy połączonej. Węzły listy są zaprojektowane w celu przechowywania części samochodowych, takich, jakie mogłyby być używane w fabryce. Choć nie jest to ostateczna wersja programu, stanowi jednak dobry przykład dość zaawansowanej struktury danych. Kod ma prawie trzysta linii; zanim przystąpisz do przeczytania analizy przedstawionej po wynikach jego działania, spróbuj przeanalizować go samodzielnie.

Listing 14.11. Program podsumowujący wiadomości

0: // **************************************************

1: //

2: // Tytuł: Program podsumowujący numer 2

3: //

4: // Plik: 4eList1411

5: //

6: // Opis: Program demonstruje tworzenie listy połączonej

7: //

8: // Klasy: PART - zawiera numery części oraz ewentualnie inne

9: // informacje na ich temat

10: //

11: // PartNode - pełni rolę węzła w liście PartsList

12: //

13: // PartsList - dostarcza mechanizmu dla połączonej

14: // listy obiektów PartNode

15: //

16: //

17: // **************************************************

18:

19: #include <iostream>

20: using namespace std;

21:

22:

23:

24: // **************** Part ************

25:

26: // Abstrakcyjna klasa bazowa części

27: class Part

28: {

29: public:

30: Part():itsPartNumber(1) {}

31: Part(int PartNumber):itsPartNumber(PartNumber){}

32: virtual ~Part(){};

33: int GetPartNumber() const { return itsPartNumber; }

34: virtual void Display() const =0; // musi być przesłonięte

35: private:

36: int itsPartNumber;

37: };

38:

39: // implementacja czystej funkcji wirtualnej, dzięki czemu

40: // mogą z niej korzystać klasy pochodne

41: void Part::Display() const

42: {

43: cout << "\nNumer czesci: " << itsPartNumber << endl;

44: }

45:

46: // **************** Część samochodu ************

47:

48: class CarPart : public Part

49: {

50: public:

51: CarPart():itsModelYear(94){}

52: CarPart(int year, int partNumber);

53: virtual void Display() const

54: {

55: Part::Display(); cout << "Rok modelu: ";

56: cout << itsModelYear << endl;

57: }

58: private:

59: int itsModelYear;

60: };

61:

62: CarPart::CarPart(int year, int partNumber):

63: itsModelYear(year),

64: Part(partNumber)

65: {}

66:

67:

68: // **************** Część samolotu ************

69:

70: class AirPlanePart : public Part

71: {

72: public:

73: AirPlanePart():itsEngineNumber(1){};

74: AirPlanePart(int EngineNumber, int PartNumber);

75: virtual void Display() const

76: {

77: Part::Display(); cout << "Nr silnika.: ";

78: cout << itsEngineNumber << endl;

79: }

80: private:

81: int itsEngineNumber;

82: };

83:

84: AirPlanePart::AirPlanePart(int EngineNumber, int PartNumber):

85: itsEngineNumber(EngineNumber),

86: Part(PartNumber)

87: {}

88:

89: // **************** Węzeł części ************

90: class PartNode

91: {

92: public:

93: PartNode (Part*);

94: ~PartNode();

95: void SetNext(PartNode * node) { itsNext = node; }

96: PartNode * GetNext() const;

97: Part * GetPart() const;

98: private:

99: Part *itsPart;

100: PartNode * itsNext;

101: };

102:

103: // Implementacje klasy PartNode

104:

105: PartNode::PartNode(Part* pPart):

106: itsPart(pPart),

107: itsNext(0)

108: {}

109:

110: PartNode::~PartNode()

111: {

112: delete itsPart;

113: itsPart = 0;

114: delete itsNext;

115: itsNext = 0;

116: }

117:

118: // Gdy nie ma następnego węzła, zwraca NULL

119: PartNode * PartNode::GetNext() const

120: {

121: return itsNext;

122: }

123:

124: Part * PartNode::GetPart() const

125: {

126: if (itsPart)

127: return itsPart;

128: else

129: return NULL; //błąd

130: }

131:

132: // **************** Klasa PartList ************

133: class PartsList

134: {

135: public:

136: PartsList();

137: ~PartsList();

138: // wymaga konstruktora kopiujacego[Author ID1: at Wed Oct 31 14:25:00 2001 ]i[Author ID1: at Wed Oct 31 14:25:00 2001 ] i operatora po[Author ID1: at Wed Oct 31 14:25:00 2001 ]rzy[Author ID1: at Wed Oct 31 14:25:00 2001 ]równania!

139: Part* Find(int & position, int PartNumber) const;

140: int GetCount() const { return itsCount; }

141: Part* GetFirst() const;

142: void Insert(Part *);

143: void Iterate() const;

144: Part* operator[](int) const;

145: private:

146: PartNode * pHead;

147: int itsCount;

148: };

149:

150: // Implementacje dla list...

151:

152: PartsList::PartsList():

153: pHead(0),

154: itsCount(0)

155: {}

156:

157: PartsList::~PartsList()

158: {

159: delete pHead;

160: }

161:

162: Part* PartsList::GetFirst() const

163: {

164: if (pHead)

165: return pHead->GetPart();

166: else

167: return NULL; // w celu wykrycia błędu

168: }

169:

170: Part * PartsList::operator[](int offSet) const

171: {

172: PartNode* pNode = pHead;

173:

174: if (!pHead)

175: return NULL; // w celu wykrycia błędu

176:

177: if (offSet > itsCount)

178: return NULL; // błąd

179:

180: for (int i=0;i<offSet; i++)

181: pNode = pNode->GetNext();

182:

183: return pNode->GetPart();

184: }

185:

186: Part* PartsList::Find(int & position, int PartNumber) const

187: {

188: PartNode * pNode = 0;

189: for (pNode = pHead, position = 0;

190: pNode!=NULL;

191: pNode = pNode->GetNext(), position++)

192: {

193: if (pNode->GetPart()->GetPartNumber() == PartNumber)

194: break;

195: }

196: if (pNode == NULL)

197: return NULL;

198: else

199: return pNode->GetPart();

200: }

201:

202: void PartsList::Iterate() const

203: {

204: if (!pHead)

205: return;

206: PartNode* pNode = pHead;

207: do

208: pNode->GetPart()->Display();

209: while (pNode = pNode->GetNext());

210: }

211:

212: void PartsList::Insert(Part* pPart)

213: {

214: PartNode * pNode = new PartNode(pPart);

215: PartNode * pCurrent = pHead;

216: PartNode * pNext = 0;

217:

218: int New = pPart->GetPartNumber();

219: int Next = 0;

220: itsCount++;

221:

222: if (!pHead)

223: {

224: pHead = pNode;

225: return;

226: }

227:

228: // jeśli ten węzeł jest mniejszy niż głowa,

229: // wtedy staje się nową głową

230: if (pHead->GetPart()->GetPartNumber() > New)

231: {

232: pNode->SetNext(pHead);

233: pHead = pNode;

234: return;

235: }

236:

237: for (;;)

238: {

239: // jeśli nie ma następnego, dołączamy ten nowy

240: if (!pCurrent->GetNext())

241: {

242: pCurrent->SetNext(pNode);

243: return;

244: }

245:

246: // jeśli trafia pomiędzy bieżący a nastepny,

247: // wstawiamy go tu[Author ID1: at Wed Oct 31 14:25:00 2001 ]o[Author ID1: at Wed Oct 31 14:25:00 2001 ]; w przeciwnym razie bierzemy następny

248: pNext = pCurrent->GetNext();

249: Next = pNext->GetPart()->GetPartNumber();

250: if (Next > New)

251: {

252: pCurrent->SetNext(pNode);

253: pNode->SetNext(pNext);

254: return;

255: }

256: pCurrent = pNext;

257: }

258: }

259:

260: int main()

261: {

262: PartsList pl;

263:

264: Part * pPart = 0;

265: int PartNumber;

266: int value;

267: int choice;

268:

269: while (1)

270: {

271: cout << "(0)Wyjscie (1)Samochod (2)Samolot: ";

272: cin >> choice;

273:

274: if (!choice)

275: break;

276:

277: cout << "Nowy numer czesci?: ";

278: cin >> PartNumber;

279:

280: if (choice == 1)

281: {

282: cout << "Model?: ";

283: cin >> value;

284: pPart = new CarPart(value,PartNumber);

285: }

286: else

287: {

288: cout << "Numer silnika?: ";

289: cin >> value;

290: pPart = new AirPlanePart(value,PartNumber);

291: }

292:

293: pl.Insert(pPart);

294: }

295: pl.Iterate();

296: return 0;

297: }

Wynik

(0)Wyjscie (1)Samochod (2)Samolot: 1

Nowy numer czesci?: 2837

Model?: 90

(0)Wyjscie (1)Samochod (2)Samolot: 2

Nowy numer czesci?: 378

Numer silnika?: 4938

(0)Wyjscie (1)Samochod (2)Samolot: 1

Nowy numer czesci?: 4499

Model?: 94

(0)Wyjscie (1)Samochod (2)Samolot: 1

Nowy numer czesci?: 3000

Model?: 93

(0)Wyjscie (1)Samochod (2)Samolot: 0

Numer czesci: 378

Nr silnika.: 4938

Numer czesci: 2837

Rok modelu: 90

Numer czesci: 3000

Rok modelu: 93

Numer czesci: 4499

Rok modelu: 94

Analiza

Przedstawiony tu listing zawiera implementację listy połączonej, przechowującej obiekty klasy Part (część). Lista połączona jest dynamiczną strukturą danych, mogącą dostosowywać swoje rozmiary do potrzeb programu.

Ta konkretna lista połączona została zaprojektowana w celu przechowywania obiektów klasy Part, przy czym klasa Part jest klasą abstrakcyjną, pełniącą rolę klasy bazowej dla wszystkich obiektów posiadających numer części. W tym przykładzie z klasy Part zostały wydzielone dwie podklasy: CarPart (część samochodowa) oraz AirPlanePart (część samolotu).

Klasa Part jest deklarowana w liniach od 27. do 37. i składa się z numeru części oraz kilku akcesorów. W rzeczywistym programie klasa ta mogłaby zawierać dodatkowe informacje o częściach, na przykład na temat komponentów, z jakich się składają, ile takich części znajduje się w magazynie i tak dalej. Klasa Part jest abstrakcyjnym typem danych, wymuszonym przez czystą funkcję wirtualną Display() (wyświetl).

Zauważmy, że metoda Display() posiada w liniach od 41. do 44. swoją implementację. W ten sposób została wyrażona intencja projektanta kodu; chciał on by klasy pochodne tworzyły własne implementacje metody Display(), a jednocześnie mogły w nich korzystać z metody Display() w klasie bazowej.

Dwie proste klasy pochodne, CarPart oraz AirPlanePart zostały zadeklarowane w liniach od 48. do 66[Author ID1: at Wed Oct 31 14:26:00 2001 ].0[Author ID1: at Wed Oct 31 14:26:00 2001 ] oraz od 70. do 88. Każda z nich zawiera przesłoniętą metodę Display(), która jednak w obu przypadkach wywołuje metodę Display() klasy bazowej.

Klasa PartNode (węzeł części) pełni rolę interfejsu pomiędzy klasą Part a klasą PartsList (lista części). Zawiera ona wskaźnik do części oraz wskaźnik do następnego węzła na liście. Jej jedynymi metodami są metody przeznaczone do ustawiania i odczytywania następnego węzła listy oraz do zwracania wskazywanego przez węzeł obiektu Part.

Cała tajemnica działania listy kryje się w klasie PartsList, zadeklarowanej w liniach od 133. do 148. Klasa PartsList przechowuje wskaźnik do pierwszego elementu listy (pHead) i używa go we wszystkich metodach przetwarzających tę listę. Przetworzenie listy oznacza odpytanie każdego z węzłów o następny węzeł, aż do czasu osiągnięcia węzła, którego wskaźnik itsNext wynosi NULL.

Jest to implementacja częściowa; w pełni zaprojektowana lista oferowywałaby albo lepszy dostęp do swojego pierwszego i ostatniego węzła, albo dostarczałaby obiektu iteratora, pozwalającego klientom na łatwe poruszanie się po liście.

Jednak klasa PartsList i tak posiada kilka interesujących metod, opisanych poniżej w kolejności alfabetycznej. Zwykle dobrze jest zachowywać taką kolejność, gdyż ułatwia ona odszukanie funkcji.

Metoda Find() otrzymuje numer części oraz referencję do typu int. Jeśli zostanie znaleziona część zgodna z PartNumber (numerem części), funkcja zwraca wskaźnik do obiektu Part i wypełnia referencję pozycją tej części na liście. Jeśli szukany numer części nie zostanie znaleziony, funkcja zwraca wartość NULL i numer pozycji nie ma znaczenia.

Metoda GetCount() zwraca ilość elementów na liście. Klasa PartsList przechowuje tę wartość jako swoją zmienną składową, itsCount, choć oczywiście mogłaby ją obliczać, przetwarzając listę.

Metoda GetFirst() zwraca wskaźnik do pierwszego obiektu Part na liście; w przypadku, gdy lista jest pusta, zwraca wartość NULL.

Metoda Insert() otrzymuje wskaźnik do obiektu klasy Part, tworzy dla niego obiekt PartNode, po czym dodaje go do listy, zgodnie z kolejnością wyznaczaną przez PartNumber.

Metoda Iterate() otrzymuje wskaźnik do funkcji składowej klasy Part, która nie posiada parametrów, zwraca void i jest funkcją const. Wywołuje tę funkcję dla każdego obiektu klasy Part na liście. W naszym przykładowym programie zostaje wywołana funkcja Display(), będąca funkcją wirtualną, więc w każdym przypadku zostaje wywołana metoda odpowiedniej podklasy klasy Part.

Operator[] umożliwia bezpośredni dostęp do [Author ID1: at Wed Oct 31 14:26:00 2001 ]obiektu Part [Author ID1: at Wed Oct 31 14:27:00 2001 ], [Author ID1: at Wed Oct 31 14:27:00 2001 ]znajdującego się we wskazanym położeniu na liście. Wykonywane jest przy tym podstawowe sprawdzanie zakresów; jeśli lista jest pusta lub podane położenie wykracza poza rozmiar listy, jako wartość błędu zostaje zwrócona wartość NULL.

Zwróć uwagę, że w rzeczywistym programie takie opisy funkcji zostałyby zapisane w deklaracji klasy.

Program sterujący jest zawarty w liniach od 260. do 298. Obiekt PartsList jest tworzony w linii 263.

W linii 278. użytkownik jest proszony o wybranie, czy część powinna być wprowadzana dla samochodu, czy dla samolotu. W zależności od tego wyboru tworzona jest odpowiednia część, która następnie jest wstawiana do listy w linii 294.

Implementacja metody Insert() klasy PartsList znajduje się w liniach od 212. do 258. Gdy zostaje wprowadzony numer pierwszej części, 2837, zostaje stworzony obiekt CarPar o tym numerze części i modelu 90, który jest przekazywany do metody LinkedList::Insert().

W linii 214. dla tej części jest tworzony nowy obiekt PartNode, zaś zmienna New jest inicjalizowana numerem części. Zmienna itsCount klasy PartsList jest inkrementowana w linii 220.

W linii 222., w teście sprawdzającym, czy pHead ma wartość NULL, otrzymujemy wartość TRUE. Ponieważ jest to pierwszy węzeł, wskaźnik pHead listy na nic jeszcze nie wskazuje. Zatem w linii 224. wskaźnik pHead jest ustawiany tak, aby wskazywał nowy węzeł, po czym następuje powrót z funkcji.

Użytkownik jest proszony o wybranie kolejnej części; tym razem zostaje wybrana część do samolotu, o numerze części 378 i numerze silnika 4938. Ponownie jest wywoływana metoda PartsList::Insert(), a pNode jest ponownie inicjalizowane nowym węzłem. Statyczna zmienna składowa itsCount zostaje zwiększona do dwóch i następuje sprawdzenie wskaźnika pHead. Ponieważ ostatnim razem temu wskaźnikowi została przypisana wartość, nie zawiera on [Author ID1: at Wed Oct 31 14:28:00 2001 ]już wartości NULL i test zwraca wartość FALSE.

W linii 230. numer części wskazywanej przez pHead, 2837, jest porównywany z numerem bieżącej części, 378. Ponieważ numer nowej części jest mniejszy od numeru części wskazywanej przez pHead, nowa część musi stać się nowym pierwszym elementem listy, więc test w linii 230. zwraca wartość TRUE.

W linii 232. nowy węzeł jest ustawiany tak, by wskazywał na węzeł aktualnie wskazywany przez wskaźnik pHead. Zwróć uwagę, że nowy węzeł nie wskazuje na pHead, ale na węzeł wskazywany przez pHead! W linii 233. pHead jest ustawiany tak, by wskazywał na nowy węzeł.

Przy trzecim wykonaniu pętli użytkownik wybrał część samochodową o numerze 4499 i modelu 94. Następuje zwiększenie licznika i tym razem numer części nie jest mniejszy od numeru wskazywanego przez pHead, więc przechodzimy do pętli for, rozpoczynającej się w linii 237.

Wartością wskazywaną przez pHead jest 378. Wartością wskazywaną przez następny węzeł jest 2837. Bieżącą wartością jest 4499. Wskaźnik pCurrent wskazuje na ten sam węzeł, co pHead, więc posiada następną wartość; pCurrent wskazuje na drugi węzeł, więc test w linii 240 zwraca wartość FALSE.

Wskaźnik pCurrent jest ustawiany tak, by wskazywał następny węzeł, po czym następuje ponowne wykonanie pętli. Tym razem test w linii 240. zwraca wartość TRUE. Nie ma następnego elementu, więc w linii 242. bieżący węzeł zostaje ustawiony tak, by wskazywał nowy węzeł, po czym proces wstawiania się kończy.

Za czwartym razem wprowadzona zostaje część o numerze 3000. Jej wstawianie jest bardzo podobne do przypadku opisanego powyżej, ale tym razem, gdy bieżący węzeł wskazuje numer 2837, a następny 4499, test w linii 250. zwraca wartość TRUE i nowy węzeł jest wstawiany właśnie w tej pozycji.

Gdy użytkownik w końcu wybierze zero, test w linii 275. zwraca wartość TRUE i następuje wyjście z pętli while(1). Wykonanie programu przechodzi do linii 296., w której wywoływana jest funkcja Iterate(). Wykonanie przechodzi do linii 202., a w linii 208. wskaźnik PNode jest wykorzystywany do uzyskania dostępu do obiektu Part i wywołania dla niego metody Display.

2 Część I Podstawy obsługi systemu WhizBang (Nagłówek strony)

2 F:\korekta\r14-06.doc



Wyszukiwarka

Podobne podstrony:
5536
5536
5536
5536
5536

więcej podobnych podstron