Пусть в классе Student
есть функция get_group()
, которая возвращает номер группы студента. Напомним, что её нет в Person
.
/* student.h */
class Student : public Person
{
int group;
public:
int get_group() const
{
return group;
}
};
Теперь рассмотрим следующую ситуацию:
Person *p = new Student('Иванов', 20, 2, 9);
p -> get_group();
При вызове p -> get_group();
получим ошибку компиляции, так как в Person
не определенна функция get_group()
.
Нужно привести p
к типу Student
.
// Старый стиль:
((Student*)pp) -> get_group;
// Совеременный стиль:
static_cast<Student*>(pp) -> get_group();
Другая ситуация:
Person & rp = *new Student("Петров", 20, 2, 9);
static_cast<Student &>(rp).get_group();
delete &rp;
Downcast в C++ работает только через указатель или ссылку на объект базового класса.
Student s("Иванов", 20, 2, 9);
Person * pp = &s;
pp -> print();
По умолчанию в C++ работает раннее связывание имени метода с конкретным типом.
Раннее связывание происходит на этапе компиляции. Таким образом в приведенном выше примере будет вызвана функция print()
как функция-член класса Person
.
Если же нам необходимо вызвать print()
как функцию-член класса Student
, мы должны реализовать позднее связывание, которое выполняется на этапе выполнения программы. Для реализации позднего связывания в C++ используются виртуальные методы.
Рассмотрим пример виртуальных функций:
class Person
{
…
public:
virtual void print()
{ … }
};
class Student: public Person
{
…
public:
void print()
{
…
Person::print();
…
}
};
Здесь, в классе Student
, функция print()
так же будет виртуальной - ключевое слово virtual можно не писать т.к у класса-
ка данная функция уже определена как виртуальная.
Теперь, когда реализовано позднее связывание, при выполнении строки pp -> print();
из предыдущего примера, print()
будет вызвана как функция-член класса Student
.
Предостережение: При выполнении следующего кода:
p = s;
p.print();
будет выполнено раннее связывание, несмотря на то, что функция print()
виртуальная.
Вывод:
Полиморфизм C++ работает только через указатели и ссылки на объекты базового класса.
Полиморфизм в C++ реализуется с помощью таблиц виртуальных функций - Virtual Methods Table(VMT).
В каждом объекте появляется дополнительный указатель vptr на таблицу виртуальных методов. Для каждого класса создается таблица виртуальных, которая содержит адреса всех виртуальных методов этого класса и всех его предков.
Если в классе и его предках нет виртуальных методов, то в его объекте поле vptr отсутствует(реализуется принцип: не платим за то, что не используем).
Замечание:
В C++ нет общего главного класса-предка, как например, Object в Java/NET?. Это делается в целях повышения эффективности.
Как работает new: сначала выделяется память, потом вызывается конструктор.
Как работает деструктор: сначала вызывается деструктор, потом возвращается память.
Рассмотрим следующий код:
Person * pp = new Student("Иванов", 20 2, 9);
pp -> Print();
delete pp;
В данном случае, при выполнении delete pp;
будет вызван деструктор ~Person()
, что плохо.
Деструкторы в C++ тоже могут быть виртуальными, однако по умолчанию они таковыми не являются.
Сделаем деструктор для класса Person
виртуальным:
class Person
{
…
virtual void Print() { ... }
virtual ~Person() { ... }
}
Теперь, при освобождении памяти на которую ссылается pp
, вызовется деструктор ~Student()
.
Если для класса Student
не написать деструктор, то он сгенерируется автоматическим и будем виртуальным.
Правило:
Если в классе есть хотя бы одна виртуальная функция, тогда обязательно делаем его деструктор виртуальным.
Полиморфным называется контейнер, состоящий из полиморфных объектов. То есть объектов, в составе которых есть виртуальные методы, переопределяемые в потомках.
Представим себе некий векторный графический редактор. Все фигуры будут наследоваться от класса Shape
, с виртуальным методом draw()
— отображением фигуры на экран.
class Shape
{
virtual void draw() {}
virtual ~Shape() {}
};
Заведем полиморфный контейнер, хранящий все эти разновидности фигур. Полиморфный контейнер — контейнер указателей или ссылок на объекты базового класса. (Поэтому пишем Shape*
).
vector<Shape*> v;
v.push_back(new Circle(20, 30, 5));
v.push_back(new Rectangle(10, 10, 20, 20));
Отрисовываем все объекты:
for (Shape* x : v)
x -> draw();
Удаляем:
for (Shape* x : v)
delete x;
Попытаемся создать новый полиморфный контейнер на основе уже существующего. Следующий код:
vector<Shape*> v1 = v;
не даст желаемого результата так как в v1
будут находиться указатели на те же объекты. Решением данной проблемы было бы создание виртуального конструктора, однако для конструкторов действует следующее правило:
Конструкторы в C++ не могут быть виртуальными.
Поэтому копирование полиморфного контейнера нужно производить следующим образом:
vector<Shape*> v1(v.size());
for (int i = 0; i < v.size(); i++)
v1[i] = v[i] -> clone();
Клонирование полиморфное, так как объект должен клонировать себя, а не объект базового типа. То есть Rectangle
клонирует Rectangle
, Circle
клонирует Circle
и т.д.
Для того, чтобы такое копирование работало объявим в классе Shape
функцию clone()
, а за тем напишем ее реализацию для каждого графического объекта:
class Shape
{
virtual Shape* сlone() {};
};
class Circle : public Shape
{
int x, y, z;
public:
Circle(int xx, int yy, in zz) {}
Shape * сlone()
{
return new Circle(x, y, z);
}
}
Обратим внимание не то, что Shape
нужен только для того, чтобы его наследовать, методы в нем должны быть обязательно переопределены. Чтобы это четко обозначить, пишем:
class Shape {
virtual void draw() = 0;
virtual Shape* сlone() = 0;
}
Теперь если не переопределить функции draw()
или clone()
, то получим ошибку. Такие методы называются чисто виртуальными.
Если в классе имеются виртуальные методы, то на этапе выполнения можно идентифицировать динамический тип переменной. Для этого существует два средства:
Как это было в PascalABC.NET:
var p : Person := new Student("Иванов", 20, 2, 9);
var s := p as Student; // as пытается сделать downcast к Student, иначе nil.
if s <> nil then
s.get_group();
Теперь рассмотрим тот же пример в C++:
Person * p = new Student("Иванов", 20, 2, 9);
Student * s = dynamic_cast<Student*>(p);
if (s != nullptr)
s->get_group();
Если виртуальных методов в классе и его предках нет, то dynamic_cast
будет работать как static_cast
.
Если приведение возможно, тогда dynamic_cast
вернет ссылку на Student
, иначе nullptr
. Так же в случае невозможности приведения будет сгенерировано исключение std::bad_cast
. Для его обработки dynamic_cast
следует поместить в блок try{…} catch(…){…}
:
try{…}
catch(bad_cast &e){…}
catch(my_ex &e){…}
catch(…){…}
В последовательности блоков catch
должны в начале должны обрабатываться более специфичные исключения.
typeid
и структура type_info
В PascalABC.NET: object.GetType()
В С++ тип объекта определяется оператором typeid()
. Данный оператор возвращает структуру type_info
, которая содержит ==
, !=
, name()
.
#include <typeinfo>
typeid(1/2) == typeid(int);
typeid()
для полиморфных типов работает полиморфным образом - возвращает динамический тип объекта.