- •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. Необычный рекуррентный шаблон. Структура, варианты применения.
42. Обработка исключений. Цели, синтаксис выброса и обработчиков. Выбор обработчика по типу. Передача данных исключения по значению, указателю и ссылке. Исключения языка и стандартной библиотеки.
Обработка исключений – поиск и устранение ошибок.
Основные принципы механизма исключений:
- разделение кода нормального функционирования и кода обработки ошибок;
- функция, обнаруживающая ошибку, транспортирует информацию о возникшей проблеме в место обработки;
- возможность выбора между тонкой обработкой конкретного типа ошибки и более широкой группы ситуаций одним или несколькими обработчиками общего назначения;
- малый расход при нормальном выполнении программы .
Синтаксис выброса и обработчиков. Блок try-catch :
try {“выброс” -- что-либо, генерирующее неподходящую инфу, которая призведёт к ошибке выполнения осн. функции,т.е. создание исключения}
catch (тип исключения) {“обработчик”}
После выхода из блока catch работа проги возобновляеться, если на выброшеное исключение не нашелся подходящий блок обработки catch, среда обработки вызовет ф-ю terminate, которая вызовет ф-ю abort, это неловко остановит прогу.
Часто исключения формируются в виде временного объекта некоторого класса. После выброса, исключения помещаются в некоторую специально зарезервированную область служебной памяти - копированием либо перемещением - где они хранятся до завершения обработки.
Разные типы ошибок = много блоков catch. Это выглядит так:
try { // код } catch (int param) { cout << "int exception"; } catch (char param) { cout << "char exception"; } catch (...) { cout << "default exception"; }
Обработчики исключений выбираются в порядке, в котором они записаны в коде по типу, указанному в () после catch, как при вызове ф-ии. Управление передается в тот блок, тип исключения которого признается подходящим первым. Типы сопоставляются по следующему принципу:
фактический тип исключения точно совпадает с типом в обработчике;
тип в обработчике является однозначным базовым классом для фактического типа;
если фактический тип исключения является указателем, а тип в обработчике является точно таким же указателем либо указателем на однозначный базовый класс;
фактический тип исключения является простым копируемым объектом либо ссылкой, а тип в обработчике является ссылкой на это же тип либо ссылкой на однозначный базовый класс;
на сопоставление не влияет дополнительный модификатор const в типе, указываемом в обработчике исключения
Так же возможен перехват всех типов исключений:
catch ( ... ) троеточие вместо типа исключения = все типы
{
std::cerr << “Fatal error. Please contact support@company.com” << std::endl; }
Oбработчики всегда нужно писать от наиболее конкретных классов к базовым.
Если catch (...) и catch(тип исключения) сосуществуют, то обработчик всех типов нужно записать последним, т.к. он примет любой тип.
Оператор throw используеться для генерации исключительных ситуаций
try { throw 20;}, к примеру.
Выбрасывать исключения логично по значению, а перехватывать по обычной либо константной ссылке.
При выбросе выражение-операнд инструкции throw копируется либо перемещается в специальную область памяти, в связи с чем никакой разницы между выбросом непосредственного значения либо ссылки нет. Реальная альтернатива - это выброс указателей. В ряде других языков объекты-исключения выделяются в динамической памяти.
Обработка исключений может быть многоуровневой и вложенной. Предположим функция main вызывает функцию runProgram, а та в свою очередь вызывает другие функции, которые могут выбросить исключения. В runProgram обрабатываются конкретные исключения, а main перехватывает исключения общего назначения:
void runProgram ()
{
try
{
ManagedIntegerArray a( 10 );
a[ rand() ] = 25;
}
catch ( ManagedIntegerArray::IndexOutOfRange & e )
{
std::cout << “Program has a problem with array index” << std::endl;
} }
int main ()
{
try
{
runProgram();
}
catch ( ... )
{
std::cout << “Program has some unknown problem” << std::endl;
} }
При выбросе исключения подходящий обработчик ищется по стеку вызовов функций. Если первый встретившийся блок обработчиков catch не содержит подходящего по типу обработчика, управление будет передаваться на следующий внешний уровень, и т.д., пока не выйдет из программы вызовом terminate.
Иногда стратегия многоуровневой обработки ошибок состоит в поэтапном восстановлении работоспособности. Ближайший к месту выброса исключения обработчик catch делает максимум возможного восстановления на своем уровне, но после должен передать управление на следующий уровень, поскольку полностью устранить последствия возникновения ошибки не удается. В таком случае применяется повторная генерация исключения (rethrow), инструкция throw пишется без аргументов. При этом те же самые данные исключения передаются далее по цепочке на обработку:
void runProgram ()
{
try
{
ManagedIntegerArray a( 10 );
a[ rand() ] = 25;
}
catch ( ManagedIntegerArray::IndexOutOfRange & e )
{
// Локальная обработка
std::cout << “Program has a problem with array index” << std::endl;
// Повторная генерация того же исключения
throw;
} }
Если повторную генерацию инициировать в момент, когда обработки исключения на самом деле не происходит (синтаксис языка позволяет написать инструкцию throw без аргументов в теле любой функции), то это приведет к немедленному вызову функции termin
Передача и перехват указателя:
try
{
throw new DerivedException( 0, 0 );
}
catch ( DerivedException * _pE )
{
delete _pE;
}
Если не удалять объекты-исключения, перехватываемые по указателю, то это может привести к утечкам памяти, когда на стороне выброса использовалось динамическое выделение памяти. Если удалять все объекты-исключения, можно получить фатальный сбой при попытке удаления объекта, не хранящегося в динамической памяти, такого как globalException. Соответственно, вариант с указателями большинство программистов отбросит из рассмотрения.
Перехватывать объекты-исключения можно по значению либо по ссылке. Если принимать исключение по значению, будет выполняться копирование его содержимого (скорее всего, избыточное с точки зрения производительности). Если же принимать исключение по ссылке, копирования происходить не будет, обработчик будет работать с содержимым объекта-исключения в специальной служебной памяти. Обычно, объект-исключение содержит данные, требующие нетривиального копирования, например, строки в динамической памяти. Отсюда, большинство разработчиков предпочтет вариант с перехватом по ссылке, а не по значению, чтобы избежать избыточного копирования этих строк. Обычно используют константные ссылки, чтобы избежать случайной модификации объекта-исключения.
Исключения стандартной библиотеки. . Все исключения, выбрасываемые операторами языка и средствами стандартной библиотеки, образуют иерархию наследования классов, вершиной которой является класс std::exception.
Определение класса std::exception и большинства производных от него классов становится доступным после включения заголовочного файла <exception>. std::exception является конкретным классом, объекты которого могут быть непосредственно созданы. Такой объект содержит минимальную функциональность, а именно:
конструктор по умолчанию;
конструктор копий и оператор присвоения;
виртуальный деструктор;
виртуальный метод what(), возвращающий строку в стиле C, описывающую исключение.
Производные классы могут переопределять метод what() для выдачи более информативных сообщений.
Часто используемые исключения:
исключение std::bad_allocвыбрасывается операторомnew, когда он не может обеспечить выделение запрошенного блока динамической памяти;
исключение std::bad_cast выбрасывается операторомdynamic_cast, когда запрошенного преобразования не существует, а работа ведется со ссылками, а не с укаталеями;
исключение std::bad_typeidвыбрасывается операторомtypeid, если его вызвать на нулевом указателе полиморфного типа;
исключение std::out_of_rangeвыбрасывается контейнерами std::vector и std::deque при попытке обращения к ячейке по некорректному индексу;
исключение std::ios_base::failureвыбрасывается библиотекой ввода/вывода в ряде ошибочных ситуаций - это поведение отключено по умолчанию, чтобы его включить, необходимо вызвать метод exceptions, и тогда потоки в ошибочных ситуациях будут генерировать исключения:
std::ifstream file; file.exceptions ( std::ifstream::failbit | std::ifstream::badbit );
Стандартные классы исключений std::logic_error и std::runtime_error принимают в качестве аргумента при создании строку std::string, описывающую возникшую проблему. По умолчанию, метод what() возвращает именно эту строку, что делает такие классы довольно удобными для обработки исключений в простых программах.
Класс std::logic_error предполагается использовать для ошибок внутренней логики программы, таких как нарушение логических предусловий и инвариантов. Такие ошибки являются, как правило, исправимыми в коде программы до ее запуска. Класс std::runtime_error, напротив, предназначен для ошибок, которые можно обнаружить только во время выполнения, например ошибки переполнения АТД “стек” фиксированного размера. Предполагается, что эти классы будут базовой отправной точкой для пользовательских классов-исключений. В прочем, часто пользовательские классы-исключения наследуют непосредственно от вершины иерархии - класса std::exception.
43. Процесс stack unwinding при выбросе исключений. Уничтожение полностью сконструированных локальных объектов. Утечки памяти при выбросе исключений. Исключения в конструкторах и деструкторах. Повторная генерация исключений.
Процесс поиска обработчика сгенерированного исключения по стеку вызовов функций называется stack unwinding(дословно, “разматывание” или “раскрутка” стека).
Собственно, в рамках этого процесса решаются 2 основные задачи:
Поиск подходящего обработчика исключения по стеку вызовов.
Вызов деструкторов локальных объектов.
Независимо от того произошла некоторая исключительная ситуация или нет, должны быть вызваны деструкторы всех локальных объектов, полностью сконструированных к этому моменту.
Поскольку выброс исключения прерывает нормальный поток выполнения программы, к моменту выброса некоторые объекты могут быть еще не сконструированными.
Рассмотрим следующий простейший пример:
void f ()
{
Date d1;
if ( d1.GetDay() == 1 )
throw std::runtime_error( “Cannot call f() on the 1st day of the month” );
Date d2( 2013, 12, 23 );
// ...
}
В зависимости от текущего числа, может быть сгенерировано исключение. К этому моменту локальный объект d1 уже сконструирован, в то время как объект d2 еще не сконструирован. Механизм stack unwinding должен гарантировать, что в таком случае будет вызван деструктор объекта d1, но не будет вызван деструктор объекта d2.
Для реализации такого поведения весьма полезным свойством языка является вызов деструкторов локальных объектов в обратном конструированию порядке. В частности, в обычной ситуации, когда исключения не происходит, при выходе потока управления из функции f, компилятор будет вызывать деструктор сначала на объекте d2, а затем на объекте d1:
void f ()
{
Date d1;
if ( d1.GetDay() == 1 )
throw std::runtime_error( “Cannot call f() on the 1st day of the month” );
Date d2( 2013, 12, 23 );
// ...
// Date::~Date( & d2 );
// Date::~Date( & d1 );
}
Эта особенность чрезвычайна удобна для организации в компиляторе очистки при выбросе исключения. В случае выброса исключения в части функции, когда объект d1 уже сконструирован, а объект d2 еще нет, необходимо каким-либо образом “перескочить” вызовы деструкторов объектов, которые еще не сконструированы, и продолжить начиная с деструкторов, которые нужно вызвать.
Любую функцию условно можно условно разбить на зоны. В данном конкретном примере 3 зоны:
зона 0 - не сконструировано ни одного объекта;
зона 1 - сконструирован объект d1, но не d2;
зона 2 - сконструированы объекты d1 и d2.
Чтобы упростить переход к нужному вызову деструктора, типичный компилятор резервирует на стеке каждом блоке инструкций специальную переменную (обычно, размером 1 байт), играющую роль текущего номера зоны. Как только один из объектов завершает конструирование, номер зоны увеличивается. По этому номеру зоны библиотека времени выполнения и определяет с какого деструктора следует начать очистку стека.
Простейший вариант решения проблемы утечкиресурса при генерации исключения состоит в перехвате всех исключений, освобождении ресурсов и повторной генерации исключения:
void f ()
{
Date * pDate = new Date();
try
{
if ( pDate->GetDay() == 1 )
throw std::runtime_error( “...” );
// ...
// Нормальное освобождение ресурса
delete pDate;
}
catch ( ... )
{
// Аварийное освобождение ресурса
delete pDate;
// Повторная генерация исключения
throw;
}
}
void g ()
{
FILE * file = fopen( “test.txt”, “rt” ); try
{
if ( some_condition )
throw std::runtime_error( “Condition failed” );
// …
// Нормальное освобождение ресурса
fclose( file );
}
catch ( ... )
{
// Аварийное освобождение ресурса
fclose( file );
// Повторная генерация исключения
throw;
}
}
Исключения в конструкторах и деструкторах
Особого внимания требует организация обработки ошибок в конструкторах и деструкторах классов, поскольку находясь в этих функциях объект подвергается опасности остаться в некорректном состоянии в случае возникновения исключения.
Если исключение генерируется при конструировании объекта, он считается неполностью сконструированным, и его деструктор не вызывается.Например, класс Date генерирует исключения, если передать неправильные компоненты даты:
try
{
Date d1( 2013, 14, 1 ); // 14 месяц не существует
// До вызова деструктора выполнение не дойдет:
// Date::~Date( & d1 );
}
catch ( ... )
{
}
В случае динамического выделения памяти для нового объекта и появления исключения в конструкторе, во избежание явной утечки памяти, будет автоматически вызван оператор delete:
try
{
Date * pDate = new Date( 2013, 14, 1 ); // 14 месяц не существует
// До вызова деструктора выполнение не дойдет, но будет вызван оператор delete
}
catch ( ... )
{
}
Если ресурс выделяется до выброса исключения в конструкторе, он не будет освобожден, поскольку деструктор не полностью сконструированного объекта вызываться не будет. Это не относится к полностью сконструированным дочерним объектам, их деструкторы будут вызваны сразу при выбросе исключения.
Также следует быть чрезвычайно аккуратным при выбросе исключений в деструкторах. Нежелательно выбрасывать исключения в деструкторах в принципе. Риск состоит в том, что деструктор объекта, выбрасывающий исключение, может быть вызван как при нормальном выполнении программы, так и в ходе процесса stack unwinding, когда уже обрабатывается другое исключение. Правила языка запрещают одному и тому же потоку управления выбрасывать второе исключение во время обработки первого, и такое нарушение приведет к прерыванию выполнения программы.
Чтобы смягчить такой риск, можно использовать функцию std::uncaught_exception, вызвав которую можно определить, что система в данный момент находится в процессе stack unwinding и обрабатывает исключение.
Иногда стратегия многоуровневой обработки ошибок состоит в поэтапном восстановлении работоспособности. Ближайший к месту выброса исключения обработчик catch (блок catch перехватывает конкретное исключение и выводит информативное сообщение об ошибке) делает максимум возможного восстановления на своем уровне, но после должен передать управление на следующий уровень, поскольку полностью устранить последствия возникновения ошибки не удается. В таком случае применяется повторная генерация исключения (rethrow), инструкция throw пишется без аргументов. При этом те же самые данные исключения передаются далее по цепочке на обработку:
void runProgram ()
{
try
{
ManagedIntegerArray a( 10 );
a[ rand() ] = 25;
}
catch ( ManagedIntegerArray::IndexOutOfRange & e )
{
// Локальная обработка
std::cout << “Program has a problem with array index” << std::endl;
// Повторная генерация того же исключения
throw;
} }
Если повторную генерацию инициировать в момент, когда обработки исключения на самом деле не происходит, то это приведет к немедленному вызову функции terminate (по умолчанию вызовет функцию abort, аварийно завершив программу).