Углубленное программирование
на языке C++
Алексей Петров
Лекция №2. Специальные вопросы
наследования и полиморфизма.
RTTI
• Раннее и позднее связывание.
• Перегрузка и перекрытие членов класса.
• Виртуальные функции.
• Абстрактные классы.
• Множественное и виртуальное наследование.
• Динамическая идентификация типов времени выполнения (RTTI) и
операции приведения типов.
• Производительность и безопасность полиморфизма и средств
поддержки RTTI.
• Постановка задач к практикуму №2.
Объектный или объектно-
ориентированный подход?
Объектно-ориентированное
программирование расширяет
объектный подход и вводит
отношения «тип – подтип»,
прибегая для этого к механизмам
наследования и полиморфизма.
Наследование:
ключевые понятия
Наследование (деривация) содействует повторному использованию
атрибутов и методов класса, а значит, делает процесс разработки ПО
более эффективным.
Возникающие между классами A и B отношения наследования
позволяют говорить, что:
• класс A является базовым (родительским) классом, классом-
предком, надклассом (англ. superclass);
• класс B является производным (дочерним) классом, классом-
потомком, подклассом (англ. subclass).
Отношения наследования связывают классы в иерархию, вид которой
зависит от количества базовых классов у каждого производного:
• при одиночном наследовании иерархия имеет вид дерева;
• при множественном наследовании — вид направленного
ациклического графа (НАГ) произвольного вида.
Наследование «в картинках»
Одиночное наследование (слева), множественное наследование (в
центре), виртуальное множественное наследование (справа)
Account
SavingAccount
Dalet
Aleph
Beth
Aleph
Gimel
Aleph
Dalet
Beth Gimel
Полиморфизм подтипов
В том случае если базовый и производный классы имеют общий
открытый интерфейс, говорят, что производный класс представляет
собой подтип базового типа.
Отношение между типом и подтипом, позволяющее указателю или
ссылке на базовый класс без вмешательства программиста адресовать
объект производного класса, возникает в C++ благодаря поддержке
полиморфизма.
Полиморфизм позволяет предложить такую реализацию ядра
объектно-ориентированного приложения, которая не будет зависеть от
конкретных используемых подтипов.
Раннее и позднее
связывание
В рамках классического объектного подхода, — а равно и процедурного
программирования, — адрес вызываемой функции (метода класса)
определяется на этапе компиляции (сборки). Такой порядок связывания
вызова функции и ее адреса получил название раннего
(статического).
Позднее (динамическое) связывание состоит в нахождении
(разрешении) нужной функции во время исполнения кода. При этом
работа по разрешению типов перекладывается с программиста на
компилятор.
В языке C++ динамическое связывание поддерживается механизмом
виртуальных методов класса, а в компиляторе с языка — таблицей
виртуальных методов (англ. VMT, virtual method table).
Базовые
и производные классы
ОО-проектирование допускает сосуществование классов, часть из
которых может выполнять чисто технические функции, моделировать
абстрактные сущности и отличаться функциональной неполнотой.
Не подлежащий реализации в виде экземпляров (объектов) базовый
класс может оставаться абстрактным. В противовес абстрактным
базовым классам классы, предполагающие создание экземпляров,
именуют конкретными.
(Абстрактные) базовые классы специфицируют открытые
интерфейсы иерархий и содержат общие для всех подтипов
атрибуты и методы (или их прототипы).
Множество подтипов любого базового класса ограничено иерархией
наследования, но потенциально бесконечно (ввиду отсутствия
пределов по расширению иерархии вглубь и вширь).
Определение
наследования (1 / 2)
Определение отношения наследования имеет вид (для одиночного
наследования):
// заголовок класса
class <имя производного класса> :
<уровень доступа> <имя базового класса>
// тело класса
{ /* … */ };
где <уровень доступа> — ключевое слово public, private или
protected, а <имя базового класса> — имя ранее определенного
(не объявленного!) класса.
В зависимости от уровня доступа к членам базового класса говорят об
открытом, закрытом или защищенном наследовании.
Определение
наследования (2 / 2)
В случае необходимости объявить производный класс список базовых
классов не включается в его заголовок.
Например:
// объявление производного класса
class SavingAccount;
/* … */
// описания классов
class Account
{ /* … */ };
class SavingAccount : public Account
{
/* … */
};
Защищенные члены класса
Атрибуты и методы базового класса, как правило, должны быть
непосредственно доступны для производных классов и
непосредственно недоступны для прочих компонентов программы.
В этом случае они помещаются в секцию protected в теле своего
класса, в результате чего защищенные члены данных и методы
базового класса:
• доступны производному классу (прямому потомку);
• недоступны классам вне рассматриваемой иерархии, глобальным
функциям и вызывающей программе.
Закрытые члены класса
Если наличие прямого доступа к члену класса со стороны
производных классов нежелательно, такой член класса объявляется
как закрытый.
Закрытые члены класса не наследуются потомками. Для доступа к ним
класс-потомок должен быть объявлен в классе-предке как
дружественный. Отношения дружественности не наследуются.
Производный объект
«в разрезе»
Согласно объектной модели языка C++
экземпляр (объект) производного класса
состоит из подобъектов, соответствующих
каждому из его базовых классов, а также части,
объединяющей нестатические члены самого
класса.
Заметим, что объект производного класса
имеет непосредственный доступ только к
защищенным членам входящего в него
подобъекта.
Перегрузка и перекрытие
членов класса (1 / 2)
Члены данных базового класса могут перекрываться одноименными
членами данных производного класса, при этом их типы не должны
обязательно совпадать. (Для доступа к члену базового класса его имя
должно быть квалифицировано.)
Методы базового и производного классов не образуют множество
перегруженных функций. В этом случае методы производного класса
не перегружают (англ. overload), а перекрывают (англ. override) методы
базового.
Для явного создания объединенного множества перегруженных
функций базового и производного классов используется объявление
using, которое вводит именованный член базового класса в область
видимости производного.
Перегрузка и перекрытие
членов класса (2 / 2)
Примечание: таким образом, в область видимости производного класса
попадают все одноименные методы базового класса, а не только
некоторые из них.
Например:
class Account
{ /* … */
void display(const char *fmt);
void display(const int mode = 0);
};
class SavingAccount : public Account
{
void display(const string &fmt);
using Account::display;
/* … */
};
Статические члены данных
и наследование
При наличии в базовом классе статического члена данных объекты
производного класса ссылаются на его единственный разделяемый
статический атрибут. Никакие другие экземпляры данного атрибута в
условия наследования от содержащего его класса не создаются.
Порядок вызова конструкторов
производных классов
Порядок вызова конструкторов объектов-членов, а также базовых
классов при построении объекта производного класса не зависит от
порядка их перечисления в списке инициализации конструктора
производного класса (см. далее) и является следующим:
• конструктор базового класса (если таковых несколько, конструкторы
вызываются в порядке перечисления имен классов в списке базовых
классов);
• конструктор объекта-члена (если таковых несколько, конструкторы
вызываются в порядке объявления членов данных в определении
класса);
• конструктор производного класса.
Примечание: правильно спроектированный конструктор производного
класса не должен инициализировать атрибуты базового класса
напрямую (путем присваивания значений).
Список инициализации при
наследовании (1 / 2)
Конструктор производного класса может вызывать конструкторы
классов, непосредственно являющихся базовыми для данного (прямых
предков), и — без учета виртуального наследования — только их.
Например (начало):
class A {
public:
// A();
A(int i);
/* … */
};
Список инициализации при
наследовании (2 / 2)
Например (окончание):
class B : public A {
public:
B() : _s("alpha") { }
// B() : A(), _s("alpha") {}
B(int i, string s)
: A(i), _s("beta") {}
protected:
string _s;
/* … */
};
Порядок вызова деструкторов
производных классов
Порядок вызова деструкторов при уничтожении объекта
производного класса прямо противоположен порядку вызова
конструкторов и является следующим:
• деструктор производного класса;
• деструктор объекта-члена (или нескольких);
• деструктор базового класса (или нескольких).
Взаимная противоположность порядка вызова конструкторов и порядка
вызова деструкторов является строгой гарантией языка C++.
Виртуальные функции (1 / 2)
Методы, результат разрешения вызова которых зависит от «реального»
(динамического) типа объекта, доступного по указателю или ссылке,
называются виртуальными и при определении в базовом классе
снабжаются спецификатором virtual.
Примечание: в этом контексте тип непосредственно определяемого
экземпляра, ссылки или указателя на объект называется статическим.
Для самого объекта любого типа статический и динамический тип
совпадают.
По умолчанию объектная модель C++ работает с невиртуальными
методами.
Механизм виртуальных функций работает только в случае косвенной
адресации (по указателю или ссылке).
Виртуальные функции (2 / 2)
Значения формальных параметров виртуальных функций
определяются (а) на этапе компиляции (б) типом объекта, через
который осуществляется вызов.
Отмена действия механизма виртуализации возможна и достигается
статическим вызовом метода при помощи операции разрешения
области видимости (::).
Например:
class Aleph {
/* … */
virtual void display();
};
class Beth : public Aleph {
void display();
}
Чистые виртуальные функции
и абстрактные классы (1 / 3)
Класс, где виртуальный метод объявляется впервые, должен
определять его тело либо декларировать метод как не имеющую
собственной реализации чистую виртуальную функцию.
Производный класс может наследовать реализацию виртуального
метода из базового класса или перекрывать его собственной
реализацией, при этом прототипы обеих реализаций обязаны
совпадать.
Примечание: единственное исключение С++ делает для
возвращаемого значения, — значение, возвращаемое реализацией в
производном классе, может иметь тип, открыто наследующий классу
значения, возвращаемого реализацией в базовом.
Например: virtual void display() = 0;
Чистые виртуальные функции
и абстрактные классы (2 / 3)
Класс, который определяет или наследует хотя бы одну чистую
виртуальную функцию, является абстрактным.
Экземпляры абстрактных классов создавать нельзя. Абстрактный
класс может реализовываться только как подобъект производного,
неабстрактного класса.
Чистые виртуальные функции могут иметь тело, вызов которого,
впрочем, может производиться только статически (при помощи
операции разрешения области видимости), но не динамически (при
помощи механизма виртуализации).
Чистые виртуальные функции
и абстрактные классы (3 / 3)
Например:
// class Aleph;
// class Beth : public Aleph;
virtual void Aleph::display() { /* … */ };
void Beth::display()
{
// вызов чистой виртуальной функции
Aleph::display();
/* … */
}
Множественное
наследование (1 / 2)
Множественное наследование в ООП — это наследование от двух и
более базовых классов, возможно, с различным уровнем доступа. Язык
не накладывает ограничений на количество базовых классов.
При множественном наследовании конструкторы базовых классов
вызываются в порядке перечисления имен классов в списке базовых
классов. Порядок вызова деструкторов ему прямо противоположен.
Унаследованные от разных базовых классов методы не образуют
множество перегруженных функций, а потому разрешаются только по
имени, без учета их сигнатур.
Множественное
наследование (2 / 2)
Например:
class Aleph
{ /* … */ };
class Beth : public Aleph
{ /* … */ };
class Gimel
{ /* … */ };
class Dalet : public Beth, public Gimel
{ /* … */ };
Виртуальное
наследование (1 / 2)
При множественном наследовании возможна ситуация неоднократного
включения (дублирования) подобъекта одного и того же базового
класса в состав производного. Связанные с нею проблемы и
неоднозначности снимает виртуальное наследование.
Суть виртуального наследования — включение в состав класса
единственного разделяемого подобъекта базового класса
(виртуального базового класса).
Виртуальное наследование не характеризует базовый класс, а лишь
описывает его отношение к производному.
Использование виртуального наследования должно быть взвешенным
проектным решением конкретных проблем объектно-ориентированного
проектирования.
Виртуальное
наследование (2 / 2)
Например:
class Alpha
{ /* … */ };
class Beta : virtual public Alpha
// то же: class Beta : public virtual Alpha
{ /* … */ };
Конструкция объектов при
виртуальном наследовании
Виртуальные базовые классы конструируются перед невиртуальными
независимо от их расположения в иерархии наследования.
Ответственность за инициализацию виртуального базового класса
несет на себе ближайший (финальный) производный класс.
В промежуточных производных классах прямые вызовы конструкторов
виртуальных базовых классов автоматически подавляются.
Динамическая идентификация
типов времени выполнения (RTTI)
Динамическая идентификация типов времени выполнения (англ. Real-
Time Type Identification) обеспечивает специальную поддержку
полиморфизма и позволяет программе узнать реальный
производный тип объекта, адресуемого по ссылке или по указателю
на базовый класс.
Поддержка RTTI в C++ реализована двумя операциями:
• операция dynamic_cast поддерживает преобразование типов
времени выполнения;
• операция typeid идентифицирует реальный тип выражения.
Операции RTTI — это события времени выполнения для классов с
виртуальными функциями и события времени компиляции для
остальных типов.
Исследование RTTI-информации полезно, в частности, при решении
задач системного программирования.
Операция dynamic_cast (1 / 3)
Встроенная унарная операция dynamic_cast языка C++ позволяет:
• безопасно трансформировать указатель на базовый класс в
указатель на производный класс (с возвратом нулевого указателя
при невозможности выполнения трансформации);
• преобразовывать леводопустимые значения, ссылающиеся на
базовый класс, в ссылки на производный класс (с возбуждением
исключения bad_cast при ошибке).
Единственным операндом dynamic_cast должен являться тип класса, в
котором имеется хотя бы один виртуальный метод.
Операция dynamic_cast (2 / 3)
Например (для указателей):
• пусть классы Alpha и Beta образуют полиморфную иерархию, в
которой класс Beta открыто наследует классу Alpha, тогда:
Alpha *al = new Beta;
if(Beta *bt = dynamic_cast<Beta*>(al))
{
/* успешно */
}
else
{
/* неуспешно */
}
Операция dynamic_cast (3 / 3)
Например (для ссылок):
• пусть классы Alpha и Beta образуют полиморфную иерархию, в
которой класс Beta открыто наследует классу Alpha, тогда:
#include <typeinfo> // для std::bad_cast
void foo(Alpha &al)
{ /* … */
try {
Beta &bt =
dynamic_cast<Beta&>(al))
}
catch(std::bad_cast) { /* … */ }
}
Операция typeid (1 / 2)
Встроенная унарная операция typeid позволяет установить
фактический тип выражения-операнда и может использоваться с
выражениями и именами любых типов (включая выражения
встроенных типов и константы).
Если операнд typeid принадлежит типу класса с одной и более
виртуальными функциями (не указателю на него!), результат typeid
может не совпадать с типом самого выражения.
Операция typeid имеет тип (возвращает значение типа) type_info и
требует подключения заголовочного файла <typeinfo>.
Реализация класса type_info зависит от компилятора, но в общем и
целом позволяет получить результат в виде C-строки (const char*),
присваивать объекты type_info друг другу (operator =), а также
сравнивать их на равенство и неравенство (operator ==, operator !=).
Операция typeid (2 / 2)
Например:
#include <typeinfo> // для type_info
Alpha *al = new Alpha;
if(typeid(al) == typeid(Alpha*)) /* … */
if(typeid(*al) == typeid(Alpha)) /* … */
Вопросы производительности
• Глубина цепочки наследования не увеличивает затраты времени и
не ограничивает доступ к унаследованным членам базовых классов.
• Вызов виртуальной функции в большинстве случаев не менее
эффективен, чем косвенный вызов функции по указателю на нее.
• При использовании встроенных конструкторов глубина иерархии
наследования почти не влияет на производительность.
• Отмена действия механизма виртуализации, как правило,
необходима по соображениям повышения эффективности.
Спасибо за внимание
Алексей Петров

