PVOID - Полиморфизм

Полиморфизм

Этот раздел является переводом туториала C++ Language


 Прежде чем углубиться в эту главу, вы должны правильно понимать указатели и наследование классов. Если вы не уверены в значении любого из следующих выражений, вам следует просмотреть указанные разделы:

 Выражение   Объясняется в: 
 int A::b(int c) { }  Классы
 a->b Структуры данных
 class A: public B {};   Дружественность и наследование 

 

Указатели на базовый класс


Одна из ключевых особенностей наследования классов заключается в том, что указатель на производный класс - это тип, совместимый с указателем базового класса. Полиморфизм - это искусство владения этой простой, но мощной и универсальной особенностью.

Пример о классах прямоугольника и треугольника можно переписать с использованием указателей, принимая эту особенность во внимание:

// указатели на базовый класс
#include <iostream>
using namespace std;

class Polygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
};

class Rectangle: public Polygon {
  public:
    int area()
      { return width*height; }
};

class Triangle: public Polygon {
  public:
    int area()
      { return width*height/2; }
};

int main () {
  Rectangle rect;
  Triangle trgl;
  Polygon * ppoly1 = &rect;
  Polygon * ppoly2 = &trgl;
  ppoly1->set_values (4,5);
  ppoly2->set_values (4,5);
  cout << rect.area() << '\n';
  cout << trgl.area() << '\n';
  return 0;
}
20
10

 В функции main объявляются два указателя на Polygon (с именами ppoly1 и ppoly2). Им присваиваются адреса rect и trgl, соответственно, являющимися объектами типа Rectangle и Triangle. Такое присваивание является корректным, так как классы Rectangle и Triangle являются производными от Polygon.

Разыменовывание ppoly1 и ppoly2 (через *ppoly1 и *ppoly2) является допустимым и также позволяет получить доступ к членам объектов, на которые они указывают. Например, следующие два выражения будут эквивалентны для предыдущего примера:

ppoly1->set_values (4,5);
rect.set_values (4,5);

Однако, по той причине, что типом ppoly1 и ppoly1 является указатель на Polygon (а не указатель на Rectangle или указатель на Triangle), доступны только члены, унаследованные то Polygon, но не члены производных классов Rectangle и Triangle. По этой причине в программе вызов метода area обоих объектов происходит непосредственно через объекты, а не через указатели. Указатели на базовый класс не имеют доступа к методу area.

Метод area может быть доступен через указатель на Polygon, если area будет членом Polygon, а не членом производных классов, но проблема заключается в том, что Rectangle и Triangle описывают разные версии метода area, поэтому не существует единой общей версии, которая может быть реализована в базовом классе.

 

Виртуальные члены


Виртуальный член-это функция-член, которая может быть переопределена в производном классе, сохраняя при этом свои свойства при вызове через указатели. Для того, чтобы сделать функцию виртуальной, необходимо предварить ее объявление ключевым словом virtual :

#include <iostream>
using namespace std;

class Polygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
    virtual int area ()
      { return 0; }
};

class Rectangle: public Polygon {
  public:
    int area ()
      { return width * height; }
};

class Triangle: public Polygon {
  public:
    int area ()
      { return (width * height / 2); }
};

int main () {
  Rectangle rect;
  Triangle trgl;
  Polygon poly;
  Polygon * ppoly1 = &rect;
  Polygon * ppoly2 = &trgl;
  Polygon * ppoly3 = &poly;
  ppoly1->set_values (4,5);
  ppoly2->set_values (4,5);
  ppoly3->set_values (4,5);
  cout << ppoly1->area() << '\n';
  cout << ppoly2->area() << '\n';
  cout << ppoly3->area() << '\n';
  return 0;
}
20
10
0

 В этом примере все три класса (Polygon, Rectangle и Triangle) имеют одни и те же члены: width, height и методы set_values и area.

 Метод area объявлен как виртуальный в базовом классе, потому что он позже переопределяется в каждом из производных классов. Невиртуальные члены также могут быть переопределены в производных классах, но невиртуальные члены производного класса недоступны по ссылке на базовый класс: т.е. если убрать virtual из объявления area в этом примере, все три вызова area вернут ноль, потому что будет вызвана версия area базового класса.

Следовательно, ключевое слово virtual позволяет члену производного класса с тем же именем, что и в базовом классе, быть вызванным надлежащим образом через указатель, а точнее, когда тип указателя это указатель на базовый класс, который указывает на объект производного класса, как в этом примере.

Класс, объявляющий или наследующий виртуальную функцию, называется полиморфным классом.

Обратите внимание, что несмотря на виртуальность одного из своих членов, Polygon является обычным классом, объект которого был создан (poly), со своим собственным определением члена area, который всегда возвращает 0.

 

