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 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; 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();
void virtual punkt::ukryj(); 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 virtual punkt::ukryj(); Metodę (albo operator) wystarczy raz zadeklarować jako wirtualny, w klasach pochodnych możemy, ale nie musimy używać słowa kl. virtual.
Metody wirtualne 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 - 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 podczas biegu 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ólnych rodziny innych klas (jej potomków) np. klasa abstrakcyjna „figura” „liczba” Dziedziczenie klas abstrakcyjnych
Metoda czysto wirtualna W C++ klasa abstrakcyjna to 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.
type_info typeid(arg) RTTI RTTI — RunTime Type Information operator typeid type_info typeid(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; };
RTTI 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
operatory rzutowania dla zastąpienia rzutowania w stylu języka C *_cast 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
static_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 }
dynamic_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 }
reinterpret_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);
const_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 }
mutable 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 }