r14.pdf
(
367 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
Plik z chomika:
Wojteczek
Inne pliki z tego folderu:
rdodc.pdf
(84 KB)
rdodb.pdf
(72 KB)
rdoda.pdf
(184 KB)
r21.pdf
(370 KB)
r20.pdf
(275 KB)
Inne foldery tego chomika:
Cisco
Kurs C++ od zera do hackera v. 1.0
Pascal
ZAHASŁOWANE
Zgłoś jeśli
naruszono regulamin