Czym jest dziedziczenie i po co to komu?! Ten mechanizm jest bardzo przydatny przy programowaniu obiektowym. Co więcej jest wręcz wymarzony przy rozwijaniu oprogramowania. Pozwala na tworzenie nowych klas i obiektów bez kopiowania treści obiektów poprzednich. No może nie do końca bez kopiowania, ale … programista nie musi “przeklejać” kodu do nowych struktur. Tym zajmie się właśnie mechanizm dziedziczenia. Przykładem samego dziedziczenia może być poniższy schemat:

Jak to odczytać? Na samej górze (jak w drzewie genealogicznym) znajduje się obiekt, który ma w sobie zdefiniowane podstawowe metody i cechy (wartości) np. zmienną: bool czyŻyje lub np.: set oddychanie {“skrzela”, “płuca”, “tchawki”, “płucotchawki”, “przetchlinki”, “inne”}; dekretujące obiekt jako opisujący materię ożywioną lub nieożywioną (wg. przykładu ze schematu powyżej) lub ze względu na rodzaj oddychania. Tworząc klasę opisującą np. ryby nadal ich cechą jest czyŻyjeoddychanie. Dodatkową cechą charakterystyczną dla ryb jest klasyfikator set kształtCiała {“ wrzecionowate “, “torpedowaty”, “strzałkowaty”, “igłowaty”, “węgorzowaty”, “wstęgowaty”, “inne”}, który zawiera inne wartości niż np.: dla ssaków. Wynika z tego, że “na górze” znajduje się klasa, która opisuje największy zbiór zawierając najmniej cech. Poruszając się “w dół”, definiując kolejne klasy doprecyzowujemy ich opis, który zaczyna obejmować coraz mniejszy zbiór, za to posiadając coraz liczniejsze i dokładniejsze cechy. Dzieje się to poprzez dziedziczenie listy cech (metody i wartości) z klasy nadrzędnej (tego “wyżej” inaczej zwanego rodzicem, z ang.: parent) i tworzenie klasy potomnej (zwanej dzieckiem, ang.: child), która wynikowo posiada w sobie metody odziedziczone oraz własne, stworzone przez programistę. Jak widać taki zabieg pozwala uniknąć “ręcznego” kopiowania ze źródła klasy nadrzędnej. Wpływa to na czytelność kodu, zmniejszenie jego skomplikowania i uniknięcie błędów przy kopiowaniu kodu i ustalaniu jego współzależności.

Poniżej przedstawiony jest kod pokazujący dziedziczenie jednobazowe i wykorzystanie tego w programie:

#include<iostream>
#include <windows.h>

using namespace std;
class Czlowiek {
public:
 string imie, nazwisko, Pesel;
 struct {
		int rok,miesiac,dzien;
	} dataUrodzenia;
 int wzrost;

public:
 Czlowiek():imie("brak"), nazwisko("brak"), Pesel("xxxxxxxxxxx"), wzrost(-1), dataUrodzenia({1990,0,0}) {
 cout << "Uruchamiam konstruktor klasy Czlowiek..." <<endl;
	}
	
};


class Pracownik :public Czlowiek {
public:
 string numerUmowy;
public:	
 Pracownik():numerUmowy("brak") {
   cout << "Uruchamiam konstruktor klasy Pracownik..." <<endl;
	}
};
	


main() {
 SetConsoleCP( 852 );
 setlocale ( LC_ALL, "" );	
 
 Czlowiek czlowiek;
 Pracownik pracownik;

 cout << pracownik.imie << " " << pracownik.nazwisko << 
      " urodzony: " << pracownik.dataUrodzenia.rok << "-" << 
      pracownik.dataUrodzenia.miesiac << "-" <<
      pracownik.dataUrodzenia.dzien << " ma umowę nr: " << 
      pracownik.numerUmowy << endl;
	
	
 return 0;
}

W programie zdefiniowaliśmy klasę Czlowiek, która zawiera takie pola jak: imie, nazwisko, Pesel, dataUrodzenia (struktura złożona z trzech pól typu int: rok, miesiac, dzien) oraz wzrost. W metodzie uruchomieniowej main powołaliśmy do życia obiekt czlowiek klasy Czlowiek.

