Глава №12. Итоговый тест Полиморфизм Теория
Язык C++ позволяет создавать указатели/ссылки родительского класса на объекты дочерних классов. Это полезно при использовании функций или массивов, которые должны работать с объектами дочерних классов.
Без виртуальных функций указатели/ссылки родительского класса на объект дочернего класса будут иметь доступ только к членам родительского класса.
Виртуальная функция — это особый тип функции, которая, при обращении к ней, вызывает наиболее дочерний метод (переопределение), существующий между родительским и дочерними классами. Чтобы считаться переопределением, метод дочернего класса должен иметь ту же сигнатуру и тип возврата, что и виртуальная функция родительского класса. Единственное исключение — ковариантный тип возврата, который позволяет переопределению возвращать указатель или ссылку на дочерний класс, если метод родительского класса возвращает указатель или ссылку на себя.
Модификатор override используется для обозначения метода переопределением.
Модификатор final запрещает переопределять виртуальную функцию или наследовать определенный класс.
Используя виртуальные функции, не забывайте добавлять в родительский класс виртуальный деструктор, чтобы, в случае удаления указателя на родительский класс, вызывался соответствующий деструктор.
Вы можете игнорировать вызов переопределений виртуальной функции, используя оператор разрешения области видимости, чтобы напрямую указать, какую функцию вы хотите вызвать. Например, parent.Parent::GetName(). Раннее связывание происходит, когда компилятор встречает прямой вызов функции. Компилятор или линкер могут напрямую обрабатывать прямые вызовы функций. Позднее связывание происходит при вызове указателя на функцию. В таких случаях невозможно знать наперёд, какая функция будет вызываться первой. Виртуальные функции используют позднее связывание и виртуальные таблицы для определения того, какую версию функции следует вызывать.
Относительные недостатки виртуальных функций:
Вызов виртуальных функций занимает больше времени.
Необходимость наличия виртуальной таблицы увеличивает размер каждого объекта класса, содержащего виртуальную функцию, на размер одного указателя.
Виртуальную функцию можно сделать чистой виртуальной/абстрактной функцией, добавив = 0 в конец её прототипа.
Класс, содержащий чистую виртуальную функцию, называется абстрактным классом.
Объекты абстрактного класса не могут быть созданы. Класс, который наследует чистые виртуальные функции, должен предоставить свои переопределения этих функций, или он также будет считаться абстрактным. Чистые виртуальные функции могут иметь тело (определение, записанное отдельно), но они по-прежнему считаются абстрактными функциями.
Интерфейс (или «интерфейсный класс») — это класс без переменных-членов, все методы которого являются чистыми виртуальными функциями. Имена интерфейсов часто начинаются с I.
Виртуальный базовый класс — это родительский класс, объект которого является общим для использования всеми дочерними классами.
При присваивании объекта дочернего класса объекту родительского класса, в объект родительского класса копируется лишь родительская часть копируемого объекта, дочерняя часть копируемого объекта обрезается. Этот процесс называется обрезкой объектов.
Динамическое приведение используется для конвертации указателя родительского класса в указатель дочернего класса. Это называется понижающим приведением типа. Если конвертация прошла неудачно, то возвращается нулевой указатель.
Самый простой способ перегрузить оператор вывода << для классов с наследованием — записать перегрузку оператора << в родительском классе, а выполнение операции вывода делегировать виртуальному методу.
Задание №1
Каждая из следующих программ имеет какую-то ошибку. Ваша задача состоит в том, чтобы найти эту ошибку визуально (не запуская код).
Предполагаемый вывод каждой программы:
Child
a)
#include <iostream>
class Parent {
protected:
int m_value;
public:
Parent (int value): m_value(value) {}
const char* getName () const {return "Parent";}
};
class Child: public Parent {
public:
Child (int value): Parent(value) {}
const char* getName () const {return "Child";}
};
int main () {
Child ch (7);
Parent &p = ch;
std::cout << p.getName();
return 0;
b)
#include <iostream>
class Parent {
protected:
int m_value;
public:
Parent (int value): m_value(value) {}
virtual const char* getName () {return "Parent";}
};
class Child: public Parent {
public:
Child (int value): Parent(value) { }
virtual const char* getName () const {return "Child";}
};
int main () {
Child ch (7);
Parent &p = ch;
std::cout << p.getName ();
return 0;
}
c)
#include <iostream>
class Parent {
protected:
int m_value;
public:
Parent (int value): m_value(value) {}
virtual const char* getName() { return "Parent"; }
};
class Child: public Parent
{
public:
Child (int value): Parent(value) {}
virtual const char* getName () override {return "Child";}
};
int main () {
Child ch (7);
Parent p = ch;
std::cout << p.getName ();
return 0;
}
d)
#include <iostream>
class Parent final {
protected:
int m_value;
public:
Parent (int value): m_value(value) {}
virtual const char* getName () {return "Parent";}
};
class Child: public Parent {
public:
Child (int value): Parent(value) {}
virtual const char* getName () override {return "Child";}
};
int main () {
Child ch (7);
Parent &p = ch;
std::cout << p.getName();
return 0; }
e)
#include <iostream>
class Parent {
protected:
int m_value;
public:
Parent (int value): m_value(value) { }
virtual const char* getName () {return "Parent";}
};
class Child: public Parent {
public:
Child (int value): Parent(value){}
virtual const char* getName () = 0;
};
const char* Child::getName() {
return "Child";
}
int main () {
Child ch (7);
Parent &p = ch;
std::cout << p.getName ();
return 0;
}
f)
#include <iostream>
class Parent {
protected:
int m_value;
public:
Parent (int value): m_value(value) {}
virtual const char* getName () {return "Parent";}
};
class Child: public Parent {
public:
Child (int value): Parent(value) {}
virtual const char* getName () {return "Child"; }
};
int main () {
Child *ch = new Child (7);
Parent *p = ch;
std::cout << p->getName();
delete p;
return 0;
}
Задание №2
a)
Создайте абстрактный класс Shape. Этот класс должен иметь три метода:
- чистую виртуальную функцию print() с параметром типа std::ostream;
- перегрузку operator<<;
- пустой виртуальный деструктор.
b)
Создайте два класса: Triangle и Circle, которые наследуют класс Shape.
Triangle должен иметь 3 точки в качестве переменных-членов.
Circle должен иметь одну центральную точку и целочисленный радиус в качестве переменных-членов.
Перегрузите функцию print (), чтобы следующий код:
int main () {
Circle c (Point (1, 2, 3), 7);
std::cout << c << '\n';
Triangle t (Point (1, 2, 3), Point (4, 5, 6), Point (7, 8, 9));
std::cout << t << '\n';
return 0;
}
Выдавал следующий результат:
Circle (Point (1, 2, 3), radius 7)
Triangle (Point (1, 2, 3), Point (4, 5, 6), Point (7, 8, 9))
Вот класс Point, который вы можете использовать:
class Point {
private:
int m_x = 0;
int m_y = 0;
int m_z = 0;
public:
Point (int x, int y, int z): m_x(x), m_y(y), m_z(z) {}
friend std::ostream& operator<<(std::ostream &out, const Point &p) {
out << "Point(" << p.m_x << ", " << p.m_y << ", " << p.m_z << ")";
return out;
}
c)
Используя код из предыдущих заданий (классы Point, Shape, Circle и Triangle) завершите следующую программу:
#include <iostream>
#include <vector>
int main () {
std::vector<Shape*> v;
v.push_back(new Circle(Point(1, 2, 3), 7));
v.push_back(new Triangle(Point(1, 2, 3), Point(4, 5, 6), Point(7, 8, 9)));
v.push_back(new Circle(Point(4, 5, 6), 3));
// Вывод элементов вектора v здесь
std::cout << "The largest radius is: " << getLargestRadius(v) << '\n'; // реализуйте эту функцию
// Удаление элементов вектора v здесь
}
Подсказка: Вам нужно добавить метод getRadius() в Circle и выполнить понижающее приведение Shape* в Circle*, чтобы получить доступ к этому методу.