More Related Content

PPTX
Наследование и полиморфизм
PDF
Лекция 1. Основы объектно-ориентированного программирования
PPTX
Лекция 6_принципы ООП : инкапсуляция, наследование
PPT
Lec 2 Java
PDF
C++ весна 2014 лекция 5
PDF
ODP
Java. Classes.
Наследование и полиморфизм
Лекция 1. Основы объектно-ориентированного программирования
Лекция 6_принципы ООП : инкапсуляция, наследование
Lec 2 Java
C++ весна 2014 лекция 5
Java. Classes.

What's hot (18)

PPTX
принципы ооп и программирование классов в C#
PDF
C++ осень 2013 лекция 4
PDF
C++ Базовый. Занятие 11.
PPT
Рефлексия в java
PPTX
Классы и объекты в Java
PPT
Java. Вложенные классы и интерфейсы.
PPT
Java. Конструкторы класса и инициализация
PDF
Лекция 2. Интерфейсы и абстрактные классы
PPTX
Java core-lect6-part3-annotation.ppt
PDF
C++ Базовый. Занятие 17.
PPT
Java. Наследование.
PPT
PDF
C++ Базовый. Занятие 10.
PDF
C++ Базовый. Занятие 14.
PPT
Java. Введение в коллекции. Классы обертки. Перечисленияю
PPT
Step 3.2
PDF
C++ осень 2012 лекция 3
PDF
C++ осень 2012 лекция 1
принципы ооп и программирование классов в C#
C++ осень 2013 лекция 4
C++ Базовый. Занятие 11.
Рефлексия в java
Классы и объекты в Java
Java. Вложенные классы и интерфейсы.
Java. Конструкторы класса и инициализация
Лекция 2. Интерфейсы и абстрактные классы
Java core-lect6-part3-annotation.ppt
C++ Базовый. Занятие 17.
Java. Наследование.
C++ Базовый. Занятие 10.
C++ Базовый. Занятие 14.
Java. Введение в коллекции. Классы обертки. Перечисленияю
Step 3.2
C++ осень 2012 лекция 3
C++ осень 2012 лекция 1
Ad