Możemy “dostać się” do zdefiniowanych elementów poprzez instancję tegoż obiektu.

Także i mamy drugą klasę o nazwie Pracownik, w której zdefiniowaliśmy zmienną numerUmowy, i która to klasa dziedziczy (jednobazowo) z klasy Czlowiek. W metodzie uruchomieniowej main powołujemy do życia instancję klasy Pracownik w postaci obiektu pracownik.

Następna linijka to kod “dostania się” do pól instancji tego obiektu pracownik. Wynikiem jest wyświetlenie ciągu znakowego:

 Uruchamiam konstruktor klasy Czlowiek… 
 Uruchamiam konstruktor klasy Czlowiek…
 Uruchamiam konstruktor klasy Pracownik…
 brak brak urodzony: 1990-0-0 ma umowę nr: brak

Co wynikło z tego programu? W ten sposób po pierwsze widać, że powołanie do życia instancji klasy Czlowiek… uruchomiło jej konstruktor (komentarz na ekranie). Powołanie do życia instancji klasy Pracownik… również uruchomiło jej konstruktor (trzecia linijka: komentarz na ekranie). Tutaj jednak zdarzyło się coś więcej, bowiem pojawiła się druga linijka (niejako powtórzona z pierwszej) – jest to dowód na to, że instancja klasy Pracownik, dziedzicząc po klasie Czlowiek będąc tworzona również prócz swojego konstruktora, uruchamia konstruktor klasy nadrzędnej. Można z tego wysnuć wniosek, że w “długim sekwencyjnie” dziedziczeniu, będą po kolei uruchamiane konstruktory klas nadrzędnych poprzez następujące po nich w dziedziczeniu klasy podrzędne, aż do konstruktora na szczycie sekwencji dziedziczenia (klasa bazowa).

Zadanie do samodzielnego wykonania: Uruchom powyższy przykład i nabądź wprawy w tworzeniu dziedziczenia poprzez dodanie kolejnego elementu w sekwencji dziedziczenia – np. klasy Szef. Spróbuj odwoływać się do poszczególnych pól zdefiniowanych w różnych klasach.

Są także szczególne sytuacje, kiedy przydałoby się odziedziczyć opisy z dwu różnych klas, wg przykładu przedstawionego poniżej:

Ten przykład opisuje potrzebę dziedziczenia wielobazowego, czyli opartego na więcej niż jednym rodzicu (klasie nadrzędnej). Widać też tutaj, że taka potrzeba jest uzasadniona. W języku C++ jest to jak najbardziej możliwe (czego nie ma np. języku Java).

Zapisując to w postaci pseudokodu mielibyśmy następujący przykład:

class Pojazd {};
class Samochód :public Pojazd {};
class Łódź :public Pojazd {};
class Samolot :public Pojazd {};
class Amfibia :public Samochód, public Łódź {};
class Hydroplan :public Łódź, public Samolot {};

Zadanie do samodzielnego wykonania: Przekoduj powyższy pseudokod na działający program. Nabądź wprawy w tworzeniu dziedziczenia poprzez dodanie kolejnych elementów w sekwencji dziedziczenia – np. klasy Poduszkowiec. Spróbuj odwoływać się do poszczególnych pól zdefiniowanych w różnych klasach.

Nadszedł czas, aby wrócić do selektorów public, private protected. Jak dało się zauważyć w kodzie występują one w dwu miejscach. Jedno z nich to określenie bloku w programie od selektora do końca bloku (bądź kolejnego selektora) oznaczanego jako public, private, czy protected. Drugim zaś jest użycie selektorów przy dziedziczeniu (w użytych przykładach były same public).

Selektor dostępu public oznacza, że wszystkie elementy leżące w jego zasięgu są dostępne spoza klasy; czyli, że można się do nich odwołać bezpośrednio – tak jak to zrobiliśmy w powyższym przykładzie wyświetlając dane pracownika. Selektor private oznacza, że następujące po nim (aż do końca bloku, lub innego selektora) treści będą dostępne tylko z wnętrza klasy – dla metod w ramach klasy. Selektor protected oznacza, że następujące po nim (aż do końca bloku, lub innego selektora) treści będą dostępne tylko z wnętrza klasy i w sekwencji dziedziczenia – dla metod w ramach klasy i potomnych.

