Metody wirtualne, czysto wirtualne oraz klasy abstrakcyjne

Autor podstrony: Krzysztof Zajączkowski

Stronę tą wyświetlono już: 16541 razy

Wstęp

Niektórzy z Czytelników mogą zastanawiać się dlaczego poruszyłem tematykę kontenerów typu vector zamiast zacząć omawiać zagadnienia związane z metodami czysto wirtualnymi, wirtualnymi oraz klasami abstrakcyjnymi. Odpowiedź jest bardzo prosta: chciałem omówić te zagadnienia na konkretnym przykładzie, a żeby to zrobić musiałem najpierw omówić kontener typu vector.

Klasy abstrakcyjne

Klasy abstrakcyjne to takie, które mają co najmniej jedną metodę czysto wirtualną. Z kolei metoda czysto wirtualna to taka, która ma nagłówek i nie ma ciała i właśnie z tego powodu nie da się utworzyć obiektu klasy abstrakcyjnej. Po cóż więc komukolwiek taka klasa, co to jej obiektu się nie da utworzyć? Ano po to, żeby można było ją dziedziczyć i poprzez mechanizm dziedziczenia wymusić na klasie bądź klasach dziedziczących obsługę metod czysto wirtualnych pochodzących od klasy abstrakcyjnej. Oczywiście klasa abstrakcyjna może zawierać również metody wirtualne, czyli takie, które mają swoje ciało i które mogą zostać przysłonięte wewnątrz klasy dziedziczącej.

Przykładowa definicja klasy abstrakcyjnej:

#include <iostream> #include <math.h> #include <windows.h> #include <vector> class iObject{ // klasa abstrakcyjna public: enum obj_type{ // identyfikatory typów obiektów undefined, // niezdefiniowany Point2d, // punkt 2W Point3d, // punkst 3W Circle, // okrąg EllipseT, // Elipsa Line // Linia 2W }; virtual enum iObject::obj_type WhoAreYou()=0; // metoda czysto wirtualna, którą każdy obiekt dziedziczący musi przysłonić odpowiedzialna za zwracanie informacji o tym jaki typ obiektu zawiera dany interfejs virtual void Draw()=0; // metoda czysto wirtualna, którą każdy obiekt musi przysłonić, odpowiedzialna za "rysowanie" obiektów virtual double GetDist(){return 0;}; // metoda wirtualna, którą nie każdy obiekt musi przysłonić, odpowiedzialna za obliczanie odległości punktu od początku układu współrzędnych kartezjańskich virtual ~iObject(){}; // wirtualny destruktor };

Jak widać w powyższym kodzie definicji klasy typu iObject zawarłem typ wyliczeniowy enum obj_type, ten sam typ zwraca metoda czysto wirtualna virtual enum iObject::obj_type WhoAreYou()=0;, która jak widać zamiast ciała ma =0 co oznacza, że jest to metoda czysto wirtualna, oczywiście równie ważna rolę odgrywa tutaj słowo kluczowe virtual. Wewnątrz klasy utworzona została również jedna metoda wirtualna, którą jest virtual double GetDist(){return 0;};, stało się tak dlatego, że nie każdy obiekt będzie mógł zwracać tą wartość.

Definicje klas dziedziczących klasę abstrakcyjną iObject

W celu zademonstrowania funkcjonalności, jaką niosą za sobą klasy abstrakcyjne należy utworzyć kilka przykładowych definicji klas dziedziczących po wcześniej utworzonej, przykładowej klasie abstrakcyjnej iObject, co też i z najdzikszą rozkoszą uczyniłem:

