20.5 Klasy abstrakcyjne

W C++ (podobnie jak w Javie) można definiować klasy abstrakcyjne. Klasami takimi będą klasy, w których pewne metody w ogóle nie są zdefiniowane, a tylko zadeklarowane. Takie metody powinny być oczywiście wirtualne — w dziedziczących klasach muszą być dostarczone konkretne implementacje tych metod, aby można było tworzyć ich obiekty. Obiektów samej klasy abstrakcyjnej tworzyć nie można, bo nie jest to klasa do końca zdefiniowana. Zazwyczaj służy tylko jako definicja interfejsu, czyli zbioru metod jakie chcemy implementować na różne sposoby w klasach dziedziczących.

Można natomiast tworzyć obiekty klas pochodnych (nieabstrakcyjnych), w których metody wirtualne zadeklarowane ale nie zdefiniowane w klasie bazowej zostały przesłonięte konkretnymi implementacjami (klasy takie stają się wtedy konkretne). Co bardzo ważne, do takich obiektów można się odnosić poprzez wskaźniki i referencje o typie statycznym abstrakcyjnej klasy bazowej.

Metodę wirtualną można zadeklarować jako czysto wirtualną pisząc po nawiasie kończącym listę argumentów ' =0', na przykład:

      virtual void fun(int i) = 0;
W ten sposób informujemy kompilator, że definicji tak zadeklarowanej metody może w ogóle nie być, a zatem cała klasa, w której tę metodę zadeklarowano, będzie abstrakcyjna.

W zasadzie, choć rzadko się to robi, metodę czysto wirtualną (albo zerową) można w klasie abstrakcyjnej zdefiniować, ale klasa pozostaje przy tym abstrakcyjna i nie można tworzyć jej obiektów. Tak czy owak, w klasach dziedziczących trzeba tę metodę przedefiniować, aby uczynić te klasy klasami konkretnymi, których obiekty będzie można tworzyć. Do wersji tej metody zdefiniowanej w abstrakcyjnej klasie bazowej odwołać się wtedy można poprzez jawną specyfikację zakresu (' Klasa::fun()').

Rozpatrzmy przykład:


P165: virtu.cpp     Funkcje czysto wirtualne

      1.  #include <iostream>
      2.  #include <cmath>     // atan
      3.  using namespace std;
      4.  
      5.  class Figura {
      6.  protected:
      7.      static const double PI;
      8.  public:
      9.      virtual double getPole()      = 0;
     10.      virtual double getObwod()     = 0;
     11.      virtual void   info(ostream&) = 0;
     12.      static  double totalPole(Figura* tab[], int size) {
     13.          double suma = 0;
     14.          for (int i = 0; i < size; ++i)
     15.              suma += tab[i]->getPole();
     16.          return suma;
     17.      }
     18.      static  Figura* maxObwod(Figura* tab[], int size) {
     19.          int ind = 0;
     20.          for (int i = 0; i < size; ++i)
     21.              if (tab[i]->getObwod() >
     22.                      tab[ind]->getObwod())
     23.                  ind = i;
     24.          return tab[ind];
     25.      }
     26.  };
     27.  const double Figura::PI = 4*atan(1.);
     28.  void Figura::info(ostream& str) {
     29.      str << "Figura: ";
     30.  }
     31.  
     32.  class Kolo : public Figura {
     33.      double promien;
     34.  public:
     35.      Kolo(double r) : promien(r){ }
     36.      double getPole()           { return PI*promien*promien; }
     37.      double getObwod()          { return 2*PI*promien; }
     38.      void   info(ostream& str)  {
     39.          Figura::info(str);
     40.          str << "kolo o promieniu  " << promien;
     41.      }
     42.  };
     43.  
     44.  class Kwadrat : public Figura {
     45.      double bok;
     46.  public:
     47.      Kwadrat(double s) : bok(s) { }
     48.      double getPole()           { return bok*bok; }
     49.      double getObwod()          { return 4*bok; }
     50.      void   info(ostream& str)  {
     51.          Figura::info(str);
     52.          str << "kwadrat o boku    " << bok;
     53.      }
     54.  };
     55.  
     56.  int main() {
     57.      Figura* tab[] = { new Kolo(1.), new Kwadrat(1.),
     58.                        new Kolo(2.), new Kwadrat(3.)
     59.                      };
     60.      int size = sizeof(tab)/sizeof(tab[0]);
     61.      for (int i = 0; i < size; ++i) {
     62.          tab[i]->info(cout);
     63.          cout << endl;
     64.      }
     65.      Figura* maxobw = Figura::maxObwod(tab,size);
     66.      cout << "Suma pol: " << Figura::totalPole(tab,size)
     67.           << "\nFigura o najwiekszym obwodzie: ";
     68.      maxobw->info(cout);
     69.      cout <<  "\n ma obwod "
     70.           << maxobw->getObwod() << endl;
     71.  }

