- •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. Необычный рекуррентный шаблон. Структура, варианты применения.
24. Композиция объектов с кратностью многие-ко-многим. Основные особенности объектных отношений, способы реализации.
Отношение многие-ко-многим в большинстве случаев должно моделироваться без ответственности за уничтожение. Такое отношение образует сложные связи между объектами, и понять используется объект где-либо еще достаточно сложно. Часто отношение многие-ко-многим представляет в предметной области некоторое логическое объединение нескольких сущностей в группу, нежели более сильное отношение владения.
Обычно объекты, связанные отношением многие-ко-многим, создает и уничтожает какой-то третий объект, ответственный за хранение всех участвующих в такой модели объектов.
25. Наследование классов. Необходимость в отношении наследования. Структура наследования в памяти. Повышающее преобразование типа.
Наследование — механизм языка, позволяющий описать новый класс на основе уже существующего (родительского, базового) класса. Класс-потомок может добавить собственные методы и свойства, а также пользоваться родительскими методами и свойствами. Позволяет строить иерархии классов. Является одним из пяти типов ассоциации.
Механизм наследования – механизм языка, который позволяет создавать новые классы расширяя возможности существующих, при этом имея возможность использовать объекты новых классов везде, где можно использовать оригинальные объекты. В отличие от композиции, создающей иерархии объектов по принципу целое-часть, при помощи наследования формируют иерархии классов с отношениями общее-частное. Наследование предполагает, что новый класс, называемый подклассом (subclass) либо классом-потомком, классом-наследником (derived class), объявляет некоторый существующий класс своим базовым (base class) или суперклассом (superclass). В результате, класс-потомок получает все члены родительского класса, может дополнять их новыми, а объекты класса-потомка могут преобразовываться к объектам базового класса.
Интересно, что на низком уровне с точки зрения структуры объектов в памяти обе версии классов Bus и Truck - до внедрения отношения наследования и после - будут выглядеть идентично - в начале объектов производных классов будут размещаться члены базового класса, а новые дополнительные члены будут находиться после них:
Вся разница, вносимая наследованием, состоит в возможности автоматического преобразования типа от класса-потомка к базовому классу. Такое преобразования типа называется ПОВЫШАЮЩИМ (upcast, движемся вверх по иерархии). Также упрощается доступ к членам базового класса. Вместо прежнего обращения через базовый объект:
std::cout << b.getVehicle().getWeight()
<< b.getVehicle().getEngine().getHorsePower();
можно воспользоваться методом базового класса напрямую:
std::cout << b.getWeight() << b.getEngine().getHorsePower();
Не следует путать преобразование типа к базовому классу для ссылок/указателей с аналогичными действиями для значений.
26. Критерии оценки корректности применения наследования. Примеры корректного и некорректного применения наследования.
Часто начинающие программисты применяют наследование в мало пригодных ситуациях, где более уместной является композиция. Наследование классов следует применять, если всегда можно однозначно без логического противоречия напрямую использовать объект производного класса в любом месте программы, где ожидается объект базового класса. Этот принцип носит специальное название - принцип подстановки Лисков (Liskov’s Substitution Principle).
Существует простой контрольный вопрос, по которому можно определить корректность применения наследования: является ли в предметной области понятие, описываемое производным классом, разновидностью понятия, описываемого базовым классом? Проговорить ответ на этот вопрос следует на естественном языке. Например:
Автобус - это разновидность транспортного средства (Bus IS A Vehicle).
Грузовик - это разновидность транспортного средства (Truck IS A Vehicle).
Приведенные утверждения истинны и не вызывают никаких противоречий. Так нельзя сказать о фразе, касающейся отношения между классами Engine и Vehicle, кажущейся очень странной:
Двигатель - это разновидность транспортного средства???
Двигатель является частью транспортного средства, но никак не его разновидностью, соответственно здесь должно применяться отношение композиции, а не наследования.
Хотя приведенный вопрос кажется наивным и очевидным, многие неквалифицированные программисты, да и некоторые авторы книг по программированию, регулярно нарушают это правило. Например, в ряде книг, от упоминания названий которых воздержимся, встречается подобный пример при объяснении темы наследования - окружность наследуется от точки:
class Point3D
{
float m_x, m_y, m_z;
};
class Circle
: public Point3D
{
float m_radius;
public:
Circle ( Point3D _center, float _radius )
: Point3D( _center ), m_radius( _radius )
{}
};
В этом примере механизм наследования применяется исключительно для сомнительного удобства реализации, где координаты центра окружности подменяются координатами базового класса. Такое решение является абсолютно неприемлемым. Окружность не является разновидностью точки! Вместо наследования здесь должна использоваться композиция, поскольку центральная точка - это часть окружности:
class Circle
{
Point3D m_center;
float m_radius;
public:
Circle ( Point3D _center, float _radius )
: m_center( _center ), m_radius( _radius )
{}
};
Ниже приведен еще один типичный неправильный пример использования наследования - эллипс наследуется от окружности для повторного использования элементов реализации (координаты центра и одного из радиусов):
class Ellipse
: public Circle
{
float m_radius2;
public:
Ellipse ( Point3D _center, float _radius1, float_radius2 )
: Circle( _center, _radius1 ), m_radius2( _radius2 )
{}
};
Такое решение нельзя назвать хорошим по все той же причине - эллипс не является разновидностью окружности. Скорее наоборот, окружность - частный случай эллипса, у которого оба радиуса равны между собой:
class Ellipse
{
Point3D m_center;
float m_radius1, m_radius2;
public:
Ellipse ( Point3D _center, float _radius1, float _radius2 )
: m_center( _center ), m_radius1( _radius1), m_radius2( _radius2 )
{}
};
class Circle
: public Ellipse
{
public:
Circle ( Point3D _center, float _radius )
: Ellipse( _center, _radius, _radius )
{} // ^ используем один и тот же радиус дважды
};
Второе решение лучше первого с точки зрения соблюдения правил проектирования объектных моделей, однако не слишком эффективно в связи с хранением избыточного второго радиуса для окружности. Такое наследование иногда называют “сужающим”. Помимо избыточности, еще один недостаток такого вида наследования состоит в том, что методы базового класса, а также код, использующий объекты-эллипсы, может случайно нарушить невидимый для них инвариант окружности о равенстве радиусов. Окружность не может гарантировать, что одна из копий значения ее радиусов, хранящаяся в определении базового класса, не будет несогласованно изменена без аналогичного изменения второй копии, хранящейся в производном классе.
Наилучший способ моделирования эллипсов и окружностей состоит в выделении более общего базового класса - фигура с центром - объединяющего схожие свойства обоих конкретных классов, при этом не создающего избыточности на уровне данных или неявных инвариантов:
class ShapeWithCenter
{
Point3D m_center;
public:
ShapeWithCenter ( Point3D _center )
: m_center( _center )
{}
};
class Circle
: public ShapeWithCenter
{
float m_radius;
public:
Circle ( Point3D _center, float _radius )
: ShapeWithCenter( _center ), m_radius( _radius )
{}
};
class Ellipse
: public ShapeWithCenter
{
float m_radius1, m_radius2;
public:
Ellipse ( Point3D _center, float _radius1, float _radius2 )
: ShapeWithCenter( _center ), m_radius1( _radius1 ), m_radius2( _radius2 )
{}
};