Projektowanie i programowanie obiektowe (materiały do wykładu cz. VI) Jacek Cichosz www.zssk.pwr.wroc.pl Katedra Systemów i Sieci Komputerowych Politechnika Wrocławska
Dziedziczenie 221 Dziedziczenie Dziedziczenie to technika pozwalająca na definiowanie nowej klasy (klasy pochodnej) przy wykorzystaniu klasy już istniejącej klasy podstawowej. W klasie pochodnej możemy: zdefiniować dodatkowe pola składowe (wyposażanie klasy pochodnej w dodatkowe atrybuty), zdefiniować dodatkowe funkcje składowe (wzbogacanie klasy pochodnej w nowe zachowania). przedefiniowywać składowe dane i operacje, które istnieją w klasie podstawowej.
Dziedziczenie 222 Dziedziczenie jako związek między klasami Klasa pochodna efektywny mechanizm definiowania klasy przez dodawanie udogodnień do istniejącej klasy bez ponownego programowania i kompilowania. Klasy pochodne ułatwiają wyrażanie wspólnych cech klas, a tym samym wspólnych własności pojęć z dziedziny rozwiązywanego problemu np: 1. linia, trójkąt, okrąg; wspólna cecha bycie figurą.
Dziedziczenie 222 Dziedziczenie jako związek między klasami Klasa pochodna efektywny mechanizm definiowania klasy przez dodawanie udogodnień do istniejącej klasy bez ponownego programowania i kompilowania. Klasy pochodne ułatwiają wyrażanie wspólnych cech klas, a tym samym wspólnych własności pojęć z dziedziny rozwiązywanego problemu np: 1. linia, trójkąt, okrąg; wspólna cecha bycie figurą. 2. wykładowca, student, dziekan; wspólna cecha bycie osobą.
Dziedziczenie 222 Dziedziczenie jako związek między klasami Klasa pochodna efektywny mechanizm definiowania klasy przez dodawanie udogodnień do istniejącej klasy bez ponownego programowania i kompilowania. Klasy pochodne ułatwiają wyrażanie wspólnych cech klas, a tym samym wspólnych własności pojęć z dziedziny rozwiązywanego problemu np: 1. linia, trójkąt, okrąg; wspólna cecha bycie figurą. 2. wykładowca, student, dziekan; wspólna cecha bycie osobą. 3. mucha, motyl, konik polny są wszystkie owadami.
Dziedziczenie 222 Dziedziczenie jako związek między klasami Klasa pochodna efektywny mechanizm definiowania klasy przez dodawanie udogodnień do istniejącej klasy bez ponownego programowania i kompilowania. Klasy pochodne ułatwiają wyrażanie wspólnych cech klas, a tym samym wspólnych własności pojęć z dziedziny rozwiązywanego problemu np: 1. linia, trójkąt, okrąg; wspólna cecha bycie figurą. 2. wykładowca, student, dziekan; wspólna cecha bycie osobą. 3. mucha, motyl, konik polny są wszystkie owadami. 4. samochód osobowy, ciężarówka, autobus to pojazdy.
Dziedziczenie 222 Dziedziczenie jako związek między klasami Klasa pochodna efektywny mechanizm definiowania klasy przez dodawanie udogodnień do istniejącej klasy bez ponownego programowania i kompilowania. Klasy pochodne ułatwiają wyrażanie wspólnych cech klas, a tym samym wspólnych własności pojęć z dziedziny rozwiązywanego problemu np: 1. linia, trójkąt, okrąg; wspólna cecha bycie figurą. 2. wykładowca, student, dziekan; wspólna cecha bycie osobą. 3. mucha, motyl, konik polny są wszystkie owadami. 4. samochód osobowy, ciężarówka, autobus to pojazdy. Klasa pochodna może być większa od swojej klasy podstawowej w tym sensie, że zawiera więcej danych i dostarcza więcej funkcji.
Dziedziczenie 223 Jak reprezentujemy dziedziczenie w języku UML? Osoba Student ZWIĄZEK DZIEDZICZENIA (UOGÓLNIENIE) W języku UML relację dziedziczenia nazywa się uogólnieniem. Za pomocą uogólnienia modelujemy związki hierarchiczne między pojęciami, a tym samym między klasami. Jeśli klasa pochodna ma jednego przodka, to takie dziedziczenie nazywamy pojedynczym.
Dziedziczenie 223 Jak reprezentujemy dziedziczenie w języku UML? Osoba Student KLASA PODSTAWOWA (PRZODEK) ZWIĄZEK DZIEDZICZENIA (UOGÓLNIENIE) W języku UML relację dziedziczenia nazywa się uogólnieniem. Za pomocą uogólnienia modelujemy związki hierarchiczne między pojęciami, a tym samym między klasami. Jeśli klasa pochodna ma jednego przodka, to takie dziedziczenie nazywamy pojedynczym.
Dziedziczenie 223 Jak reprezentujemy dziedziczenie w języku UML? Osoba KLASA PODSTAWOWA (PRZODEK) ZWIĄZEK DZIEDZICZENIA (UOGÓLNIENIE) Student KLASA POCHODNA (POTOMEK) W języku UML relację dziedziczenia nazywa się uogólnieniem. Za pomocą uogólnienia modelujemy związki hierarchiczne między pojęciami, a tym samym między klasami. Jeśli klasa pochodna ma jednego przodka, to takie dziedziczenie nazywamy pojedynczym.
Dziedziczenie 224 Dziedziczenie kilkupokoleniowe Klasa pochodna może być równocześnie klasą podstawową dla innej klasy pochodnej. Osoba Student Pracownik Wykładowca Hierarchia klas ma najczęściej strukturę drzewa, którego korzeń stanowi klasa podstawowa nie mająca przodków.
Dziedziczenie 225 Modelowanie związków hierarchicznych Figura Elipsa Wielokąt Okrąg Trójkąt Prostokąt Kwadrat Dzięki dziedziczeniu możemy modelować skomplikowane hierarchiczne związki pojęciowe.
Dziedziczenie 226 Wyodrębnianie wspólnych cech pojęć Pojazd Jednoślad Osobowy Ciężarowy Rower Motocykl Autobus Wywrotka Dzięki dziedziczeniu nie musimy powielać tych cech (właściwości) i zachowań, które są wspólne dla wielu pojęć.
Dziedziczenie 227 Podejście bez dziedziczenia class Osoba{ char nazw[32]; class Wykladowca{ char nazw[32]; float pensja; short wydzial; class Student{ char nazw[32]; short grupa; class Pracownik{ char nazw[32]; float pensja;
Dziedziczenie 227 Podejście bez dziedziczenia class Osoba{ char nazw[32]; class Wykladowca{ char nazw[32]; float pensja; short wydzial; class Student{ char nazw[32]; short grupa; class Pracownik{ char nazw[32]; float pensja; Pojęcia: Osoba, Student, Wykladowca są reprezentowane jako osobne klasy.
Dziedziczenie 227 Podejście bez dziedziczenia class Osoba{ char nazw[32]; class Wykladowca{ char nazw[32]; float pensja; short wydzial; class Student{ char nazw[32]; short grupa; class Pracownik{ char nazw[32]; float pensja; Pojęcia: Osoba, Student, Wykladowca są reprezentowane jako osobne klasy. Zauważamy wspólne atrybuty pojęć: np. nazwisko.
Dziedziczenie 228 Podejście z dziedziczeniem class Osoba{ // Klasa podstawowa char nazw[32]; class Student : public Osoba{ // Student jest osobą short grupa; class Pracownik : public Osoba{ // Pracownik jest osobą float pensja; class Wykladowca : public Pracownik{ // Wykładowca jest pracownikiem short wydzial; Dzięki dziedziczeniu reprezentujemy pojęcia bez gubienia ich wspólnych cech.
Dziedziczenie 229 Odwołania do składowych klas void f(){ Wykladowca w; Student s; strcpy (w.nazw, "Kowalski"); w.pensja = 2550.40; w.wydzial = 4; strcpy( s.nazw, "Nowak" ); s.grupa = 16; } Obiekt klasy podstawowej jest podobiektem obiektu klasy pochodnej. Klasa pochodna dziedziczy wszystkie składniki klasy podstawowej, do których możemy się odwoływać tak, jakby były one zdefiniowane w klasie pochodnej.
Dziedziczenie 230 Niejawne konwersje typów Osoba o, * op; Pracownik p, * pp; Student s, * sp; Wykladowca w, *wp; op = &o; // O.K. op = &s; // O.K. student jest osobą op = &w; // O.K. wykładowca jest osobą pp = &w; // O.K. wykładowca jest pracownikiem sp = &o; // Błąd, nie każda osoba jest studentem pp = &o; // Błąd, nie każda osoba jest pracownikiem wp = &o; // Błąd, nie każda osoba jest wykładowcą wp = &p; // Błąd, nie każdy pracownik musi być wykładowcą Obiekt klasy pochodnej można traktować jak obiekt klasy podstawowej, gdy sięga się do niego za pomocą wskaźnika. Stwierdzenie odwrotne nie jest prawdziwe.
Dziedziczenie 231 Niejawne konwersje typów void main(){ Osoba o("abacki"); Student s("kowalski", 2); Pracownik p("adamczyk", 1880.90 ); Wykladowca w("nowak",2400.40, 16); Osoba & rs = s; // O.K. student jest osobą Osoba & rw = w; // O.K. wykładowca jest osobą Osoba & rp = p; // O.K. pracownik jest osobą Pracownik & rp1 = w; // O.K. wykładowca jest pracownikiem Wykladowca & wr = p; // Źle; nie każdy pracownik jest wykładowcą Wykladowca & wr1 = o; // Źle; nie każda osoba jest wykładowcą } Obiekt klasy pochodnej można traktować jak obiekt klasy podstawowej, gdy sięga się do niego za pomocą referencji. Stwierdzenie odwrotne nie jest prawdziwe.
Dziedziczenie 232 Hermetyzacja składowych klas Składowa klasy pochodnej może używać publicznych nazw swojej klasy podstawowej tak samo jak innych składowych, tzn. bez specyfikowania obiektu. Składowa klasy pochodnej nie może sięgać do prywatnych składowych swojej klasy podstawowej. Gdyby było inaczej, to każdy mógłby mieć dostęp do prywatnej składowej klasy przez proste wyprowadzenie klasy pochodnej.
Dziedziczenie 233 Dostęp do składowych klas class Osoba{ char nazw[32]; // Dana prywatna (niedostępna w klasach pochodnych) Osoba( const char * n ); class Student : public Osoba{ short grupa; // Dana prywatna Student( const char * n, short s ); void drukuj() const; void Student :: drukuj() const{ cout << "nazwisko: " << nazw << endl; // Źle; brak dostępu do: nazw cout << "grupa: " << grupa; }
Dziedziczenie 234 Składowe, których się nie dziedziczy Konstruktor klasy podstawowej nie staje sie automatycznie konstruktorem klasy pochodnej gdyż: nic nie wie o dodatkowych atrybutach zdefiniowanych w klasie pochodnej, Użycie konstruktora z klasy podstawowej spowodowałoby zainicjowanie tylko składników klasy podstawowej, a dane zdefiniowane w klasie pochodnej zostałyby zignorowane. W konstruktorze klasy pochodnej do inicjacji części odziedziczonej można wywołać konstruktor klasy podstawowej. Operator przypisania (operator =) z powodów jak wyżej. Destruktor jest ściśle związany z konstruktorem i dlatego musi być zdefiniowany w klasie pochodnej.
Dziedziczenie 235 Konstruktory w obrębie klas pochodnych Obiekt klasy podstawowej jest w pewnym sensie podobiektem klasy pochodnej. Konstruowanie obiektu klasy pochodnej składa się ze skonstruowania obiektu klasy podstawowej i dobudowania tego, co jest właściwe klasie pochodnej. class Osoba{ char nazw[32]; Osoba( const char * n ) { strcpy( nazw, n );} Jeśli konstruktor klasy podstawowej potrzebuje argumentów, to musimy je dostarczyć. class Pracownik : public Osoba{ float pensja; Pracownik( const char * n, float p ) : Osoba( n ), pensja( p ){}
Dziedziczenie 236 Kolejność konstruowania w klasach pochodnych Argumenty konstruktora klasy podstawowej podajemy w definicji konstruktora klasy pochodnej. Pod tym względem klasa podstawowa zachowuje się dokładnie tak, jak składowa klasy pochodnej. Kolejność konstruowania: class Student : public Osoba{ short grupa; Student( const char * n, short g ) : Osoba( n ), grupa( g ){ }
Dziedziczenie 236 Kolejność konstruowania w klasach pochodnych Argumenty konstruktora klasy podstawowej podajemy w definicji konstruktora klasy pochodnej. Pod tym względem klasa podstawowa zachowuje się dokładnie tak, jak składowa klasy pochodnej. Kolejność konstruowania:najpierw obiekt klasy podstawowej class Student : public Osoba{ short grupa; Student( const char * n, short g ) : Osoba( n ), grupa( g ){ }
Dziedziczenie 236 Kolejność konstruowania w klasach pochodnych Argumenty konstruktora klasy podstawowej podajemy w definicji konstruktora klasy pochodnej. Pod tym względem klasa podstawowa zachowuje się dokładnie tak, jak składowa klasy pochodnej. Kolejność konstruowania:najpierw obiekt klasy podstawowej, składowe klasy pochodnej class Student : public Osoba{ short grupa; Student( const char * n, short g ) : Osoba( n ), grupa( g ){ }
Dziedziczenie 236 Kolejność konstruowania w klasach pochodnych Argumenty konstruktora klasy podstawowej podajemy w definicji konstruktora klasy pochodnej. Pod tym względem klasa podstawowa zachowuje się dokładnie tak, jak składowa klasy pochodnej. Kolejność konstruowania:najpierw obiekt klasy podstawowej, składowe klasy pochodnej, reszta, czyli instrukcje w treści konstruktora klasy pochodnej. class Student : public Osoba{ short grupa; Student( const char * n, short g ) : Osoba( n ), grupa( g ){ }
Dziedziczenie 237 Kolejność niszczenia w klasach pochodnych Niszczenie przebiega w odwrotnej kolejności niż konstruowanie:
Dziedziczenie 237 Kolejność niszczenia w klasach pochodnych Niszczenie przebiega w odwrotnej kolejności niż konstruowanie: Klasa pochodna (instrukcje w ciele destruktora).
Dziedziczenie 237 Kolejność niszczenia w klasach pochodnych Niszczenie przebiega w odwrotnej kolejności niż konstruowanie: Klasa pochodna (instrukcje w ciele destruktora). Składowe klasy pochodnej w kolejności odwrotnej do deklaracji.
Dziedziczenie 237 Kolejność niszczenia w klasach pochodnych Niszczenie przebiega w odwrotnej kolejności niż konstruowanie: Klasa pochodna (instrukcje w ciele destruktora). Składowe klasy pochodnej w kolejności odwrotnej do deklaracji. Klasy podstawowe w kolejności odwrotnej do deklaracji.
Dziedziczenie 237 Kolejność niszczenia w klasach pochodnych Niszczenie przebiega w odwrotnej kolejności niż konstruowanie: Klasa pochodna (instrukcje w ciele destruktora). Składowe klasy pochodnej w kolejności odwrotnej do deklaracji. Klasy podstawowe w kolejności odwrotnej do deklaracji. Dla klas bez jawnie zdefiniowanego destruktora jest podobnie: class Wykladowca : public Pracownik{ short wydzial; Wykladowca( const char * n, float p, short w ) : Pracownik( n, p ), wydzial( w ){ } Najpierw niszczona jest klasa Wykladowca, następnie klasa Pracownik, a na końcu Osoba.
Dziedziczenie 238 Zasłanianie class A{ int x; A() : x( 1 ) {} int y; void f(); void A :: f(){ cout << "x = " << x << ", y = " << y << endl; } class B : public A{ int y; void f(); void B :: f(){ cout << "y = " << y << endl; } void main(){ B ob; ob.y = 3; // Odwolanie do B :: y ob.a :: y = 45; // Odwolanie do A :: y ob.f(); // Odwołanie do B :: f() ob.a :: f(); // Odwołanie do A :: f()
Dziedziczenie 239 Zasłanianie Jeśli w klasie podstawowej i klasie pochodnej są składniki o tej samej nazwie, wówczas w zakresie klasy pochodnej składnik z tej klasy zasłania składnik z klasy podstawowej. W klasie pochodnej mogą wystąpić funkcje o takich samych nazwach jak w klasie podstawowej.
Dziedziczenie 240 Dziedziczenie a zagnieżdżanie bloków analogia { // Zakres klasy podstawowej int x; // Składniki dziedziczone int y; { // Zakres klasy pochodnej int x; // Składniki klasy pochodnej int y; } } Dziedziczenie jest w pewnym sensie analogiczne do zagnieżdżania bloków. Wewnętrzny blok odpowiada klasie pochodnej, a zewnętrzny klasie podstawowej.
Dziedziczenie 241 Przedefiniowywanie zachowania Nadawanie tych samych nazw funkcjom składowym klas podstawowych i pochodnych może służyć do przedefiniowania zachowania w klasach pochodnych. Jest to realizowane przez zasłonięcie nazwy operacji składowej klasy podstawowej przez nazwę operacji w klasie pochodnej. class Osoba{ char nazw[32]; Osoba( const char * ); // Drukuj info o osobie void drukuj() const; class Pracownik : public Osoba{ float pensja; Pracownik( const char *, float ); // Drukuj info o pracowniku void drukuj() const;
Dziedziczenie 242 Przedefiniowywanie zachowania // Drukuj info o osobie void Osoba :: drukuj() const{ cout << "Nazwisko: " << nazw << endl; } // W klasie pochodnej możemy wywołać drukuj() // z klasy podstawowej void Pracownik :: drukuj() const{ Osoba :: drukuj(); // Drukuj nazwisko osoby cout << "Zarobki: " << pensja << endl; } void Wykladowca :: drukuj(){ Pracownik :: drukuj(); cout << "Wydzial: " << wydzial << endl; }
Dziedziczenie 243 Diagram klas z przesłoniętą funkcją drukuj() Osoba -nazw: char[32] + drukuj() Wspólne pole danych w klasie podstawowej Student -grupa: + drukuj() short Przedefiniowane operacje w klasach potomnych Pracownik -pensja: + drukuj() Wykladowca -wydzial: + drukuj() float short Pola właściwe dla klas potomnych
Dziedziczenie 244 Problemy z przedefiniowywaniem zachowania Jeśli mamy wskaźnik do klasy podstawowej np. Osoba, to do jakiego typu należy wskazywany obiekt? Student s("nowak", 16); Wykladowca w("kowalski", 2400.70 ); Pracownik p("kowalczyk", 1870.20 ); Osoba *op; op = &s; op->drukuj(); // Wywoła się: Osoba :: drukuj(); op = &w; op->drukuj(); // Wywoła się: Osoba :: drukuj(); op = &p; op->drukuj(); // Wywoła się: Osoba :: drukuj();
Dziedziczenie 245 Rozwiązanie za pomocą pola typu class Osoba{ enum TypOsoby{ O, S, P, W Osoba( const char * s ){ strcpy( nazw, s ); typ_os = O; } void drukuj() const; int typ() const{ return typ_os; } protected: TypOsoby typ_os; private: char nazw[32];
Dziedziczenie 246 Konstruktory ustawiają pola typu class Student : public Osoba{ short grupa; Student( const char * s, short g ) : Osoba( s ), grupa( g ){ typ_os = S; } void drukuj() const; class Pracownik : public Osoba{ float pensja; Pracownik( const char * s, float p ) : Osoba( s ), pensja( p ) { typ_os = P; } }
Dziedziczenie 247 Funkcja drukująca testuje pole typu // Drukowanie informacji o osobie void drukuj_osobe( const Osoba * op ){ switch( op->typ() ){ case Osoba :: S: ((Student *)op)->drukuj(); // Wywoła: Student :: drukuj() break; case Osoba :: P: ((Pracownik *)op)->drukuj(); // Wywoła: Pracownik :: drukuj() break; case Osoba :: W: ((Wykladowca *)op)->drukuj(); // Wywoła: Wykladowca :: drukuj() break; default: op->drukuj(); // Wywoła: Osoba :: drukuj() break; } }
Dziedziczenie 248 Użycie funkcji drukującej w programie void main(){ Pracownik p("kowalski", 3275.20 ); Student s("nowak", 16 ); Wykladowca w("pawlak", 2450.60, 4 ); Osoba o("jankowski"); drukuj_osobe( &p ); drukuj_osobe( &s ); drukuj_osobe( &w ); drukuj_osobe( &o ); // Drukowanie tablicy osób Osoba * tab[] = {&p, &s, &w, &o for( int i = 0; i < sizeof( tab )/ sizeof(tab[0]) ; i++ ) drukuj_osobe( tab[i] ); }
Dziedziczenie 249 Wady rozwiązania z polem typu Każda funkcja korzystająca z pola typu musi znać reprezentację i szczegóły implementacji każdej klasy, która pochodzi od klasy zawierającej pole typu.
Dziedziczenie 249 Wady rozwiązania z polem typu Każda funkcja korzystająca z pola typu musi znać reprezentację i szczegóły implementacji każdej klasy, która pochodzi od klasy zawierającej pole typu. Rozwiązanie z polem sprawdza się w małych programach pisanych przez jedną osobę.
Dziedziczenie 249 Wady rozwiązania z polem typu Każda funkcja korzystająca z pola typu musi znać reprezentację i szczegóły implementacji każdej klasy, która pochodzi od klasy zawierającej pole typu. Rozwiązanie z polem sprawdza się w małych programach pisanych przez jedną osobę. W dużych programach prowadzi ono do dwóch rodzajów błędów:
Dziedziczenie 249 Wady rozwiązania z polem typu Każda funkcja korzystająca z pola typu musi znać reprezentację i szczegóły implementacji każdej klasy, która pochodzi od klasy zawierającej pole typu. Rozwiązanie z polem sprawdza się w małych programach pisanych przez jedną osobę. W dużych programach prowadzi ono do dwóch rodzajów błędów: 1. zapomnienie o sprawdzeniu pola typu,
Dziedziczenie 249 Wady rozwiązania z polem typu Każda funkcja korzystająca z pola typu musi znać reprezentację i szczegóły implementacji każdej klasy, która pochodzi od klasy zawierającej pole typu. Rozwiązanie z polem sprawdza się w małych programach pisanych przez jedną osobę. W dużych programach prowadzi ono do dwóch rodzajów błędów: 1. zapomnienie o sprawdzeniu pola typu, 2. nieumieszczenie wszystkich możliwych przypadków w instrukcji switch.
Dziedziczenie 249 Wady rozwiązania z polem typu Każda funkcja korzystająca z pola typu musi znać reprezentację i szczegóły implementacji każdej klasy, która pochodzi od klasy zawierającej pole typu. Rozwiązanie z polem sprawdza się w małych programach pisanych przez jedną osobę. W dużych programach prowadzi ono do dwóch rodzajów błędów: 1. zapomnienie o sprawdzeniu pola typu, 2. nieumieszczenie wszystkich możliwych przypadków w instrukcji switch. Każde dodanie nowego typu np. Profesor : public Wykladowca powoduje konieczność zmiany wszystkich funkcji, które testują pole typu.
Dziedziczenie 249 Wady rozwiązania z polem typu Każda funkcja korzystająca z pola typu musi znać reprezentację i szczegóły implementacji każdej klasy, która pochodzi od klasy zawierającej pole typu. Rozwiązanie z polem sprawdza się w małych programach pisanych przez jedną osobę. W dużych programach prowadzi ono do dwóch rodzajów błędów: 1. zapomnienie o sprawdzeniu pola typu, 2. nieumieszczenie wszystkich możliwych przypadków w instrukcji switch. Każde dodanie nowego typu np. Profesor : public Wykladowca powoduje konieczność zmiany wszystkich funkcji, które testują pole typu. Wniosek: Technika stosująca pole typu jest podatna na błędy.
Dziedziczenie 250 Funkcje wirtualne class Osoba{ char nazw[32]; Osoba( const char * ); virtual void drukuj() const; Osoba :: drukuj() const{ cout << "Nazwisko: " << nazw << endl; } class Student : public Osoba{ short grupa; Student( const char*,short ); virtual void drukuj() const; Student :: drukuj() const{ Osoba :: drukuj(); cout << "Grupa: " << grupa << endl; }
Dziedziczenie 251 Funkcje wirtualne class Pracownik: public Osoba{ float pensja; Pracownik( const char*, float ); virtual void drukuj() const; class Wykladowca: public Pracownik{ short wydzial; Wykladowca( const char*, float, short); // Uwaga: ta funkcja też może być wirtualna void drukuj() const;
Dziedziczenie 252 Zastosowanie funkcji wirtualnych // Każda osoba zostanie wydrukowana // zgodnie ze swoim typem void drukuj_tablice( const Osoba *p, int rozmiar ){ for( int i = 0; i < rozmiar ; i++ ) p++->drukuj(); // Wywołanie funkcji wirtualnej } void main(){ Student s( "Nowak", 13 ); Wykladowca w1( "Pawlak", 2200.0, 4 ), w2( "Kowalski", 2350.40, 6 ); Osoba *tab[] = {&s, &w1, &w2 drukuj_tablice( tab, sizeof(tab)/sizeof( tab[0] )); }
Dziedziczenie 253 Właściwości funkcji wirtualnych Funkcje wirtualne pozwalają na przedefiniowanie w każdej klasie pochodnej funkcji zdeklarowanej w klasie podstawowej.
Dziedziczenie 253 Właściwości funkcji wirtualnych Funkcje wirtualne pozwalają na przedefiniowanie w każdej klasie pochodnej funkcji zdeklarowanej w klasie podstawowej. Słowo kluczowe virtual wskazuje, że funkcja drukuj() może mieć różne wersje dla różnych klas pochodnych i że znalezienie właściwej funkcji dla każdego wywołania drukuj() jest zadaniem kompilatora.
Dziedziczenie 253 Właściwości funkcji wirtualnych Funkcje wirtualne pozwalają na przedefiniowanie w każdej klasie pochodnej funkcji zdeklarowanej w klasie podstawowej. Słowo kluczowe virtual wskazuje, że funkcja drukuj() może mieć różne wersje dla różnych klas pochodnych i że znalezienie właściwej funkcji dla każdego wywołania drukuj() jest zadaniem kompilatora. Typ funkcji wirtualnej jest zdeklarowany w klasie podstawowej i nie może ulec zmianie w klasach pochodnych.
Dziedziczenie 253 Właściwości funkcji wirtualnych Funkcje wirtualne pozwalają na przedefiniowanie w każdej klasie pochodnej funkcji zdeklarowanej w klasie podstawowej. Słowo kluczowe virtual wskazuje, że funkcja drukuj() może mieć różne wersje dla różnych klas pochodnych i że znalezienie właściwej funkcji dla każdego wywołania drukuj() jest zadaniem kompilatora. Typ funkcji wirtualnej jest zdeklarowany w klasie podstawowej i nie może ulec zmianie w klasach pochodnych. Funkcja wirtualna musi być zdefiniowana dla klasy, w której po raz pierwszy została zdeklarowana chyba, że została zdeklarowana jako czysta funkcja wirtualna.
Dziedziczenie 253 Właściwości funkcji wirtualnych Funkcje wirtualne pozwalają na przedefiniowanie w każdej klasie pochodnej funkcji zdeklarowanej w klasie podstawowej. Słowo kluczowe virtual wskazuje, że funkcja drukuj() może mieć różne wersje dla różnych klas pochodnych i że znalezienie właściwej funkcji dla każdego wywołania drukuj() jest zadaniem kompilatora. Typ funkcji wirtualnej jest zdeklarowany w klasie podstawowej i nie może ulec zmianie w klasach pochodnych. Funkcja wirtualna musi być zdefiniowana dla klasy, w której po raz pierwszy została zdeklarowana chyba, że została zdeklarowana jako czysta funkcja wirtualna. Dzięki temu, można używać funkcji wirtualnej nawet wtedy, kiedy z klasy, w której występuje nie wyprowadzi się żadnej klasy pochodnej.
Dziedziczenie 253 Właściwości funkcji wirtualnych Funkcje wirtualne pozwalają na przedefiniowanie w każdej klasie pochodnej funkcji zdeklarowanej w klasie podstawowej. Słowo kluczowe virtual wskazuje, że funkcja drukuj() może mieć różne wersje dla różnych klas pochodnych i że znalezienie właściwej funkcji dla każdego wywołania drukuj() jest zadaniem kompilatora. Typ funkcji wirtualnej jest zdeklarowany w klasie podstawowej i nie może ulec zmianie w klasach pochodnych. Funkcja wirtualna musi być zdefiniowana dla klasy, w której po raz pierwszy została zdeklarowana chyba, że została zdeklarowana jako czysta funkcja wirtualna. Dzięki temu, można używać funkcji wirtualnej nawet wtedy, kiedy z klasy, w której występuje nie wyprowadzi się żadnej klasy pochodnej. Klasa pochodna, która nie potrzebuje specjalnej wersji funkcji wirtualnej, nie musi jej dostarczać.
Dziedziczenie 254 Klasy pochodne i pamięć wolna void main(){ Osoba *tab[] = { new Student( "Nowak", 13 ), new Wykladowca( "Pawlak", 2200.0, 4 ), new Pracownik( "Kowalski", 2350.40 ) } drukuj_tablice( tab, sizeof( tab ) / sizeof( tab[0] )); // Zwalnianie pamięci wskazywanych przez elementy tablicy for( i = 0 ; i < sizeof( tab ) / sizeof( tab[0] ) ; i++ ) delete tab[i]; } Kłopot: operator delete nie dostanie właściwych rozmiarów obiektów klas potomnych klasy Osoba, których pamięć zwalniamy.
Dziedziczenie 255 Destruktor wirtualny class Osoba{ char nazw[32]; Osoba( const char * ); virtual void drukuj() const; virtual ~Osoba(); Osoba :: ~Osoba() {} class Student : public Osoba{ short grupa; Student( const char*,short ); virtual void drukuj() const; // Ten destruktor może też być wirtualny ~Student(); Student :: ~Student() {}
Dziedziczenie 256 Destruktor wirtualny class Pracownik: public Osoba{ float pensja; Pracownik( const char*, float ); virtual void drukuj() const; virtual ~Pracownik() {} class Wykladowca: public Pracownik{ short wydzial; Wykladowca( const char*, float, short); // Uwaga: ta funkcja też może być wirtualna void drukuj() const; // Ten destruktor też może być wirtualny ~Wykladowca() {}
Dziedziczenie 257 Destruktor wirtualny podsumowanie Kompilator musi dostarczyć operatorowi delete właściwy rozmiar wskazywanego obiektu.
Dziedziczenie 257 Destruktor wirtualny podsumowanie Kompilator musi dostarczyć operatorowi delete właściwy rozmiar wskazywanego obiektu. Jeśli typ na który wskazuje wskaźnik wyspecyfikowany w operacji delete jest typem klasy podstawowej, a rzeczywistym typem wskazywanym jest klasa pochodna, to rozmiar obiektu nie zostanie właściwie określony przez kompilator; np.
Dziedziczenie 257 Destruktor wirtualny podsumowanie Kompilator musi dostarczyć operatorowi delete właściwy rozmiar wskazywanego obiektu. Jeśli typ na który wskazuje wskaźnik wyspecyfikowany w operacji delete jest typem klasy podstawowej, a rzeczywistym typem wskazywanym jest klasa pochodna, to rozmiar obiektu nie zostanie właściwie określony przez kompilator; np. Osoba * ptr = new Student( "Kowalski", 3 ); //... delete ptr; // Zadziała niewłaściwie, jeśli klasa: Osoba // nie ma destruktora wirtualnego
Dziedziczenie 257 Destruktor wirtualny podsumowanie Kompilator musi dostarczyć operatorowi delete właściwy rozmiar wskazywanego obiektu. Jeśli typ na który wskazuje wskaźnik wyspecyfikowany w operacji delete jest typem klasy podstawowej, a rzeczywistym typem wskazywanym jest klasa pochodna, to rozmiar obiektu nie zostanie właściwie określony przez kompilator; np. Osoba * ptr = new Student( "Kowalski", 3 ); //... delete ptr; // Zadziała niewłaściwie, jeśli klasa: Osoba // nie ma destruktora wirtualnego Nawet pusty destruktor (tzn. nie zawierający żadnych instrukcji) spełni swoje zadanie.
Dziedziczenie 258 Destruktor wirtualny zasada użycia Jeśli manipuluje się obiektami pochodnymi za pomocą wskaźnika do klasy pochodnej (przydział i zwalnianie pamięci za pomocą operatorów new i delete), to trzeba dostarczyć destruktor wirtualny w klasach, które zachowują się jak klasy podstawowe (przeznaczone do dalszego wyprowadzania klas pochodnych).
Dziedziczenie 258 Destruktor wirtualny zasada użycia Jeśli manipuluje się obiektami pochodnymi za pomocą wskaźnika do klasy pochodnej (przydział i zwalnianie pamięci za pomocą operatorów new i delete), to trzeba dostarczyć destruktor wirtualny w klasach, które zachowują się jak klasy podstawowe (przeznaczone do dalszego wyprowadzania klas pochodnych). Jeśli klasa zawiera choć jedną funkcję wirtualną, to prawie zawsze zachodzi konieczność zdefiniowania dla niej destruktora wirtualnego.
Dziedziczenie 259 Nieuporządkowana tablica rekordów class RecTbl{ RecTbl( int r ); RecTbl( const RecTbl & ); RecTbl & operator = ( const RecTbl & ); virtual ~RecTbl(); virtual int dodaj( const char * nzw, Tele tel ); virtual OsobaInfo * szukaj( const char * nzw ); virtual int usun( const char * nzw ); int ile_elementow(); int max_rozmiar(); int jest_pusta(); protected: int rozmiar; int licznik; OsobaInfo * tab;
Dziedziczenie 260 Uporządkowana tablica rekordów class SortedTbl : public RecTbl{ SortedTbl( int r ) : RecTbl( r ){ SortedTbl( const RecTbl & src ) : RecTbl( src ) { SortedTbl & operator = ( const SortedTbl & ); ~SortedTbl() {} // Dodaj rekord z zachowaniem porządku int dodaj( const char * nzw, Tele tel ); // Wyszukiwanie binarne OsobaInfo * szukaj( const char * nzw ); // Usuwanie z zachowaniem uporządkowania int usun( const char * nzw );
Dziedziczenie 261 Usuwanie rekordu z tablicy nieuporządkowanej // // Na miejsce usuwanego elementu wstawiamy ostatni // Ta operacja nie zachowuje porządku elementów // int RecTbl :: usun( const char * nzw ){ // Wyszukiwanie liniowe RecTbl :: szukaj() OsobaInfo * op = szukaj( nzw ); if( op!= NULL ){ *op = tab[--licznik]; return 1; } else return 0; }
Dziedziczenie 262 Usuwanie rekordu z tablicy uporządkowanej int SortedTbl :: usun( const char * nzw ){ // Wyszukiwanie binarne SortedTbl :: szukaj() OsobaInfo * op = szukaj( nzw ); if( op!= NULL ){ int i; for( i = op - tab; i < (licznik - 1) ; i++ ) tab[i] = tab[i+1]; --licznik; return 1; } else return 0; }
Dziedziczenie 263 Wiązanie dynamiczne void szukaj_i_drukuj( RecTbl * p, const char * nzw ){ OsobaInfo * op; if( ( op = p->szukaj( nzw ) )!= NULL ) op->drukuj(); else cout << "Nie znaleziono rekordu\n" << endl; } void main(){ RecTbl ltab( 100 ); SortedTbl stab( 500 ); //... Wstaw rekordy do tablic szukaj_i_drukuj( <ab, "Kowalski" ); szukaj_i_drukuj( &stab, "Pawlak" ); }
Dziedziczenie 264 Indeks c J. Cichosz Start Projektowanie i programowanie obiektowe