- •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. Необычный рекуррентный шаблон. Структура, варианты применения.
8. Моменты копирования объектов. Поведение по умолчанию. Конструктор копий и оператор копирующего присвоения.
На протяжении жизни объекта возникает множество ситуаций, при которых его содержимое может копироваться в другой объект. К моментам копирования объекта относятся:
- инициализация нового объекта из уже существующего:
Date today;
Date d = today; // Объект d будет идентичен объекту today
- передача объекта по значению в функцию:
void takesDate ( Date _d ) { ... }
// Объект в функции takesDate будет копией объекта today
Date today;
takesDate ( today );
- возврат объекта по значению из функции:
Date returnsDate () { return Date(); }
// Объект d будет идентичен объекту, возвращенному из returnsDate
Date d = returnsDate();
Обычно в подобном стиле используют классы, представляющие собой некие сложные ЗНАЧЕНИЯ (values), которыми программист манипулирует подобно переменным встроенных типов. Копирование редко применяется для классов, представляющих собой СУЩНОСТИ (entities) реального мира. Для классов-сущностей чаще свойственна передача через функции по ссылке/указателю, а операцию копирования часто в явном виде запрещают.
Логику копирования объектов обеспечивают КОНСТРУКТОРЫ КОПИЙ (Copy Constructors).
Тривиальный конструктор копий, осуществляющий ПОЧЛЕННОЕ КОПИРОВАНИЕ (memberwise copy) полей объектов генерируется автоматически для всех классов по умолчанию, даже если классы определяют собственные конструкторы другого типа. Элементы встроенных типов, включая указатели и ссылки, при почленном копировании просто присваиваются. Для дочерних объектов структур и классов вызываются соответствующие им конструкторы копий. Данные массивов переносятся поэлементно.
О вмешательстве в логику работы конструктора копий следует задуматься в том случае, когда объект выделяет какие-либо ресурсы, не подлежащие прямому копированию. Например, если речь идет о выделении ресурсов в динамической памяти либо открытии файлов, сетевых соединений и др. ресурсов, требующих ручного выделения и освобождения. В таких случаях логика копирования по умолчанию, основанная на почленном копировании, часто может привести к краху программы:
// Создаем стек, конструктор по умолчанию выделяет блок для хранения данных
Stack s;
s.Push( 3 );
s.Push( 5 );
// Создаем копию стека, почленное копирование, новый блок не выделяется
Stack s2 = s;
// Неявные вызовы деструкторов в обратном созданию порядке:
// Stack::~Stack( & s2 );
// Stack::~Stack( & s1 );
// Завершается крахом, попытка повторного удаления памяти через скопированный адрес
Сначала первый стек выделяет в конструкторе память для хранения элементов, которая частично заполняется значениями после вызовов метода Push:
Затем, создается второй объект-копия на основе почленного копирования, в результате которого второй объект ссылается на тот же самый блок памяти, что и первый объект, за счет копирования указателей m_pDataStart и m_pDataTop:
На выходе потока управления из данной области видимости вызываются деструкторы объектов, при чем, в порядке, обратном порядку создания, т.е. для второго стека уничтожение инициируется раньше первого. Второй стек освобождает общий блок памяти в деструкторе:
Наконец, вызывается деструктор для первого стека. Происходит попытка освобождения блока памяти через указатель, который в данный момент продолжает указывать на уже освобожденный вторым стеком блок памяти. Удаление через такой “висячий” указатель (dangling pointer) приводит к краху программы.
Для решения описанной проблемы, дополним стек собственным корректным конструктором копий. Конструктор копий принимает единственный аргумент - ссылку на другой объект-оригинал такого же класса, предпочтительно константную, поскольку оригинал в типичной ситуации не должен менять свое состояние при создании копии.
class Stack
{
// ... public:
// Объявление конструктора копий
Stack ( const Stack & _s );
// ...
};
Корректным способом копирования для рассматриваемого случая является создание вторым стеком идентичного блока памяти для хранения данных и копирования всех хранящихся в первом стеке значений в этот новый блок. Разумеется, с точки зрения производительности нет смысла копировать блок памяти из стека-оригинала целиком, лишь ту часть, которая фактически используется стеком-оригиналом:
// Реализация конструктора копий
Stack::Stack ( const Stack & _s )
: m_size( _s.m_size )
{
// Выделяем такой же блок данных
m_pDataStart = new int[ m_size ];
// Выясняем сколько данных фактически имеется в объекте-оригинале
int nActuallyStored = _s.m_pDataTop - _s.m_pDataStart;
// Смещаем вершину нового стека относительно начала нового блока как в оригинале
m_pDataTop = m_pDataStart + nActuallyStored;
// Копируем только фактически занятую часть памяти из оригинала в новый стек
memcpy( m_pDataStart, _s.m_pDataStart, nActuallyStored * sizeof( int ) ); }
При уничтожении стеков проблемы не возникнет, поскольку каждый из деструкторов будет работать с собственным блоком данных.
Конструкторы копий классов всегда принимают ссылки на объекты такого же класса. Предпочтительным является использование константных ссылок, что подчеркивает неизменность объекта-оригинала и защищает программиста от случайной модификации. В некоторых случаях необходимы конструкторы копий, принимающие неконстантные ссылки на оригинальные объекты, однако этого не следует делать без необходимости (из-за невозможности передачи временных объектов, см. материал ниже).
Конструктор копий не следует определять для классов, семантика копирования которых идентичная почленному копированию. Для объекта-даты допустимо определить конструктор копий. Однако это действие не имеет никакого смысла в виду идентичности синтезированному, соответственно, без необходимости такие конструкторы копий создавать не нужно:
class Date
{
// ... public:
// Объявление конструктора копий
Date( const Date & _d );
// ...
};
// Реализация конструктора копий
Date::Date ( const Date & _d )
: m_Year( _d.m_Year ),
m_Month( _d.m_Month ),
m_Day( _d.m_Day )
{
}
Еще одним относительно простым признаком, по которому можно определить необходимость в собственном конструкторе копий для объектов-значений, является логика операции сравнения на равенство. Если объекты можно сравнить на равенство сравнением всех их полей (это подходит для объекта-даты), значит, скорее всего, специальная логика копирования не потребуется. Напротив, если необходимо выполнение специальных действий для сравнения значений (объект-стек предполагает сравнение содержимого внутренних массивов, а не указателей), скорее всего, понадобится собственный конструктор копий. Можно смело утверждать, что для абсолютного большинства классов в сравнении на равенство и в копировании будут участвовать одни и те же внутренние данные.
К слову, сравнивать объекты-сущности на равенство, как и копировать такие объекты, не имеет смысла. Двух равных объектов-сущностей в программе одновременно существовать не должно.
Уже существующему объекту класса, так же как и переменным структурного типа, в течение жизни может быть присвоено содержимое другого объекта, например, следующим образом:
Date d( 2013, 2, 19 );
// ...
Date today;
// После присвоения объект d будет идентичен объекту today
d = today;
Логику работы в данном случае обеспечивает ОПЕРАТОР КОПИРУЮЩЕГО ПРИСВОЕНИЯ (copy assignment operator), или просто, оператор присвоения.
Аналогично созданию объекта-копии при конструировании, по умолчанию при присвоении применяется сгенерированная компилятором функция почленного копирования.
Компилятор не будет генерировать оператор присвоения для классов, объекты которых содержат переменные-члены ссылочного типа либо константы, поскольку изменить состояние таких переменных-членов после инициализации в конструкторе нельзя:
class MyClass
{
public:
MyClass ( Stack & _stack )
: m_stack( _stack )
{
}
private:
Stack & m_stack; // Переменная-член ссылочного типа
};
int main () {
Stack s1, s2;
MyClass myObject1( s1 ), myObject2( s2 );
myObject1 = myObject2; // ОШИБКА КОМПИЛЯЦИИ! }
error C2582: 'operator =' function is unavailable in 'MyClass'
Вмешиваться в логику работы присвоения следует в тех же самых случаях, что и в логику создания объектов-копий. Если не определить собственного оператора присвоения для класса, зависящего от ручной работы с динамически выделяемыми ресурсами, то сгенерированный оператор на основе почленного копирования просто перезапишет ключевые указатели. Во-первых, это приведет к утечке памяти, поскольку без выполнения освобождения памяти перезаписываются указатели, адресовавшие прежний блок для хранения данных. Во-вторых, это приведет к краху программы при вызове деструкторов, поскольку так же как и при копировании будет существовать более одного объекта, ссылающегося на один и тот же блок данных.
// Создаем первый объект
Stack s1;
s1.Push( 2 );
// Создаем второй объект
Stack s2;
s2.Push( 3 );
s2.Push( 5 );
// Присваиваем содержимое первого объекта второму. Утечка памяти - старый блок
s1 = s2;
// Неявные вызовы деструкторов в обратном порядке:
// Stack::~Stack( & s2 );
// Stack::~Stack( & s1 );
// Завершается крахом из-за попытки повторного удаления памяти
// через скопированный при присвоении адрес
Для решения проблемы следует определить собственный вариант оператора присвоения:
class Stack
{
// ... public:
// Объявление оператора присвоения
Stack& operator = ( const Stack & _s );
// ...
};
// Реализация оператора присвоения
Stack & Stack::operator = ( const Stack & _s )
{
// Защита от присвоения самому себе
if ( & _s == this )
return * this;
// Освобождение ранее занятых ресурсов
delete[] m_pDataStart;
// Так будет сделать нельзя, если поле константно
m_size = _s.m_size;
// Выделяем новый блок данных
m_pDataStart = new int[ m_size ];
// Выясняем сколько данных фактически имеется в объекте-оригинале
int nActuallyStored = _s.m_pDataTop - _s.m_pDataStart;
// Смещаем вершину стека относительно начала нового блока как в оригинале
m_pDataTop = m_pDataStart + nActuallyStored;
// Копируем только фактически занятую часть памяти из оригинала в новый стек
memcpy( m_pDataStart, _s.m_pDataStart, nActuallyStored * sizeof( int ) );
// Возвращаем ссылку на себя
return * this; }
Аналогично конструктору копий, оператор присвоения предполагает передачу единственного аргумента - ссылки на объект такого же класса. Также как и в конструкторе копий, следует отдавать предпочтение передаче по константной ссылке в типичном случае, а ссылкой с правом на запись следует пользоваться лишь при необходимости.
Оператор присвоения должен корректно реагировать на присвоение объекта самому себе, во избежание уничтожения все еще востребованных ресурсов / выделения избыточных. Если не защититься от такой ситуации, возможен крах программы:
Stack s;
s = s; // Объект присваивают самому себе
Вполне корректным поведением для данного граничного случая является игнорирование присвоения. Для этого первым действием в перегруженном оператор является проверка на равенство адресов текущего объекта и объекта-оригинала:
if ( & _s == this ) // Защита от присвоения объекта самому себе СЕБЕ
return * this;
Оператор присвоения должен возвращать ссылку с правом на запись на объект в левой части присвоения, т.е. на текущий объект ( return * this), что позволит использовать значение сразу после присвоений:
Stack s1;
s1.Push( 2 );
Stack s2;
if ( ( s2 = s1 ).IsEmpty() )
// ^ присвоение возвращает ссылку на объект в левой части, // можно использовать дальше для построения более сложного выражения
Часто, имеется существенное перекрытие функциональности между конструктором копий и оператором присвоения. Если такой код нетривиален, его следует обобщить в виде вспомогательной функции. Ниже приведены усовершенствованные варианты конструктора копий и оператора присвоения для стека, имеющие общие части в разделяемой закрытой вспомогательной функции:
class Stack
{
public:
// ...
// Объявление конструктора копий
Stack ( const Stack & _s );
// Объявление оператора присвоения
Stack & operator = ( const Stack & _s );
// ...
private:
// Объявление вспомогательной функции
void CopyDataFrom ( const Stack & _s );
// ...
};
// Реализация конструктора копий
Stack::Stack ( const Stack & _s )
{
CopyDataFrom( _s ); // Все необходимое делает вспомогательная функция
}
// Реализация оператора присвоения
Stack & Stack::operator= ( const Stack & _s )
{
// Защита от присвоения объекта самому себе
if ( this == & _s )
return * this;
// Освобождение ранее занятых ресурсов
delete[] m_pDataStart;
// Выделение нового блока и копирование данных - в теле вспомогательной функции
CopyDataFrom( _s );
return * this;
}
// Реализация вспомогательной функции
void Stack::CopyDataFrom ( const Stack & _s )
{
// Так будет сделать нельзя, если поле константно
m_size = _s.m_size;
// Выделяем новый блок данных
m_pDataStart = new int[ m_size ];
// Выясняем сколько данных фактически имеется в объекте-оригинале
int nActuallyStored = _s.m_pDataTop - _s.m_pDataStart;
// Смещаем вершину стека относительно начала нового блока как в оригинале
m_pDataTop = m_pDataStart + nActuallyStored;
// Копируем только фактически занятую часть памяти из оригинала в новый стек
memcpy( m_pDataStart, _s.m_pDataStart, nActuallyStored * sizeof( int ) );
}
Если логика освобождения занятых ресурсов также является нетривиальной, то можно ввести еще одну вспомогательную функцию, обобщающую код освобождения старых ресурсов, и вызвать из 2 контекстов: оператора присвоения и деструктора.