Viewers also liked (12)

PDF
Современный взгляд на АГ и ХСН. Спорные и нерешенные вопросы. Новые возможнос...
PPT
Интеллектуальная игра самый умный
PDF
Основы ооп на языке C#. Часть 2. базовый синтаксис.
PDF
основы ооп на языке C#. часть 1. введение в программирование
PPTX
Introduction to Programming Languages
PPTX
Programming languages
PPT
Lect 1. introduction to programming languages
PDF
Counters
PDF
8 Ways a Digital Media Platform is More Powerful than “Marketing”
PDF
Slides That Rock
PPTX
Why Content Marketing Fails
PDF
SlideShare 101
Современный взгляд на АГ и ХСН. Спорные и нерешенные вопросы. Новые возможнос...
Интеллектуальная игра самый умный
Основы ооп на языке C#. Часть 2. базовый синтаксис.
основы ооп на языке C#. часть 1. введение в программирование
Introduction to Programming Languages
Programming languages
Lect 1. introduction to programming languages
Counters
8 Ways a Digital Media Platform is More Powerful than “Marketing”
Slides That Rock
Why Content Marketing Fails
SlideShare 101
Ad

Similar to C++ осень 2012 лекция 2 (20)

PDF
C++ осень 2013 лекция 3
PDF
Объектно-Ориентированное Программирование на C++, Лекции 3 и 4
PPTX
особенности программирования на с++
PDF
C++ Базовый. Занятие 09.
PPT
Step 3.1
PPTX
Статический и динамический полиморфизм в C++, Дмитрий Леванов
PPTX
C++ CoreHard Autumn 2018. Кодогенерация C++ кроссплатформенно. Продолжение - ...
PDF
4.6 Особенности наследования в C++
PDF
4.1 Наследование
PDF
C++ осень 2012 лекция 9
PPTX
Статический и динамический полиморфизм в C++, Дмитрий Леванов
PDF
C++ Базовый. Занятие 08.
ODP
основы ооп
PPTX
Некоторые паттерны реализации полиморфного поведения в C++ – Дмитрий Леванов,...
PDF
книга с++
PDF
C++ for real_programmers
PDF
лек11 5
PDF
лек11 5
PDF
Дмитрий Прокопцев — R-ссылки в С++11
PPT
C++ осень 2013 лекция 3
Объектно-Ориентированное Программирование на C++, Лекции 3 и 4
особенности программирования на с++
C++ Базовый. Занятие 09.
Step 3.1
Статический и динамический полиморфизм в C++, Дмитрий Леванов
C++ CoreHard Autumn 2018. Кодогенерация C++ кроссплатформенно. Продолжение - ...
4.6 Особенности наследования в C++
4.1 Наследование
C++ осень 2012 лекция 9
Статический и динамический полиморфизм в C++, Дмитрий Леванов
C++ Базовый. Занятие 08.
основы ооп
Некоторые паттерны реализации полиморфного поведения в C++ – Дмитрий Леванов,...
книга с++
C++ for real_programmers
лек11 5
лек11 5
Дмитрий Прокопцев — R-ссылки в С++11

