Dziedziczenie proste
Przykład:
class PRACOWNIK
{
protected:
char * Nazwisko, * Dział;
int Uposażenie;
. . . . . . . . . . . . . . . . . . . . .
};
class KIEROWNIK: public PRACOWNIK
{
int Uprawnienia[ 8 ];
. . . . . . . . . . . . . . . . . . . . .
};
Przykłady poprawnych definicji obiektów i wskazań do obiektów w warunkach dziedziczenia prostego:
KIEROWNIK kk;
PRACOWNIK * p = & kk;
// każdy kierownik jest jednocześnie pracownikiem,
PRACOWNIK pp;
KIEROWNIK * k = & pp;
// . . . ale nie każdy pracownik kierownikiem
|
Dziedziczenie i polimorfizm
W poniższym przykładzie zaprezentowane zostanie użycie: klas abstrakcyjnych, funkcji wirtualnych i czysto wirtualnych (abstrakcyjnych), oraz omówiony mechanizm polimorfizmu.
Problem:
Zdefiniować klasę, mogącą przechowywać wskaźniki do obiektów różnych typów i zapewniającą ich wizualizacje za pomocą jednej funkcji Out( ).
class ELEMENT
{
public:
void Out( );
};
rozmiar = 5
Rys. Klasa KOSZ ze wskazaniami do obiektów różnych klas
typedef BOOL;
class KOSZ
{
ELEMENT **tab; // tablica wskaźników do obiektów
unsigned rozmiar, ostatni;
public:
KOSZ( unsigned rozm = 10 );
~ KOSZ( void );
BOOL DoKosza( ELEMENT * );
// weźmie do kosza wskazanie obiektu każdej klasy,
// dziedziczącej z klasy ELEMENT
void Out( void );
// wysyła na wyjście wszystkie obiekty znajdujące się w
// koszu niezależnie od ich typu
};
KOSZ:: KOSZ( unsigned rozm ):
tab( new ELEMENT *[ rozm ], rozmiar( rozm),
ostatni( 0 ) { }
KOSZ:: ~KOSZ( void )
{
for(int i = 0; i < ostatni; ++i) delete tab[ i ];
delete tab;
};
BOOL KOSZ:: Do Kosza( ELEMENT * pE1)
{ if( ostatni < rozmiar )
{ tab[ ostatni ] = pE1; ++ ostatni; return 1; }
else return 0;
}
void KOSZ:: Out( void )
{
for( int i=0; i < ostatni; i++) tab[ i ] → Out( );
}
|
Teraz możemy definiować klasy zupełnie dowolnych obiektów dziedziczących z klasy ELEMENT
class STUDENT: public ELEMENT
{
char * nazwisko;
int wiek;
public:
STUDENT( char *_nazwisko, int _wiek);
~STUDENT( );
void Out( );
};
STUDENT:: STUDENT( char *_nazwisko, int _wiek):
nazwisko( strdup( _nazwisko ), wiek( _wiek) { }
STUDENT:: Out( void )
{ cout<< “\nNazwisko “<<nazwisko
<<”\nWiek “<<wiek; }
|
Przedefiniujmy więc klasę ELEMENT
class ELEMENT
{
public:
virtual ~ELEMENT( );
virtual void Out( ) = 0;
// deklaracja funkcji abstrakcyjnej (czysto wirtualnej)
};
Klasę KOSZ tez można wyprowadzić z klasy ELEMENT. Wtedy będzie można UMIESZCZAĆ W Koszu inne Kosze wraz z zawartością (sic ! ).
Oto poprawiona definicja klasy KOSZ:
class KOSZ: public ELEMENT
{
ELEMENT **tab;
unsigned rozmiar, ostatni;
public:
KOSZ( unsigned rozm = 10 );
virtual ~ KOSZ( void ); // wirtualny destruktor
BOOL DoKosza( ELEMENT * );
virtual void Out( void ); // wirtualna metoda
};
Również klasy STUDENT i LITERA, dziedziczące z klasy ELEMENT będą miały wirtualne destruktory i metody Out( ).
Funkcja testująca zdefiniowaną wyżej strukturę klas:
void main( )
{
KOSZ *k1= new KOSZ(3), *k2=new KOSZ(2);
// oba obiekty są dynamiczne
k1→DoKosza( new STUDENT(„Jan Nowak”, 20);
k1→DoKosza( new LITERA(`B');
k1→DoKosza( new STUDENT(„Anna Jopek”, 25);
k2→DoKosza( new LITERA(`Q');
k2→DoKosza(k1);
k2→Out( ); // polimorfizm
delete k2; // polimorfizm
return 0;
};
|
Zasady używania funkcji wirtualnych:
Można tworzyć obiekty klas zawierających funkcje wirtualne, ale jeśli chociaż jedną z tych funkcji uczynimy abstrakcyjną, klasa staje się abstrakcyjną.
Typ funkcji wirtualnej jest deklarowany w klasie bazowej i nie może ulec zmianie w klasach pochodnych.
Funkcja wirtualna musi być zdefiniowana w klasie, w której po raz pierwszy została zadeklarowana.
Klasy pochodne nie muszą korzystać z funkcji wirtualnych klas bazowych.
Jeśli wywołuje się funkcję wirtualną z kwalifikatorem zakresu, np. KOSZ:: Out( ), mechanizm wirtualny nie działa.
Jeszcze o funkcjach wirtualnych i klasach abstrakcyjnych
class X {
public:
virtual void f( )=0; // funkcja czysto wirtualna
virtual void g( )=0; // inna funkcja czysto wirtualna
};
X x; // błąd: deklaracja obiektu klasy abstrakcyjnej X
class A: public X {
public:
void f( ); // unieważnienie X::f( )
};
A a; // błąd: deklaracja obiektu klasy abstrakcyjnej A
class B: public A {
public:
void g( ); // unieważnienie X::g( )
};
B b; // w porządku
class K: public X {
public:
virtual void f( ); // wymagana definicja K::f( )
void g( ); // unieważnienie X::g( )
};
K k; // w porządku
|
Dziedziczenie wielobazowe
Postać ogólna nagłówka definicji klasy pochodnej:
class oznacznik : lista_klas_bazowych
Element listy klas bazowych ma postać:
< public | protected | private > ozn_klasy_bazowej
Dostępność klas bazowych w klasach pochodnych:
Rodzaj dziedziczenia |
Składowe klasy bazowej ... |
w klasach pochodnych są ... |
public |
private |
niedostępne |
|
protected |
protected |
|
public |
public |
protected |
private |
niedostępne |
|
protected |
protected |
|
public |
protected |
private |
private |
niedostępne |
|
protected |
prywatnymi klasy pochodnej |
|
public |
prywatnymi klasy pochodnej |
Jeśli żadne słowo kluczowe nie wystąpiło przyjmuje się:
domyślnie private dla klas zdefiniowanych z użyciem słowa class,
domyślnie public dla klas zdefiniowanych z użyciem słów struct lub union.
Przykład:
class X: public A, B, protected C { };
Tutaj klasa X dziedziczy:
z klasy A - publicznie,
z klasy B - prywatnie,
z klasy C - w sposób zabezpieczony.
Przywracanie praw dostępu
Przykład:
class A class B: A
{ int a; {
protected: protected:
double x; A:: x; // teraz zabezpieczone
public: public:
int b; A:: b; // teraz publiczne
}; };
Prawa dostępu mogą być tylko przywracane. Nie można zmieniać praw dostępu. |
Związki między klasami
Związki między klasami widziane przez pryzmat struktury programu (implementację):
dziedziczenie,
zawieranie
należenie, albo posiadanie
używanie,
związki zaprogramowane.
Związki dziedziczenia
Problem: Symulacja ruchu ulicznego w celu szacowania
czasu dojazdu pojazdów uprzywilejowanych
do określonych punktów miasta.
Struktura klas z użyciem klasycznego dziedziczenia gen-spec:
POJAZD
OSOBOWY CIĘŻAROWY
UPRZYWILEJOWANY
POLICYJNY KARETKA
PRZECIWPOŻAROWY
POMPA_STRAŻACKA
Strzałki pokazują związki miedzy klasami, zwane na poziomie analityczno-projektowym uogólnieniami, a na poziomie implementacji - dziedziczeniem.
Ta sama struktura klas z użyciem pojęcia należenia:
UPRZYWILEJOWANY
POJAZD
OSOBOWY CIĘŻAROWY
POLICYJNY KARETKA
PRZECIWPOŻAROWY
POMPA_STRAŻACKA
Klasa UPRZYWILEJOWANY znajduje się poza strukturą klas, powiązanych związkiem dziedziczenia, natomiast obiekt tej klasy może należeć do obiektów różnych klas odpowiadających pojazdom.
class UPRZYWILEJOWANY { . . . };
class POJAZD
{ public: UPRZYWILEJOWANY * u_wsk; };
class OSOBOWY: public POJAZD { . . . };
class POLICYJNY: public OSOBOWY { . . . };
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Teraz dowolny pojazd będzie uprzywilejowany, jeśli u_wsk będzie różne od zera.
// konstruktor klasy OSOBOWY
OSOBOWY:: OSOBOWY( )
{ u_wsk = 0; }
// konstruktor klasy POLICYJNY
POLICYJNY:: POLICYJNY( )
{ u_wsk = new UPRZYWILEJOWANY; }
Funkcja konwersji każdego pojazdu do uprzywilejowanego i na odwrót:
void convert( POJAZD *p)
{ if(p)
{delete p→u_wsk; p→u_wsk = 0; };
// już nie uprzywilejowany
else p→u_wsk = new UPRZYWILEJOWANY;
// znów uprzywilejowany
}
Związek zawierania
class X
{. . . . .
public:
X(int);
. . . . . };
class C1
{ X a; // klasyczne zawieranie
public:
C1(int i): a(i) { }
// niejawne wywołanie konstruktora klasy X
};
Związki posiadania
class C2
{ X *p; obiekt C2
// wskaźnik do obiektu posiadanego
public:
C2(int i): p( new X(i)) { } obiekt typu X
// ten konstruktor kreuje i inicjuje obiekt posiadany
C2(X *q): p(q) { }
// ... a ten „dopina się” do obiektów istniejących,
// może się też dopinać do obiektów klas potomnych
~C2( ) {delete p;} // utrata obiektu posiadanego
X *udostępnij( ) { return p; }
X *zamien( X *q) { X *t = p; p = q; return t; }
};
Niech
class XX: public X { };
class XXX: public X { };
wtedy
void f( )
{
C2 *p1 = new C2( new X );
//obiekt dynamiczny klasy C2 „posiada” obiekt klasy X
C2 *p2 = new C2( new XX );
//obiekt dynamiczny klasy C2 „posiada” obiekt klasy XX
C2 *p3 = new C2( new XXX );
//obiekt dynamiczny klasy C2 „posiada” obiekt klasy XXX
}
Użycie referencji tworzy klasy operujące na obiektach klas X bezpośrednio, bez konieczności używania wskaźników.
class C3
{
X &r; obiekt C3
public:
C3( X &q): r(q) { }; obiekt o nazwie q klasy X
. . . . . . . . . . . . . . . .
};
|
Związki używania
Cytaty: [ B. Stroustrup ]
1/„Wiedza o tym, jakich klas używa dana klasa i w jaki sposób,
jest często decydująca do wyrażenia i zrozumienia
projektu.”
2/„Klasa może używać jedynie nazw, które zostały już gdzieś
(wcześniej) zadeklarowane.”
Związek, zwany na poziomie implementacji związkiem użycia, jest na poziomie analizy obiektowej zwany związkiem zależności.
W tym przykładzie klasa RozkładZajęć używa klasy Przedmiot. Zmiany dokonane w specyfikacji klasy Przedmiot mogą mieć wpływ na definicję klasy RozkładZajęć, ale nie na odwrót.
Klasyfikacja sposobów, w jakich jedna klasa X może używać innej klasy Y:
X używa nazwy klasy Y (jak w przykładzie powyżej),
X używa Y, ponieważ:
2.1. X czyta składową Y,
2.2. X zapisuje składową Y,
2.3. X wywołuje funkcje składową Y,
X tworzy obiekt klasy Y, tj. X przydziela pamięć dla statycznego lub automatycznego obiektu klasy Y, lub
tworzy dynamiczny obiekt Y za pomocą operatora new,
X pobiera rozmiar Y
Związki zaprogramowane
Załóżmy, że w projekcie tworzonego systemu wyspecyfikowano, że każda operacja, która nie może być obsłużona przez klasę A, powinna być obsłużona przez klasę B, posiadaną przez klasę A.
class B
{ . . . . . .
void f( ); void g( ); void h( ); }
class A
{ B *p;
. . . . . .
void f( ); void ff( );
void g( ) { p→g( ); } // delegowanie g( )
void h( ) { p→h( ); } // delegowanie h( )
};
Związki zaprogramowane są głęboko ukryte w implementacji, przez to mało widoczne a ich skutki są trudne do przewidzenia.
Interfejsy i implementacje
Komponent jest fizyczną, wymienną, częścią systemu informatycznego. Komponent, obok swojej implementacji, wykorzystuje i realizuje pewien zbiór własnych interfejsów. Interfejs komponentu jest zestawem operacji, zamkniętych w klasie (lub klasach) interfejsowych, które to operacje wyznaczają usługi oferowane przez komponent. Taki zestaw usług określa tzw. szwy systemu.
Idealny interfejs:
udostępnia wszystkie obowiązki komponentu „reszcie świata” w sposób pełny i logicznie spójny,
nie ujawnia użytkownikowi szczegółów implementacyjnych,
jest wyrażony za pomocą typów z poziomu użytkownika,
w ograniczony i dobrze zdefiniowany sposób zależy od innych interfejsów.
// przykład interfejsu w złym stylu
class X {
Y a;
public:
void f( const char *, . . . );
// funkcja interfejsowa ze zmienną liczbą parametrów
void g( int[ ], int );
// argument funkcji interfejsowej w postaci wskazania
void ustaw_a( Y& );
// funkcja interfejsowa z parametrem w postaci referencji do
nieznanej klasy
Y& wez_a( );
// typ wyniku funkcji interfejsowej w postaci referencji do
nieznanej klasy
};
Zawarte w klasie interfejsowej powyższego przykładu metody realizują interfejs na bardzo niskim poziomie abstrakcji, ujawniając szczegóły implementacji. Nie są samoopisujące się.
Zasady praktyczne projektowania
Motto:
„Nie ma jednej „właściwej” metody projektowania. Projektowanie wymaga wyczucia, doświadczenia i inteligencji.”
Użyj publicznego dziedziczenia do reprezentowania relacji bycia.
Jeśli class Y: public X { ... }; to klasa Y jest swego
rodzaju klasą X.
Użyj wskaźników do reprezentowania relacji posiadania.
Upewnij się, że zależności używania są zrozumiałe, minimalne i niecykliczne.
Wyrażaj interfejsy w kategoriach typów z dziedziny zastosowań.
36
Ta klasa mogłaby być klasą abstrakcyjną dla takiej klasy
Zdefiniujmy wobec tego klasę podstawową KOSZ zawierającą wszystko co niezbędne
student
ostatni
litera
tab
ELEMENT
void Out( void );
LITERA
char ch;
void Out( void );
STUDENT
char * nazwisko;
int wiek;
void Out( void );
Klasa abstrakcyjna (!!!) - nie można tworzyć obiektów !!!
C1
X
p
r
Zależność
Zależność
implementacja
interfejsy
komponent