- •1. Абстракция и декомпозиция. Основные виды декомпозиции программ.
- •Модульная декомпозиция
- •В заголовочный файл не следует помещать элементы реализации модуля, в том числе и внутренние функции, которые необходимы для реализации, однако не существенны для клиентского когда модуля.
- •Объектная декомпозиция
- •2. Понятие класса и объекта. Переменные-члены и функции-члены. Обращение к членам класса через объект. Указатель this. Константные функции-члены.
- •3. Спецификаторы доступа. Понятие инкапсуляции. Отличие конструкций class и struct. Методы доступа.
- •4. Конструкторы классов, синтаксис, разновидности, моменты вызова конструкторов. Роль конструкторов в соблюдении инвариантов классов.
- •5. Конструкторы по умолчанию (default constructors). Тривиальные и нетривиальные сгенерированные конструкторы классов. Конструирование массивов объектов.
- •6. Списки инициализации. Синтаксис, отличие от присвоений в теле конструктора, необходимость в существовании.
- •7. Деструкторы классов, синтаксис, цель, моменты вызова деструкторов.
- •8. Моменты копирования объектов. Поведение по умолчанию. Конструктор копий и оператор копирующего присвоения.
- •9. Временные объекты. Явные и неявные конструкторы. Оптимизации rvo/nrvo. Временные объекты
- •Неявные и явные конструкторы
- •Запрещение копирования
- •Оптимизация копирования
- •10. Основные отличия между классами-значениями и классами-сущностями. Запрещение копирования объектов. Основные отличия между классами-значениями и классами-сущностями.
- •11. Перемещение объектов. Конструктор перемещения и оператор перемещающего присвоения. Понятие rvalue-ссылки. Функция std::move.
- •12. Перегрузка операторов. Оправданное и неоправданное использование. Пример перегрузки простейшего оператора. Операторы, которые нельзя перегружать.
- •13. Внутриклассовые и глобальные перегруженные операторы. Перегрузка операторов сдвига. Применение перегрузки сдвига для взаимодействия с потоками ввода/вывода.
- •14. Перегрузка операторов сравнения и арифметических операторов. Основные правила реализации и применения.
- •15. Перегрузка операторов индексной выборки, префиксного и постфиксного инкремента/декремента. Перегрузка операторов преобразования типа.
- •16. Статические переменные-члены. Цель применения. Синтаксис. Особенности компоновки.
- •17. Статические функции-члены. Синтаксис, особенности применения. Фабричный метод. Статические функции-члены
- •Фабричный метод
- •19. Физическое и логическое постоянство объектов. Модификатор mutable.
- •20. Класс std::string из стандартной библиотеки. Основная функциональность, способы применения. Особенности внутренней структуры.
- •21. Композиция объектов. Иерархии целое-часть. Структура простейшей композиции по значению в памяти. Ответственность за уничтожение объектов при композиции.
- •22. Ссылочная композиция. Разрываемая композиция. Кратность композиции. Одиночная, множественная и недетерминированная кратность.
- •23. Применение контейнера std::vector для композиции с недетерминированной кратностью. Композиция объектов-значений и объектов-сущностей.
- •24. Композиция объектов с кратностью многие-ко-многим. Основные особенности объектных отношений, способы реализации.
- •25. Наследование классов. Необходимость в отношении наследования. Структура наследования в памяти. Повышающее преобразование типа.
- •26. Критерии оценки корректности применения наследования. Примеры корректного и некорректного применения наследования.
- •27. Конструкторы и деструкторы при наследовании. Моменты и порядок вызовов конструкторов. Передача аргументов конструкторам базового класса.
- •28. Спецификатор доступа protected. Защищенные конструкторы и методы.
- •29. Понижающее преобразование типа (downcast). Опасности. Поля идентификации типов.
- •30. Виртуальные функции. Полиморфизм. Цель. Синтаксис, примеры использования.
- •31. Реализация виртуальных функций. Указатель vptr и таблица vtable. Вызов виртуальной функции. Инициализация служебных данных для работы виртуальных функций в конструкторах.
- •32. Контроль переопределения виртуальных функций. Требования к сигнатурам. Ключевые слова override и final. Ковариантность возвращаемых типов.
- •33. Чисто виртуальные функции и абстрактные классы. Вызов чисто виртуальной функции в конструкторе до завершения инициализации объекта.
- •34. Понятие интерфейса. Применение интерфейсов.
- •35. Множественное наследование конкретных классов. Синтаксис, структура в памяти, особенности применения и реализации.
- •36. Преобразование типов при множественном наследовании в верхнем и нижнем направлениях. Коррекция указателя this.
- •37. Множественное наследование классов с повторяющимся базовым. Синтаксис, структура в памяти, особенности применения и реализации.
- •38. Виртуальные базовые классы. Синтаксис, структура в памяти, особенности применения и реализации. Понятие “самого производного” класса и его роль в организации работы виртуальных базовых классов.
- •39. Механизм rtti - назначение, особенности применения. Структура std::type_info, оператор typeid для выражений и типов.
- •40. Применение оператора dynamic_cast для указателей и ссылок. Основные цели использования. Отличия от операторов static_cast, reinterpret_cast и const_cast.
- •41. Альтернативные решения, заменяющие dynamic_cast. Виртуальные функции для понижающего преобразования. Типовое решение Visitor.
- •42. Обработка исключений. Цели, синтаксис выброса и обработчиков. Выбор обработчика по типу. Передача данных исключения по значению, указателю и ссылке. Исключения языка и стандартной библиотеки.
- •44. Шаблоны функций и классов. Синтаксис определения шаблонов. Инстанцирование шаблонов. Модель включения и явное инстанцирование.
- •Шаблоны классов
- •45. Аргументы шаблонов - типы, константы, шаблонные аргументы шаблонов. Дедукция фактических аргументов шаблонов.
- •46. Понятие обобщенной концепции. Статический полиморфизм по сравнению с динамическим полиморфизмом.
- •Статический полиморфизм
- •47. Итераторы stl - основные разновидности, итераторы контейнеров, итераторы, не связанные с контейнерами.
- •48. Классификация алгоритмов стандартной библиотеки. Примеры применения наиболее часто используемых алгоритмов.
- •49. Функциональные объекты stl. Простые функциональные объекты. Стандартные функциональные объекты. Связыватели std::bind.
- •50. Понятие лямбда-выражения. Синтаксис, особенности использования. Реализация лямбда-выражений компилятором. Список захвата лямбда-выражения.
- •51. Специализация шаблонов. Полная и частичная специализация. Статический выбор вариантов на основе специализации шаблонов.
- •52. Необычный рекуррентный шаблон. Структура, варианты применения.
38. Виртуальные базовые классы. Синтаксис, структура в памяти, особенности применения и реализации. Понятие “самого производного” класса и его роль в организации работы виртуальных базовых классов.
Внутренняя структура объектов в данной иерархии достаточно сложна. Поскольку только самый производный класс (т.е. класс, объект которого фактически создается в данный момент) может знать полный набор всех междуклассовых отношений, то именно самый производный класс решает в каком месте в результирующем объекте расположить содержимое виртуального базового. Кроме того, базовые классы, не имея возможности определить местоположение виртуального базового класса самостоятельно, должны опираться на некоторые дополнительные данные, позволяющие узнать отступ от начала объекта до начала содержимого виртуального базового класса. Т.е., если взять, например, класс Printer, и создать объект Printer, то местоположение содержимого класса PoweredDevice в таком объекте будет отличаться от местоположения в объекте Copier, несмотря на то, что Copier - это подвид Printer.
Существует 2 типовые схемы реализации виртуального наследования, различные между собой - одна из реализаций применяется в компиляторе компании Microsoft, другая - в компиляторе GCC. Начнем с компилятора Microsoft.
Предположим, создается конкретный объект класса Printer. Отладчик показывает его размер как 24 байта (VisualStudio 2010, 32-битный режим, настройки по умолчанию). Что же входит в этот размер? Считаем размер информационных полей:
m_nominalPower (4 байта);
m_turned (1 байт);
m_pagesPerMinute (4 байта).
Из этого расчета выходит, что объект должен содержать 9 байт, а ни как не 24. На что же уходит еще 15 байт?
Во-первых, компилятор применяет схемы выравнивания структур по границе (настройка по умолчанию = 4 байта), поэтому поле m_turned занимает не 1 байт, а 4. Остается 12 байт.
Во-вторых, абстрактный класс PoweredDevice содержит виртуальный деструктор. Это резервирует в объекте место под указатель vptr. Остается 8 байт.
В-третьих, класс Printer вводит собственную виртуальную функцию printDocument. В такой сложной иерархии классов не получается обходиться только одной таблицей виртуальных функций из-за сложных смещений, потому существует второй указатель vptr.. Остается 4 байта.
Наконец, последние 4 байта расходуются на еще один служебный указатель, который называется vbptr (virtual base class pointer), и представляет собой адрес массива, содержащего смещения (число байт) виртуальных базовых классов относительно местоположения указателя vbptr. В нашем случае, виртуальный базовый класс только один, потому массив будет содержать одну полезную запись. Если добавить в иерархию еще несколько виртуальных базовых классов, это дополнит данный массив, не вводя новых указателей в память объектов.
Графически это можно представить следующим образом:
В начале объекта Printer находится собственный указатель на таблицу виртуальных функций, добавленных данным классом (vptr_1). Далее следует указатель на массив смещений базовых классов (vbptr) в объекте относительно этого же указателя. Элемент с индексом 0 - это смещение самого класса Printer, т.е. на 4 байта назад. Элемент с индексом 1 - это уже смещение виртуального базового класса PoweredDevice относительно vbptr. Далее следует поле m_pagesPerMinute, за которым размещаются члены класса PoweredDevice. Первым из них является еще один указатель vptr для соответствующих классу PoweredDevice виртуальных функций.
Соответственно, любой код, который имея указатель или ссылку на объект класса Printer, при этом обращающийся к членам унаследованного виртуального базового класса PoweredDevice, делает следующие действия:
Printer * p = new Printer( 300, 15 );
std::cout << p->getNominalPower();
из адреса p извлекается указатель vbptr;
из указателя vbptr извлекается 2 смещения по индексу 1 и по индексу 0;
извлеченный смещения складываются, формируя смещение объекта PoweredDevice относительно начала объекта Printer;
выполняется вызов метода getNominalPower (если, конечно, он не будет встраиваться в месте вызова), и ему передается адрес this, равный адресу в указателе p + нужное смещение.
Как это очевидно из пояснений, наличие виртуальных базовых классов является довольно дорогостоящим и с точки зрения памяти, и с точки зрения производительности всех вызовов. Разумеется, такой тип наследования следует применять лишь в случаях, когда такое функциональное поведение является реально востребованным в задаче.
Внутренняя структура объекта Scanner в целом формируется аналогичным образом. Интерес представляет объект Copier, с размером 36 байт. Внутренняя структура данного объекта выглядит следующим образом:
Такой сложный объект состоит из 4 информационных полей (16 байт), 3 указателей vptr по каждому из базовых классов (12 байт) и 2 указателей vbptr на виртуальный базовый класс из промежуточных базовых классов (8 байт). Итого, 36 байт.
При внимательном чтении кода примера должен вызвать удивление выделенный код в списке инициализации конструктора Copier:
// Реализация конструктора
Copier::Copier ( int _nominalPower, int _scanDPI, int _pagesPerMinute )
// Вызов конструктора виртуального базового класса через всю иерархию!
: PoweredDevice( _nominalPower )
// Вызов конструкторов обычных базовых
, Scanner( _nominalPower, _scanDPI )
, Printer( _nominalPower, _pagesPerMinute )
{
}
Т.е., конструктор класса Copier, состоящих в двух уровнях иерархии наследования от класса PoweredDevice осуществляет вызов данного конструктора. При чем, это действие выполняется раньше, чем произойдут вызовы конструкторов Scanner и Printer, для которых общая часть виртуального базового класса должна быть уже инициализированной.
Подытожим обнаруженные нестыковки с ранее изученным материалом. Если в иерархии наследования имеется виртуальный базовый класс, то его местоположение в объекте определяется непосредственным конкретным классом, к которому он относится. Только самый производный класс иерархии (“most derived” class) знает точное местоположение виртуального базового. Местоположение может отличаться для каждого конкретного класса в иерархии. Чтобы промежуточные базовые классы могли работать с членами виртуального базового, самый производный класс должен обеспечить корректную инициализацию указателей vbptr в собственном конструкторе. Это можно увидеть при дизассебмлировании. Конструктор класса Copier содержит такой код, который устанавливает два указателя vbptr в нужное значение:
01378E72 mov eax,dword ptr [ebp-14h]
01378E75 mov dword ptr [eax+4],offset Copier::`vbtable' (139BE8Ch)
01378E7C mov eax,dword ptr [ebp-14h]
01378E7F mov dword ptr [eax+10h],offset Copier::`vbtable' (139BE80h)
01378E86 mov eax,dword ptr [ebp+8]
В то же время, класс Printer (и Scanner аналогично), конструкторы которых вызываются раньше Copier, содержат такой код:
01388923 mov eax,dword ptr [this]
01388926 mov dword ptr [eax+4],offset Printer::`vbtable' (139D840h)
Помимо сложной структуры, виртуальное наследование оказывает существенное влияние на порядок инициализации и уничтожения объектов. В приведенном примере порядок выполнения тел конструкторов участвующих классов для объекта Copier будет следующим, при этом конструктор виртуального базового класса вызовется первым и единственный раз:
PoweredDevice::PoweredDevice
Printer::Printer
Scanner::Scanner
Copier::Copier
Если будет создан объект класса Printer, его порядок конструирования такой:
PoweredDevice::PoweredDevice
Printer::Printer
Если будет создан объект класса Scanner, его порядок конструирования такой:
PoweredDevice::PoweredDevice
Scanner::Scanner
Возникает вопрос - если конструкторы классов Printer и Scanner вызывают конструктор класса PoweredDevice, то каким же образом достигается единственность его вызова при конструировании объекта Copier? Хитрость состоит в передаче в конструкторы классы иерархии еще одного неявного целого аргумента, означающего является ли класс текущего конструктора “самым производным”. Этот аргумент не увидеть нигде, кроме дизассебмлера. Предположим создается объект Copier. Код, который инициирует создание объекта, всегда знает конкретный тип, поскольку фактически его указывает, соответственно в этом случае передается 1:
Copier c( 500, 300, 15 );
01302285 push 1
01302287 push 0Fh
01302289 push 12Ch
0130228E push 1F4h
01302293 lea ecx,[ebp-34h]
01302296 call Copier::Copier (12E3311h)
Аналогично, если создать объект класса Printer, подобная единица будет неявно передана его конструктору:
Printer p( 300, 15 );
013E223D push 1
013E223F push 0Fh
013E2241 push 12Ch
013E2246 lea ecx,[ebp-28h]
013E2249 call Printer::Printer (13C349Ch)
В то же время, когда объект Copier в собственном конструкторе вызывает конструктор класса Printer, вместо единицы передается 0, поскольку в таком случае Printer не является “самым производным” классом:
01378EC4 push 0
01378EC6 mov eax,dword ptr [ebp+10h]
01378EC9 push eax
01378ECA mov ecx,dword ptr [ebp+8]
01378ECD push ecx
01378ECE mov ecx,dword ptr [ebp-14h]
01378ED1 add ecx,0Ch
01378ED4 call Printer::Printer (137349Ch)
Этот аргумент используется в конструкторах Printer, Scanner и Copier таким образом, чтобы вызывать конструктор виртуального базового класса только при переданном значении 1 (т.е., когда текущий класс является “самым производным”). Это можно выразить следующим псевдокодом конструктора Printer:
Printer::Printer ( Printer * this,
int _nominalpower,
int _pagesPerMinute,
int mostDerived )
{
if ( mostDerived )
PoweredDevice::PoweredDevice( this + vbptr[ 0 ] + vbptr[ 1 ], _nominalPower );
this->m_pagesPerMinute = _pagesPerMinute;
}
Аналогично, конструктор класса Copier содержит нечто подобное:
Copier::Copier ( Copier * this,
int _nominalpower,
int _pagesPerMinute,
int _scanDPI,
int mostDerived )
{
if ( mostDerived )
PoweredDevice::PoweredDevice( this + offset3, _nominalPower );
Printer::Printer( this + offset1, _nominalPower, _pagesPerMinute, 0 );
Scanner::Scanner( this + offset2, _nominalPower, _scanDPI, 0 );
}
Похожим образом обеспечивается обратный порядок вызова деструкторов:
Copier::~Copier
Scanner::~Scanner
Printer::~Printer
PoweredDevice::~PoweredDevice
Реализация виртуального базового наследования в других компиляторах, в частности в компиляторе GCC - основном компиляторе на платформе Linux, отличается иным размещением смещений виртуальных базовых классов. Указатели vbptr и дополнительные таблицы vbtable здесь вообще не используются. Вместо этого, смещения размещаются в таблице виртуальных функций по отрицательным индексам. Если словосочетание “отрицательные индексы” в массиве вызывает недоумение, следует вспомнить, что vptr является указателем, а значит может указывать на любой из элементов массива. Разумеется, нумерация ячеек в самой таблице начинается с 0, однако в начале таблицы помещают смещения виртуальных базовых классов, а только за ними - адреса виртуальных функций. Указатель vptr устанавливается таким образом, чтобы по нулевому смещению размещался адрес первой виртуальной функции. Соответственно, все предыдущие данные становятся доступны по отрицательным индексам.
Такая структура является более компактной по сравнению с реализацией в компиляторе Microsoft, несмотря на более сложную для восприятия схему реализации. Для объектов Printer и Scanner экономия составляет 1 указатель на каждом объекта, а для Copier - целых 2 указателя.