Абстрактные базовые классы


 Абстрактные базовые классы - это нечто похожее на класс Polygon в предыдущем примере. Это классы, которые могут быть использованы только как базовые классы, и поэтому позволяют иметь виртуальные методы без определения (известные как чисто виртуальные функции). Для объявления чисто виртуальной функции необходимо в качестве определения использовать =0 (знак равенства и ноль):

Абстрактный базовый класс Polygon может выглядеть так:

// абстрактный класс Polygon
class Polygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
    virtual int area () =0;
};

 Обратите внимание, что area не имеет реализации; она заменена на =0, что делает ее чисто виртуальной функцией. Классы, содержащие хотя бы одну чисто виртуальную функцию, известны как абстрактные базовые классы.

Абстрактные базовые классы не могут быть использованы для создания объектов. Следовательно, эта версия абстрактного базового класса Polygon не может быть использована для объявления объектов:

Polygon mypolygon;   // не работает, если Polygon является абстрактным классом

 Но абстрактный базовый класс не является абсолютно бесполезным. Его можно использовать для создания указателей на него и использовать все его полиморфные возможности. Например, следующие объявления указателей будут корректны:

Polygon * ppoly1;
Polygon * ppoly2;

 И может быть разыменован при указании на объекты производных (не абстрактных) классов. Вот полный пример:

// абстрактный базовый класс
#include <iostream>
using namespace std;

class Polygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
    virtual int area (void) =0;
};

class Rectangle: public Polygon {
  public:
    int area (void)
      { return (width * height); }
};

class Triangle: public Polygon {
  public:
    int area (void)
      { return (width * height / 2); }
};

int main () {
  Rectangle rect;
  Triangle trgl;
  Polygon * ppoly1 = &rect;
  Polygon * ppoly2 = &trgl;
  ppoly1->set_values (4,5);
  ppoly2->set_values (4,5);
  cout << ppoly1->area() << '\n';
  cout << ppoly2->area() << '\n';
  return 0;
}
20
10

 

 В этом примере объекты разных, но связанных типов, используют единственный тип указателя (Polygon*), и каждый раз вызывается соответствующий метод, потому что они являются виртуальными. Это может быть действительно полезно в некоторых обстоятельствах. Например, есть возможность для члена абстрактного базового класса Polygon использовать специальный указатель this для доступа к необходимому виртуальному члену, даже при том, что сам класс Polygon не имеет описания этой функции:

// чисто виртуальные члены могут быть вызваны
// из абстрактного базового класса
#include <iostream>
using namespace std;

class Polygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
    virtual int area() =0;
    void printarea()
      { cout << this->area() << '\n'; }
};

class Rectangle: public Polygon {
  public:
    int area (void)
      { return (width * height); }
};

class Triangle: public Polygon {
  public:
    int area (void)
      { return (width * height / 2); }
};

int main () {
  Rectangle rect;
  Triangle trgl;
  Polygon * ppoly1 = &rect;
  Polygon * ppoly2 = &trgl;
  ppoly1->set_values (4,5);
  ppoly2->set_values (4,5);
  ppoly1->printarea();
  ppoly2->printarea();
  return 0;
}
20
10

  Виртуальные члены и абстрактные классы предоставляют полиморфные характеристики C++, наиболее полезные для объектно-ориентированных проектов. Конечно, приведенные примеры очень просты, но эти возможности могут быть применены к массивам объектов или к динамически создаваемым объектам.

Это простой пример, который сочетает некоторые из возможностей, изложенных в предыдущих главах, таких как динамическая память, инициализация в конструкторе и полиморфизм:

// динамическое выделение памяти и полиморфизм
#include <iostream>
using namespace std;

class Polygon {
  protected:
    int width, height;
  public:
    Polygon (int a, int b) : width(a), height(b) {}
    virtual int area (void) =0;
    void printarea()
      { cout << this->area() << '\n'; }
};

class Rectangle: public Polygon {
  public:
    Rectangle(int a,int b) : Polygon(a,b) {}
    int area()
      { return width*height; }
};

class Triangle: public Polygon {
  public:
    Triangle(int a,int b) : Polygon(a,b) {}
    int area()
      { return width*height/2; }
};

int main () {
  Polygon * ppoly1 = new Rectangle (4,5);
  Polygon * ppoly2 = new Triangle (4,5);
  ppoly1->printarea();
  ppoly2->printarea();
  delete ppoly1;
  delete ppoly2;
  return 0;
}
20
10

 

 Обратите внимание, что указатели ppoly:

Polygon * ppoly1 = new Rectangle (4,5);
Polygon * ppoly2 = new Triangle (4,5);

 имеют тип "указатель на полигон", но создаваемые объекты объявляются непосредственно с тпом производного класса (Rectangle и Triangle).