3. Dziedziczenie i polimorfizm

3.3. Abstrakcje / interfejsy

Metoda zadeklarowana w klasie jako wirtualna, która nie ma definicji – jest metodą abstrakcyjną. W C++ oznaczamy ten fakt pisząc =0 po deklaracji metody. Klasa która ma choć jedną metodę abstrakcyjną – jest klasą abstrakcyjną. Nie można tworzyć obiektów typu klas abstrakcyjnych. Klasa która ma wszystkie metody abstrakcyjne i nie posiada pól – jest interfejsem.

W praktyce stosuje się często jako interfejsy klasy, które zawierają kilka metod z implementacjami. Jednakże, te metody, które mają być częścią interfejsu, powinny być oznaczone jako czysto wirtualne. Dodatkowo - by być zgodnym z semantycznym znaczeniem interfejsu - klasa taka nie powinna mieć pól. 


class CFiguraBaza {
public:
  virtual void rysuj() = 0;
};

class CFigura : public CFiguraBaza {
public:
  void przesun(int _x, int _y) {
    m_x = _x;
    m_y = _y;
    rysuj();
  };
private:
  int m_x;
  int m_y;
};

class CKolo :public CFigura {
public:
  virtual void rysuj() {
    // kod rysowania
  }
};

Zmodyfikowany przykład dziedziczenia figur, z uwzględnioną koncepcją interfejsu możecie zobaczyć poniżej:

Wprowadzenie tutaj koncepcji interfejsów ściśle wiąże się z pojęciem typów abstrakcyjnych. Typy abstrakcyjne w założeniu mają izolować użytkownika od ich implementacji. Z typów abstrakcyjnych korzystamy tylko przez referencje / wskaźniki. Nie można tworzyć bezpośrednio obiektów typów abstrakcyjnych - próba utworzenia instancji ICFigura albo CFiguraBaza zakończy się błędem kompilacji.  Typy takie możemy tworzyć jedynie jako typy konkretne - i potem przekazywać interfejsy do nich.

Typowa klasa abstrakcyjna zazwyczaj nie ma konstruktora - bo i tak nie ma pól do inicjowania. Natomiast prawie zawsze powinna mieć wirtualny destruktor - dzięki czemu kasowanie obiektu przy wykorzystaniu wskaźnika na klasę bazową i tak spowoduje wywołanie odpowiedniej postaci destruktora.

Nie da się także ich prosto kopiować ani klonować.

Klonowanie obiektów

W przypadku obiektów będących w hierarchii dziedziczenia, kopiowanie ich nie da się prosto wykonać korzystając jedynie z mechanizmów polimorfizmu – przecież konstruktor nie jest wirtualny i nie jest dziedziczony. Dlatego też rozwiązanie poniżej raczej nie zadziała:


class A {
public:
  A() {};
  A(const A& _s) { };
  virtual void f() {
    cout<<"A nadaje\n";
  }
};

class B : public A {
public:
  B() {};
  B(const B& _s) : A(_s) { };
  virtual void f() {
    cout<<"B nadaje\n";
  }
};

int main(int argc, char *argv[])
{
  B *b = new B;
  A *a1 = b;
  A *a2 = new A(*a1);
  //A *a3 = new B(*a1);

  a1->f();
  a2->f();
}

Możecie sami się przekonać, uruchamiając powyższy przykład. Mimo że a2 zostało utworzone jako kopia a1 – które jest typu B, to i tak wywołany został konstruktor A, a nie B. Jeśli znamy typ obiektu pochodnego przed kopiowaniem – to możemy jawnie wywołać konstruktor B. Ale przecież cała zabawa z polimorfizmem polega na tym, by móc korzystać z obiektu jedynie w oparciu o jego interfejs – a więc nie znając docelowego typu obiektu. Czy da się kopiować elementy w taki sposób? Da ... wystarczy odpowiednio wykorzystać mechanizm funkcji wirtualnych.


class A {
public:
  A() { };
  A(const A& _s) {};
  virtual void f() { cout<<"A " << FMyNum << " nadaje\n"; }
  virtual A* clone() { return new A(*this); };
protected:
  static int m_cnt;
  int m_myNum;
};
int A::m_cnt = 0;

class B : public A {
public:
  B() {};
  B(const B& _s) : A(_s) { };
  virtual void f() { cout<<"B " << FMyNum << " nadaje\n"; }
  virtual A* clone() { return new B(*this); };
};

int main(int argc, char *argv[])
{
  B *b = new B;
  A *a1 = b;
  A *a2 = new A(*a1);
  A *a4 = a1->clone();

  a1->f();
  a2->f();
  a4->f();
}

Kod powyżej jest jeszcze uzupełniony o dodatkową informację – wykorzystując pole statyczne, zliczamy wszystkie egzemplarze danej klasy.

Ogólnie - z pojęciem tworzenia instancji typów abstrakcyjnych jest związane kilka wzorców programistycznych, z fabryką abstrakcyjną na czele.