Лекция 16

Downcast

Пусть в классе 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.

Виртуальные методы в C++

Если же нам необходимо вызвать 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(), то получим ошибку. Такие методы называются чисто виртуальными.

Система RTTI (Runtime Type Identification)

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

dynamic_cast

Как это было в 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() для полиморфных типов работает полиморфным образом - возвращает динамический тип объекта.

  1. На главную