340 likes | 518 Views
Metody wirtualne. Dlaczego metody wirtualne?. W C++ dla wskaźników i referencji dozwolona jest konwersja potomna → bazowa . przez taki wskaźnik lub referencję można odwoływać się jedynie do danych zadeklarowanych w klasie bazowej, oraz jedynie do metod klasy bazowej
E N D
Dlaczego metody wirtualne? • W C++ dla wskaźników i referencji dozwolona jest konwersja potomna → bazowa. • przez taki wskaźnik lub referencję można odwoływać się jedynie do danych zadeklarowanych w klasie bazowej, oraz jedynie do metod klasy bazowej • na podstawie klasy wskaźnika/referencji kompilator zdecyduje o wywołaniu metody kl. bazowej nazwet jeżeli obiekt jest klasy potomnej.
class punkt { int x,y; public: void pokaz(); //rysuje punkt void ukryj(); }; class okrag: public punkt { int r; public: void pokaz(); //rysuje punkt void ukryj(); }; okrag o; punkt &rp=o; rp.pokaz(); //punkt::pokaz
Dlaczego metody wirtualne? okrag o; punkt &rp=o; rp.pokaz(); // niech wywoła się okrag::pokaz • jak to zrealizować?
class punkt { int x,y; public: char klasa; void pokaz(); //rysuje punkt punkt(int x, int y) :x(x), y(y), { klasa=‘p’; } }; Rozwiązanie niedoskonałe Wadliwe class okrag: public punkt { int r; public: void pokaz(); //rysuje punkt okrag(int x, int y, int r) :punkt(x,y), r(r) { klasa=‘o’; } }; okrag o; punkt &rp=o; if (rp.klasa==‘p’) rp.punkt::pokaz(); else rp.okrag::pokaz();
Metody wirtualne • Jeżeli zadeklarujemy metodę jako wirtualną to kompilator uzupełni obiekty o pole determinujące klasę obiektu i przy wywoływaniu wybranych przez nas metod wywoła metodę z właściwej klasy. void virtualpunkt::ukryj(); • Metodę (albo operator) wystarczy raz zadeklarować jako wirtualny, w klasach pochodnych możemy, ale nie musimy używać słowa kl. virtual.
class punkt { int x,y; public: void virtual pokaz(); void virtual ukryj(); }; class okrag: public punkt { int r; public: void pokaz(); // virtual void ukryj();// virtual }; Metody wirtualne
Metody wirtualne - działanie • Do pierwszej klasy w której w hierarchii klas pojawi się metoda wirtualna dodane zostanie dodatkowe niejawne pole — adres tablicy metod wirtualnych. • zwiększy się rozmiar obiektów tej klasy. • Dla obiektu, którego klasy nie można jednoznacznie określić na etapie kompilacji, odwołania do metody, bądź metod zadeklarowanych jako wirtualne będą się odbywały pośrednio poprzez tablicę metod wirtualnych • będzie to działało wolniej niż odwołanie bezpośrednie, • metody wirtualne nie będą rozwijane inline, • będzie to działało szybciej, niż gdybyśmy taką sztuczkę robili ręcznie, • kompilator nie pomyli się (człowiek – wiadomo).
Metody wirtualne - działanie • Odwołania przez wskaźnik i referencje będą pośrednie • Odwołania przez kwalifikację obiektem będą bezpośrednie • a więc szybsze, metody (nawet zadeklarowane z virtual) mogą być rozwijane inline. • Uwaga: metody klasy mogą zostać odziedziczone i aktywowane na rzecz obiektu klasy pochodnej, a więc odwołania do wirtualnych metod danej klasy z innych metod tej klasy będą też pośrednie!
class punkt { int x,y; public: void virtual pokaz(); void virtual ukryj(); void przesun(int dx, int dy) { ukryj();// wirtualna w punkt x+=dx; y+=dy; pokaz(); // wirtualna w punkt } }; class okrag: public punkt { int r; public: void pokaz(); void ukryj(); }; okrag o; punkt &rp=o; rp.pokaz(); //okrag::pokaz rp.przesun(); //punkt::przesun wywoła //okrag::pokaz i okrag::ukyj !!!
Klasa polimorficzna • klasa polimorficzna to taka w której występuje przynajmniej jedna metoda wirtualna • Przykład korzyści z polimorfizmu: • deklarujemy listę przechowującą wskaźniki do punktów (klasa polimorficzna) - na liście umieszczać punkty okręgi i inne figury. • przez wskaźniki możemy pokazać wszystkie figury (wywołując metodę wirtualną pokaz() – pośrednio), możemy też przesuwać figury – wyw. się niewirtualna metoda przesuń, ona wywoła właściwe, bo wirtualne pokaz i ukryj.
Wczesne i późne wiązanie • Wczesne wiązanie: • gdy metoda nie jest wirtualna lub jest wirtualna ale można określić z której klasy ma pochodzić to nazwa metody jest kojarzona z jej kodem (wywołanie metody albo nawet rozwinięcie inline) już na etapie kompilacji/linkowania. • Późne wiązanie • decyzja co do wyboru klasy z zakresu której metodę wykonać,zostaje podjęta podczasbiegu programu.
Wczesne i późne wiązanie • Metody nie-wirtualne – zawsze wczesne wiązanie. • Zadeklarowanie metody jako wirtualnej nie wyklucza jej wczesnego wiązania, nastąpi ono, gdy: • jawnie (operatorem zakresu) podamy o którą klasę nam chodzi, • wywołamy metodę bezpośrednio na rzecz obiektu (nie przez wskaźnik lub referencję).
Metody wirtualne • zaleta: ogromna łatwość rozbudowy programu, • Pisząc kod możemy wykorzystywać metody których jeszcze nie napisano ! • Nie musimy powielać takiego samego kodu w metodach różnych klas (vide przesun()). • Nie grozi nam „uzupełnienie” już gotowego kodu o nowe błędy.
Konstruktory i destruktory • Konstruktor nie może być wirtualny (dlaczego?) • Destruktor może i czasami powinien być virtual (dlaczego?) • Uwaga: Gdy w jakiejś klasie zadeklarujemy destruktor jako wirtualny, to w klasach pochodnych destruktory też będą wirtualne (mimo że ich nazwy w klasach pochodnych będą inne).
Static i virtual • Metoda statyczna nie może być wirtualna (dlaczego?).
Dziedziczenie metod wirtualnych • meoda wirtualna może zostać odziedziczona przez klasę pochodną • może zostać przedefiniowana, w jej ciele możemy wywołać metodę wirtualną klasy bazowej. • np.: void okrag::pokaz() { punkt::pokaz(); // narysuj środek okręgu //narysuj okrąg }
Przeciążanie a wirtualność • wirtualność dotyczy tylko tej metody/operatora która została w danej klasie lub przodku zadeklarowana jako virtual, • inne metody (o innych parametrach) są zwykłymi metodami/operatorami. • np. metoda nie wirtualna: void punkt::pokaz(char * opis){...};
Zaprzyjaźnianie a wirtualność • Wirtualność jest niezależna od zaprzyjaźniania. • Zaprzyjaźnianie nie jest przechodnie, • Zaprzyjaźnianie dotyczy tylko tej metody (klasa::metoda) która została zaprzyjaźniona, • Metoda przedefiniowana w klasie pochodnej, wirtualna czy nie, nie będzie automatycznie zaprzyjaźniona.
Widoczność metod wirtualnych • Widoczność jest rozstrzygana na etapie kompilacji • zatem decyduje typ wskaźnika/referencji. • np., przyjmijmy, że wszystkie składowe klasy okrąg sa prywatne: okrag o; punkt &rp=o; rp.pokaz(); // OK. – dlaczego?
Klasa abstrakcyjna • Klasa abstrakcyjna, to klasa, która nie zawiera żadnych obiektów. • Klasa abstrakcyjna służy do definiowania interfejsu/cech wspólnychrodziny innych klas (jej potomków) • np. klasa abstrakcyjna „figura” „liczba” • Dziedziczenie klas abstrakcyjnych
Metoda czysto wirtualna • WC++ klasa abstrakcyjnato taka, która zawiera przynajmniej jedną metodę czysto wirtualną – tj. taką która jest wirtualna, i nie ma zdefiniowanego ciała. void virtual figura::rysuj()=0;
RTTI • Ponieważ wskaźnikowi na przodka można przypisać adres potomka to typ wskaźnika nie determinuje klasy do której należy obiekt wskazywany. • Czasami w trakcie działania programu pojawia się potrzeba sprawdzenia do jakiej klasy dany obiekt należy. Jeżeli wskaźnik bądź referencja dotyczy klasy polimorficznej to obiekt zawiera pole umożliwiające identyfikację klasy (wskaźnik do tablicy metod wirtualnych), na jego podstawie mechanizm RTTI pozwala sprawdzać typy w trakcie biegu programu. • Jeżeli wskaźnik, bądź referencja dotyczy typu podstawowego bądź klasy nie-polimorficznej to typ określany jest na podstawie typu wskaźnika/referencji a nie rzeczywistego obiektu/zmiennej — bo nie ma danych by to zrobić inaczej.
RTTI • RTTI — RunTime Type Information • operator typeid type_infotypeid(arg) • arg: typ, klasa, zmienna bądź obiekt
RTTI class type_info { // tu prywatny konstruktor kopiujący i operator przypisania // nie można z zewnątrz tworzyć kopii obiektów tej klasy public: virtual ~type_info(); bool operator==(const type_info& rhs) const; bool operator!=(const type_info& rhs) const; bool before(const type_info& rhs) const; const char* name() const; };
class A { virtual void a(){}; }; class B:public A {}; class C:public B {}; A a, *pa; C c; pa=&c; typeid(C)==typeid(pa)// 0 typeid(a)==typeid(*pa)// 0 typeid(c)==typeid(*pa) // 1 typeid(B).name() // B typeid(pa).name() // A * typeid(*pa).name() // C typeid(A).before(typeid(*pa)) // 1 typeid(B).before(typeid(*pa)) // 1 typeid(C).before(typeid(*pa)) // 0 typeid(C).before(typeid(a)) // 0 RTTI
operatory rzutowania dla zastąpienia rzutowania w stylu języka C static_cast< type-id >(wyrażenie) dynamic_cast< type-id >(wyrażenie) reinterpret_cast< type-id >(wyrażenie) const_cast< type-id >(wyrażenie) to 4 operatory i 4 słowa kluczowe C++ zaprojektowane by umożliwić większą kontrolę na poziomie kompilacji i wykonania kodu nad (podatnymi na błędy) rzutowaniami pozwalają wyrazić intencję programisty *_cast
static_cast< type-id >(wyrażenie) rzutownie oparte na typach znanych podczas kompilacji podobnie niebezpiczne jak rzutowanie C: (type-id) wyrażenie używać ostożnie działa szybko nie dopuszcza się modyfikacji kwalifikacji const i volatile wyrażenia static_cast
static_cast< type-id >(wyrażenie ) class A {}; class B:public A { int method(); }; int fun(A * pa) { static_cast<B*>(pa) -> method() // miejmy nadzieję, że pa wskazuje na B } static_cast
dynamic_cast< type-id >(wyrażenie ) rzutowanie oparte o RTTI, używane ze wskaźnikami i referencjami dla wskaźników zwraca NULL, jeśli type-id nie jest ani typu wyrażenia ani jego rodzica dla referencji w ww. przypadku zgłaszany jest wyjątek bezpieczne, mniej szybkie nie dopuszcza się modyfikacji kwalifikacji const i volatile wyrażenia dynamic_cast
dynamic_cast< type-id >(wyrażenie ) class A {}; class B:public A { }; int fun() { A *pa, *aptr=new A; B *pb, *bptr=new B; ... pa=dynamic_cast<A*>(bptr); // OK pb=dynamic_cast<B*>(aptr); // NULL !!! pb=dynamic_cast<B*>(pa); // OK } dynamic_cast
reinterpret_cast< type-id >(wyrażenie ) rzutownie oparte na typach znanych podczas kompilacji używane dla arytmetyki adresów (wskaźników), do konwersji typu „w górę” używaj dla wskaźników i liczb całkowitych (wyraź swoją intencję) niech Twój szef myśli że jesteś guru programowania w C++ ;) CLASS *ptr; ... unsigned int adress=reinterpret_cast<unsigned int>(ptr); reinterpret_cast
const_cast< type-id >(wyrażenie ) rzutownie oparte na typach znanych podczas kompilacji używaj dla wskaźników i referencji używaj dla usunięcia kwalifikacji cost i/lub volatile wyrażenia class CLASS { int member; int CLASS::constmethod() const { const_cast<CLASS *>(this) -> member++; // const_cast<int>(member)++; ERROR } } const_cast
słowo kluczowe mutable sprawia, że nie-stała i nie-statyczna zmienna klasowa pozostanie nie-stała w stałych metodach klasy class CLASS { mutable int member; int CLASS::constmethod() const { member++; // bo member jest mutable } } mutable