More from Technopark (20)

PDF
Лекция 11. Вычислительная модель Pregel
PDF
Лекция 14. Hadoop в Поиске Mail.Ru
PDF
Лекция 13. YARN
PDF
Лекция 12. Spark
PDF
Лекция 10. Apache Mahout
PDF
Лекция 9. ZooKeeper
PDF
Лекция 7. Введение в Pig и Hive
PDF
Лекция 6. MapReduce в Hadoop (графы)
PDF
Лекция 5. MapReduce в Hadoop (алгоритмы)
PDF
Лекция 4. MapReduce в Hadoop (введение)
PDF
Лекция 3. Распределённая файловая система HDFS
PDF
Лекция 2. Основы Hadoop
PDF
Лекция 1. Введение в Big Data и MapReduce
PPTX
СУБД 2013 Лекция №10 "Нереляционное решение в области баз данных — NoSQL"
PPT
СУБД 2013 Лекция №10 "Нереляционное решение в области баз данных — NoSQL" Час...
PPTX
СУБД 2013 Лекция №9 "Безопасность баз данных"
PPTX
СУБД 2013 Лекция №8 "Конфигурирование базы данных"
PPTX
СУБД 2013 Лекция №7 "Оптимизация запросов и индексирование"
PPTX
СУБД 2013 Лекция №5 "Определение узких мест"
PPTX
СУБД 2013 Лекция №6 "Профилирование запросов. Сложноструктурированные SQL-зап...
Лекция 11. Вычислительная модель Pregel
Лекция 14. Hadoop в Поиске Mail.Ru
Лекция 13. YARN
Лекция 12. Spark
Лекция 10. Apache Mahout
Лекция 9. ZooKeeper
Лекция 7. Введение в Pig и Hive
Лекция 6. MapReduce в Hadoop (графы)
Лекция 5. MapReduce в Hadoop (алгоритмы)
Лекция 4. MapReduce в Hadoop (введение)
Лекция 3. Распределённая файловая система HDFS
Лекция 2. Основы Hadoop
Лекция 1. Введение в Big Data и MapReduce
СУБД 2013 Лекция №10 "Нереляционное решение в области баз данных — NoSQL"
СУБД 2013 Лекция №10 "Нереляционное решение в области баз данных — NoSQL" Час...
СУБД 2013 Лекция №9 "Безопасность баз данных"
СУБД 2013 Лекция №8 "Конфигурирование базы данных"
СУБД 2013 Лекция №7 "Оптимизация запросов и индексирование"
СУБД 2013 Лекция №5 "Определение узких мест"
СУБД 2013 Лекция №6 "Профилирование запросов. Сложноструктурированные SQL-зап...

