- •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. Необычный рекуррентный шаблон. Структура, варианты применения.
46. Понятие обобщенной концепции. Статический полиморфизм по сравнению с динамическим полиморфизмом.
Рассмотрим простейший обобщенный алгоритм поиска элемента в массиве:
#include <cassert>
template< typename T >
const T * MyFind ( const T * _pArray, int _nElements, const T & _value )
{
for ( int i = 0; i < _nElements; i++ )
if ( _pArray[ i ] == _value )
return _pArray + i;
return nullptr;
}
int main ()
{
int data[ 5 ] = { 1, 2, 3, 4, 5 };
const int * pResult = MyFind( data, 5, 3 );
assert( pResult && ( ( pResult - data ) == 2 ) );
}
Данный алгоритм может быть использован для организации линейного поиска в массиве любого типа. К сожалению, его нельзя применить к более сложным структурам данных, например к связными спискам. Тем не менее, суть алгоритма линейного поиска для связного списка бы выглядела практически идентично, с отличиями в способе перебора элементов для сравнения с искомым значением.
В обычном массиве данные расположены в памяти последовательно, и достаточно сдвигать на каждом шаге значение индекса. В связном списке необходимо перебирать узлы двигаясь по межузловым связям. Соответственно, нужна подобная реализация алгоритма, отличающаяся от предыдущей лишь способом выбора следующего значения для сравнения.
template< typename T >
class List
{
public:
struct Node
{
T m_value;
Node * m_pNextNode;
Node ( const T& _value )
: m_value( _value ),
m_pNextNode( nullptr )
{
}
};
private:
Node * m_pFirstNode, * m_pLastNode;
List ( const List< T >& );
List< T > & operator = ( const List< T > & );
public:
List ()
{
m_pFirstNode = m_pLastNode = nullptr;
}
~List ()
{
Node * pCurrent = m_pFirstNode;
while ( pCurrent )
{
Node * pTemp = pCurrent->m_pNextNode;
delete pCurrent;
pCurrent = pTemp;
}
}
void push_back ( const T & _value )
{
Node * pNewNode = new Node( _value );
if ( m_pLastNode )
{
m_pLastNode->m_pNextNode = pNewNode;
m_pLastNode = pNewNode;
}
else
m_pFirstNode = m_pLastNode = pNewNode;
}
const Node * first_node () const
{
return m_pFirstNode;
}
};
template< typename T >
const typename List< T >::Node *
MyFind ( const List< T > & _list, const T & _value )
{
const List< T >::Node* pCurrentNode = _list.first_node();
while ( pCurrentNode )
{
if ( pCurrentNode->m_value == _value )
return pCurrentNode;
pCurrentNode = pCurrentNode->m_pNextNode;
}
return nullptr;
}
int main ()
{
List < int > myList;
myList.push_back( 1 );
myList.push_back( 2);
myList.push_back( 3 );
myList.push_back( 4 );
myList.push_back( 5 );
const List< int >::Node * pNode = MyFind( myList, 3 );
assert( pNode && pNode->m_value == 3 );
}
Хотелось бы универсализировать алгоритм поиска таким образом, чтобы он работал с любой структурой данных, допускающей перебирание элементов. Это бы позволило инстанцировать один и тот же обобщенный алгоритм для различных хранилищ данных не зависимо от природы и сложности их внутренней организации.
Осуществим ряд предварительных шагов, преследующих данное намерение. Во-первых, незначительно видоизменим вариант реализации для массива, заменив количество элементов в массиве на адрес элемента, следующего за последним. Если элемент не будет найден вообще, реализация возвращает переданный адрес элемента, следующего за последним. Технически прежняя реализация остается практически неизменной, функционально идентичной, однако это принципиальный шаг к достижению преследуемой цели, как будет показано ниже.
template< typename T >
const T * MyFind ( const T * _pArrayFirst,
const T * _pArrayLast,
const T & _value )
{
const T * pCurrent = _pArrayFirst;
while ( pCurrent != _pArrayLast )
{
if ( * pCurrent == _value )
return pCurrent;
}
return _pArrayLast;
}
Тестовый код также меняется незначительно:
int main ()
{
int data[ 5 ] = { 1, 2, 3, 4, 5 };
const int * pResult = MyFind( data, data + 5, 3 );
assert( pResult && ( ( pResult - data ) == 2 ) );
}
На первом шаге мы избавились от непосредственной работы с элементами массивов напрямую через индексы, полностью заменив их указателями.
Пара передаваемых указателей задает интервал последовательности, которую необходимо обработать. Когда эти указатели корректны и адресуют существующий цельный набор значений в памяти, такой интервал называют ДЕЙСТВИТЕЛЬНЫМ, в противном случае интервал был бы НЕДЕЙСТВИТЕЛЬНЫМ.
На втором шаге преобразовываем код в форму, избегающую явного использования указателей. Для этой цели вводим дополнительный уровень косвенности - специальный аргумент шаблона функции, скрывающий указатели. При этом требуем от данного аргумента шаблона наличия операций сравнения со значением такого же типа (==, !=), присвоения (=), операции префиксного инкремента (++X), означающего переход к следующему элементу, а также операцию разыменования ( * x ), выбирающую адресуемое в данный момент значение. Во всех прежних контекстах использования указателя на аргумент-тип используем новый аргумент шаблона.
template< typename It, typename T >
It MyFind ( It _first, It _last, const T & _value )
{
It current = _first;
while ( current != _last )
{
if ( * current == _value )
return current;
++ current;
}
return _last;
}
Тестовый код остается без изменений. При инстанцировании с массивом, компилятор автоматически выводит, что в качестве фактического аргумента шаблона используется указатель на целочисленный тип.
Такой обобщенный аргумент, скрывающий указатель на обрабатываемые данные, называется ИТЕРАТОРОМ. Итератор представляет собой пример понятия ОБОБЩЕННОЙ КОНЦЕПЦИИ (generic concept) - именованного набора операций, которые должен поддерживать тип-аргумент для взаимодействия с тем или иным алгоритмом. В случае итератора, речь идет о наборе операций, необходимых для обеспечения последовательного доступа алгоритма к данным, хранящимся в структуре любой сложности. Если в качестве типа для инстанцирования в качестве итератора будет предоставлено нечто, что соответствует требованиям обобщенной концепции итератора, то такой тип и будет являться итератором (принцип “если это похоже на черепаху, движется как черепаха, значит это черепаха”).
Ключевая роль итераторов состоит в минимизации зависимости между конкретным способом хранения данных и алгоритмами обработки этих данных. Имея в распоряжении подходящий итератор, однажды написанный алгоритм можно применить к любой структуре данных.
В частности, чтобы этот код алгоритма заработал будучи примененным к связным спискам, необходимо передать объекты некоторого типа, которые способны перебирать внутренние узлы списка и извлекать из них значения. Ниже приведен пример реализации концепции итератора для узлов односвязного списка:
template< typename T >
class List
{
public:
// ...
struct Iterator
{
Node * m_pCurrentNode;
Iterator ( Node * _pCurrentNode )
: m_pCurrentNode( _pCurrentNode )
{
}
bool operator == ( const Iterator & _it ) const
{
return m_pCurrentNode == _it.m_pCurrentNode;
}
bool operator != ( const Iterator & _it ) const
{
return !( * this == _it );
}
Iterator & operator ++ ()
{
assert( m_pCurrentNode );
m_pCurrentNode = m_pCurrentNode->m_pNextNode;
return * this;
}
const T& operator * () const
{
assert( m_pCurrentNode );
return m_pCurrentNode->m_value;
}
};
Iterator begin ()
{
return Iterator( m_pFirstNode );
}
Iterator end ()
{
return Iterator( nullptr );
}
// ...
};
Соответственно, можем инстанцировать единый обобщенный алгоритм поиска для узлов нашего связного списка при помощи следующего кода:
const List< int >::Iterator it = MyFind( myList.begin(), myList.end(), 3 );
assert( * it == 3 );
Алгоритм будет успешно взаимодействовать с любой структурой данных, способной предоставить некоторый объект встроенного или пользовательского типа, реализующий описанную обобщенную концепцию итератора. Примечательно, что как алгоритм может быть применен к любому набору структур данных, так и итераторы, соответствующие различным структурам данных широко используются в различного рода алгоритмах.