Metody wirtualne, czysto wirtualne oraz klasy abstrakcyjne

Stronę tą wyświetlono już: 287 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:

Listing 1
  1. #include <iostream>
  2. #include <math.h>
  3. #include <windows.h>
  4. #include <vector>
  5. class iObject{ // klasa abstrakcyjna
  6. public:
  7. enum obj_type{ // identyfikatory typów obiektów
  8. undefined, // niezdefiniowany
  9. Point2d, // punkt 2W
  10. Point3d, // punkst 3W
  11. Circle, // okrąg
  12. EllipseT, // Elipsa
  13. Line // Linia 2W
  14. };
  15. 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
  16. virtual void Draw()=0; // metoda czysto wirtualna, którą każdy obiekt musi przysłonić, odpowiedzialna za "rysowanie" obiektów
  17. 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
  18. virtual ~iObject(){}; // wirtualny destruktor
  19. };

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:

Listing 2
  1. // Definicja klasy point2d (punkt 2W)
  2. class point2d : public iObject{
  3. protected:
  4. int x;
  5. int y;
  6. public:
  7. 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";};
  8. point2d(int xy):x(xy), y(xy){};
  9. point2d(int x,int y):x(x), y(y){};
  10. point2d(point2d &p):x(p.x),y(p.y){};
  11. virtual enum obj_type WhoAreYou(){return Point2d;}; // tutaj przysłonięta została metoda czysto wirtualna dla klasy point2d
  12. virtual void Draw(){std::cout<<"Punkt 2W o współrzędnych: x="<<x<<"; y="<<y<<"\n\n";}; // i tutaj również
  13. inline int GetX(){return x;};
  14. inline int GetY(){return y;};
  15. inline void SetX(int x){this->x = x;};
  16. inline void SetY(int y){this->y = y;};
  17. inline void SetXY(){std::cout<<"Podaj współrzędną x: ";std::cin>>x;std::cout<<"Podaj współrzędną y: ";std::cin>>y;};
  18. inline virtual double GetDist(){return sqrt(double(x * x + y * y));}; // a tu przysłonięta została metoda wirtualna klasy abstrakcyjenj iObject
  19. virtual ~point2d(){};
  20. };
  21. // definicja klasy line (linia)
  22. class line : public iObject{
  23. protected:
  24. point2d firstPoint;
  25. point2d secondPoint;
  26. public:
  27. 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();}
  28. line(point2d firstPoint, point2d secondPoint):firstPoint(firstPoint),secondPoint(secondPoint){};
  29. line(int x1,int y1,int x2, int y2):firstPoint(x1,y1), secondPoint(x2,y2){};
  30. virtual enum obj_type WhoAreYou(){return Line;}; // tutaj przysłonięta została metoda czysto wirtualna dla klasy line
  31. 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ż
  32. };
  33. // definicja klasy point3d (punkt 3W)
  34. class point3d : public point2d{
  35. private:
  36. int z;
  37. public:
  38. point3d():point2d(),z(0){std::cout<<"Podaj współrzędną z:"; std::cin>>y;std::cout<<"\n";};
  39. point3d(int xyz):point2d(xyz),z(xyz){};
  40. point3d(int x,int y,int z):point2d(x,y), z(z){};
  41. point3d(point3d &p):point2d(p.GetX(),p.GetY()),z(p.z){};
  42. virtual enum obj_type WhoAreYou(){return Point3d;}; // tutaj przysłonięta została metoda czysto wirtualna dla klasy point3d
  43. virtual void Draw(){std::cout<<"Punkt 3W o współrzędnych: x="<<x<<"; y="<<y<<"; z="<<z<<"\n\n";}; // i tutaj również
  44. inline int GetZ(){return z;};
  45. inline void SetZ(int z){this->z = z;};
  46. 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
  47. };
  48. // definicja klasy circle (okrąg)
  49. class circle : public point2d{
  50. protected:
  51. int ray;
  52. public:
  53. circle():point2d(),ray(0){std::cout<<"Podaj promień R:"; std::cin>>ray;ray = abs(ray);std::cout<<"\n";};
  54. circle(int x, int y, int ray):point2d(x,y),ray(ray){};
  55. circle(point2d p2d, int ray):point2d(p2d),ray(ray){};
  56. circle(circle &c):point2d(c.x,c.y), ray(c.ray){};
  57. virtual enum obj_type WhoAreYou(){return Circle;}; // tutaj przysłonięta została metoda czysto wirtualna dla klasy circle
  58. virtual void Draw(){std::cout<<"Okrąg o współrzędnych: x="<<x<<"; y="<<y<<" i promieniu r="<<ray<<"\n\n";}; // i tutaj też
  59. inline int GetRay(){return ray;};
  60. inline void SetRay(int ray){this->ray = ray;};
  61. };
  62. // definicja klasy ellipse (elipsa)
  63. class ellipse : public point2d{
  64. protected:
  65. int r1;
  66. int r2;
  67. public:
  68. 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";};
  69. ellipse(int x, int y, int r1, int r2):point2d(x,y),r1(r1),r2(r2){};
  70. ellipse(point2d p,int r1, int r2):point2d(p),r1(r1), r2(r2){};
  71. ellipse(ellipse &e):point2d(e), r1(e.r1), r2(e.r2){};
  72. virtual enum obj_type WhoAreYou(){return EllipseT;}; // tutaj przysłonięta została metoda czysto wirtualna dla klasy ellipse
  73. 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
  74. inline int GetR1(){return r1;};
  75. inline int GetR2(){return r2;};
  76. inline void SetR1(int r1){this->r1=r1;};
  77. inline void SetR2(int r2){this->r2=r2;};
  78. };

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:

Listing 3
  1. // rysowanie linii
  2. void WriteLine(){
  3. std::cout<<"==============================================================================="<<std::endl;
  4. }
  5. // obsługa dodawania obiektów
  6. void AddObject(std::vector<iObject*> &tObject){
  7. int obj_t = 0;
  8. do{
  9. WriteLine();
  10. A: // znacznik adresu pamięci
  11. std::cout<<"Wybież typ obiektu, który chcesz dodać:\n";
  12. WriteLine();
  13. 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";
  14. std::cout<<"Wpisz numer identyfikacyjny obiektu [0/1/2/3/4/5]:";
  15. std::cin>>obj_t;
  16. switch((enum iObject::obj_type)obj_t){
  17. case iObject::obj_type(iObject::undefined):
  18. break;
  19. case iObject::obj_type(iObject::Point2d):
  20. tObject.push_back(new point2d); // dynamiczne tworzenie obiektu typu point2d z jednoczesnym dodaniem wskaźnika klasy abstrakcyjnej do kontenera
  21. break;
  22. case iObject::obj_type(iObject::Point3d):
  23. tObject.push_back(new point3d); // dynamiczne tworzenie obiektu typu point3d z jednoczesnym dodaniem wskaźnika klasy abstrakcyjnej do kontenera
  24. break;
  25. case iObject::obj_type(iObject::Circle):
  26. tObject.push_back(new circle); // dynamiczne tworzenie obiektu typu circle z jednoczesnym dodaniem wskaźnika klasy abstrakcyjnej do kontenera
  27. break;
  28. case iObject::obj_type(iObject::EllipseT):
  29. tObject.push_back(new ellipse); // dynamiczne tworzenie obiektu typu ellipse z jednoczesnym dodaniem wskaźnika klasy abstrakcyjnej do kontenera
  30. break;
  31. case iObject::obj_type(iObject::Line):
  32. tObject.push_back(new line); // dynamiczne tworzenie obiektu typu line z jednoczesnym dodaniem wskaźnika klasy abstrakcyjnej do kontenera
  33. break;
  34. default:
  35. std::cout<<std::endl;
  36. WriteLine();
  37. std::cout<<"Nie ma takiej opcji:"<<std::endl<<std::endl;
  38. WriteLine();
  39. goto A; // skok do znacznika A w pamięci
  40. break;
  41. }
  42. }while(obj_t);
  43. }
  44. // rysowanie obiektów kontenera
  45. void DrawObject(std::vector<iObject*> &tObject){
  46. WriteLine();
  47. std::cout<<"Oto lista obiektów utworzonych:\n";
  48. WriteLine();
  49. if(!tObject.size())
  50. std::cout<<"\nBrak elementów w tablicy.\n\n";
  51. for(std::vector<iObject*>::iterator i = tObject.begin(); i < tObject.end(); i++){
  52. std::cout<<"["<<int(i - tObject.begin())+1<<"] ";
  53. (*i)->Draw(); // tutaj wywoływana jest metoda Draw() dla danego typu obiektu stojącego za nią
  54. }
  55. std::cout<<"\nCałkowita liczba elementów w tablicy: "<<tObject.size()<<"\n\n";
  56. }
  57. // Obsługa usuwania wszystkich obiektó kontenera
  58. void DeleteAllObject(std::vector<iObject*> &tObject, bool ask){
  59. if(ask){
  60. std::cout<<"Żeś pewny taki, że chcesz usunąć wszystkie obiekty? [0 - nie/ 1 - tak] : ";
  61. int remove = 0;
  62. std::cin>>remove;
  63. if(!remove)
  64. return ;
  65. std::cout<<"\nUsunięto "<< tObject.size() << " elementów.\n\n";
  66. }
  67. for(std::vector<iObject*>::iterator i = tObject.begin(); i < tObject.end(); i++){
  68. if(*i){
  69. delete *i; // zwalnianie pamięci dynamicznie przydzielonej
  70. *i = NULL;
  71. }
  72. }
  73. tObject.clear();
  74. }
  75. // Obsługa usuwania obiektów danego typu
  76. void DeleteSomeObject(std::vector<iObject*> &tObject){
  77. while(true){
  78. WriteLine();
  79. std::cout<<"Wybież typ obiektów do usunięcia:\n";
  80. WriteLine();
  81. 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";
  82. std::cout<<"Live or die, make your choose [0/1/2/3/4/5]: ";
  83. int type = 0;
  84. std::cin>>type;
  85. if(!type){
  86. return ;
  87. }
  88. int k = 0;
  89. for(std::vector<iObject*>::iterator i = tObject.begin(); i < tObject.end(); i++){
  90. if(*i && (*i)->WhoAreYou()==type){
  91. delete *i; // zwalnianie pamięci dynamicznie przydzielonej
  92. *i = NULL;
  93. tObject.erase(i); // usuwanie wskaźnika z wektora
  94. i--; // indeks przeszukiwania o jeden w dół
  95. k ++; // zwiąkszam licznik usuniętych elementów o 1
  96. }
  97. }
  98. std::cout<<"Usunięto "<< k << " elementów.\n\n";
  99. }
  100. }
  101. // Obsługa inerfejstu programu
  102. void WhatYouWantToDo(std::vector<iObject*> &tObject){
  103. int id = 0;
  104. do{
  105. WriteLine();
  106. std::cout<<"Co chcesz zrobić?\n";
  107. WriteLine();
  108. 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";
  109. std::cout<<"Wybież opcję [0/1/2/3/4]: ";
  110. std::cin>>id;
  111. std::cout<<"\n";
  112. switch(id){
  113. case 1:
  114. AddObject(tObject); // Wywołanie funkcji odpowiedzialnej za dodawanie obiektów
  115. break;
  116. case 2:
  117. DrawObject(tObject); // wywołanie funkcji odpowiedzialnej za rysowanie obiektów
  118. break;
  119. case 3:
  120. {
  121. DeleteAllObject(tObject,true); // wywołanie funkcji odpowiedzialnej za usuwanie wszystkich obiektów z opcą potwierdzenia
  122. }
  123. break;
  124. case 4:
  125. {
  126. DeleteSomeObject(tObject); // wywołanie funkcji usuwającej obiekty danego typu
  127. }
  128. break;
  129. }
  130. }while(id);
  131. }
  132. int main(){
  133. setlocale(LC_CTYPE,"Polish"); // to po to, żeby mieć polskie znaki
  134. std::vector<iObject*> tObject; // kontener na wskaźniki typu iObject
  135. tObject.push_back(new point2d(12,20)); // tworzenie wskaźnika obiektu typu point2d i wstawienie wskaźnika jego klasy bazowej do konenera
  136. tObject.push_back(new point3d(100,4,20)); // tworzenie wskaźnika obiektu typu point3d i wstawienie wskaźnika jego klasy bazowej do konenera
  137. tObject.push_back(new circle(20,40,100)); // tworzenie wskaźnika obiektu typu circle i wstawienie wskaźnika jego klasy bazowej do konenera
  138. tObject.push_back(new ellipse(0,200,40,80)); // tworzenie wskaźnika obiektu typu ellipse i wstawienie wskaźnika jego klasy bazowej do konenera
  139. WhatYouWantToDo(tObject); // obsługa interfejsu programu
  140. DeleteAllObject(tObject,false); // usuwanie bez pytania o zgodę wszystkich obiektów
  141. std::cout<<"Wciśnij enter, aby zamknąć program...";
  142. cin.get();
  143. return 0;
  144. }

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.

Komentarze