Klasa Figura jest tu abstrakcyjna, bo zawiera metody czysto wirtualne; trudno byłoby rozsądnie zaimplementować metody takie jak getPole czy getObwod dla figur geometrycznych „w ogóle”. Dopiero dla konkretnych figur, jak kwadrat czy koło, można takie wielkości obliczać. Dlatego dopiero w klasach dziedziczących z klasy Figura, a opisujących konkretne figury, definiujemy implementacje metod wirtualnych. Taka hierarchia klas ma wiele zalet. Spójrzmy na funkcje statyczne z klasy Figura. Ich parametrem jest tablica wskaźników do figur. Jakich figur? Wskazywane obiekty nie będą obiektami typu Figura — takich się nie da utworzyć, bo klasa ta jest abstrakcyjna. Ale będą na pewno obiektami klas dziedziczących z  Figura, w których na pewno będą zaimplementowane wszystkie metody. Tak więc wywoływanie tych metod poprzez wskaźniki (lub referencje) na rzecz obiektów wskazywanych powiedzie się, niezależnie od ich konkretnego typu. Zauważmy, że nie muszą to być obiekty tego samego konkretnego typu — w naszym przykładzie w tablicy są wskaźniki zarówno do kół jak i kwadratów. Co więcej, możemy dodać inne klasy dziedziczące z  Figura, na przykład opisujące trójkąty, a sama klasa bazowa, w szczególności funkcje statyczne totalPolemaxObwod, nie ulegną żadnej zmianie! Tak więc abstrakcyjna klasa bazowa definiuje interfejs (czyli „sposób użycia”) dla całej rodziny klas, nawet takich, które dopiero będą napisane.

Zauważmy też, że metoda info jest zadeklarowana jako czysto wirtualna, chociaż jest zaimplementowana (linie 28-30). Pozostaje jednak czysto wirtualna — wszystkie konkretne klasy dziedziczące muszą ją zaimplemetować. Do implementacji z abstrakcyjnej klasy bazowej mogą się jednak odwołać poprzez jej nazwę kwalifikowaną (linie 39 i 51).


Rozpatrzmy jeszcze jeden przykład klasy czysto abstrakcyjnej: klasa ta definiuje interfejs do tworzenia stosów i operowaniu na nich.