Zestawienie selektorów zasięgu oraz dziedziczenia daje nam wynikowo podejście najbardziej restrykcyjne, czyli jeżeli dziedziczymy publicznie to dostęp do klasy nadrzędnej jest zgodny z selektorem dostępu zdefiniowanym w tej klasie jako public lub protected.. Jeśli dziedziczymy jako private, to wszystkie dziedziczone elementy stają się w ramach klasy dziedziczącej elementami typu private jeśli były oznaczone w klasie nadrzędnej selektorem public lub protected. Jeśli dziedziczymy jako protected, to wszystkie elementy klasy dziedziczonej stają się w ramach klasy dziedziczącej jako protected jeśli były oznaczone w klasie nadrzędnej jako public lub protected. Jest jedna sytuacja, kiedy dziedziczenie powoduje, że pomimo skopiowania elementów do obiektu potomnego nie mamy do nich dostępu – jest to sytuacja, kiedy w klasie nadrzędnej są one oznaczone jako private – czyli dostępne TYLKO w ramach tej klasy. Wtedy chcąc się odwołać do tych elementów MUSIMY robić to przez odpowiednie, odziedziczone metody klasy nadrzędnej, lub… zawrzeć relację przyjaźni pomiędzy klasą nadrzędną, a podrzędną.

Sposób dziedziczenia
public private protected
Widoczność

w klasie

bazowej
public public private protected
private * * *
protected protected private protected

Relacja przyjaźni jest to “wskazanie” w ramach klasy nadrzędnej, że w przypadku dziedziczenia po tej klasie wskazane klasy MAJĄ MIEĆ dostęp do stref oznaczonych selektorem private tak jak na poniższej modyfikacji naszego przykładowego programu z początku tematu:

#include<iostream>
#include <windows.h>

using namespace std;
class Czlowiek {
private:   //zmieniamy selektor na prywatny, aby zabronić dostępu!
 string imie, nazwisko, Pesel;
 struct {
		int rok,miesiac,dzien;
	} dataUrodzenia;
 int wzrost;

public:
 Czlowiek():imie("brak"), nazwisko("brak"), Pesel("xxxxxxxxxxx"), wzrost(-1), dataUrodzenia({1990,0,0}) {
 cout << "Uruchamiam konstruktor klasy Czlowiek..." <<endl;
	}
 friend class Pracownik;  //dopisujemy relację przyjaźni zezwalając na dostęp do pól prywatnych poprzez klasę Pracownik.
 friend int main();	// dopisujemy relację przyjaźni do metody main() pozwalając na to aby miała dostęp do pól prywatnych.	
};


class Pracownik :public Czlowiek {
public:
 string numerUmowy;
public:	
 Pracownik():numerUmowy("brak") {
   cout << "Uruchamiam konstruktor klasy Pracownik..." <<endl;
	}
};
	

main() {
 SetConsoleCP( 852 );
 setlocale ( LC_ALL, "" );	
 
 Czlowiek czlowiek;
 Pracownik pracownik;

 cout << pracownik.imie << " " << pracownik.nazwisko << 
      " urodzony: " << pracownik.dataUrodzenia.rok << "-" << 
      pracownik.dataUrodzenia.miesiac << "-" <<
      pracownik.dataUrodzenia.dzien << " ma umowę nr: " << 
      pracownik.numerUmowy << endl;
	
 return 0;
}

Jak widać jeżeli zmienimy selektor public na private, to nagle przestajemy mieć dostęp do oznaczonych przez niego struktur. Ale dodanie relacji przyjaźni “odblokowuje” ten dostęp dla wskazanych elementów 1 Dyskusyjne jest tutaj tworzenie relacji przyjaźni z metodą uruchomieniową main(), ale zostało to zrobione dla uproszczenia kodu i pokazania, że jest taka możliwość. Z zasady niektóre metody jak najbardziej mogą być “zaufane” dla konkretnej klasy, ale niektóre, a nie główna main() powodując, że selektor private traci sens w tym miejscu..

Zadanie do samodzielnego wykonania: Utwórz struktury wzajemnie od siebie zależne i poeksperymentuj z selektorami dostępu oraz dziedziczenia wraz z tworzeniem relacji przyjaźni pomiędzy obiektami.