C++ bez cholesterolu: Programowanie hierarchiczne w C++:
Dziedziczenie
4.3 Dziedziczenie
Konsekwencje dziedziczenia
Każdy język obiektowy musi posiadać narzędzia umożliwiające wyprowadzanie
innych typów na bazie już istniejących, a przynajmniej możliwości określania
jakichkolwiek związków hierarchicznych między typami (w najgorszym wypadku -
możliwości przekazywania sobie obiektom nawzajem rozkazów wykonania operacji,
ale może nie komplikujmy aż tak - w C++ zdecydowano się na najprostsze
rozwiązanie). Stosowanie tych narzędzi nie determinuje jeszcze programowania
obiektowego co prawda, ale przecież C++ nie jest językiem służącym do
programowania obiektowego (jak Smalltalk), tylko do programowania. Co prawda
konsekwencją takiego podejścia jest konieczność wielokrotnie jawnego
określania, że rzeczywiście jakiejś - często ponoszącej koszty dodatkowe -
właściwości rzeczywiście chce się używać. Jednak w tym przypadku jeszcze nic
takiego się nie dzieje. Dziedziczenie powoduje bowiem tylko inny (często
dzięki temu skrócony) zapis definicji typów, możliwość częściowego
definiowania operacji i tak zwane "składanie z klocków".
Typem bazowym dla innych typów oczywiście może być tylko struktura (nie
unia!). Nie jest to jednak - jak się później przekonamy - takie znów bolesne
ograniczenie. Aby wyprowadzić nowy typ na bazie istniejącego (nazwijmy go
`A'), należy zadeklarować:
struct B: A
{
// definicje dodatkowe
};
Definicja taka pociąga za sobą odpowiednie konsekwencje. Mianowicie wszystko,
co zostało zadeklarowane w strukturze A, znajduje się również w strukturze B.
Pamiętajmy jednak, że nie każde A::x staje się w strukturze B dostępne jako
B::x (nazywa się to "podleganiem dziedziczeniu"); nie zachowuje się
tego mianowicie dla operatorów przypisania i konstruktorów. Te metody są
oczywiście dostępne wewnątrz metod struktury B, ale z przedrostkiem `A::'.
Powód jest prosty - definicja operatora przypisania i konstruktora kopiującego
jest dostosowana do struktury `A' i wcale nie oznacza, że będzie pasowała
do struktury `B' (nic nie stoi na przeszkodzie, żeby z nich skorzystać).
Dziedziczenie umożliwia przede wszystkim grupowanie typów (nie mówię
`klasyfikację', żeby nie zaperzyć ;). Typ B bowiem nie jest w takiej
sytuacji typem jakimś zupełnie nowym, lecz typem pochodnym A. A co za tym
idzie, wszelkie operacje, które można wykonać na obiekcie typu A, można
wykonać na obiekcie typu B. W związku z tym np. wskaźnik na typ `A' może
wskazywać na obiekt typu `B', aczkolwiek będzie go traktował jak obiekt
typu `A'. Część obiektu typu `B', która jest sama z siebie obiektem typu
`A' nazywamy PODOBIEKTEM.
Przedstawię jeszcze - trochę dla ciekawostki - jak ominąć niemożliwość
dziedziczenia po typach ścisłych i innych, które nie mogą być dziedziczone
(enumy i unie):
struct Int
{
int in;
operator int() { return in; }
Int() {}
Int( int i ): in( i ) { }
};
Teraz obiekt typu `Int' można śmiało przekazywać do wszystkich funkcji,
przyjmujących argument typu `int' (z wbudowanymi operatorami włącznie).
Niestety nie powoduje to bynajmniej, że typ `Int' jest pochodną `int' -
to jest tylko takie małe "oszustwo". W większości przypadków działa jak
trzeba, niestety nie zawsze - pewne właściwości C++ wymagają, aby podawać
wartości statyczne typów ścisłych (a *nie* wartości konwertowalne do ścisłych,
bo takowe nie mogą być konwertowane na wartości statyczne!). Nie można np.
stałymi tego typu oznaczać wymiaru tablic "surowych" (tych zwykłych).
Rzutowanie statyczne
Konsekwencją grupowania typów jest to, że wskaźnik lub referencja do typu
podstawowego może przyjmować również obiekty typów dla niego pochodnych. Tak
więc na przykład:
struct D: B { ... };
...
B b;
D d;
B* p = &b; // wiadomo
...
p = &d; // obiekt `d' jest traktowany jak obiekt struktury B
Poprzez wskaźnik dla struktury bazowej oczywiście można uzyskiwać dostęp tylko
do tych składowych obiektu, które są zadeklarowane w strukturze bazowej.
Jednak jeśli mamy obiekt struktury pochodnej przypisany do wskaźnika na
strukturę bazową, to można go przekonwertować na obiekt struktury pochodnej,
używając poznanego już operatora static_cast:
D* pd = static_cast<D*>( p );
Operator ten może być użyty w sytuacji, kiedy konwersja w odwrotnym kierunku
może być wykonana niejawnie. Jednak taka konwersja - w odróżnieniu od
dynamic_cast - wykonuje się podczas kompilacji i nie dokonuje żadnego
sprawdzenia. Można więc go użyć tylko wtedy, kiedy się jest absolutnie pewnym,
że dany wskaźnik rzeczywiście wskazuje na obiekt takiego typu, na jaki się
chce konwertować.
Wyszukiwarka
Podobne podstrony:
inheritsproper inheritancebasics of inheritanceprivate inheritanceInheritanceinheritance1InheritedInheritableThreadLocalstrange inheritanceinheritsjava lab05 inheritanceInherit the?rthinheritsinheritsInheritableThreadLocalinheritsinheritance?B9F437więcej podobnych podstron