R14-06.DOC

(311 KB) Pobierz
Szablon dla tlumaczy

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ę:

·         czym jest dziedziczenie wielokrotne i jak z niego korzystać,

·         czym jest dziedziczenie wirtualne,

·         czym są abstrakcyjne typy danych,

·         czym są czyste funkcje wirtualne.

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, ż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 błędachów. 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 tymipodanymi wyżej 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 przeniesienieosząc metodyę Fly() w górę albo poprzez rzutowanieując wskaźnika w dół, a mimo to wciąż nie osiągnęliśmy pełnej funkcjonalności.

Pojawia się jeszcze jedno, ostatnie rozwiązanie, również wykorzystujące pojedyncze dziedziczenie. Możemy umieścić metody Fly(), Whinny() oraz Gallop() w klasie ba...

Zgłoś jeśli naruszono regulamin