// Definicja klasy point2d (punkt 2W) class point2d : public iObject{ protected: int x; int y; public: point2d():x(0), y(0){std::cout<<"Podaj współrzędną x:"; std::cin>>x;std::cout<<"Podaj współrzędną y:"; std::cin>>y;std::cout<<"\n";}; point2d(int xy):x(xy), y(xy){}; point2d(int x,int y):x(x), y(y){}; point2d(point2d &p):x(p.x),y(p.y){}; virtual enum obj_type WhoAreYou(){return Point2d;}; // tutaj przysłonięta została metoda czysto wirtualna dla klasy point2d virtual void Draw(){std::cout<<"Punkt 2W o współrzędnych: x="<<x<<"; y="<<y<<"\n\n";}; // i tutaj również inline int GetX(){return x;}; inline int GetY(){return y;}; inline void SetX(int x){this->x = x;}; inline void SetY(int y){this->y = y;}; inline void SetXY(){std::cout<<"Podaj współrzędną x: ";std::cin>>x;std::cout<<"Podaj współrzędną y: ";std::cin>>y;}; inline virtual double GetDist(){return sqrt(double(x * x + y * y));}; // a tu przysłonięta została metoda wirtualna klasy abstrakcyjenj iObject virtual ~point2d(){}; }; // definicja klasy line (linia) class line : public iObject{ protected: point2d firstPoint; point2d secondPoint; public: line():firstPoint(0,0),secondPoint(0,0){std::cout<<"\nPodaj współrzędne punktu początkowego:\n\n";firstPoint.SetXY();std::cout<<"\nPodaj współrzędne punktu końcowego:\n\n";secondPoint.SetXY();} line(point2d firstPoint, point2d secondPoint):firstPoint(firstPoint),secondPoint(secondPoint){}; line(int x1,int y1,int x2, int y2):firstPoint(x1,y1), secondPoint(x2,y2){}; virtual enum obj_type WhoAreYou(){return Line;}; // tutaj przysłonięta została metoda czysto wirtualna dla klasy line virtual void Draw(){std::cout<<"Linia 2W o współrzędnych:\n\nPierwszego punktu:\n";firstPoint.Draw(); std::cout<<"\nDrugiego punktu:\n";secondPoint.Draw();}; // i tutaj też }; // definicja klasy point3d (punkt 3W) class point3d : public point2d{ private: int z; public: point3d():point2d(),z(0){std::cout<<"Podaj współrzędną z:"; std::cin>>y;std::cout<<"\n";}; point3d(int xyz):point2d(xyz),z(xyz){}; point3d(int x,int y,int z):point2d(x,y), z(z){}; point3d(point3d &p):point2d(p.GetX(),p.GetY()),z(p.z){}; virtual enum obj_type WhoAreYou(){return Point3d;}; // tutaj przysłonięta została metoda czysto wirtualna dla klasy point3d virtual void Draw(){std::cout<<"Punkt 3W o współrzędnych: x="<<x<<"; y="<<y<<"; z="<<z<<"\n\n";}; // i tutaj również inline int GetZ(){return z;}; inline void SetZ(int z){this->z = z;}; inline virtual double GetDist(){return sqrt(double(x * x + y * y + z * z));}; // a tu przysłonięta została metoda wirtualna klasy abstrakcyjenj iObject }; // definicja klasy circle (okrąg) class circle : public point2d{ protected: int ray; public: circle():point2d(),ray(0){std::cout<<"Podaj promień R:"; std::cin>>ray;ray = abs(ray);std::cout<<"\n";}; circle(int x, int y, int ray):point2d(x,y),ray(ray){}; circle(point2d p2d, int ray):point2d(p2d),ray(ray){}; circle(circle &c):point2d(c.x,c.y), ray(c.ray){}; virtual enum obj_type WhoAreYou(){return Circle;}; // tutaj przysłonięta została metoda czysto wirtualna dla klasy circle virtual void Draw(){std::cout<<"Okrąg o współrzędnych: x="<<x<<"; y="<<y<<" i promieniu r="<<ray<<"\n\n";}; // i tutaj też inline int GetRay(){return ray;}; inline void SetRay(int ray){this->ray = ray;}; }; // definicja klasy ellipse (elipsa) class ellipse : public point2d{ protected: int r1; int r2; public: ellipse():point2d(),r1(0),r2(0){std::cout<<"Podaj promień r1:"; std::cin>>r1;r1=abs(r1);std::cout<<"Podaj promień r2:"; std::cin>>r2;r2=abs(r2);std::cout<<"\n";}; ellipse(int x, int y, int r1, int r2):point2d(x,y),r1(r1),r2(r2){}; ellipse(point2d p,int r1, int r2):point2d(p),r1(r1), r2(r2){}; ellipse(ellipse &e):point2d(e), r1(e.r1), r2(e.r2){}; virtual enum obj_type WhoAreYou(){return EllipseT;}; // tutaj przysłonięta została metoda czysto wirtualna dla klasy ellipse virtual void Draw(){std::cout<<"Elipsa o współrzędnych: x="<<x<<"; y="<<y<<" i promieniach r1="<<r1<<"; r2="<<r2<<"\n\n";}; // i tutaj także inline int GetR1(){return r1;}; inline int GetR2(){return r2;}; inline void SetR1(int r1){this->r1=r1;}; inline void SetR2(int r2){this->r2=r2;}; };

