C++ bez cholesterolu

Wstęp

Programowanie hierarchiczne stanowi wstęp do programowania obiektowego. Programowanie obiektowe składa się w istocie z dwóch rzeczy: hierarchizmu (czyli rozszerzania znaczenia typów podstawowych przez dodawanie im nowych właściwości w typach pochodnych) oraz polimorfizmu (czyli specjalizacji różnych podstawowych elementów z typów podstawowych w typach pochodnych). Programowanie hierarchiczne polega przede wszystkim na wykorzystaniu narzędzi umożliwiających hierarchizowanie struktur danych. Ich wykorzystanie wszakoż nie jest ograniczone do zastosowań obiektowych.

Podstawą programowania hierarchicznego jest tzw. dziedziczenie. Jest to druga z dostępnych w C++ metod wyprowadzania jednych typów z drugich – pierwszą jest, jak wiemy, zawieranie się pól w strukturach. Ponieważ w efekcie pozwala to na wyróżnianie w obiekcie części, która dysponuje osobną definicją typu, część taką nazywamy pod-obiektem. Symetrycznie zatem, obiekt nadrzędny dla tej części nazywamy nad-obiektem, np.:

struct X { int a, b; };
X x;
int z = x.b;

W powyższym przykładzie `x.b' referuje do pod-obiektu obiektu `x'. Natomiast `x' jest nad-obiektem dla `x.b'. Jak widać, pod-obiekty mają swoje osobne typy, definicje itd. Z dziedziczeniem jest podobnie, z tym tylko że nie wszystkie typy mogą być dziedziczone no i dziedziczyć z konkretnego typu można tylko raz.

struct X { int a; };
struct Y: X { int b; }
X x;
Y y;

Przy takich definicjach, dla `y' istnieje: pod-obiekt `a' typu int, pod-obiekt `b' typu int oraz pod-obiekt typu odziedziczonego X. Jak się do niego dobrać? No dobrze, ale po co? Przecież do zawartości X i tak mamy dostęp, gdyż jego zawartość jest częścią zawartości Y. Niemniej konstrukcja taka pozwala traktować obiekt typu Y jak obiekt typu X i będzie na nim operować wszystko, co zdefiniowano dla typu X. Gdyby zatem obiekt typu Y przekazać do pewnej funkcji przez referencję do typu X, to taka funkcja będzie operowała na pod-obiekcie typu X obiektu typu Y (i to nic nie wiedząc o tym, że ten obiekt typu X jest tylko pod-obiektem innego obiektu). W tym rozdziale skupimy się na samym dziedziczeniu, wszelkich jego właściwościach oraz konsekwencjach w programowaniu w C++.

Tu należy też wspomnieć o paru istotnych szczegółach. Pierwsza rzecz, to jak wiadomo, C++ jest wyposażony w dziedziczenie wielorakie. Jest to, mówiąc oględnie, niemożliwe do zrealizowania w normalny sposób w żadnym języku obiektowym (słyszałem o językach obiektowych, które są podobno wyposażone w wielorakie dziedziczenie – nie słyszałem, by zdobyły popularność większą, niż tradycyjne języki obiektowe, a implementacja owego dziedziczenia jest cokolwiek dziwna). W normalnym prorgramowaniu obiektowym posiadanie przez obiekt więcej, niż jednego przodka jest bzdurą; dlatego też zazwyczaj dziedziczenia wielorakiego nie stosuje się w C++ przy programowaniu obiektowym, a jeśli się stosuje, to są to tylko krzyżujące się hierarchie, traktowane jednak oddzielnie. Nie sposób bowiem wykonać planu projektu, gdzie występuje wielorakie dziedziczenie – inaczej, niż przez utworzenie kilku diagramów, gdzie na każdym dziedziczenie jest pojedyncze.

Niemniej wykorzystanie samej hierarchizacji struktur pozwala uzyskać jedną ważną rzecz – właściwość. Każda klasa bazowa może zawierać jakieś konkretne definicje. Jeśli taka definicja podlega dziedziczeniu, to wystarczy jedynie zdefiniować klasę, która daną właściwość definiuje i "podpiąć" jako klasę bazową. Klasa pochodna będzie wtedy za pomocą jednej krótkiej deklaracji (dziedziczenia) zawierać wiele złożonych definicji. Nie ze wszystkim tak się da, bo jak wiemy, nie wszystko co się świeci podlega dziedziczeniu (nawet metody musimy ponownie zapowiedzieć w klasie pochodnej, jeśli chcemy je przedefiniować).

Jednakoż hierarchizacja struktur może nam także pomóc w definiowaniu części wspólnych struktur. Uważni na pewno jednak zaoponują, że to samo można uzyskać za pomocą zawierania pól. I tu mają rację – zawieranie pod-obiektów jako pola niewiele w gruncie rzeczy się różni od dziedziczenia.

Wymieńmy zatem podstawowe różnice:

No i oczywiście nie zapominajmy o skutkach "podlegania dziedziczeniu", czyli automatycznym przechodzeniu niektórych definicji do klasy pochodnej z klasy bazowej. Zwracam przede wszystkim uwagę na definicje statyczne, np. typedef'y, klasy zagnieżdżone oraz pola i metody statyczne. Te definicje w przypadku pól nie są nawet w ogóle dostępne; tylko pola i metody statyczne są dostępne za pośrednictwem nazwy pola. Pozostałe tylko za pośrednictwem nazwy klasy.

Reguły te oczywiście mocno się zmienią przy wzorcach klas (patrz rozdział "Programowanie generyczne") – nie będę się teraz na ten temat rozpisywał, tylko ostrzegam tak dla porządku :).