P166: stack.cpp     Interfejs stosu z metodą fabrykującą

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  class STACK
      5.  {
      6.  public:
      7.      virtual void push(int) = 0;
      8.      virtual int pop()      = 0;
      9.      virtual bool empty()   = 0;
     10.      static STACK* getInstance(int);
     11.      virtual ~STACK() { }
     12.  };
     13.  
     14.  class ListStack: public STACK {
     15.  
     16.      struct Node {
     17.          int   data;
     18.          Node* next;
     19.          Node(int data, Node* next)
     20.              : data(data), next(next)
     21.          { }
     22.      };
     23.  
     24.      Node* head;
     25.  
     26.      ListStack() {
     27.          head = NULL;
     28.          cerr << "Tworzenie ListStack" << endl;
     29.      }
     30.  
     31.      ListStack(const ListStack&) { }
     32.      void operator=(ListStack&) { }
     33.  
     34.  public:
     35.      friend STACK* STACK::getInstance(int);
     36.  
     37.      int pop() {
     38.          int   data = head->data;
     39.          Node* temp = head->next;
     40.          delete head;
     41.          head = temp;
     42.          return data;
     43.      }
     44.  
     45.      void push(int data) {
     46.          head = new Node(data, head);
     47.      }
     48.  
     49.      bool empty() {
     50.          return head == NULL;
     51.      }
     52.  
     53.      ~ListStack() {
     54.          cerr << "Usuwanie ListStack" << endl;
     55.          while (head) {
     56.              Node* node = head;
     57.              head = head->next;
     58.              cerr << " usuwanie wezla" << node->data <<endl;
     59.              delete node;
     60.          }
     61.      }
     62.  };
     63.  
     64.  class ArrayStack : public STACK {
     65.  
     66.      int  top;
     67.      int* arr;
     68.      enum {MAX_SIZE = 100};
     69.  
     70.      ArrayStack() {
     71.          top = 0;
     72.          arr = new int[MAX_SIZE];
     73.          cerr << "Tworzenie ArrayStack" << endl;
     74.      }
     75.  
     76.      ArrayStack(const ArrayStack&) { }
     77.      void operator=(ArrayStack&) { }
     78.  
     79.  public:
     80.      friend STACK* STACK::getInstance(int);
     81.  
     82.      void push(int data) {
     83.          arr[top++] = data;
     84.      }
     85.  
     86.      int pop() {
     87.          return arr[--top];
     88.      }
     89.  
     90.      bool empty() {
     91.          return top == 0;
     92.      }
     93.  
     94.      ~ArrayStack() {
     95.          cerr << "Usuwanie ArrayStack z " << top
     96.               << " elementami wciaz na stosie" << endl;
     97.          delete [] arr;
     98.      }
     99.  };
    100.  
    101.  STACK* STACK::getInstance(int size) {
    102.      if (size > 100)
    103.          return new ListStack();
    104.      else
    105.          return new ArrayStack();
    106.  }
    107.  
    108.  int main() {
    109.  
    110.      STACK* stack;
    111.  
    112.      stack = STACK::getInstance(120);
    113.      stack->push(1);
    114.      stack->push(2);
    115.      stack->push(3);
    116.      stack->push(4);
    117.      cerr << "pop " << stack->pop() << endl;
    118.      cerr << "pop " << stack->pop() << endl;
    119.      delete stack;
    120.  
    121.      stack = STACK::getInstance(50);
    122.      stack->push(1);
    123.      stack->push(2);
    124.      stack->push(3);
    125.      stack->push(4);
    126.      cerr << "pop " << stack->pop() << endl;
    127.      cerr << "pop " << stack->pop() << endl;
    128.      delete stack;
    129.  }

Klasa STACK zawiera deklaracje typowych metod do operowania na stosach (w tym przypadku stosach liczb całkowitych), oraz definicję funkcji statycznej getInstance, która zwraca wskaźnik do stosu. Żadnej implementacji metod niestatycznych nie ma; są one implementowane w dwóch klasach dziedziczących: ListStackArrayStack. W pierwszej z nich stos implementowany jest za pomocą listy jednokierunkowej, a w drugiej przez tablicę. W obu tych klasach konstruktor jest prywatny. Jedynym sposobem na utworzenie obiektów tych klas jest użycie statycznej funkcji „fabrykującej" getInstance z klasy bazowej —  może ona to zrobić, gdyż została zaprzyjaźniona z obiema klasami konkretnymi (linie 35 i 80). Funkcja ta tworzy obiekt klasy ListStack albo ArrayStack, w zależności od rądanego rozmiaru stosu: dla małych stosów wybiera implementację za pomocą tablicy, a dla dużych za pomocą listy (linie 101-106). Zauważmy, że ponieważ w funkcji tej tworzone są obiekty klas ListStackArrayStack, jej definicja musiała zostać umieszczona po definicji klas dziedziczących, gdy kompilator już „wie”, że żadna z nich nie pozostała klasą abstrakcyjną (i zna rozmiar tworzonych obiektów).

Funkcja main jest klientem klasy STACK. Tworzone są tu dwa obiekty o typie statycznym STACK; typ dynamiczny jest w każdym przypadku inny, jak widzimy z wydruku

    Tworzenie ListStack
    pop 4
    pop 3
    Usuwanie ListStack
     usuwanie wezla2
     usuwanie wezla1
    Tworzenie ArrayStack
    pop 4
    pop 3
    Usuwanie ArrayStack z 2 elementami wciaz na stosie
Zauważmy, że oba stosy używane są w analogiczny sposób; w zasadzie klient nie wie, jakiej konkretnej klasy są zwrócone obiekty. Co więcej, nie musi nawet wiedzieć, że są tak naprawdę różnych klas.

Konstruktor kopiujący i operator przypisania zostały w obu klasach zdefiniowane jako prywatne (linie 31-32 i 76-77). Zapobiega to kopiowaniu i przypisywaniu obiektów, co nie miałoby sensu, bo obiekty klas ListStackArrayStack są zupełnie różne (mają inne pola, a nawet rozmiary).

T.R. Werner, 25 lutego 2017; 22:31