Jak widać, każda z metod czysto wirtualnych musi być indywidualnie obsłużona przez każdą z klas dziedziczących, a jedynie kasa point2d oraz point3d obsługują metodę wirtualną Dist() zwracając długość wektora, jakimi są obiekty tego typu klas.

Interfejs programu i praktyczne wykorzystanie klas abstrakcyjnych

Czas najwyższy napisać parę funkcji oraz pokazać, jak w praktyczny sposób wykorzystać powyżej napisany kod:

// rysowanie linii void WriteLine(){ std::cout<<"==============================================================================="<<std::endl; } // obsługa dodawania obiektów void AddObject(std::vector<iObject*> &tObject){ int obj_t = 0; do{ WriteLine(); A: // znacznik adresu pamięci std::cout<<"Wybież typ obiektu, który chcesz dodać:\n"; WriteLine(); std::cout<<"Powrót to menu głównego\t[0]\nPunkt 2W\t\t[1]\nPunkt 3W\t\t[2]\nOkrąg\t\t\t[3]\nElipsa\t\t\t[4]\nLinia 2W\t\t[5]\n\n"; std::cout<<"Wpisz numer identyfikacyjny obiektu [0/1/2/3/4/5]:"; std::cin>>obj_t; switch((enum iObject::obj_type)obj_t){ case iObject::obj_type(iObject::undefined): break; case iObject::obj_type(iObject::Point2d): tObject.push_back(new point2d); // dynamiczne tworzenie obiektu typu point2d z jednoczesnym dodaniem wskaźnika klasy abstrakcyjnej do kontenera break; case iObject::obj_type(iObject::Point3d): tObject.push_back(new point3d); // dynamiczne tworzenie obiektu typu point3d z jednoczesnym dodaniem wskaźnika klasy abstrakcyjnej do kontenera break; case iObject::obj_type(iObject::Circle): tObject.push_back(new circle); // dynamiczne tworzenie obiektu typu circle z jednoczesnym dodaniem wskaźnika klasy abstrakcyjnej do kontenera break; case iObject::obj_type(iObject::EllipseT): tObject.push_back(new ellipse); // dynamiczne tworzenie obiektu typu ellipse z jednoczesnym dodaniem wskaźnika klasy abstrakcyjnej do kontenera break; case iObject::obj_type(iObject::Line): tObject.push_back(new line); // dynamiczne tworzenie obiektu typu line z jednoczesnym dodaniem wskaźnika klasy abstrakcyjnej do kontenera break; default: std::cout<<std::endl; WriteLine(); std::cout<<"Nie ma takiej opcji:"<<std::endl<<std::endl; WriteLine(); goto A; // skok do znacznika A w pamięci break; } }while(obj_t); } // rysowanie obiektów kontenera void DrawObject(std::vector<iObject*> &tObject){ WriteLine(); std::cout<<"Oto lista obiektów utworzonych:\n"; WriteLine(); if(!tObject.size()) std::cout<<"\nBrak elementów w tablicy.\n\n"; for(std::vector<iObject*>::iterator i = tObject.begin(); i < tObject.end(); i++){ std::cout<<"["<<int(i - tObject.begin())+1<<"] "; (*i)->Draw(); // tutaj wywoływana jest metoda Draw() dla danego typu obiektu stojącego za nią } std::cout<<"\nCałkowita liczba elementów w tablicy: "<<tObject.size()<<"\n\n"; } // Obsługa usuwania wszystkich obiektó kontenera void DeleteAllObject(std::vector<iObject*> &tObject, bool ask){ if(ask){ std::cout<<"Żeś pewny taki, że chcesz usunąć wszystkie obiekty? [0 - nie/ 1 - tak] : "; int remove = 0; std::cin>>remove; if(!remove) return ; std::cout<<"\nUsunięto "<< tObject.size() << " elementów.\n\n"; } for(std::vector<iObject*>::iterator i = tObject.begin(); i < tObject.end(); i++){ if(*i){ delete *i; // zwalnianie pamięci dynamicznie przydzielonej *i = NULL; } } tObject.clear(); } // Obsługa usuwania obiektów danego typu void DeleteSomeObject(std::vector<iObject*> &tObject){ while(true){ WriteLine(); std::cout<<"Wybież typ obiektów do usunięcia:\n"; WriteLine(); std::cout<<"Powrót do menu głównego\t[0]\nPunkt 2W\t\t[1]\nPunkt 3W\t\t[2]\nOkrąg\t\t\t[3]\nElipsa\t\t\t[4]\nLinia 2W\t\t[5]\n\n"; std::cout<<"Live or die, make your choose [0/1/2/3/4/5]: "; int type = 0; std::cin>>type; if(!type){ return ; } int k = 0; for(std::vector<iObject*>::iterator i = tObject.begin(); i < tObject.end(); i++){ if(*i && (*i)->WhoAreYou()==type){ delete *i; // zwalnianie pamięci dynamicznie przydzielonej *i = NULL; tObject.erase(i); // usuwanie wskaźnika z wektora i--; // indeks przeszukiwania o jeden w dół k ++; // zwiąkszam licznik usuniętych elementów o 1 } } std::cout<<"Usunięto "<< k << " elementów.\n\n"; } } // Obsługa inerfejstu programu void WhatYouWantToDo(std::vector<iObject*> &tObject){ int id = 0; do{ WriteLine(); std::cout<<"Co chcesz zrobić?\n"; WriteLine(); std::cout<<"Wyjść z programu trzaskając drzwiami\t\t[0]\nDodać nowy obiekt\t\t\t\t[1]\nWyświetlić wszystkie obiekty\t\t\t[2]\nUsunąć w bezwzględny sposób wszystkie obiekty\t[3]\nUsunąć dany typ obiektów\t\t\t[4]\n\n"; std::cout<<"Wybież opcję [0/1/2/3/4]: "; std::cin>>id; std::cout<<"\n"; switch(id){ case 1: AddObject(tObject); // Wywołanie funkcji odpowiedzialnej za dodawanie obiektów break; case 2: DrawObject(tObject); // wywołanie funkcji odpowiedzialnej za rysowanie obiektów break; case 3: { DeleteAllObject(tObject,true); // wywołanie funkcji odpowiedzialnej za usuwanie wszystkich obiektów z opcą potwierdzenia } break; case 4: { DeleteSomeObject(tObject); // wywołanie funkcji usuwającej obiekty danego typu } break; } }while(id); } int main(){ setlocale(LC_CTYPE,"Polish"); // to po to, żeby mieć polskie znaki std::vector<iObject*> tObject; // kontener na wskaźniki typu iObject tObject.push_back(new point2d(12,20)); // tworzenie wskaźnika obiektu typu point2d i wstawienie wskaźnika jego klasy bazowej do konenera tObject.push_back(new point3d(100,4,20)); // tworzenie wskaźnika obiektu typu point3d i wstawienie wskaźnika jego klasy bazowej do konenera tObject.push_back(new circle(20,40,100)); // tworzenie wskaźnika obiektu typu circle i wstawienie wskaźnika jego klasy bazowej do konenera tObject.push_back(new ellipse(0,200,40,80)); // tworzenie wskaźnika obiektu typu ellipse i wstawienie wskaźnika jego klasy bazowej do konenera WhatYouWantToDo(tObject); // obsługa interfejsu programu DeleteAllObject(tObject,false); // usuwanie bez pytania o zgodę wszystkich obiektów std::cout<<"Wciśnij enter, aby zamknąć program..."; cin.get(); return 0; }

W powyższym kodzie, chociażby w funkcji głównej programu main pokazane zostało, jak do wektora tObject wskaźników klasy iObject są dodawane nowe elementy i to dla różnych typów klas! Dzięki takiemu rozwiązaniu możliwe jest przechowywanie obiektów różnego typu w jednym kontenerze, jak również wywoływanie metod wewnętrznych różnych obiektów. Takie zjawisko nazywa się polimorfizmem, a o klasie abstrakcyjnej iObject mówi się, że stanowi ona interfejs dla obsługi obiektów klas dziedziczących.

Propozycje książek