C++ осень 2012 лекция 2

  • 2. Лекция №2. Специальные вопросы наследования и полиморфизма. RTTI • Раннее и позднее связывание. • Перегрузка и перекрытие членов класса. • Виртуальные функции. • Абстрактные классы. • Множественное и виртуальное наследование. • Динамическая идентификация типов времени выполнения (RTTI) и операции приведения типов. • Производительность и безопасность полиморфизма и средств поддержки RTTI. • Постановка задач к практикуму №2.
  • 3. Объектный или объектно- ориентированный подход? Объектно-ориентированное программирование расширяет объектный подход и вводит отношения «тип – подтип», прибегая для этого к механизмам наследования и полиморфизма.
  • 4. Наследование: ключевые понятия Наследование (деривация) содействует повторному использованию атрибутов и методов класса, а значит, делает процесс разработки ПО более эффективным. Возникающие между классами A и B отношения наследования позволяют говорить, что: • класс A является базовым (родительским) классом, классом- предком, надклассом (англ. superclass); • класс B является производным (дочерним) классом, классом- потомком, подклассом (англ. subclass). Отношения наследования связывают классы в иерархию, вид которой зависит от количества базовых классов у каждого производного: • при одиночном наследовании иерархия имеет вид дерева; • при множественном наследовании — вид направленного ациклического графа (НАГ) произвольного вида.
  • 5. Наследование «в картинках» Одиночное наследование (слева), множественное наследование (в центре), виртуальное множественное наследование (справа) Account SavingAccount Dalet Aleph Beth Aleph Gimel Aleph Dalet Beth Gimel
  • 6. Полиморфизм подтипов В том случае если базовый и производный классы имеют общий открытый интерфейс, говорят, что производный класс представляет собой подтип базового типа. Отношение между типом и подтипом, позволяющее указателю или ссылке на базовый класс без вмешательства программиста адресовать объект производного класса, возникает в C++ благодаря поддержке полиморфизма. Полиморфизм позволяет предложить такую реализацию ядра объектно-ориентированного приложения, которая не будет зависеть от конкретных используемых подтипов.
  • 7. Раннее и позднее связывание В рамках классического объектного подхода, — а равно и процедурного программирования, — адрес вызываемой функции (метода класса) определяется на этапе компиляции (сборки). Такой порядок связывания вызова функции и ее адреса получил название раннего (статического). Позднее (динамическое) связывание состоит в нахождении (разрешении) нужной функции во время исполнения кода. При этом работа по разрешению типов перекладывается с программиста на компилятор. В языке C++ динамическое связывание поддерживается механизмом виртуальных методов класса, а в компиляторе с языка — таблицей виртуальных методов (англ. VMT, virtual method table).
  • 8. Базовые и производные классы ОО-проектирование допускает сосуществование классов, часть из которых может выполнять чисто технические функции, моделировать абстрактные сущности и отличаться функциональной неполнотой. Не подлежащий реализации в виде экземпляров (объектов) базовый класс может оставаться абстрактным. В противовес абстрактным базовым классам классы, предполагающие создание экземпляров, именуют конкретными. (Абстрактные) базовые классы специфицируют открытые интерфейсы иерархий и содержат общие для всех подтипов атрибуты и методы (или их прототипы). Множество подтипов любого базового класса ограничено иерархией наследования, но потенциально бесконечно (ввиду отсутствия пределов по расширению иерархии вглубь и вширь).
  • 9. Определение наследования (1 / 2) Определение отношения наследования имеет вид (для одиночного наследования): // заголовок класса class <имя производного класса> : <уровень доступа> <имя базового класса> // тело класса { /* … */ }; где <уровень доступа> — ключевое слово public, private или protected, а <имя базового класса> — имя ранее определенного (не объявленного!) класса. В зависимости от уровня доступа к членам базового класса говорят об открытом, закрытом или защищенном наследовании.
  • 10. Определение наследования (2 / 2) В случае необходимости объявить производный класс список базовых классов не включается в его заголовок. Например: // объявление производного класса class SavingAccount; /* … */ // описания классов class Account { /* … */ }; class SavingAccount : public Account { /* … */ };
  • 11. Защищенные члены класса Атрибуты и методы базового класса, как правило, должны быть непосредственно доступны для производных классов и непосредственно недоступны для прочих компонентов программы. В этом случае они помещаются в секцию protected в теле своего класса, в результате чего защищенные члены данных и методы базового класса: • доступны производному классу (прямому потомку); • недоступны классам вне рассматриваемой иерархии, глобальным функциям и вызывающей программе.
  • 12. Закрытые члены класса Если наличие прямого доступа к члену класса со стороны производных классов нежелательно, такой член класса объявляется как закрытый. Закрытые члены класса не наследуются потомками. Для доступа к ним класс-потомок должен быть объявлен в классе-предке как дружественный. Отношения дружественности не наследуются.
  • 13. Производный объект «в разрезе» Согласно объектной модели языка C++ экземпляр (объект) производного класса состоит из подобъектов, соответствующих каждому из его базовых классов, а также части, объединяющей нестатические члены самого класса. Заметим, что объект производного класса имеет непосредственный доступ только к защищенным членам входящего в него подобъекта.
  • 14. Перегрузка и перекрытие членов класса (1 / 2) Члены данных базового класса могут перекрываться одноименными членами данных производного класса, при этом их типы не должны обязательно совпадать. (Для доступа к члену базового класса его имя должно быть квалифицировано.) Методы базового и производного классов не образуют множество перегруженных функций. В этом случае методы производного класса не перегружают (англ. overload), а перекрывают (англ. override) методы базового. Для явного создания объединенного множества перегруженных функций базового и производного классов используется объявление using, которое вводит именованный член базового класса в область видимости производного.
  • 15. Перегрузка и перекрытие членов класса (2 / 2) Примечание: таким образом, в область видимости производного класса попадают все одноименные методы базового класса, а не только некоторые из них. Например: class Account { /* … */ void display(const char *fmt); void display(const int mode = 0); }; class SavingAccount : public Account { void display(const string &fmt); using Account::display; /* … */ };
  • 16. Статические члены данных и наследование При наличии в базовом классе статического члена данных объекты производного класса ссылаются на его единственный разделяемый статический атрибут. Никакие другие экземпляры данного атрибута в условия наследования от содержащего его класса не создаются.
  • 17. Порядок вызова конструкторов производных классов Порядок вызова конструкторов объектов-членов, а также базовых классов при построении объекта производного класса не зависит от порядка их перечисления в списке инициализации конструктора производного класса (см. далее) и является следующим: • конструктор базового класса (если таковых несколько, конструкторы вызываются в порядке перечисления имен классов в списке базовых классов); • конструктор объекта-члена (если таковых несколько, конструкторы вызываются в порядке объявления членов данных в определении класса); • конструктор производного класса. Примечание: правильно спроектированный конструктор производного класса не должен инициализировать атрибуты базового класса напрямую (путем присваивания значений).
  • 18. Список инициализации при наследовании (1 / 2) Конструктор производного класса может вызывать конструкторы классов, непосредственно являющихся базовыми для данного (прямых предков), и — без учета виртуального наследования — только их. Например (начало): class A { public: // A(); A(int i); /* … */ };
  • 19. Список инициализации при наследовании (2 / 2) Например (окончание): class B : public A { public: B() : _s("alpha") { } // B() : A(), _s("alpha") {} B(int i, string s) : A(i), _s("beta") {} protected: string _s; /* … */ };
  • 20. Порядок вызова деструкторов производных классов Порядок вызова деструкторов при уничтожении объекта производного класса прямо противоположен порядку вызова конструкторов и является следующим: • деструктор производного класса; • деструктор объекта-члена (или нескольких); • деструктор базового класса (или нескольких). Взаимная противоположность порядка вызова конструкторов и порядка вызова деструкторов является строгой гарантией языка C++.
  • 21. Виртуальные функции (1 / 2) Методы, результат разрешения вызова которых зависит от «реального» (динамического) типа объекта, доступного по указателю или ссылке, называются виртуальными и при определении в базовом классе снабжаются спецификатором virtual. Примечание: в этом контексте тип непосредственно определяемого экземпляра, ссылки или указателя на объект называется статическим. Для самого объекта любого типа статический и динамический тип совпадают. По умолчанию объектная модель C++ работает с невиртуальными методами. Механизм виртуальных функций работает только в случае косвенной адресации (по указателю или ссылке).
  • 22. Виртуальные функции (2 / 2) Значения формальных параметров виртуальных функций определяются (а) на этапе компиляции (б) типом объекта, через который осуществляется вызов. Отмена действия механизма виртуализации возможна и достигается статическим вызовом метода при помощи операции разрешения области видимости (::). Например: class Aleph { /* … */ virtual void display(); }; class Beth : public Aleph { void display(); }
  • 23. Чистые виртуальные функции и абстрактные классы (1 / 3) Класс, где виртуальный метод объявляется впервые, должен определять его тело либо декларировать метод как не имеющую собственной реализации чистую виртуальную функцию. Производный класс может наследовать реализацию виртуального метода из базового класса или перекрывать его собственной реализацией, при этом прототипы обеих реализаций обязаны совпадать. Примечание: единственное исключение С++ делает для возвращаемого значения, — значение, возвращаемое реализацией в производном классе, может иметь тип, открыто наследующий классу значения, возвращаемого реализацией в базовом. Например: virtual void display() = 0;
  • 24. Чистые виртуальные функции и абстрактные классы (2 / 3) Класс, который определяет или наследует хотя бы одну чистую виртуальную функцию, является абстрактным. Экземпляры абстрактных классов создавать нельзя. Абстрактный класс может реализовываться только как подобъект производного, неабстрактного класса. Чистые виртуальные функции могут иметь тело, вызов которого, впрочем, может производиться только статически (при помощи операции разрешения области видимости), но не динамически (при помощи механизма виртуализации).
  • 25. Чистые виртуальные функции и абстрактные классы (3 / 3) Например: // class Aleph; // class Beth : public Aleph; virtual void Aleph::display() { /* … */ }; void Beth::display() { // вызов чистой виртуальной функции Aleph::display(); /* … */ }
  • 26. Множественное наследование (1 / 2) Множественное наследование в ООП — это наследование от двух и более базовых классов, возможно, с различным уровнем доступа. Язык не накладывает ограничений на количество базовых классов. При множественном наследовании конструкторы базовых классов вызываются в порядке перечисления имен классов в списке базовых классов. Порядок вызова деструкторов ему прямо противоположен. Унаследованные от разных базовых классов методы не образуют множество перегруженных функций, а потому разрешаются только по имени, без учета их сигнатур.
  • 27. Множественное наследование (2 / 2) Например: class Aleph { /* … */ }; class Beth : public Aleph { /* … */ }; class Gimel { /* … */ }; class Dalet : public Beth, public Gimel { /* … */ };
  • 28. Виртуальное наследование (1 / 2) При множественном наследовании возможна ситуация неоднократного включения (дублирования) подобъекта одного и того же базового класса в состав производного. Связанные с нею проблемы и неоднозначности снимает виртуальное наследование. Суть виртуального наследования — включение в состав класса единственного разделяемого подобъекта базового класса (виртуального базового класса). Виртуальное наследование не характеризует базовый класс, а лишь описывает его отношение к производному. Использование виртуального наследования должно быть взвешенным проектным решением конкретных проблем объектно-ориентированного проектирования.
  • 29. Виртуальное наследование (2 / 2) Например: class Alpha { /* … */ }; class Beta : virtual public Alpha // то же: class Beta : public virtual Alpha { /* … */ };
  • 30. Конструкция объектов при виртуальном наследовании Виртуальные базовые классы конструируются перед невиртуальными независимо от их расположения в иерархии наследования. Ответственность за инициализацию виртуального базового класса несет на себе ближайший (финальный) производный класс. В промежуточных производных классах прямые вызовы конструкторов виртуальных базовых классов автоматически подавляются.
  • 31. Динамическая идентификация типов времени выполнения (RTTI) Динамическая идентификация типов времени выполнения (англ. Real- Time Type Identification) обеспечивает специальную поддержку полиморфизма и позволяет программе узнать реальный производный тип объекта, адресуемого по ссылке или по указателю на базовый класс. Поддержка RTTI в C++ реализована двумя операциями: • операция dynamic_cast поддерживает преобразование типов времени выполнения; • операция typeid идентифицирует реальный тип выражения. Операции RTTI — это события времени выполнения для классов с виртуальными функциями и события времени компиляции для остальных типов. Исследование RTTI-информации полезно, в частности, при решении задач системного программирования.
  • 32. Операция dynamic_cast (1 / 3) Встроенная унарная операция dynamic_cast языка C++ позволяет: • безопасно трансформировать указатель на базовый класс в указатель на производный класс (с возвратом нулевого указателя при невозможности выполнения трансформации); • преобразовывать леводопустимые значения, ссылающиеся на базовый класс, в ссылки на производный класс (с возбуждением исключения bad_cast при ошибке). Единственным операндом dynamic_cast должен являться тип класса, в котором имеется хотя бы один виртуальный метод.
  • 33. Операция dynamic_cast (2 / 3) Например (для указателей): • пусть классы Alpha и Beta образуют полиморфную иерархию, в которой класс Beta открыто наследует классу Alpha, тогда: Alpha *al = new Beta; if(Beta *bt = dynamic_cast<Beta*>(al)) { /* успешно */ } else { /* неуспешно */ }
  • 34. Операция dynamic_cast (3 / 3) Например (для ссылок): • пусть классы Alpha и Beta образуют полиморфную иерархию, в которой класс Beta открыто наследует классу Alpha, тогда: #include <typeinfo> // для std::bad_cast void foo(Alpha &al) { /* … */ try { Beta &bt = dynamic_cast<Beta&>(al)) } catch(std::bad_cast) { /* … */ } }
  • 35. Операция typeid (1 / 2) Встроенная унарная операция typeid позволяет установить фактический тип выражения-операнда и может использоваться с выражениями и именами любых типов (включая выражения встроенных типов и константы). Если операнд typeid принадлежит типу класса с одной и более виртуальными функциями (не указателю на него!), результат typeid может не совпадать с типом самого выражения. Операция typeid имеет тип (возвращает значение типа) type_info и требует подключения заголовочного файла <typeinfo>. Реализация класса type_info зависит от компилятора, но в общем и целом позволяет получить результат в виде C-строки (const char*), присваивать объекты type_info друг другу (operator =), а также сравнивать их на равенство и неравенство (operator ==, operator !=).
  • 36. Операция typeid (2 / 2) Например: #include <typeinfo> // для type_info Alpha *al = new Alpha; if(typeid(al) == typeid(Alpha*)) /* … */ if(typeid(*al) == typeid(Alpha)) /* … */
  • 37. Вопросы производительности • Глубина цепочки наследования не увеличивает затраты времени и не ограничивает доступ к унаследованным членам базовых классов. • Вызов виртуальной функции в большинстве случаев не менее эффективен, чем косвенный вызов функции по указателю на нее. • При использовании встроенных конструкторов глубина иерархии наследования почти не влияет на производительность. • Отмена действия механизма виртуализации, как правило, необходима по соображениям повышения эффективности.