Все специализации

C++ разработчик: вопросы на собеседовании

136 вопросов с разбором ответов — те формулировки, которые действительно встречаются на интервью.

std::map — это ассоциативный контейнер, хранящий элементы как пары ключ-значение, отсортированные по ключам. Основан на сбалансированном бинарном дереве поиска (обычно красно-черном дереве).

std::unordered_map — также ассоциативный контейнер, хранящий пары ключ-значение, но без определенного порядка. Основан на хеш-таблице.

Основные отличия:

Порядок элементов:

std::map: Элементы отсортированы по ключам.

std::unordered_map: Элементы не отсортированы, порядок зависит от хеш-функции и состояния хеш-таблицы.

Производительность:

std::map:

Поиск, вставка и удаление: в среднем O(log N), где N — количество элементов.

Время доступа не зависит от содержимого.

std::unordered_map:

Поиск, вставка и удаление: в среднем O(1).

В худшем случае (при коллизиях) O(N). Время доступа зависит от качества хеш-функции и коэффициента заполнения.

std::map:

std::unordered_map:

Использование памяти:

std::map: Требует немного больше памяти для хранения узлов дерева.

std::unordered_map: Может требовать больше памяти при низком коэффициенте заполнения (для уменьшения коллизий). Зависит от реализации.

Требования к ключу:

std::map: Ключи должны поддерживать операцию сравнения (operator<).

std::unordered_map: Ключи должны поддерживать:

Равенство (operator==).

Хеширование (специализация std::hash или предоставление хеш-функции).

Итераторы:

std::map: Двунаправленные итераторы, обходят элементы в отсортированном порядке.

std::unordered_map: Итераторы вперед, обходят элементы в произвольном порядке (зависящем от структуры хеш-таблицы).

Пример использования:

Выбор между std::map и std::unordered_map зависит от требуемого функционала: если важен отсортированный порядок элементов или ключи нельзя хешировать/сравнивать на равенство эффективно, используется std::map. Если важна максимальная производительность для поиска/вставки/удаления, и порядок элементов не имеет значения, предпочтительнее std::unordered_map (при условии хорошей хеш-функции).

std::future — это класс-шаблон в C++, представляющий результат (значение или исключение) асинхронной операции, который станет доступным в будущем. Он используется для получения результата выполнения задачи, запущенной в отдельном потоке или посредством std::async.

Ключевые особенности:

Асинхронность: Позволяет выполнить операцию, не блокируя текущий поток.

Получение результата: Метод get() позволяет получить результат операции. Если результат еще не готов, get() заблокирует текущий поток до его готовности или до получения исключения.

Передача данных: Связывается с объектом std::promise, который предоставляет способ установить результат или исключение.

Состояние: std::future имеет состояние, указывающее, готов ли результат. Можно проверить с помощью wait() или wait_for().

Однократное получение: Метод get() можно вызвать только один раз для каждого объекта std::future.

Тип результата: Типизируется типом возвращаемого значения асинхронной операции.

Пример использования с std::async:

Пример использования с std::promise:

Важные методы std::future:

std::future является частью библиотеки <future> и предоставляет удобный способ управления результатами асинхронных вычислений, упрощая разработку многопоточных приложений.

std::back_inserter — это итератор-вставитель (inserter iterator) из стандартной библиотеки C++.

Он позволяет вставлять элементы в конец последовательного контейнера (например, std::vector, std::list, std::deque) с помощью алгоритмов, которые обычно требуют итераторов для записи (например, std::copy, std::transform).

При использовании std::back_inserter оператор присваивания (*it = value) вызывает соответствующий метод вставки в конец контейнера (push_back).

Преимущество std::back_inserter в том, что он автоматически управляет выделением памяти, расширяя контейнер по мере необходимости, в отличие от обычного итератора, который требует, чтобы целевая область памяти уже была достаточного размера.

Простой пример использования с std::copy:

Асимптотическая сложность операций для std::unordered_set и std::set в C++17:

std::unordered_set основан на хеш-таблице. Средний случай сложности O(1) достигается при хорошей хеш-функции и отсутствии большого количества коллизий. Худший случай O(n) возникает при сильных коллизиях (например, все элементы хешируются в один бакет) или при неэффективном распределении элементов.

std::set основан на сбалансированном бинарном дереве поиска (обычно красно-черном дереве). Сложность O(log n) обусловлена логарифмической высотой дерева, где n — количество элементов.

Важные заметки:

Худший случай для std::unordered_set вставка и удаление может включать рехеширование, что при надобности перестроить всю таблицу займет O(n).

Операция доступа для std::set через итератор также имеетлогаррифмическую сложность.

Для денежного эквивалента следует использовать тип данных, который может точно представлять фиксированную десятичную точку, чтобы избежать ошибок округления, присущих типам с плавающей запятой (float, double).

Наиболее подходящие варианты:

long long (в комбинации с масштабированием): Хранить денежное значение в виде целого числа в наименьших единицах (например, копейках, центах). Это обеспечивает точность и избегает проблем с плавающей точкой.

long long amount_in_kopecks = 12345; // 123 рубля 45 копеек

Библиотека для работы с десятичными числами: Использовать специализированные библиотеки, предоставляющие тип данных для финансовых вычислений. Это предпочтительный вариант для сложных операций и обеспечения максимальной точности. Примеры: GMP, Boost.Multiprecision.

// Пример с использованием гипотетической библиотеки Decimal

Decimal amount = "123.45";

Не следует использовать:

float, double: Могут привести к ошибкам округления при точных финансовых расчетах.

Инвалидация итераторов std::unordered_map происходит при изменении структуры хеш-таблицы или удалении элементов.

Основные случаи:

Вставка: При вставке элемента может потребоваться перехеширование (реаллокация и перестройка таблицы), если коэффициент загрузки превышает max_load_factor(). В этом случае все итераторы, ссылки и указатели на элементы становятся недействительными. Если перехеширования не происходит, итераторы на существующие элементы остаются валидными, а итератор, указывающий на конец контейнера (end()), может стать недействительным.

Удаление: Удаление одного элемента с помощью итератора (erase(pos)) инвалидирует только итератор pos и ссылки/указатели на удаленный элемент. Итераторы на другие элементы остаются валидными. Удаление диапазона элементов (erase(first, last)) инвалидирует все итераторы, ссылки и указатели в удаленном диапазоне. Использование clear() или удаление с помощью ключа (erase(key)) инвалидирует все итераторы, ссылки и указатели на удаленные элементы. Итераторы на другие элементы остаются валидными.

Перехеширование (rehash): Явный вызов rehash() или автоматическое перехеширование из-за вставки инвалидирует все итераторы, ссылки и указатели.

Резервирование (reserve): Аналогично перехешированию, явный вызов reserve() может вызвать реаллокацию и инвалидацию всех итераторов, ссылок и указателей.

Важно помнить, что в unordered_map порядок элементов не гарантируется, и при изменении контейнера расположение элементов может меняться независимо от инвалидации итераторов.

Написание собственной библиотеки может потребоваться по нескольким причинам:

Отсутствие готового решения: Не всегда существуют готовые библиотеки, реализующие необходимую уникальную функциональность, специфичную для решаемой задачи или предметной области.

Производительность и оптимизация: Существующие библиотеки могут быть недостаточно производительными или иметь неоптимальную реализацию для конкретных сценариев использования. Создание собственной позволяет точно контролировать алгоритмы и структуры данных.

Интеграция и совместимость: Типичной проблемой является необходимость интеграции с существующей кодовой базой или использование специфических платформ/железа, для которых нет поддерживаемых сторонних библиотек.

Улучшение качества кода и снижение зависимостей: Собственная библиотека позволяет использовать стандарты кодирования команды, минимизировать сторонние зависимости, что упрощает поддержку и снижает риск проблем с совместимостью версий.

Обучение и понимание: Разработка библиотеки дает глубокое понимание underlying принципов и механизмов.

Пример сценария, где может возникнуть необходимость написать свою библиотеку:

Необходима высокопроизводительная библиотека параллельной обработки изображений на специфичном оборудовании (например, с использованием пользовательских IP-ядер на ПЛИС), для которой нет готовых решений в OpenCL или CUDA.

Механизм виртуальности в C++ реализуется с помощью указателей на функции (таблица виртуальных функций, или VMT - Virtual Method Table) и указателя на эту таблицу (vptr - virtual pointer), который добавляется в каждый объект класса с виртуальными функциями или унаследованный от такого класса.

При объявлении функции как virtual компилятор создает VMT для данного класса. Эта таблица содержит указатели на фактические реализации виртуальных функций для этого класса.

В каждом объекте класса (или производного от него) добавляется скрытый указатель vptr, который ссылается на VMT соответствующего типа объекта.

Когда виртуальная функция вызывается через указатель или ссылку на базовый класс, компилятор генерирует код, который использует vptr объекта для поиска адреса нужной функции в VMT и ее вызова. Это называется динамической диспетчеризацией (или поздним связыванием), так как решение о том, какая именно функция будет вызвана, принимается во время выполнения программы, а не во время компиляции.

Пример:

В этом примере, несмотря на то что ptr имеет тип Base*, при вызове ptr->greet() фактически будет вызвана реализация функции greet из класса Derived, потому что vptr объекта Derived, на который указывает ptr, ссылается на VMT класса Derived.

Невртуальные функции вызываются через статическую диспетчеризацию (раннее связывание), где адрес функции определяется во время компиляции на основе типа указателя или ссылки.

Ключевые аспекты:

Полиморфизм времени выполнения: Виртуальные функции обеспечивают возможность вызывать правильную версию функции в зависимости от фактического типа объекта, а не от типа указателя/ссылки.

Накладные расходы: Виртуальные функции добавляют небольшие накладные расходы на создание объекта (для vptr) и каждый вызов (для поиска в VMT).

override: Ключевое слово override в C++11 (и более поздних) помогает предотвратить ошибки при переопределении виртуальных функций, указывая, что функция в производном классе должна переопределять функцию из базового класса.

Виртуальный деструктор: Важно объявлять деструктор базового класса виртуальным, если планируется удалять объекты производных классов через указатель на базовый класс, чтобы избежать утечек памяти.

В C++ std::set и std::map обычно реализуются с использованием сбалансированных бинарных деревьев поиска, чаще всего красно-черных деревьев.

Красно-черное дерево:

Это самобалансирующееся бинарное дерево поиска, которое поддерживает порядок элементов и обеспечивает логарифмическую сложность для большинства операций (вставка, удаление, поиск).

Основные свойства красно-черного дерева:

Каждый узел имеет цвет: красный или черный.

Корень дерева всегда черный.

Листья (нулевые узлы) всегда черные.

Красный узел не может иметь красного потомка.

Все простые пути от любого узла до листьев, входящих в поддерево этого узла, содержат одинаковое количество черных узлов.

Как это применяется в std::set и std::map:

std::set: Хранит уникальные элементы в отсортированном порядке. Каждый узел дерева содержит сам элемент. Сравнение элементов используется для определения порядка в дереве.

// Пример узла в std::set (концептуально)

template <typename Key>

struct SetNode {

Key key;

SetNode* left;

SetNode* right;

SetNode* parent;

Color color; // Красный или черный

};

std::map: Хранит пары "ключ-значение". Элементы сортируются по ключу. Каждый узел дерева содержит пару "ключ-значение". Сравнение производится по ключу.

// Пример узла в std::map (концептуально)

template <typename Key, typename Value>

struct MapNode {

std::pair<const Key, Value> value; // value.first - ключ, value.second - значение

MapNode* left;

MapNode* right;

MapNode* parent;

};

Операции и их сложность:

Благодаря свойствам красно-черного дерева, основные операции имеют следующую временную сложность:

n - количество элементов в контейнере.

Вставка и удаление в красно-черном дереве включают перекрашивание узлов и вращения дерева для поддержания баланса. Это гарантирует, что глубина дерева остается логарифмической, что важно для производительности. Итераторы std::set и std::map обычно реализуются как итераторы по дереву, которые позволяют обходить элементы в отсортированном порядке (in-order traversal).

Вызов free для указателя, равного nullptr, не приводит к ошибке или аварийному завершению программы. Стандарт языка C и C++ гарантируют такое поведение. Функция free просто ничего не делает в этом случае.

shared_ptr — это умный указатель, реализующий семантику владения разделяемым ресурсом. Он хранит указатель на объект и указатель на управляющий блок.

Управляющий блок содержит:

Счетчик сильных ссылок (reference count).

Счетчик слабых ссылок (weak count).

Пользовательский функтор удаления (deleter), если задан.

Пользовательский аллокатор, если задан.

Указатель на хранимый объект (то же самое, что хранится в самом shared_ptr).

Принцип работы:

Создание: При создании первого shared_ptr, указывающего на объект, создается управляющий блок, счетчики ссылок инициализируются: сильных — 1, слабых — 0.std::shared_ptr<int> ptr1 = std::make_shared<int>(10); // Создает объект int и управляющий блок

Копирование: При копировании shared_ptr счетчик сильных ссылок в управляющем блоке увеличивается на 1.std::shared_ptr<int> ptr2 = ptr1; // Увеличивает счетчик сильных ссылок

Присваивание: При присваивании shared_ptr старому ресурсу Decrement-ится (уменьшается) счетчик сильных ссылок, а новому ресурсу Increment-ится (увеличивается).std::shared_ptr<int> ptr3;

ptr3 = ptr1; // Decrement ptr3's старого ресурса, Increment ptr1's ресурса

Удаление: При уничтожении shared_ptr (например, выход из области видимости) счетчик сильных ссылок Decrement-ится.{

std::shared_ptr<int> ptr4 = ptr1; // Увеличивает счетчик сильных ссылок

// ptr4 выходит из области видимости

} // Decrement-ит счетчик сильных ссылок

Освобождение ресурса: Когда счетчик сильных ссылок достигает нуля, ресурс (объект, на который указывает shared_ptr) удаляется с использованием заданного функтора удаления (или delete по умолчанию).

Освобождение управляющего блока: Управляющий блок освобождается, когда оба счетчика (сильных и слабых) достигают нуля.

Преимущества:

Автоматическое управление памятью для разделяемых ресурсов.

Безопасность от двойного освобождения.

Поддержка пользовательских функторов удаления.

Недостатки:

Циклические ссылки могут привести к утечкам памяти (решается с помощью weak_ptr).

Незначительное увеличение накладных расходов из-за управляющего блока и атомарных операций со счетчиками.

Потокобезопасность операций со самим shared_ptr (копирование, присваивание, изменение хранимого указателя), но не с объектом, на который он указывает.

unique_ptr — эксклюзивное владение ресурсом, shared_ptr — совместное владение ресурсом с подсчетом ссылок.

unique_ptr:

Не может быть скопирован, только перемещен (std::move).

Низкие накладные расходы (аналогично сырому указателю).

При уничтожении unique_ptr ресурс освобождается.

shared_ptr:

Может быть скопирован.

Использует счетчик ссылок: ресурс удаляется, когда последний shared_ptr на него уничтожается.

Большие накладные расходы (хранит счетчик ссылок и указатель на ресурс).

Возможны циклические ссылки (решаются с помощью weak_ptr).

Сравнение:

Для использования пользовательского класса в качестве ключа в std::unordered_map необходимо:

Перегрузить оператор сравнения на равенство (operator==) для вашего класса. unordered_map использует его для определения идентичности ключей.

Предоставить хеш-функцию для вашего класса. Это может быть сделано одним из следующих способов:

Специализация шаблонной структуры std::hash для вашего класса.

Передача объекта хеш-функции в качестве третьего аргумента конструктора std::unordered_map.

Пример специализации std::hash:

Пример передачи функтора хеширования в конструктор:

Важно, чтобы хеш-функция была детерминированной (всегда возвращала один и тот же хеш для одного и того же объекта) и обеспечивала хорошее распределение хешей для минимизации коллизий.

std::map — ассоциативный контейнер, хранящий пары "ключ-значение", отсортированные по ключу. Основан на красно-черном дереве. Время доступа, вставки и удаления элементов логарифмическое (O(log n)).

std::unordered_map — ассоциативный контейнер, хранящий пары "ключ-значение" в хэш-таблице. Элементы не отсортированы. В среднем время доступа, вставки и удаления элементов константное (O(1)), но в худшем случае может быть линейным (O(n)) при наличии коллизий. Требует наличия хэш-функции для типа ключа и оператора сравнения на равенство (operator==).

Пример использования:

std::function — это полиморфный обёртка для любых вызываемых объектов (функций, указателей на функции, лямбда-выражений, фанкторов, указателей на функции-члены). Она позволяет унифицировать синтаксис вызова для различных типов объектов, которые можно вызвать.

Основные возможности:

Хранение callable объектов: Может хранить любой объект, для которого определён оператор () или который может быть вызван как функция.

Типобезопасность: Проверяет сигнатуру хранимого объекта на этапе компиляции.

Полиморфизм: Позволяет работать с различными типами callable объектов единообразно.

Пример использования:

std::function удобен для реализации колбэков, хранения функций с различными источниками, но одним интерфейсом, и для передачи вызываемых объектов в качестве аргументов функции. Стоит учитывать, что использование std::function может повлечь небольшие издержки по сравнению с прямым вызовом или использованием шаблонов, связанные с аллокацией памяти и виртуальными вызовами.

std::cout — это экземпляр класса std::ostream. При выводе данных в std::cout (operator<<) запускается цепочка действий:

Форматирование данных: Данные преобразуются в последовательность символов в зависимости от текущего формата вывода (std::ios_base::fmtflags).

Буферизация: Преобразованные символы помещаются во внутренний буфер потока (std::streambuf). Это может быть буфер строк (std::stringbuf), или буфер файла (std::filebuf), или другой специализированный буфер.

Запись в целевое устройство: Когда буфер заполняется, или явно делается сброс буфера (std::cout << std::endl, std::cout.flush(), при завершении программы), или при связывании с другим потоком (например, std::cin), данные из буфера записываются в целевое устройство вывода (стандартный поток вывода консоли - stdout).

Связь с stdout осуществляется через объект std::streambuf, который ассоциирован с std::cout. Этот streambuf управляет передачей данных из буфера в операционную систему для печати на консоль.

Переопределение (override) – это механизм в объектно-ориентированном программировании, где производный класс предоставляет свою специфическую реализацию виртуальной функции, объявленной в базовом классе. При вызове такой функции через указатель или ссылку на базовый класс будет выполнена реализация из фактического типа объекта (производного или базового), в зависимости от того, была ли функция переопределена.

Основные моменты:

Применяется только к виртуальным функциям.

Сигнатура (имя, тип возвращаемого значения, типы и количество параметров) переопределяющей функции в производном классе должна точно совпадать с сигнатурой переопределяемой функции в базовом классе.

Ключевое слово override (начиная с C++11) используется для явного указания намерения переопределить функцию. Компилятор проверит, действительно ли функция из базового класса существует с такой сигнатурой, и выдаст ошибку, если нет. Это помогает избежать опечаток и делает код более читабельным.

Пример:

В C++ для вычисления значений на этапе компиляции используются спецификаторы consteval и constexpr, а также шаблоны метапрограммирования. В C11+ для ограниченного набора выражений — ключевое слово const.

constexpr: Указывает, что переменная или функция может быть вычислена на этапе компиляции. Если все аргументы известны на этапе компиляции, компилятор попытается выполнить вычисление. Если нет, вычисление произойдет в рантайме.

// C++

constexpr int factorial(int n) {

return (n <= 1) ? 1 : n * factorial(n - 1);

}

int main() {

// Вычисление на этапе компиляции

constexpr int fact5 = factorial(5);

// Вычисление в рантайме (если аргумент не известен на этапе компиляции)

int runtime_val = 6;

int fact_runtime = factorial(runtime_val);

return 0;

}

consteval: (C++20) Указывает, что функция должна быть вычислена на этапе компиляции. Вызов такой функции в контексте, где результат не может быть вычислен на этапе компиляции, приведет к ошибке компиляции.

// C++20

consteval int compile_time_add(int a, int b) {

return a + b;

}

int main() {

// OK: Вычисление на этапе компиляции

constexpr int sum = compile_time_add(10, 20);

// Ошибка компиляции: аргумент не известен на этапе компиляции

// int runtime_val = 5;

// int sum_runtime = compile_time_add(sum, runtime_val);

return 0;

}

Шаблоны метапрограммирования: Используют инстанцирование шаблонов для выполнения вычислений на этапе компиляции. Чаще всего используются для рекурсивных вычислений и генерации типов.

// C++

// Вычисление факториала с помощью шаблонов

template<int N>

struct Factorial {

static const int value = N * Factorial<N - 1>::value;

};

template<>

struct Factorial<0> {

static const int value = 1;

};

int main() {

// Вычисление на этапе компиляции через инстанцирование шаблона

constexpr int fact6 = Factorial<6>::value;

return 0;

}

const: В C11 и более поздних версиях C, переменные, объявленные с const и инициализированные константным выражением, могут использоваться в контекстах, требующих констант времени компиляции (например, размер статического массива).

// C11+

const int array_size = 10; // Константное выражение

int static_array[array_size]; // OK в C11+

// В C++ это всегда было возможно для const с константным инициализатором.

Вычисление на этапе компиляции (constexpr evaluation, compile-time evaluation) позволяет:

Повысить производительность, избегая выполнения кода в рантайме.

Сделать код безопаснее, выявляя ошибки вычислений (например, деление на ноль) на этапе компиляции.

Использовать вычисленные значения в контекстах, требующих констант (размеры массивов, аргументы шаблонов и т.д.).

Необходим для проверки, связан ли объект потока с реальным потоком выполнения.

Основные причины:

Предотвращение Undefined Behavior: Вызов join() или detach() на объекте std::thread, который не связан с потоком выполнения (!t.joinable()), приводит к неопределенному поведению.

Управление ресурсами: Позволяет определить, владеет ли объект std::thread ресурсами реального потока и нужно ли их освободить (join() или detach()).

Условная обработка: Дает возможность выполнять действия с потоком (ожидание завершения, отделение) только при условии, что он активен.

Инициализация и присваивание: Позволяет безопасно переприсваивать объект std::thread новый поток только если предыдущий не был связан.

std::vector

Доступ по индексу: O(1)

Добавление в конец (push_back): амортизированное O(1)

Вставка или удаление в середине: O(n), так как требуется сдвиг элементов

Итерация: O(n)

std::list (двусвязный список)

Доступ по индексу: O(n), так как требуется последовательный проход

Вставка и удаление в любом месте (если есть итератор): O(1)

Итерация: O(n)

Таким образом, vector эффективен для быстрого доступа и добавления в конец, а list — для частых вставок и удалений в середине без необходимости сдвигать элементы.

Да, знаю. Placement new позволяет разместить объект по уже выделенному адресу памяти, без использования стандартного выделения кучи.

Основные особенности:

Не выделяет память самостоятельно, а использует переданный адрес.

Вызывает конструктор объекта по указанному адресу.

Необходимо самостоятельно управлять временем жизни объекта (вызывать деструктор).

Часто используется при работе с пулами памяти или преаллоцированной памятью.

Сравнение со стандартным new:

Перегрузка методов (Method Overloading):

Позволяет иметь несколько методов с одним и тем же именем в одном классе.

Отличаются по сигнатуре (количеству и/или типам параметров).

Возвращаемый тип не участвует в определении сигнатуры для перегрузки.

Выбор конкретного метода определяется компилятором на основе типов аргументов при вызове (статическое связывание).

Переопределение методов (Method Overriding):

Позволяет классу-наследнику предоставить специфическую реализацию метода, который уже определен в базовом классе.

Метод в классе-наследнике должен иметь ту же сигнатуру (имя, количество и типы параметров) и тот же возвращаемый тип (или ковариантный возвращаемый тип).

Используется с виртуальными функциями (virtual) для реализации полиморфизма.

Выбор конкретной реализации метода определяется во время выполнения (динамическое связывание), в зависимости от фактического типа объекта.

Основные различия:

Коллизия в хеш-таблице — это ситуация, когда хеш-функция вычисляет один и тот же индекс для двух разных ключей.

Методы разрешения коллизий:

Метод цепочек (Separate Chaining): В ячейке таблицы хранится указатель на связный список (или другую структуру данных), содержащий все элементы, хеширующиеся в этот индекс.

// Пример структуры для связного списка в ячейке таблицы

struct Node {

KeyType key;

ValueType value;

Node* next;

};

// Хеш-таблица: массив указателей на Node

Node* table[TABLE_SIZE];

// Вставка: если ячейка занята, добавляем в связный список

void insert(KeyType key, ValueType value) {

int index = hash_function(key) % TABLE_SIZE;

Node* newNode = new Node{key, value, table[index]};

table[index] = newNode;

}

Метод открытой адресации (Open Addressing): При коллизии ищется следующая свободная ячейка в таблице согласно некоторому правилу пробирования. Элементы хранятся непосредственно в ячейках самой таблицы.

Линейное пробирование (Linear Probing): Ищется следующая ячейка по формуле (hash(key) + i) % TABLE_SIZE, где i увеличивается на 1 при каждой попытке.

Квадратичное пробирование (Quadratic Probing): Ищется следующая ячейка по формуле (hash(key) + i^2) % TABLE_SIZE, где i увеличивается при каждой попытке.

Двойное хеширование (Double Hashing): Ищется следующая ячейка по формуле (hash1(key) + i * hash2(key)) % TABLE_SIZE, где i увеличивается при каждой попытке, а hash2 - другая хеш-функция.

// Пример структуры для ячейки таблицы с открытой адресацией

enum State { EMPTY, OCCUPIED, DELETED };

struct Cell {

KeyType key;

ValueType value;

State state;

};

// Хеш-таблица: массив Cell

Cell table[TABLE_SIZE];

// Вставка с линейным пробированием

void insert_open_addressing(KeyType key, ValueType value) {

int i = 0;

while (table[(index + i) % TABLE_SIZE].state == OCCUPIED) {

i++;

}

table[(index + i) % TABLE_SIZE] = {key, value, OCCUPIED};

}

Метод цепочек обычно проще реализовать и имеет предсказуемую производительность при высокой загрузке. Открытая адресация может иметь лучшую локальность данных, но более чувствительна к коэффициенту загрузки и требует особого подхода при удалении элементов (пометку как "удаленные"). Выбор метода зависит от требований к производительности, использованию памяти и простоты реализации.

Правило пяти (rule of five) требует явного определения пяти специальных функций-членов, если требуется контролировать копирование, перемещение или разрушение объекта:

Деструктор (~Class()): Освобождает ресурсы.

Конструктор копирования (Class(const Class& other)): Создает новый объект как копию существующего.

Оператор присваивания копированием (Class& operator=(const Class& other)): Присваивает содержимое одного объекта другому.

Конструктор перемещения (Class(Class&& other)): Создает новый объект, "крадя" ресурсы у существующего временного объекта.

Оператор присваивания перемещением (Class& operator=(Class&& other)): Перемещает ресурсы из существующего временного объекта в текущий.

Если хотя бы одна из этих функций определена явно, компилятор перестает автоматически генерировать остальные (или генерирует их в зависимости от правил C++11/14/17). Это правило пришло на смену правилу трех (деструктор, конструктор копирования, оператор присваивания копированием) с появлением семантики перемещения в C++11.

Правило нуля (rule of zero) гласит: если класс не управляет каким-либо ресурсом, то все пять (или три) специальные функции не должны быть определены явно. Вместо этого, следует полагаться на сгенерированные компилятором версии или использовать RAII-объекты (Resource Acquisition Is Initialization), такие как std::vector, std::string, std::unique_ptr, которые сами управляют ресурсами. Класс, использующий такие объекты, автоматически получает корректные сгенерированные специальные функции. Это предпочтительный подход в современном C++, так как снижает вероятность ошибок при управлении ресурсами и упрощает код.

Пример, иллюстрирующий правило пяти (неправильное управление ресурсом):

Пример, иллюстрирующий правило нуля (использование RAII):

Условная переменная — это примитив синхронизации, используемый для организации взаимодействия между потоками. Она позволяет потоку (или потокам) ждать наступления определенного условия, пока другой поток не уведомит его (их) о выполнении этого условия. Обычно используется совместно с мьютексом для обеспечения атомарности проверки условия и ожидания.

Основные операции:

wait(mutex): Поток блокируется до тех пор, пока не будет получен сигнал. Перед блокировкой мьютекс атомарно освобождается. После получения сигнала мьютекс атомарно захватывается снова.

notify_one(): Разблокирует один поток из тех, кто ждет на этой условной переменной.

notify_all(): Разблокирует все потоки, которые ждут на этой условной переменной.

Проблема ложных пробуждений (spurious wakeups): Поток может быть разбужен, даже если notify_one или notify_all не были вызваны. Поэтому ожидание на условной переменной всегда должно происходить в цикле, проверяющем условие.

Пример на C++ (используя std::condition_variable):

Да, некоторые операции инвалидируют итераторы unordered_map.

Вставка элемента (insert, emplace, try_emplace, insert_or_assign) может инвалидировать все итераторы, если она приводит к перехешированию контейнера (увеличению количества корзин и перераспределению элементов). Если перехеширования не происходит, итераторы остаются действительными.

Удаление элемента (erase) инвалидирует итератор, указывающий на удаленный элемент, и может инвалидировать другие итераторы в той же корзине, если внутренняя организация корзины меняется. Итераторы на элементы в других корзинах остаются действительными.

clear(): Инвалидирует все итераторы.

rehash() и reserve(): Явно вызывают перехеширование и инвалидируют все итераторы.

Пример, демонстрирующий инвалидацию при перехешировании после вставки:

Следует всегда получать новый итератор после операций, которые могут его инвалидировать, чтобы избежать неопределенного поведения.

decltype используется для получения типа выражения.

Основные случаи применения:

Определение возвращаемого типа функции после списка параметров ( trailing return type ):

template<typename T, typename U>

auto add(T t, U u) -> decltype(t + u) {

return t + u;

}

Это позволяет определить тип возвращаемого значения на основе выражения, включающего параметры функции.

Получение типа переменной:

int x = 10;

decltype(x) y; // y имеет тип int

Получение типа выражения (не обязательно переменной):

const int ci = 0;

decltype(ci) z; // z имеет тип const int

struct S { double d; };

S s;

decltype(s.d) val; // val имеет тип double

int arr[5];

decltype(arr[0]) elem; // elem имеет тип int

int& ref_int();

decltype(ref_int()) r = ref_int(); // r имеет тип int&

В шаблонном метапрограммировании: Позволяет определять типы внутри шаблонных конструкций.

Связь с категорями выражений ( lvalue/rvalue ):

Если выражение является lvalue, decltype(expr) возвращает тип T& для типа T.

Если выражение является rvalue, decltype(expr) возвращает тип T для типа T.

Исключение: если выражение является некруглыми скобками заключенным именем переменной или членом класса, decltype возвращает его объявленный тип (T), независимо от того, является ли оно lvalue.

int i = 0;

decltype(i) t1; // int (имя переменной)

decltype((i)) t2; // int& (lvalue выражение в скобках)

int&& rv_ref = 5;

decltype(rv_ref) t3; // int&& (имя переменной ссылочного типа)

decltype((rv_ref)) t4; // int&& (lvalue выражение ссылочного типа в скобках)

decltype вычисляет тип выражения во время компиляции, не вычисляя само выражение.

std::set хранит элементы отсортированными по возрастанию и обеспечивает логарифмическую сложность для операций поиска, вставки и удаления (O(log n)). Реализован на основе сбалансированного бинарного дерева (обычно красно-черного дерева).

std::unordered_set хранит элементы в хеш-таблице. Порядок элементов не гарантируется. Обеспечивает в среднем константное время для операций поиска, вставки и удаления (O(1)), но в худшем случае может достигать линейной сложности (O(n)) при коллизиях хеш-функции.

Ключевые различия:

Порядок элементов: set – отсортирован, unordered_set – нет.

Сложность операций (поиск, вставка, удаление): set – O(log n), unordered_set – в среднем O(1), в худшем O(n).

Реализация: set – дерево, unordered_set – хеш-таблица.

Требования к типу элементов: set требует наличия операции сравнения (operator<), unordered_set – наличия хеш-функции и операции сравнения на равенство (operator==).

Предикат — это функция или объект функции (например, лямбда-выражение), который возвращает булево значение (true или false).

В контексте стандартной библиотеки C++ предикаты часто используются для определения условий в алгоритмах, таких как сортировка, фильтрация или поиск.

Предикаты делятся на:

Унарные предикаты: принимают один аргумент.

Бинарные предикаты: принимают два аргумента.

Примеры использования:

std::move не перемещает данные сама по себе. Она преобразует lvalue в rvalue-ссылку, что позволяет вызвать конструктор перемещения (или оператор присваивания перемещением), если таковой существует для данного типа. Это дает компилятору понять, что исходный объект может быть безопасно "опустошен", так как его ресурс (например, память) будет передан новому объекту, а не скопирован.

Основная цель: оптимизация при передаче временных объектов или объектов, которые больше не потребуются, избегая дорогостоящего копирования.

Пример:

В данном примере, вместо копирования всех элементов вектора source в destination при использовании std::move, ресурс (вероятно, указатель на динамически выделенный массив) передается из source в destination. В результате, source становится пустым (размер 0), а destination содержит данные.

Константный метод помечен ключевым словом const после списка параметров. Он гарантирует, что не изменит состояние объекта (поля класса).

Особенности константных методов:

Доступ к членам данных: Могут только читать нестатические члены данных класса. Изменять их запрещено, если только они не помечены как mutable.

Вызов других методов: Могут вызывать только другие константные методы того же объекта. Вызов неконстантных методов запрещен, так как они потенциально могут изменить состояние объекта.

Перегрузка: Метод может быть перегружен с константной и неконстантной версией.

Константные объекты: Только константные методы могут вызываться на константных объектах.

Пример:

Неконстантный метод не имеет ключевого слова const после списка параметров.

Особенности неконстантных методов:

Доступ к членам данных: Могут читать и изменять нестатические члены данных класса.

Вызов других методов: Могут вызывать как константные, так и неконстантные методы того же объекта.

Константные объекты: Не могут вызываться на константных объектах.

Использование const в методах повышает безопасность кода, позволяет компилятору выполнять дополнительные проверки и дает пользователям класса гарантию, что вызовы определенных методов не изменят состояние объекта. Это особенно важно при работе с константными ссылками или указателями на объекты.

Указатель хранит адрес первой ячейки памяти, выделенной под объект или функцию, на которую он ссылается.

Нет. std::shared_ptr потокобезопасен для одновременного доступа к управляющему блоку (increment/decrement счетчика ссылок), но не для доступа к объекту, на который он указывает. Несколько потоков могут безопасно увеличивать или уменьшать счетчик ссылок одного std::shared_ptr одновременно. Однако, одновременный доступ к самому объекту, которым управляет std::shared_ptr, требует внешних механизмов синхронизации, таких как мьютексы.

Пример:

Главное отличие struct от class в C++ заключается в стандартном уровне доступа к членам:

struct: По умолчанию члены public.

class: По умолчанию члены private.

Во всем остальном они функционально идентичны и могут использоваться как для определения структур данных без методов, так и для полноценных объектов с инкапсуляцией и наследованием.

Пример использования разного уровня доступа:

Тайп-трейты (type traits) — это классовые шаблоны, используемые для получения информации о свойствах типов во время компиляции. Они позволяют писать более гибкий и общий код, адаптирующийся в зависимости от характеристик используемых типов.

Примеры информации, которую можно получить с помощью тайп-трейтов:

Является ли тип арифметическим, integral (целочисленным), плавающей точки?

Является ли тип указателем, ссылкой, функцией?

Является ли тип константным, volatile?

Можно ли применить к типу операции побитового копирования или перемещения?

Размер типа, выравнивание.

Тайп-трейты обычно находятся в заголовке <type_traits>. Результат проверки свойства типа выражается через статическое поле value булевого типа или через тип type (часто std::true_type или std::false_type).

Пример использования std::is_integral:

Многие тайп-трейты имеют v-версии (value-версии) для более удобного доступа к полю value:

Тайп-трейты часто используются с:

SFINAE (Substitution Failure Is Not An Error) для условного включения или исключения перегрузок функций или специализаций шаблонов на основе свойств типов.

if constexpr (в C++17 и выше) для метапрограммирования во время компиляции.

static_assert для проверки свойств типов на этапе компиляции.

Реализациями стандартной библиотеки, например, в алгоритмах или контейнерах, для оптимизации или выбора правильной реализации в зависимости от свойств типов элементов.

Вызвать чистый виртуальный метод напрямую через указатель на базовый класс с абстрактным методом или через объект этого класса невозможно, так как он не имеет определения в базовом классе.

Однако, чистый виртуальный метод может быть вызван из конструктора или деструктора производного класса, в котором этот метод имеет реализацию. При этом вызов будет разрешен в статическом режиме, будет вызвана реализация того класса, в конструкторе или деструкторе которого происходит вызов.

Пример вызова из конструктора производного класса:

В данном примере, при создании объекта obj класса Derived, будет вызван конструктор Derived, в котором происходит вызов pure_virtual_method(). Поскольку в классе Derived есть реализация этого метода, он будет успешно вызван.

При попытке вызвать чистый виртуальный метод из конструктора или деструктора абстрактного базового класса, в котором он объявлен, произойдет нарушение выполнения (undefined behavior), так как на момент выполнения конструктора или деструктора базового класса vtable еще не полностью построена или уже разрушена, и соответствующая реализация в производном классе недоступна.

Также можно вызвать чистый виртуальный метод из производного класса, используя квалификацию области видимости (Base::pure_virtual_method()), но это возможно только из реализации чистого виртуального метода в производном классе, если он вызывает реализацию из базового класса (чего для чистого виртуального метода нет) или другую функцию, которая в свою очередь вызывает чистый виртуальный метод (что также приводит к неопределенному поведению), или для вызова невиртуального метода с таким же именем из базового класса.

Правильный способ взаимодействия с логикой, представленной чистым виртуальным методом, заключается в создании объекта производного класса и вызове этой логики через указатель или ссылку на базовый класс (полиморфизм).

Таким образом, "вызвать чистый виртуальный метод" в общепринятом смысле (через объект или указатель на базовый класс, не имеющий определения) невозможно. Вызов возможен только из конструктора или деструктора производного класса с его реализацией, или косвенно, через полиморфизм, создав объект производного класса.

Статическая библиотека — это набор скомпилированных объектных файлов (.o в Linux, .obj в Windows), упакованных в единый архивный файл (.a в Linux, .lib в Windows). При сборке исполняемого файла код из статической библиотеки полностью копируется в него.

Основные характеристики:

Размер исполняемого файла: Увеличивается, так как код библиотеки встраивается.

Зависимости: Исполняемый файл не зависит от наличия статической библиотеки во время выполнения. Он самодостаточен.

Обновление: Требует перекомпиляции исполняемого файла при изменении библиотеки.

Совместное использование: Код библиотеки дублируется в каждом исполняемом файле, который ее использует, что может увеличивать общий объем дискового пространства.

Использование:

Создание библиотеки: Объектные файлы архивируются утилитой ar (Linux) или lib (Windows).

# Пример для Linux:

# Компиляция исходных файлов в объектные:

gcc -c file1.c -o file1.o

gcc -c file2.c -o file2.o

# Создание статической библиотеки libmylib.a из объектных файлов:

ar rcs libmylib.a file1.o file2.o

Компоновка с библиотекой: При сборке исполняемого файла компоновщику указывается путь к статической библиотеке.

# Пример для Linux:

# Компиляция исходного файла main.c и компоновка с libmylib.a:

gcc main.c -L/path/to/library/ -lmylib -o myprogram

// -L указывает путь к каталогу с библиотекой

// -l указывает имя библиотеки (без "lib" и расширения)

Преимущества:

Не требует установки или распространения библиотеки отдельно от исполняемого файла.

Отсутствие проблем с версиями библиотек (DLL/shared library hell), так как код библиотеки включен в исполняемый файл.

Недостатки:

Увеличение размера исполняемого файла.

Невозможность обновления функционала библиотеки без перекомпиляции всех использующих ее исполняемых файлов.

Дублирование кода библиотеки в памяти, если запущено несколько программ, использующих одну и ту же статическую библиотеку.

POD (Plain Old Data) тип в C++ — это классификация типов данных, объединяющая характеристики тривиально копируемых (trivially copyable) и тривиально конструируемых/деструктируемых (trivially default constructible) типов. По сути, это типы, поведение которых при копировании и перемещении аналогично C-структурам: можно копировать побитово.

До C++11 понятие POD было менее формализованным и касалось в основном простых структур и встроенных типов. В C++11 и последующих стандартах определение стало строже:

Тип является POD, если он одновременно:

Тривиально копируемый (Trivially Copyable):

Отсутствуют определяемые пользователем операторы копирования (копирующий и перемещающий конструкторы, операторы присваивания).

Все нестатические члены данных тривиально копируемы.

Нет виртуальных функций или виртуальных базовых классов.

Тривиально по умолчанию конструируемый (Trivially Default Constructible):

Отсутствует определяемый пользователем конструктор по умолчанию.

Примеры POD-типов:

Встроенные типы: int, float, char, bool и т.д.

Массивы POD-типов.

Классы/структуры, удовлетворяющие вышеописанным критериям.

Преимущества работы с POD-типами:

Простота и предсказуемость поведения при копировании и перемещении.

Возможность использовать функции C-стиля, работающие с сырой памятью (memcpy, memset).

Возможность использования placement new для создания объектов в предварительно выделенной памяти без вызова конструктора (для тривиально конструируемых).

Проверить, является ли тип POD, можно с помощью трейтов типов в <type_traits>:

В современном C++ более точные характеристики тривиальностью и стандартным расположением (standard layout) чаще используются напрямую, но понимание концепции POD все еще важно. Тип является POD, если он одновременно тривиально копируемый и имеет стандартное расположение (хотя для тривиально конструируемых POD типов это тоже справедливо).

Базово: little-endian — младший байт вначале, big-endian — старший байт вначале.

Пример представления 4-байтового числа 0x12345678 в памяти:

little-endian: Младший значащий байт (0x78) хранится по наименьшему адресу.

big-endian: Старший значащий байт (0x12) хранится по наименьшему адресу.

Проверить endianness системы:

Большинство современных процессоров (например, x86) используют little-endian. Сетевые протоколы (например, IP) часто используют big-endian (сетевой порядок байтов). Возможны проблемы при обмене данными между системами с разным порядком байтов. Для решения этой проблемы используются функции преобразования порядка байтов (например, htonl, ntohl).

В C++20 добавлена поддержка std::endian.

Ключевое слово volatile в C/C++ используется для указания компилятору, что значение переменной может быть изменено внешними факторами, не контролируемыми текущим потоком выполнения программы. Это может быть:

Аппаратное обеспечение (например, регистры периферийных устройств).

Другой поток выполнения в многопоточной программе.

Обработчик прерываний.

Без volatile компилятор может оптимизировать доступ к переменной, предполагая, что ее значение меняется только в явном коде. Например, он может кэшировать значение в регистре и многократно использовать его, игнорируя фактические изменения в памяти. Использование volatile запрещает подобные оптимизации, заставляя компилятор каждый раз считывать или записывать значение переменной непосредственно из или в память.

Примеры использования:

Чтение аппаратного регистра:

Доступ к переменной, разделяемой между потоками без использования явных механизмов синхронизации (не самый безопасный подход, но демонстрирует применение):

Важно отметить, что volatile гарантирует только то, что компилятор не будет оптимизировать доступ к переменной. Он не обеспечивает атомарность операций или синхронизацию между потоками. Для безопасной работы с общими данными в многопоточных средах необходимо использовать атомарные операции (из <atomic>) или примитивы синхронизации (мьютексы, семафоры и т.д.).

Оптимизации работы со строками в C++ включают:

Copy-on-Write (COW): Техника, при которой данные строки копируются только при попытке их модификации. При обычном копировании объекта (например, std::string b = a;), b и a разделяют один и тот же буфер данных. Только когда происходит изменение одной из строк, создается отдельная копия буфера для этой строки. Это уменьшает накладные расходы на копирование для немодифицируемых строк. В современных реализациях std::string COW встречается реже из-за проблем с потокобезопасностью и производительностью в многопоточных средах.

Short String Optimization (SSO): Для коротких строк оптимизация заключается в выделении буфера фиксированного размера внутри самого объекта std::string. Это позволяет хранить короткие строки без динамического выделения памяти в куче, что существенно быстрее. Размер этого встроенного буфера зависит от конкретной реализации стандартной библиотеки.

String Literals: Использование строковых литералов (например, "hello") обеспечивает их хранение в статической памяти, обычно в сегменте данных исполняемого файла. Это избегает динамического выделения памяти при их создании.

Строковые виды (std::string_view): Доступны с C++17, std::string_view представляет собой легковесный объект, который ссылается на существующую последовательность символов. Он не владеет данными строки и не выделяет память. Это идеально подходит для передачи строковых данных в функции без копирования, выполнения сравнений и поиска подстрок, когда оригинальная строка не модифицируется.

#include <string_view>

#include <string>

#include <iostream>

void print_string(std::string_view sv) {

std::cout << sv << std::endl;

}

int main() {

std::string s = "This is a long string.";

print_string(s); // Передаем без копирования

return 0;

}

Custom Allocators: Для специализированных сценариев с интенсивной работой со строками можно использовать пользовательские аллокаторы памяти, оптимизированные под конкретные нужды приложения.

Алгоритмы и функции: Использование эффективных стандартных алгоритмов (например, std::search, std::find) вместо ручной реализации может быть быстрее, так как они часто оптимизированы.

Предварительное резервирование памяти (reserve()): Для std::string, если заранее известен минимальный необходимый размер строки, вызов reserve() позволяет выделить достаточно памяти с самого начала, избегая многократных перевыделений и копирований данных при наращивании строки.

#include <string>

#include <iostream>

int main() {

std::string s;

s.reserve(100); // Резервируем место для 100 символов

for (int i = 0; i < 50; ++i) {

s += 'a'; // Добавление символов не приведет к реаллокации до достижения 100

}

std::cout << "Capacity: " << s.capacity() << std::endl;

return 0;

}

Avoid unnecessary temporaries: Минимизация создания временных строковых объектов в выражениях. Например, вместо конкатенации в несколько шагов с промежуточными результатами, используйте методы, которые позволяют строить строку более эффективно.

Использование C-стиля строк (char*) с осторожностью: В некоторых низкоуровневых сценариях или при работе с унаследованным кодом C-стиль строк может быть быстрее (за счет отсутствия overhead'а объектов std::string), но требует тщательного управления памятью и безопасностью (избегание переполнения буфера). В большинстве современных C++ приложений предпочтительнее использовать std::string и std::string_view.

Lvalue (locator value) — выражение, которое имеет идентифицируемую область памяти. Это объект, который сохраняется за пределами одного выражения. Пример: переменная, разыменователь указателя *p.

Rvalue (right value) — выражение, которое не имеет постоянного адреса. Это временный объект, который существует только в пределах выражения. Пример: литерал 10, результат арифметической операции a + b, временный объект, возвращаемый функцией по значению.

Основные отличия:

Примеры:

Количество байт, занимаемое указателем, зависит от архитектуры процессора и операционной системы.

На 32-разрядных системах указатель обычно занимает 4 байта.

На 64-разрядных системах указатель обычно занимает 8 байт.

Это связано с размером адресного пространства, которое может быть адресовано. 32-разрядной системе достаточно 32 бит (4 байта) для адресации памяти до 4 ГБ, тогда как 64-разрядной системе требуются 64 бита (8 байт) для адресации значительно большего объема памяти.

Размер указателя можно увидеть, используя оператор sizeof:

Big Endian: старший (самый значимый) байт многобайтового числа помещается в младший адрес памяти, а младший байт — в старший.

Little Endian: младший (самый значимый) байт многобайтового числа помещается в младший адрес памяти, а старший байт — в старший.

Middle Endian: не является общепринятой нотацией. Иногда под ней понимают случаи, когда порядок байт отличается от BigEndian и LittleEndian, например, при обработке многобайтовых чисел блоками по 2 байта.

Пример для 4-байтового числа 0x12345678:

Большинство современных процессоров (Intel, AMD) используют Little Endian. Сетевые протоколы, как правило, используют Big Endian (так называемый "сетевой порядок байт").

Проверить endianness системы можно так:

Для преобразования между порядками байт в C++ используются функции из заголовков <arpa/inet.h> (для сетевого порядка) или <endian.h> (более общие).

Можно рассмотреть следующие варианты:

Использование целочисленного типа (например, long long int): Умножить цену на константу (например, 100 или 1000), чтобы преобразовать ее в целое число, которое затем можно использовать как ключ. При этом нужно будет учесть точность.

// Пример: хранение цены с точностью до двух знаков после запятой

long long price_as_int = static_cast<long long>(price * 100);

Использование структуры или пары с переопределенным оператором сравнения: Создать структуру или использовать std::pair для хранения значения float и переопределить оператор < или предоставить функцию сравнения для использования в ассоциативных контейнерах (например, std::map). Это позволит контейнеру правильно сравнивать значения float, учитывая возможные проблемы с точностью.

struct PriceKey {

float value;

bool operator<(const PriceKey& other) const {

// Сравнение с учетом допусков для плавающей точки

return std::abs(value - other.value) > std::numeric_limits<float>::epsilon() && value < other.value;

}

// Возможно, также понадобится переопределить operator== и operator>

};

Использование специальных контейнеров, учитывающих особенности чисел с плавающей точкой: Некоторые библиотеки могут предоставлять специализированные контейнеры, предназначенные для работы с числами с плавающей точкой в качестве ключей, которые учитывают проблемы сравнения.

Использование типа double: Хотя double также является числом с плавающей точкой, он имеет большую точность, что может уменьшить вероятность проблем со сравнением в зависимости от требуемой точности.

Выбор альтернативного решения зависит от требуемой точности, производительности и гибкости. Использование целых чисел является наиболее надежным для точного сравнения, но требует дополнительных операций преобразования. Структуры с переопределенным сравнением дают больше гибкости, но требуют осторожности при реализации оператора сравнения.

std::unique_ptr — это смарт-указатель, который реализует эксклюзивное владение динамически выделенным объектом. Он обеспечивает автоматическое управление памятью: объект, на который указывает unique_ptr, автоматически удаляется при уничтожении unique_ptr.

Ключевые особенности:

Эксклюзивное владение: В любой момент времени только один unique_ptr может владеть конкретным объектом.

Отсутствие копирования: unique_ptr не может быть скопирован. Это предотвращает множественное владение одним и тем же объектом и проблему "двойного удаления".

Семантика перемещения: Владение объектом может быть передано от одного unique_ptr к другому с использованием семантики перемещения (std::move).

Настраиваемый удалитель: Можно предоставить собственный удалитель (deleter) для управления специфическими способами освобождения ресурсов.

Принцип работы основан на RAII (Resource Acquisition Is Initialization): ресурс (динамически выделенная память) приобретается при создании объекта (unique_ptr), а освобождается при уничтожении этого объекта.

Сравнение с raw pointer:

В целом, unique_ptr — это предпочтительный способ управления динамической памятью в C++11 и выше для случаев, когда требуется строгое владение и автоматическое освобождение ресурсов.

Ключевое слово explicit в C++ используется для предотвращения неявных (implicit) преобразований типов, особенно при вызове конструкторов и операторов преобразования.

Без explicit, конструкторы с одним аргументом и операторы преобразования могут быть использованы компилятором для автоматического преобразования типов. Detta kan leda till oväntat beteende och svårlösta buggar.

Примеры использования:

С конструкторами:

class MyClass {

public:

// Разрешено неявное преобразование из int

MyClass(int value) : data_(value) {}

// Запрещено неявное преобразование из double

explicit MyClass(double value) : data_(static_cast<int>(value)) {}

int data_;

};

int main() {

MyClass obj1 = 10; // OK: использует конструктор MyClass(int) неявно

// MyClass obj2 = 20.5; // Ошибка компиляции: неявное преобразование из double запрещено explicit

MyClass obj3(30.5); // OK: явный вызов конструктора MyClass(double)

return 0;

}

С конструкторами:

С операторами преобразования:

class MyWrapper {

public:

explicit MyWrapper(int value) : value_(value) {}

// Разрешено неявное преобразование в int

operator int() const { return value_; }

// Запрещено неявное преобразование в double

explicit operator double() const { return static_cast<double>(value_); }

private:

int value_;

};

int main() {

MyWrapper wrapper(100);

int val_int = wrapper; // OK: неявное преобразование в int

// double val_double = wrapper; // Ошибка компиляции: неявное преобразование в double запрещено explicit

double val_double_explicit = static_cast<double>(wrapper); // OK: явное преобразование в double

return 0;

}

Использование explicit повышает ясность кода, делает его более предсказуемым и позволяет избежать непреднамеренных преобразований типов, которые могут скрыть ошибки.

Таблица виртуальных методов (vtable, virtual method table, virtual function table) — это механизм, используемый в C++ (и других объектно-ориентированных языках) для реализации полиморфизма во время выполнения (динамической диспетчеризации).

Это таблица указателей на виртуальные функции класса.

Каждый объект с хотя бы одной виртуальной функцией или наследованный от класса с виртуальными функциями содержит скрытый указатель (vptr), который указывает на таблицу виртуальных методов своего класса.

При вызове виртуальной функции через указатель или ссылку на базовый класс, vptr используется для определения фактического типа объекта и, следовательно, для нахождения соответствующего указателя на функцию в таблице виртуальных методов для вызова правильной реализации функции.

Vtable создается компилятором для каждого класса, имеющего виртуальные функции. В таблице содержатся указатели на конкретные реализации виртуальных функций для данного класса. При наследовании и переопределении виртуальных функций, таблица производного класса содержит указатели на переопределенные реализации, а для функций, которые не были переопределены - указатели на реализации из базового класса.

std::set и std::unordered_set используются для хранения уникальных элементов. Выбор между ними зависит от приоритетов: упорядоченность или производительность доступа/вставки/удаления.

std::set основан на сбалансированном бинарном дереве поиска (обычно красно-черном дереве).

Характеристики std::set:

Элементы хранятся в отсортированном порядке.

Вставка, удаление и поиск элементов занимают время О(log N), где N — количество элементов.

Подходит, когда важен отсортированный порядок элементов, или когда требуется получение элементов в определенном диапазоне.

std::unordered_set основан на хеш-таблице.

Характеристики std::unordered_set:

Элементы не хранятся в отсортированном порядке.

Вставка, удаление и поиск элементов в среднем занимают время О(1), но в худшем случае (при большом количестве коллизий хеш-функции) может достигать О(N).

Требуется, чтобы тип элемента имел определенную хеш-функцию (std::hash) и оператор сравнения на равенство (operator==).

Подходит, когда требуется максимально быстрая вставка, удаление и поиск, и порядок элементов не имеет значения.

Сводная таблица:

Пример использования std::set:

Пример использования std::unordered_set:

Строгая гарантия исключений (Strong Exception Guarantee) означает, что в случае возникновения исключения состояние программы остается неизменным, как если бы операция никогда не выполнялась.

Реализуется чаще всего с помощью идиомы Copy-and-Swap:

Создается неявная или явная копия объекта.

Операция выполняется с копией.

Если операция успешна, состояние исходного объекта атомарно обменивается с состоянием копии (обычно через swap).

Принцип: все потенциально бросающие исключения операции выполняются на копии. Если исключение происходит, копия (недоделанная) уничтожается, а исходный объект остается в прежнем состоянии. Если операции на копии проходят успешно, состояние исходного объекта обновляется в одном атомарном, не бросающем исключений шаге (обычно swap).

Функция thread::join() блокирует вызывающий поток до тех пор, пока поток, связанный с объектом thread, не завершит свое выполнение.

Основные назначения:

Синхронизация: Обеспечивает, что определенный набор операций в другом потоке будет завершен перед продолжением выполнения текущего потока.

Избежание ресурсоутечек (joinable threads): Объект std::thread, представляющий выполняющийся поток (не отсоединенный — joinable), должен быть либо присоединен (join), либо отсоединен (detach) перед тем, как объект std::thread будет разрушен. Отсутствие этого приведет к вызову std::terminate.

Пример:

std::vector представляет собой динамический массив. Данные хранятся в непрерывном блоке памяти.

Преимущества vector:

Быстрый доступ по индексу (O(1)).

Хорошая локальность данных, что полезно для кэша процессора.

Быстрое добавление/удаление в конец (в среднем O(1)).

Недостатки vector:

Сложность добавления/удаления в произвольное место (O(n)).

Перевыделение памяти при росте размера, требующее копирования элементов.

std::list является двусвязным списком. Каждый элемент хранит указатели на предыдущий и следующий элементы.

Преимущества list:

Сложность добавления/удаления элементов в любое место (O(1)) после нахождения позиции (которая может быть O(n)).

Не требует непрерывного блока памяти.

Недостатки list:

Медленный доступ по индексу (O(n)).

Занимает больше памяти на элемент (из-за указателей).

Плохая локальность данных.

Основное отличие:

Выбор между vector и list зависит от типичных операций с контейнером. Если часты случайный доступ и добавление/удаление в конец, предпочтительнее vector. Если важны частые вставки/удаления в середине списка, предпочтительнее list.

Пра­ви­ло од­но­го оп­ре­де­ле­ния (One Definition Rule, ODR) тре­бу­ет, что­бы в про­грам­ме каж­дая су­щ­ность (функ­ция, пе­ре­мен­ная, класс, пе­ре­чис­ле­ние, шаб­лон и т.д.), име­ю­щая внеш­нее свя­зы­ва­ние (external linkage) или не име­ю­щая свя­зы­ва­ния (no linkage), бы­ла оп­ре­де­ле­на ров­но один раз. Име­ю­щие внут­рен­нее свя­зы­ва­ние (internal linkage) или ло­каль­ные пе­ре­мен­ные мо­гут оп­ре­де­лять­ся в каж­дой еди­ни­це пре­об­ра­зо­ва­ния (translation unit).

Осо­бые слу­чаи для ODR:

Встраиваемые фу­нк­ции и пе­ре­мен­ные (inline functions and variables): Мо­гут оп­ре­де­лять­ся в не­сколь­ких еди­ни­цах пре­об­ра­зо­ва­ния, но все их оп­ре­де­ле­ния дол­ж­ны быть иден­тич­ны с то­ч­ки зре­ния то­ке­нов (token by token).

Шаб­ло­ны (templates): Оп­ре­де­ле­ния шаб­ло­нов (функ­ций, клас­сов, пе­ре­мен­ных, али­а­сов) мо­гут при­сут­ст­во­вать в не­сколь­ких еди­ни­цах пре­об­ра­зо­ва­ния там, где они яв­но или не­яв­но ин­стан­циру­ют­ся. Все та­кие оп­ре­де­ле­ния дол­ж­ны быть иден­тич­ны.

Ти­пы с раз­ным пред­став­ле­ни­ем (types with different representations): На­ру­ше­ние ODR мо­жет при­ве­с­ти к не­о­п­ре­де­лен­но­му по­ве­де­нию, ес­ли об­щая су­щ­ность (на­при­мер, класс) оп­ре­де­ле­на по-раз­но­му в раз­ных еди­ни­цах пре­об­ра­зо­ва­ния.

При­ме­ры ODR:

На­ру­ше­ния ODR:

Со­от­ве­тс­твие ODR:

На­ру­ше­ние ODR ча­с­то при­во­дит к ошиб­кам ком­по­нов­ки (linker errors) или, что ху­же, к не­о­п­ре­де­лен­но­му по­ве­де­нию во вре­мя вы­пол­не­ния.

std::map в C++ — это ассоциативный контейнер, который хранит пары ключ-значение, упорядоченные по ключу. Внутренне он обычно реализован как сбалансированное бинарное дерево (например, красно-черное дерево), что обеспечивает логарифмическое время доступа, вставки и удаления элементов.

Основные характеристики:

Ключи уникальны.

Элементы упорядочены по ключу с помощью компаратора (по умолчанию std::less<Key>).

Хранит элементы в узлах дерева, каждый узел содержит пару std::pair<const Key, T>.

Пример использования:

В этом примере элементы будут выведены в порядке ключей: 5, 10, 20.

Память выделяется динамически для узлов дерева, и управление памятью происходит внутри контейнера. При удалении элементов или уничтожении контейнера память освобождается автоматически.

std::unordered_map в C++ представляет собой хеш-таблицу, состоящую из массива корзин (bucket). Каждая корзина является связным списком (или другой структурой, например, деревом, для оптимизации в случае хеш-коллизий).

Ключевые шаги при работе с unordered_map:

Вычисление хеша: Для каждого ключа вычисляется хеш с помощью хеш-функции (std::hash по умолчанию или пользовательская).

Определение корзины: Хеш-код преобразуется в индекс корзины с помощью операции по модулю: индекс_корзины = хеш_код % количество_корзин.

Поиск/вставка/удаление элемента:

Поиск: Происходит последовательный перебор элементов в связном списке соответствующей корзины. Сравнение ключей выполняется с помощью оператора равенства (==).

Вставка: Новый элемент добавляется в конец связного списка соответствующей корзины. Если ключ уже существует, то (по умолчанию) элемент не вставляется или обновляется (в зависимости от операции).

Удаление: Элемент удаляется из связного списка соответствующей корзины после его обнаружения.

Разрешение коллизий: Если разные ключи имеют одинаковый хеш или попадают в одну и ту же корзину, это называется хеш-коллизией. unordered_map разрешает коллизии методом цепочек: все элементы, хеши которых указывают на одну и ту же корзину, хранятся в списке этой корзины.

Рехеширование: Для поддержания эффективной работы (снижения вероятности коллизий и уменьшения длины списков), unordered_map автоматически увеличивает количество корзин и перераспределяет элементы в них (выполняет рехеширование), когда коэффициент загрузки (отношение количества элементов к количеству корзин) превышает определенный порог.

Важные аспекты:

Хеш-функция: Должна быть эффективной и распределять ключи равномерно для минимизации коллизий. Хорошая хеш-функция критична для производительности.

Оператор равенства: Используется для окончательной проверки, действительно ли найденный в корзине элемент соответствует искомому ключу (поскольку разные ключи могут иметь одинаковый хеш).

Коэффициент загрузки: Влияет на производительность. Высокий коэффициент загрузки увеличивает вероятность коллизий и замедляет операции.

Производительность: В среднем, основные операции (вставка, поиск, удаление) имеют временную сложность O(1). В худшем случае (например, при плохой хеш-функции и большом количестве коллизий) сложность может деградировать до O(n), где n — количество элементов.

Placement new - это версия оператора new, позволяющая конструировать объект в заранее выделенном участке памяти.

Синтаксис:

Особенности:

Не выделяет память самостоятельно, а использует предоставленный буфер.

Возвращает указатель на сконструированный объект.

Необходимо вручную вызывать деструктор объекта, когда он больше не нужен.

Для освобождения памяти, выделенной для буфера, используется обычный delete или delete[], но после вызова деструктора объекта.

Пример использования:

Применение:

Создание объектов в разделяемой памяти (shared memory).

Реализация пулов объектов (object pools).

Низкоуровневое управление памятью в специализированных системах.

Избегание выделения и освобождения памяти при создании временных объектов в критических по производительности участках кода.

Placement delete существует, но встречается реже и используется для отмены неудачного конструирования с использованием placement new, вызывая деаллокатор, который был бы вызван обычным new, но не деструктор объекта.

Базовая гарантия исключений (Basic Exception Guarantee) в C++ означает, что при выбросе исключения из функции:

Ресурсы, которыми функция владела на момент выброса исключения (например, память, файловые дескрипторы), не будут утекать.

Объекты, состояние которых было изменено до выброса исключения, будут находиться в валидном, хотя и неопределенном состоянии. Дальнейшее использование таких объектов возможно, но их значение предсказать нельзя.

Это минимально допустимая гарантия, которую желательно выполнять для большинства функций. Она обеспечивает безопасность ресурсов, но не целостность данных.

Применение:

RAII (Resource Acquisition Is Initialization): Классы-обёртки, которые управляют ресурсами в своем конструкторе и освобождают их в деструкторе. Это основной механизм обеспечения базовой гарантии. Деструкторы таких классов должны быть noexcept.// Пример RAII для мьютекса

#include <mutex>

class LockGuard {

public:

explicit LockGuard(std::mutex& m) : mutex_(m) {

mutex_.lock(); // Захват ресурса в конструкторе

}

~LockGuard() noexcept { // Деструктор noexcept освобождает ресурс

mutex_.unlock();

}

LockGuard(const LockGuard&) = delete;

LockGuard& operator=(const LockGuard&) = delete;

private:

std::mutex& mutex_;

};

void some_function(std::mutex& m) {

LockGuard lock(m); // Мьютекс будет автоматически разблокирован при выходе из блока, даже при исключении

// ... потенциально опасный код, который может выбросить исключение ...

} // Деструктор LockGuard вызывается здесь

Откат изменений (Rollback): В более сложных сценариях, где изменения состояния затрагивают несколько объектов или данных, может потребоваться явный механизм отката при исключении. Однако, базовая гарантия сама по себе этого не обеспечивает; это скорее условие для сильной гарантии.

Базовая гарантия является фундаментом для более строгих гарантий (сильной и отсутствия исключений) и значительно упрощает написание надёжного кода, устойчивого к ошибкам.

SFINAE (Substitution Failure Is Not An Error) — это правило в C++, которое гласит, что если компилятор пытается подставить аргументы шаблона в сигнатуру функции или класса, и эта подстановка приводит к некорректному коду (например, попытка использовать член, который не существует для данного типа), это не считается ошибкой компиляции немедленно. Вместо этого, компилятор просто игнорирует этот экземпляр шаблона при разрешении перегрузки или специализации.

Это правило используется для метапрограммирования на этапе компиляции, позволяя создавать шаблоны, поведение которых зависит от свойств аргументов шаблона.

Примеры применения SFINAE:

Выбор между перегруженными функциями или специализациями шаблонов классов в зависимости от наличия определенных членов или поддерживаемых операций.

Реализация концептов (до C++20, где появились явные концепты) для ограничения типов шаблонов.

Создание типажей (type traits).

Пример использования SFINAE с std::enable_if:

В этом примере std::enable_if используется как возвращаемый тип. Если условие std::is_integral<T>::value истинно, то std::enable_if предоставляет тип void. Если условие ложно, std::enable_if не предоставляет член type, что приводит к неудаче подстановки в сигнатуре функции, и компилятор игнорирует эту специализацию функции.

Циклическая зависимость возникает, когда два или более модулей (классов, функций, заголовочных файлов и т.д.) напрямую или косвенно зависят друг от друга.

Проблемы, которые она может вызвать:

Проблемы с компиляцией: При использовании заголовочных файлов #include в C/C++ прямые циклические зависимости между ними невозможно разрешить без дополнительных мер (например, forward declarations), так как компилятор не знает, с какой стороны начать компиляцию.

Увеличение сложности кода: Циклические зависимости делают код более трудным для понимания и анализа, так как трудно отследить поток зависимостей.

Усложнение тестирования: Отдельные модули становится сложнее тестировать изолированно, так как для их работы требуются другие, зависимые модули, которые в свою очередь зависят от тестируемого.

Снижение гибкости и возможности повторного использования: Модули с циклическими зависимостями труднее извлечь и использовать в других проектах без включения всего цикла зависимости.

Утечки памяти или некорректное управление ресурсами: В языках с ручным управлением памятью или подсчетом ссылок (например, при использовании std::shared_ptr без std::weak_ptr) циклические зависимости могут препятствовать освобождению памяти, вызывая утечки.

В данном примере #include "A.h" в B.h и использование B* в A.h (после forward declaration) демонстрирует, как может выглядеть циклическая зависимость между классами.

Виртуальная функция позволяет реализовать полиморфизм во время выполнения. Когда вызывается виртуальная функция объекта, фактическая вызываемая версия функции определяется типом объекта, на который указывает указатель или ссылка, а не типом указателя или ссылки.

Для реализации этого механизма компилятор добавляет к каждому объекту класса с виртуальными функциями скрытый указатель — vptr (virtual pointer). Этот указатель указывает на таблицу виртуальных функций — vtable (virtual table).

vtable — это статическая таблица, общая для всех объектов данного класса, которая содержит указатели на реализации виртуальных функций этого класса. Для производного класса, который переопределяет виртуальные функции, его vtable содержит указатели на переопределенные версии. Если производный класс не переопределяет виртуальную функцию, его vtable содержит указатель на версию из базового класса.

При вызове виртуальной функции через указатель или ссылку компилятор генерирует код, который:

Получает указатель на vtable через vptr объекта.

Находит в vtable указатель на нужную виртуальную функцию (по известному смещению).

Вызывает функцию по полученному указателю.

Ключевое слово virtual перед объявлением функции в базовом классе делает ее виртуальной. Ключевое слово override в производном классе явно указывает, что функция переопределяет виртуальную функцию из базового класса (хорошая практика для избежания ошибок).

Виртуальный деструктор в базовом классе важен для корректного освобождения ресурсов, когда объект производного класса удаляется через указатель на базовый класс.

Если деструктор в Base не виртуальный, при удалении delete obj; будет вызван только деструктор Base, что приведет к утечкам памяти или другим проблемам, если Derived имеет свои ресурсы.

Константные методы (const methods) в C++ — это методы класса, которые объявлены с ключевым словом const после списка параметров.

Ключевое слово const после списка параметров гарантирует, что метод не будет изменять состояние объекта, для которого он вызван. Это означает, что внутри константного метода:

Нельзя изменять нестатические члены данных объекта.

Можно вызывать только другие константные методы того же объекта.

Назначение константных методов:

Безопасность: Предотвращают случайное изменение состояния объекта.

Использование с константными объектами: Константные объекты (объявленные с const) могут вызывать только константные методы. Неконстантные методы им недоступны.

Перегрузка: Методы могут быть перегружены по константности. Это позволяет иметь два метода с одинаковым именем и сигнатурой параметров, но один будет константным, а другой неконстантным. Компилятор выберет версию в зависимости от того, на константном или неконстантном объекте вызывается метод.

Неконстантные методы:

Это обычные методы класса, которые могут изменять состояние объекта. Объявляются без ключевого слова const после списка параметров.

Использование:

Использование константности является хорошей практикой, так как повышает читаемость и безопасность кода, а также позволяет компилятору выполнять дополнительные оптимизации.

std::list - это двусвязный список.

Преимущества:

Быстрая вставка и удаление в любой позиции (O(1)) при наличии итератора.

Не требует сдвига элементов при вставке/удалении.

Недостатки:

Медленный доступ по индексу (O(n)).

Больший расход памяти по сравнению с std::vector из-за хранения указателей.

Итераторы могут инвалидироваться только при удалении элемента, на который они указывают (кроме clear).

Пример вставки/удаления:

Спинлок — это примитив синхронизации, используемый для защиты критических секций от конкурентного доступа нескольких потоков.

Характеристики:

Активное ожидание (Spinning): Поток, который не может захватить спинлок, не переходит в состояние ожидания (sleep), а непрерывно проверяет, освободился ли он. Это потребляет процессорное время.

Накладные расходы: В отличие от мьютексов, захват и освобождение спинлока обычно имеют меньшие накладные расходы, так как не требуют переключения контекста потока.

Применимость: Эффективен, когда ожидание освобождения спинлока предполагается коротким. В противном случае активное ожидание становится неэффективным и потребляет ресурсы ЦП без пользы.

Типичная реализация:

Часто реализуется с использованием атомарных операций, таких как test-and-set или compare-and-swap, чтобы обеспечить безопасность при конкурентном доступе к флагу состояния спинлока.

Сравнение со мьютексом:

Статическая линковка (пример):

При компиляции main.cpp, код функции add из static_lib.cpp будет включен непосредственно в исполняемый файл.

Динамическая линковка (пример):

В этом примере, программа main загружает библиотеку dynamic_lib.dll (или dynamic_lib.so) во время выполнения и вызывает функцию add через указатель.

std::map реализует ассоциативный массив на основе красно-черного дерева. Элементы хранятся в отсортированном порядке по ключу. Операции вставки, удаления и поиска имеют логарифмическую сложность (O(log n)).

std::unordered_map реализует ассоциативный массив на основе хеш-таблицы. Элементы хранятся без определенного порядка. В среднем, операции вставки, удаления и поиска имеют константную сложность (O(1)), но в худшем случае (при плохой хеш-функции или большом количестве коллизий) могут достигать линейной сложности (O(n)). Требуется, чтобы тип ключа имел реализованную хеш-функцию (std::hash) и оператор сравнения на равенство.

Сравнение:

Пример использования std::map:

Пример использования std::unordered_map:

В стандартной библиотеке C++ (начиная с C++11) доступны следующие виды мьютексов:

std::mutex: Базовый, нерекурсивный мьютекс. Может быть заблокирован только один раз одним потоком. При попытке повторной блокировки из того же потока возникает неопределенное поведение.

std::recursive_mutex: Рекурсивный мьютекс. Поток может блокировать его несколько раз. Для каждого lock() требуется соответствующий unlock().

std::timed_mutex: Нерекурсивный мьютекс с возможностью попытки блокировки в течение определенного времени (try_lock_for, try_lock_until).

std::recursive_timed_mutex: Рекурсивный мьютекс с возможностью попытки блокировки в течение определенного времени.

Кроме того, для реализации общей блокировки (множество потоков могут одновременно читать, но только один может писать) существует:

std::shared_mutex (C++17): Обеспечивает два уровня блокировки: совместную (для чтения, несколько потоков) и эксклюзивную (для записи, один поток).

std::shared_timed_mutex (C++14): Аналогичен std::shared_mutex, но с возможностью попытки блокировки в течение определенного времени.

Наиболее часто используемым является std::mutex. std::recursive_mutex следует использовать осторожно, так как он может скрывать логические ошибки. В C++14/17 для многих сценариев чтения/записи предпочтительнее использовать std::shared_mutex или std::shared_timed_mutex.

Деструктор помечается как noexcept по умолчанию в C++11 и последующих стандартах для обеспечения корректной работы механизма обработки исключений, особенно при раскрутке стека (stack unwinding).

Основные причины:

Предотвращение двойной ошибки: Если из деструктора во время раскрутки стека (вызванной другим исключением) выбрасывается новое исключение, это приводит к вызову std::terminate и аварийному завершению программы. Пометка noexcept указывает компилятору, что деструктор не должен выбрасывать исключений, предотвращая этот сценарий.

Оптимизация: Компилятор может выполнять больше оптимизаций, зная, что деструктор не выбросит исключение. Это позволяет более эффективно генерировать код, например, для освобождения ресурсов.

Корректная работа механизмов STL: Контейнеры стандартной библиотеки (например, std::vector) полагаются на то, что деструкторы их элементов не выбрасывают исключений. Это необходимо для обеспечения strong exception safety (строгой гарантии безопасности исключений) или basic exception safety (базовой гарантии безопасности исключений). Если деструктор члена контейнера выбросит исключение, контейнер окажется в некорректном состоянии.

Явная спецификация намерений: Пометка noexcept явно указывает намерение программиста, что деструктор не должен завершаться выбросом исключения. Если же исключение все-таки выбрасывается из деструктора, помеченного как noexcept, вызывается std::terminate.

Исключение: Спецификатор noexcept(false) явно указывает компилятору, что деструктор может выбросить исключение.

Коллизия в контексте хэширования — это ситуация, когда для двух или более различных ключей функция хэширования вычисляет одинаковое хэш-значение. Это происходит, когда диапазон возможных входных ключей значительно больше, чем диапазон возможных хэш-значений, что характерно для большинства реальных сценариев хэширования.

Коллизии могут возникать по следующим причинам:

Принцип Дирихле: Если число входных элементов (ключей) превышает число возможных хэш-значений, то хотя бы два входных элемента должны иметь одинаковое хэш-значение. Это неотъемлемое свойство многих к одному отображений.

Свойства хэш-функции: Неидеальность (хотя и достаточно хорошей для практических целей) хэш-функции. Идеальная хэш-функция должна равномерно распределять входные значения по всему диапазону хэш-значений, но это сложнодостижимо на практике. Некоторые хэш-функции могут иметь тенденцию генерировать больше коллизий для определенных наборов входных данных.

Одинаковые входные данные после преобразования: Если хэш-функция использует только часть входных данных или выполняет преобразования, которые приводят к одинаковым результатам для разных исходных данных.

Например, простая хэш-функция для строк, суммирующая ASCII-значения символов, приведет к коллизии для строк "ab" и "ba", так как сумма ASCII-значений будет одинаковой.

Несмотря на то, что коллизии неизбежны в большинстве практических сценариев, существует множество методов для их разрешения, таких как:

Метод цепочек (Separate Chaining): Каждый элемент хэш-таблицы (корзина) хранит список или другую структуру данных для хранения всех элементов, хэширующихся в эту корзину.

Метод открытой адресации (Open Addressing): При коллизии элемент помещается в другую корзину, найденную с помощью определенной стратегии (линейное, квадратичное зондирование, двойное хэширование и т.д.).

Строгая гарантия исключений (strong exception guarantee) означает, что в случае выброса исключения состояние программы остается неизменным (rollback) по отношению к началу операции, либо операция успешно завершается. Иначе говоря, если операция не смогла завершиться успешно, то она гарантирует откат всех произведенных изменений.

Для обеспечения строгой гарантии часто используют идиому "копирование и обмен" (copy-and-swap):

Копируем данные

Выполняем операцию над копией

Атомарно обмениваем (swap) текущие данные с измененной копией

Пример:

В этом примере, если при создании temp произойдет исключение, исходный объект *this останется нетронутым (сохраняется строгая гарантия). Обмен std::swap(data_, temp.data_) обычно является noexcept, поскольку он просто меняет указатели или другие внутренние ресурсы векторов без возможности выброса исключения.

Применение:

Операторы присваивания: Основное место применения идиомы копирования и обмена для обеспечения строгой гарантии.

Функции, изменяющие состояние: Любые функции, которые могут выбросить исключение в процессе модификации объекта.

Управление ресурсами: Принцип RAII (Resource Acquisition Is Initialization) в сочетании со строгой гарантией позволяет безопасно управлять ресурсами, даже при исключениях.

Строгая гарантия наиболее сложна в реализации, но обеспечивает наибольшую устойчивость программы к исключениям.

std::weak_ptr используется для создания невладеющих ссылок на объект, управляемый std::shared_ptr. Это позволяет избежать циклических зависимостей между объектами, которые владеют друг другом посредством shared_ptr, тем самым предотвращая утечки памяти.

Её ключевые особенности:

Не увеличивает счетчик ссылок: Когда создается weak_ptr из shared_ptr, он не увеличивает счетчик сильных ссылок. Счетчик слабых ссылок увеличивается, но он не влияет на время жизни объекта.

Проверка валидности: weak_ptr позволяет проверить, существует ли еще объект, на который он ссылается, с помощью метода expired() или путем попытки преобразования в shared_ptr с помощью метода lock(). Метод lock() возвращает shared_ptr, если объект все еще жив, или пустой shared_ptr в противном случае.

Решение проблемы циклических ссылок: Это основное назначение weak_ptr. Если два объекта владеют друг другом посредством shared_ptr, счетчик ссылок никогда не опустится до нуля, и память, занимаемая объектами, не будет освобождена. Используя weak_ptr для одной из ссылок, можно разрешить эту циклическую зависимость.

Пример:

В этом примере, если бы B::a_ptr был std::shared_ptr, объекты A и B никогда бы не были уничтожены, так как каждый объект держал бы сильную ссылку на другой. Использование std::weak_ptr для B::a_ptr предотвращает эту проблему.

В C/C++ управление выравниванием памяти в структурах достигается с помощью директив препроцессора или специфичных для компилятора атрибутов.

Основные способы:

Директива препроцессора #pragma pack():

Позволяет задать размер выравнивания для членов структуры.

Применяется перед определением структуры и влияет на последующие определения структур до тех пор, пока не будет явно отменена.

#pragma pack(push, n) - сохраняет текущее выравнивание и устанавливает новое кратное n.

#pragma pack(pop) - восстанавливает предыдущее сохраненное выравнивание.

#pragma pack(n) - устанавливает новое выравнивание кратное n без сохранения предыдущего.

#pragma pack() - восстанавливает выравнивание по умолчанию для платформы.

#include <iostream>

#pragma pack(push, 1) // Установка выравнивания в 1 байт

struct PackedStruct {

char a;

int b;

short c;

};

#pragma pack(pop) // Восстановление предыдущего выравнивания

struct AlignedStruct {

char a;

int b;

short c;

};

int main() {

std::cout << "Размер PackedStruct: " << sizeof(PackedStruct) << std::endl;

std::cout << "Размер AlignedStruct: " << sizeof(AlignedStruct) << std::endl;

return 0;

}

Атрибуты компилятора:

__attribute__((packed)) (GCC/Clang): Применяется непосредственно к структуре или ее членам для отключения выравнивания.

__attribute__((aligned(n))) (GCC/Clang): Устанавливает минимальное выравнивание для структуры или ее членов равное n.

__declspec(align(n)) (MSVC): Устанавливает минимальное выравнивание для структуры, класса, union'а или переменной равное n.

#include <iostream>

struct __attribute__((packed)) GccPackedStruct { // GCC/Clang

char a;

int b;

short c;

};

struct __declspec(align(1)) MsvcPackedStruct { // MSVC

char a;

int b;

short c;

};

struct __attribute__((aligned(16))) AlignedStruct16 { // GCC/Clang

int x;

int y;

};

int main() {

#ifdef __GNUC__ // Проверка на GCC/Clang

std::cout << "Размер GccPackedStruct: " << sizeof(GccPackedStruct) << std::endl;

#endif

#ifdef _MSC_VER // Проверка на MSVC

std::cout << "Размер MsvcPackedStruct: " << sizeof(MsvcPackedStruct) << std::endl;

#endif

std::cout << "Размер AlignedStruct16: " << sizeof(AlignedStruct16) << std::endl;

return 0;

}

Выравнивание по умолчанию зависит от архитектуры процессора и типа данных. Оно оптимизировано для повышения производительности доступа к памяти, но может приводить к padding'у (дополнению) в структурах для выравнивания членов. Явное управление выравниванием может быть полезно для взаимодействия с внешними интерфейсами, экономии памяти или оптимизации доступа в специфичных сценариях, но может также снизить производительность, если выбрано неоптимальное выравнивание.

Исключения в C++ не хранятся как статически выделенные объекты. При возникновении исключения происходит следующее:

Создается временный объект типа исключения.

Этот объект передается механизму обработки исключений.

Механизм раскрутки стека (stack unwinding) ищет подходящий обработчик (catch).

Если обработчик найден, он получает копию (по значению, по ссылке или по указателю) этого временного объекта.

Пример:

Существует несколько основных механизмов синхронизации потоков в C++:

Мьютексы (Mutexes): Обеспечивают взаимное исключение. Только один поток может владеть мьютексом в любой момент времени. Используются для защиты общих ресурсов от одновременного доступа.

#include <mutex>

std::mutex myMutex;

void criticalSection() {

std::lock_guard<std::mutex> lock(myMutex); // Захват мьютекса

// Работа с общим ресурсом

} // Мьютекс автоматически освобождается при выходе из области видимости

Сетью (Semaphores): Управляют доступом к ограниченному количеству ресурсов. Семафор имеет счетчик, который указывает количество доступных ресурсов. Потоки могут уменьшать счетчик (для получения ресурса) и увеличивать (для освобождения).

#include <semaphore.h> // Обычно используется в POSIX-системах

sem_t mySemaphore;

void initSemaphore(int count) {

sem_init(&mySemaphore, 0, count); // Инициализация семафора со счетчиком count

}

void acquireResource() {

sem_wait(&mySemaphore); // Уменьшение счетчика, блокировка при 0

// Использование ресурса

}

void releaseResource() {

sem_post(&mySemaphore); // Увеличение счетчика

}

Условные переменные (Condition Variables): Позволяют потокам ждать определенного условия. Обычно используются вместе с мьютексами. Поток, ожидающий условия, освобождает мьютекс и блокируется до тех пор, пока другой поток не уведомит его о выполнении условия.

#include <condition_variable>

#include <mutex>

#include <queue>

std::condition_variable myCondition;

std::mutex myMutex;

std::queue<int> myQueue;

void producer(int value) {

std::lock_guard<std::mutex> lock(myMutex);

myQueue.push(value);

myCondition.notify_one(); // Уведомление одного ждущего потока

}

int consumer() {

std::unique_lock<std::mutex> lock(myMutex);

myCondition.wait(lock, [&]{ return !myQueue.empty(); }); // Ожидание условия

int value = myQueue.front();

myQueue.pop();

return value;

}

Флаги атомарной переменной (Atomic Variables): Позволяют выполнять простые операции (чтение, запись, модификация) над переменными атомарно, без необходимости использования мьютексов для этих конкретных операций.

#include <atomic>

std::atomic_int atomicCounter(0);

void incrementCounter() {

atomicCounter++; // Атомарное увеличение

}

Барьеры (Barriers): Синхронизируют несколько потоков так, чтобы ни один из них не мог продолжить выполнение до тех пор, пока все потоки не достигнут барьера.

#include <barrier> // C++20

std::barrier myBarrier(3); // Барьер для 3 потоков

void workerThread() {

// Выполнение части работы

myBarrier.arrive_and_wait(); // Ожидание всех потоков у барьера

// Выполнение следующей части работы

}

Выбор конкретного механизма зависит от характера взаимодействия между потоками:

Мьютексы: Самый распространенный способ защиты общих данных.

Семафоры: Управление доступом к ограниченному количеству ресурсов.

Условные переменные: Ожидание потоком наступления определенного события или состояния.

Атомарные переменные: Эффективное выполнение простых, атомарных операций.

Барьеры: Синхронизация потоков для совместного перехода к следующему этапу.

priority_queue в C++ — это контейнерный адаптер, предоставляющий интерфейс очереди с приоритетами. Он работает следующим образом:

Основа: По умолчанию priority_queue реализована поверх вектора (std::vector) и использует кучу (heap) для управления своими элементами. В частности, std::make_heap, std::push_heap и std::pop_heap используются для поддержания свойства кучи.

Куча: Поддерживается свойство максимальной кучи (max-heap) по умолчанию. Это означает, что наибольший элемент всегда находится в корне кучи.

Вставка (push):

Новый элемент добавляется в конец базового контейнера (например, вектора).

Затем вызывается std::push_heap, чтобы восстановить свойство кучи. Элемент "поднимается" вверх по дереву кучи до тех пор, пока не окажется на правильной позиции относительно своих родителей и потомков.

Вставка (push):

Извлечение (pop):

Самый приоритетный (наибольший в случае max-heap) элемент находится в корне (на первой позиции в плоском представлении вектора).

Чтобы удалить его, последний элемент базового контейнера переносится в корень.

Реальный корень (который мы хотим удалить) сохраняется.

Размер базового контейнера уменьшается.

Затем вызывается std::pop_heap, которая перемещает новый корневой элемент ("проваливает" его) вниз по дереву, меняя местами с наибольшим из своих потомков до тех пор, пока свойство кучи не будет восстановлено.

Остается только извлечь сохраненный ранее максимальный элемент.

Извлечение (pop):

Доступ к верхнему элементу (top): Возвращает ссылку на элемент в корне кучи (наиболее приоритетный). Эта операция не изменяет контейнер.

Порядок: По умолчанию используется std::less для сравнения элементов, что приводит к max-heap (наибольший элемент имеет наивысший приоритет). Можно указать пользовательский компаратор (например, std::greater для min-heap) и базовый контейнер.

Пример использования (max-heap):

Пример использования (min-heap):

Сложность операций:

Где N — количество элементов в очереди.

Да, знаком. Protobuf (Protocol Buffers) - это нейтральный к языку и платформе механизм сериализации структурированных данных, разработанный Google.

Основные принципы:

Определение структуры данных: Схема данных описывается в .proto файлах с использованием специального синтаксиса.

Генерация кода: Специальный компилятор protoc генерирует код для работы с этими данными на различных языках программирования (C++, Java, Python, Go и т. д.).

Эффективная сериализация/десериализация: Данные сериализуются в компактный бинарный формат, что обеспечивает высокую производительность и экономию трафика/памяти.

Преимущества:

Высокая производительность: Быстрее и компактнее, чем XML или JSON.

Поддержка множества языков: Легко интегрируется в полиглотные системы.

Простота определения схемы: .proto файлы читабельны и удобны для описания структуры данных.

Обратная совместимость: Позволяет добавлять новые поля в схему без нарушения работы старых версий клиентов.

Недостатки:

Нечитаемый бинарный формат: Отлаживать данные без инструментов сложно.

Требуется этап компиляции: Необходима генерация кода перед использованием.

Пример .proto файла:

Программа будет немедленно завершена вызовом std::terminate(). Это стандартное поведение, определенное в C++ для обработки исключений, выходящих за пределы функции, объявленной как noexcept.

Разделы стандарта C++, относящиеся к этому:

[except.spec] Спецификация noexcept.

[except.terminate] Обработка вызовом std::terminate().

Пример:

Вывод этой программы будет содержать сообщение от обработчика std::terminate(), который может зависеть от реализации, но обычно включает информацию о причине завершения.

Deque (double-ended queue) — это структура данных, которая позволяет добавлять и удалять элементы с обоих концов очереди: как с начала, так и с конца. Это удобно, когда нужно гибко управлять элементами, например, реализовать очередь с приоритетом или стек с возможностью доступа к обоим концам.

В C++ стандартная библиотека предоставляет контейнер std::deque, который эффективно поддерживает операции вставки и удаления с обеих сторон.

Пример использования std::deque:

Deque часто используется, когда нужна высокая производительность при операциях с обоих концов, в отличие от std::vector, где вставка в начало менее эффективна.

fastcall — это спецификатор вызова (calling convention) в C/C++, который определяет, как передаются аргументы функции и возвращается значение между вызывающей и вызываемой функциями. Основное назначение fastcall — повышение производительности вызова функции за счет передачи части или всех аргументов в регистрах процессора, а не через стек.

Особенности использования:

Передача аргументов: Первые несколько аргументов (количество зависит от архитектуры и компилятора, обычно 2-4) передаются в регистрах общего назначения. Остальные аргументы передаются через стек.

Регистры: Используются конкретные регистры, зависящие от архитектуры и компилятора (например, в x86 это могут быть ECX, EDX).

Стек: Если аргументов больше, чем помещается в регистры, остальные аргументы помещаются в стек справа налево.

Очистка стека: Очистка стека после вызова функции выполняется вызываемой функцией (callee-cleanup).

Типы данных: Обычно в регистрах передаются целочисленные и указательные типы данных. Структуры и большие объекты обычно передаются через стек.

Совместимость: fastcall не является стандартным спецификатором вызова в C/C++. Его поддержка и конкретное поведение зависят от компилятора (например, MSVC, GCC). Нельзя смешивать вызовы с разными спецификаторами без явного объявления.

Применение: Может использоваться для оптимизации часто вызываемых функций с небольшим количеством аргументов.

Объявление: Объявляется перед типом возвращаемого значения функции.

Пример:

noexcept - это спецификатор исключений в C++, появившийся в C++11. Он указывает, что функция обещает не генерировать исключений.

Применение:

Оптимизация: Компилятор может выполнять оптимизации, зная, что из функции не будет выброшено исключение. Например, он может избежать генерирования кода для раскрутки стека.

Улучшение производительности: Перемещение объектов (через конструктор перемещения или оператор присваивания перемещением) может быть значительно быстрее, если эти операции помечены как noexcept, так как это позволяет использовать более эффективные реализации (например, std::vector может менять указатели вместо копирования элементов).

Безопасность исключений (Exception Safety): Помогает гарантировать, что определенные операции (особенно перемещение) не приведут к выбросу исключения, что важно для реализации сильной гарантии безопасности исключений ("strong exception safety").

Документация: Явно указывает намерение разработчика относительно исключений, делая код более понятным.

Синтаксис:

Применяется к объявлению функции после списка параметров и перед спецификатором const, если применимо, и телом функции.

Поведение при нарушении:

Если функция, помеченная как noexcept, все же выбрасывает исключение, программа завершается вызовом std::terminate. Это происходит потому, что компилятор предполагает, что исключений не будет, и не генерирует необходимый код для их обработки.

Отличие от старого throw():

Старый спецификатор throw() (C++98) также указывал, что функция не выбрасывает исключений, но его семантика отличалась. При нарушении throw() вызывалась std::unexpected, а не std::terminate. Кроме того, throw() имел негативные последствия для производительности в некоторых случаях, тогда как noexcept скорее предназначен для оптимизации. С++11 устаревший throw() был заменен на noexcept.

Главное различие в том, как элементы хранятся и извлекаются:

std::map: Хранит элементы в отсортированном порядке ключей. Обычно реализуется на основе красно-черного дерева. Поиск, вставка и удаление имеют логарифмическую сложность O(log N), где N — количество элементов.

std::unordered_map: Хранит элементы в хэш-таблице. Порядок элементов произвольный. В среднем, поиск, вставка и удаление имеют константную сложность O(1). В худшем случае, при наличии коллизий, сложность может достигать O(N).

Пример использования:

Архитектурный паттерн, разделяющий приложение на три взаимосвязанные части:

Model (Модель): Представляет данные, бизнес-логику и правила работы с данными. Не зависит от представления и контроллера. Отвечает за состояние данных.

View (Представление): Отображает данные из модели пользователю. Отвечает за пользовательский интерфейс. Не содержит бизнес-логики и напрямую не взаимодействует с моделью (получает данные через контроллер). Может уведомлять контроллер о событиях пользователя.

Controller (Контроллер): Связывает модель и представление. Обрабатывает ввод пользователя, вызывает соответствующие методы модели для обновления данных и выбирает представление для отображения результата.

Взаимодействие:

Пользователь взаимодействует с Представлением.

Представление уведомляет Контроллер о действии пользователя.

Контроллер обрабатывает действие, возможно, взаимодействуя с Моделью для обновления состояния или получения данных.

Модель уведомляет Контроллер об изменениях данных (в некоторых реализациях, Модель напрямую уведомляет Представление).

Контроллер выбирает соответствующее Представление для обновления пользовательского интерфейса и передает ему данные из Модели.

Представление обновляется, отображая актуальные данные.

Преимущества:

Разделение ответственности.

Повышение тестируемости (можно тестировать Модель и Контроллер независимо).

Улучшение сопровождаемости и модифицируемости кода.

Возможность использовать несколько Представлений для одной Модели.

Недостатки:

Усложнение структуры для простых приложений.

Может возникнуть "толстый" контроллер (Massive Controller) при неправильном проектировании.

Взаимодействие между компонентами может быть сложным.

Применение:

Широко используется в веб-разработке (например, фреймворки вроде Ruby on Rails, Django) и разработке десктопных приложений.

При помощи объектов. При выбрасывании исключения с помощью throw создается временный объект, который затем передается механизму обработки исключений. Тип этого объекта определяет, какой обработчик catch будет выбран.

Вот пример демонстрации сохранения исключений в виде объектов:

Делеторы в умных указателях (например, std::unique_ptr, std::shared_ptr) используются для определения пользовательской функции или объекта, который будет вызван для освобождения ресурса, управляемого указателем, вместо стандартного оператора delete.

Типичные случаи использования:

Управление не-Heap ресурсами: Освобождение ресурсов, выделенных не с помощью new (например, файлы с помощью fclose, хэндлы WinAPI, ресурсы в памяти с использованием malloc и free).

Ресурсы с пользовательским освобождением: Когда требуется выполнить дополнительные действия перед освобождением памяти или использовать специфические функции освобождения для определенных объектов.

Интероперабельность с C API: Работа с указателями, полученными из библиотек C, где освобождение ресурса требует вызова определенной функции.

Пример с std::unique_ptr:

Пример с std::shared_ptr:

Аббревиатура KISS расшифровывается как "Keep It Simple, Stupid" — "Делай проще, глупец".

Это принцип разработки программного обеспечения, который призывает создавать максимально простые и понятные решения, избегая излишней сложности. Простота облегчает поддержку, тестирование и понимание кода.

Сравнивать напрямую оператором == не рекомендуется из-за особенностей представления и округления. Вместо этого следует проверить, находится ли абсолютное значение разности чисел в пределах некоторого малого порога (эпсилон).

Более устойчивый подход учитывает относительную ошибку, особенно при сравнении очень больших или очень маленьких чисел:

Выбор epsilon зависит от требуемой точности и диапазона сравниваемых значений. std::numeric_limits<float>::epsilon() представляет наименьшее число, такое что 1.0 + epsilon отлично от 1.0.

Важно помнить, что сравнение NaN (Not a Number) всегда возвращает false, включая сравнение NaN с самим собой (nan == nan всегда false). Если требуется обрабатывать NaN, это должно делаться отдельно.

Семантика перемещения для вектора в C++ позволяет эффективно передавать владение ресурсами (например, выделенной памятью) от одного объекта другому, избегая дорогостоящего копирования. Это достигается за счет использования rvalue-ссылок (&&) и функций-членов, помеченных как noexcept.

Основные механизмы:

Конструктор перемещения: Принимает rvalue-ссылку на другой вектор и "крадет" его внутренние ресурсы, обнуляя указатели у исходного объекта.

std::vector(std::vector&& other) noexcept;

Оператор присваивания перемещения: Аналогично конструктору перемещения, "крадет" ресурсы у правого операнда.

std::vector& operator=(std::vector&& other) noexcept;

Функции, возвращающие вектор по значению/rvalue-ссылке: Компилятор может применить оптимизации (например, NRVO или возврат rvalue) для избежания копирования.

std::vector<int> create_vector() {

std::vector<int> v = {1, 2, 3};

return v; // Здесь может сработать возврат rvalue/NRVO

}

std::vector<int> process_vector(std::vector<int>&& v) {

// работа с перемещенным вектором

return std::move(v); // Явное перемещение, если нужно

}

std::move: Приводит lvalue к rvalue-ссылке, позволяя выбрать перегрузку с перемещением.

std::vector<int> source = {10, 20, 30};

std::vector<int> destination = std::move(source); // Вызов оператора присваивания перемещения

// source становится в валидном, но неопределенном состоянии (обычно пуст)

Преимущества:

Производительность: Значительно уменьшает время выполнения операций, связанных с передачей владения большими векторами (например, в функциях или при возврате из них).

Сокращение потребления памяти: Избегает создания дубликатов данных.

Применение:

Возврат векторов из функций по значению.

Передача векторов в функции по rvalue-ссылке.

Использование std::move для явного перемещения из lvalue, когда исходный объект больше не нужен.

Важно понимать, что после перемещения исходный вектор находится в валидном, но неопределенном состоянии (moved-from state). Для std::vector это обычно означает, что он пуст, но полагаться на это не стоит, кроме случаев, когда это явно гарантировано стандартом или документацией.

Итератор в STL — это обобщенная концепция, предоставляющая способ доступа к элементам контейнера (например, vector, list, map) последовательно, без раскрытия внутренней структуры этого контейнера. Это похоже на указатель на элемент, но с дополнительными возможностями в зависимости от категории итератора.

Основные функции итератора:

Получение доступа к текущему элементу (*it).

Перемещение к следующему элементу (++it).

Сравнение с другим итератором (например, для определения конца последовательности it != end()).

Категории итераторов (в порядке расширения возможностей):

Input Iterator: Могут считывать элементы однократно (например, ввод из потока). Поддерживают *it (для чтения), ++it, it == other.

Output Iterator: Могут записывать элементы однократно (например, вывод в поток). Поддерживают *it (для записи), ++it.

Forward Iterator: Могут считывать и записывать элементы многократно и перемещаться только вперед. Поддерживают *it (чтение/запись), ++it, it == other.

Bidirectional Iterator: Могут перемещаться как вперед, так и назад. Поддерживают все операции Forward Iterator, а также --it.

Random Access Iterator: Могут перемещаться на произвольное количество элементов за один шаг (как указатели). Поддерживают все операции Bidirectional Iterator, а также:

it + n, it - n (перемещение на n элементов)

it += n, it -= n

it[n] (доступ к элементу со смещением n)

it < other, <=, >, >= (сравнение позиций)

it += n, it -= n

Пример использования:

Итераторы обеспечивают абстракцию над конкретным типом контейнера, позволяя алгоритмам STL работать с различными типами данных единообразно.

Ссылка — это синоним существующего объекта. Указатель — переменная, хранящая адрес другого объекта.

Основные отличия:

Инициализация: Ссылки должны быть инициализированы при объявлении и привязаны к существующему объекту. Указатели могут быть объявлены без инициализации (будут иметь неопределенное значение) или с присвоением значения nullptr.

Перепривязка: Ссылки нельзя перепривязать к другому объекту после инициализации. Указатели можно изменить, чтобы они указывали на другой объект.

nullptr: Ссылки не могут быть nullptr (обязаны ссылаться на действительный объект). Указатели могут быть nullptr (указывать в никуда).

Размер: Размер ссылки, как правило, такой же, как у объекта, на который она ссылается (хотя это зависит от реализации компилятора). Размер указателя фиксирован и соответствует размеру адреса в памяти (обычно 4 или 8 байт).

Разыменование: Указатели требуют явного оператора разыменования (*) для доступа к значению объекта. Ссылки не требуют явного разыменования, доступ к значению осуществляется напрямую через имя ссылки.

Арифметика: Над указателями можно выполнять адресную арифметику (сложение/вычитание целых чисел), что позволяет перемещаться по блокам памяти (например, в массивах). Адресная арифметика над ссылками невозможна.

Примеры:

Для проверки исключения в потоке можно использовать несколько подходов, зависящих от того, как поток был создан и как обрабатываются его результаты.

Использование std::future и std::async:

Если поток был запущен с помощью std::async, результатом будет объект std::future. Вызов методов get() или wait() на этом объекте приведет к тому, что исключение, выброшенное в потоке, будет повторно выброшено в вызывающем потоке.

#include <future>

#include <iostream>

#include <stdexcept>

void thread_function() {

throw std::runtime_error("Ошибка в потоке!");

}

int main() {

std::future<void> future = std::async(std::launch::async, thread_function);

try {

future.get(); // Ждет завершения и кидает исключение, если оно было

} catch (const std::exception& e) {

std::cerr << "Поймано исключение из потока: " << e.what() << std::endl;

}

return 0;

}

Использование std::promise и std::future:

Можно явно передать std::promise в поток или хранить его вне потока, чтобы поток мог сохранить исключение.

#include <future>

#include <iostream>

#include <thread>

void thread_function_with_promise(std::promise<void>&& promise) {

try {

throw std::runtime_error("Еще одна ошибка в потоке!");

promise.set_value(); // Установить значение (если бы не было исключения)

} catch (...) {

promise.set_exception(std::current_exception()); // Сохранить исключение

}

}

int main() {

std::promise<void> promise;

std::future<void> future = promise.get_future();

std::thread t(thread_function_with_promise, std::move(promise));

try {

future.get(); // Ждет завершения и кидает сохраненное исключение

std::cerr << "Поймано исключение из потока (с помощью promise): " << e.what() << std::endl;

}

t.join(); // Ждем завершения потока

return 0;

}

Ручное управление исключениями:

Поток может перехватить исключение внутри себя и сохранить его (например, в std::exception_ptr) или уведомить другой поток о его возникновении (например, через атомарную переменную или мьютекс с условной переменной).

#include <iostream>

#include <thread>

#include <atomic>

#include <exception>

#include <mutex>

#include <condition_variable>

std::exception_ptr global_exception_ptr = nullptr;

std::mutex exception_mutex;

std::condition_variable exception_cv;

bool exception_occurred = false;

void thread_function_manual() {

try {

throw std::logic_error("Логическая ошибка в потоке!");

} catch (...) {

std::lock_guard<std::mutex> lock(exception_mutex);

global_exception_ptr = std::current_exception(); // Сохранить текущее исключение

exception_occurred = true;

exception_cv.notify_one(); // Уведомить ожидающий поток

}

}

int main() {

std::thread t(thread_function_manual);

// Основной поток может ждать уведомления об исключении

std::unique_lock<std::mutex> lock(exception_mutex);

exception_cv.wait(lock, []{ return exception_occurred; });

if (global_exception_ptr) {

try {

std::rethrow_exception(global_exception_ptr); // Перебросить сохраненное исключение

std::cerr << "Поймано исключение из потока (ручное управление): " << e.what() << std::endl;

}

}

t.join();

return 0;

}

Выбор метода зависит от контекста: std::async с std::future наиболее удобен для получения результата или исключения из отдельной задачи, тогда как std::promise/std::future или ручное управление могут быть полезны в более сложных сценариях синхронизации.

Наследование от std::vector не рекомендуется из-за отсутствия виртуальных деструкторов и других виртуальных функций, что нарушает LSP (принцип подстановки Лисков) и приводит к проблемам при полиморфном использовании.

Примеры проблем:

Проблема со срезом (slicing): При передаче объекта производного класса по значению или ссылке на базовый класс std::vector, специфичные для производного класса данные и поведение будут потеряны.

#include <vector>

#include <iostream>

class MyVector : public std::vector<int> {

public:

int my_data = 100;

// Без виртуального деструктора

~MyVector() {

std::cout << "MyVector destructor" << std::endl;

}

};

void process_vector(std::vector<int> vec) {

// Принимает по значению - происходит срез

// Деструктор MyVector не будет вызван

std::cout << "Processing std::vector" << std::endl;

}

int main() {

MyVector mv;

mv.push_back(1);

process_vector(mv); // Происходит срез объекта MyVector

return 0;

} // Здесь будет вызван только деструктор std::vector

Отсутствие виртуального деструктора: Если вы удаляете объект производного класса через указатель на базовый класс std::vector, деструктор производного класса не будет вызван, что может привести к утечкам ресурсов.

#include <vector>

#include <iostream>

#include <memory> // Для unique_ptr

class DerivedVector : public std::vector<int> {

public:

int* resource;

DerivedVector() : std::vector<int>(), resource(new int) {

std::cout << "DerivedVector constructor" << std::endl;

}

// Отсутствует виртуальный деструктор

~DerivedVector() {

std::cout << "DerivedVector destructor" << std::endl;

delete resource; // Может не быть вызвано

}

};

int main() {

// Удаление через указатель на базовый класс

std::vector<int>* base_ptr = new DerivedVector();

// При delete base_ptr вызывается только деструктор std::vector,

// деструктор DerivedVector не вызывается, происходит утечка resource

delete base_ptr;

// Пример с unique_ptr

// std::unique_ptr<std::vector<int>> up = std::make_unique<DerivedVector>();

// up->push_back(5);

// При выходе из области видимости unique_ptr вызовет delete на сыром указателе base_ptr.

// Это эквивалентно delete base_ptr; и приведет к той же проблеме, если vector не имеет виртуального деструктора.

return 0;

} // Память, выделенная для resource, не будет освобождена

Конструкторы: Поведение конструкторов std::vector (например, конструктора копирования, перемещения) может не соответствовать ожиданиям для производного класса, если он добавляет свое состояние или логику.

Вместо наследования от std::vector, лучшими подходами являются:

Композиция: Использовать std::vector как член класса. Это позволяет контролировать интерфейс и поведение нового класса, используя std::vector внутри.#include <vector>

#include <iostream>

class MyContainer {

private:

std::vector<int> data;

int my_extra_data = 100;

public:

void add(int val) {

data.push_back(val);

}

const std::vector<int>& get_data() const { // Предоставляем доступ к вектору при необходимости

return data;

}

// Деструктор MyContainer корректно вызовет деструктор data

~MyContainer() {

std::cout << "MyContainer destructor" << std::endl;

}

};

int main() {

MyContainer mc;

mc.add(1);

std::cout << "Data size: " << mc.get_data().size() << std::endl;

return 0;

} // Деструктор MyContainer вызывается, который в свою очередь вызывает деструктор std::vector

Свободные функции и алгоритмы: Расширять функциональность std::vector с помощью обычных функций или использовать стандартные алгоритмы.

Эти подходы более гибкие, безопасные и соответствуют принципам ООП и проектирования библиотек в C++. Стандартные контейнеры не предназначены для использования в качестве базовых классов.

std::vector: динамический массив, размер которого может изменяться во время выполнения. Память выделяется в куче.

std::array: фиксированный по размеру массив, размер которого определяется на этапе компиляции. Память выделяется в стеке (для локальных переменных) или в статической области памяти (для глобальных/статических переменных).

Основные отличия:

Пример std::vector:

Пример std::array:

Итераторы в C++ представляют собой обобщенные указатели, предоставляющие доступ к элементам контейнера и возможность их обхода. Согласно стандарту C++, различают 5 основных категорий итераторов, упорядоченных по возрастанию их возможностей:

Input iterators (Входные итераторы):

Позволяют только чтение элементов (operator*() const).

Позволяют инкрементировать итератор (operator++()).

Поддерживают сравнение на равенство (operator==(), operator!=()).

Пример: итераторы для потоков ввода (std::istream_iterator).

Output iterators (Выходные итераторы):

Позволяют только запись элементов (operator*()).

Пример: итераторы для потоков вывода (std::ostream_iterator).

Forward iterators (Однонаправленные итераторы):

Поддерживают все возможности входных и выходных итераторов.

Гарантируют, что инкрементирование итератора всегда ведет к следующему элементу или концу последовательности.

Пример: итераторы для односвязных списков (std::forward_list).

Bidirectional iterators (Двунаправленные итераторы):

Поддерживают все возможности однонаправленных итераторов.

Позволяют декрементировать итератор (operator--()) для перемещения к предыдущему элементу.

Пример: итераторы для списков (std::list) и множеств (std::set).

Random access iterators (Итераторы произвольного доступа):

Поддерживают все возможности двунаправленных итераторов.

Позволяют выполнять арифметические операции с итераторами (сложение/вычитание целых чисел, вычитание итераторов) для быстрого перехода к любому элементу.

Позволяют использовать оператор [] для доступа к элементам по индексу.

Пример: итераторы для векторов (std::vector) и массивов (std::array).

Эта иерархия подразумевает, что каждая последующая категория итераторов обладает возможностями всех предыдущих. Например, итератор произвольного доступа также является двунаправленным, однонаправленным, входным и выходным.

Существуют также адаптеры итераторов (например, std::reverse_iterator, std::move_iterator), которые изменяют поведение базовых итераторов.

Да, можно.

В C++ операторы new и delete можно перегрузить для конкретного класса или глобально. Перегрузка позволяет изменить поведение выделения и освобождения памяти.

Перегрузка для конкретного класса:

Это достигается путем определения функций-членов operator new, operator new[], operator delete, operator delete[] в классе.

Перегрузка глобально:

Это достигается путем определения не-членов функций operator new, operator new[], operator delete, operator delete[] в глобальной области видимости.

Важные моменты при перегрузке:

Перегруженная функция operator new должна возвращать void* и принимать в качестве первого параметра size_t (размер выделяемой памяти в байтах).

Перегруженная функция operator delete должна принимать void* (указатель на освобождаемую память). Для версий C++11 и выше рекомендуется использовать версию с noexcept.

Перегрузка operator new[] и operator delete[] аналогична перегрузке operator new и operator delete, но предназначена для массивов объектов.

Перегрузка operator delete может принимать дополнительный параметр size_t (размер освобождаемого объекта), который доступен только при определенных условиях (зависит от компилятора и версии стандарта).

При перегрузке операторов new и delete важно обеспечить корректное выделение и освобождение памяти, избегая утечек или двойного освобождения.

Обычно перегрузка операторов new и delete используется для реализации специализированных аллокаторов памяти (например, пулов объектов, выравнивания памяти) или для отладки.

Мьютекс — это бинарный семафор, который либо свободен (значение 1), либо занят (значение 0). Он используется для защиты критических секций и обеспечивает взаимоисключающий доступ к ресурсу. Только поток, который захватил мьютекс, может его освободить.

Семафор — это обобщение мьютекса, представляющее собой счетчик. Он может принимать неотрицательные значения и используется для управления доступом к ограниченному количеству ресурсов. Поток может захватить семафор, если счетчик больше нуля (уменьшая его), и освободить семафор (увеличивая счетчик). Семафор может быть освобожден другим потоком, не тем, который его захватывал.

Основные отличия:

Назначение: Мьютекс для защиты критических секций, Семафор для управления доступом к ресурсам.

Значение: Мьютекс (0 или 1), Семафор (любое неотрицательное число).

Владение: Мьютекс имеет владельца (поток, который его захватил), Семафор не имеет четкого понятия владения.

Кто может освободить: Мьютекс может освободить только владелец, Семафор может освободить любой поток.

Сравнение в таблице:

Лямбда-функции — это безымянные inline-функции, которые можно определить и использовать непосредственно в точке вызова. Появились в C++11.

Синтаксис:

[capture list] (список захвата) — определяет, какие внешние переменные доступны внутри лямбды и как они захватываются (по значению [var] или по ссылке [&var]). [=] захватывает все по значению, [&] — все по ссылке. [] означает отсутствие захвата.

(parameter list) (список параметров) — аналогичен списку параметров обычной функции.

-> return type (тип возвращаемого значения) — указывает тип возвращаемого значения. Может быть опущен, если тип может быть выведен компилятором (начиная с C++14).

{} (тело лямбда-функции) — содержит исполняемый код лямбды.

Примеры использования:

Сортировка с пользовательским критерием:

Использование захвата:

Захват по ссылке для модификации внешней переменной (требует mutable для захвата по значению):

Преимущества:

Краткость и наглядность: Упрощают код, когда функция нужна только в одном месте.

Местоположение: Определяются там, где используются, улучшая локальность кода.

Захват переменных: Легкий доступ к переменным из окружающего контекста.

Эффективность: Часто компилируются как inline-функции, избегая накладных расходов на вызов.

Недостатки:

Могут усложнить отладку, если используются в сложном контексте.

Чрезмерное использование для сложных задач может сделать код менее читаемым.

Инкапсуляция (Encapsulation)

Наследование (Inheritance)

Полиморфизм (Polymorphism)

cout — это предопределенный объект класса ostream в стандартной библиотеке C++, используемый для вывода данных на стандартное устройство вывода, которым, как правило, является консоль. Он является частью библиотеки <iostream>.

Ключевые особенности:

Перегрузка оператора <<: cout использует оператор сдвига влево (<<) для вывода данных различных типов (целые числа, числа с плавающей запятой, строки, символы и т.д.).

Буферизация: cout обычно буферизует выводимые данные. Это означает, что данные не сразу отправляются на устройство вывода, а накапливаются в буфере и сбрасываются при определенных условиях (например, при заполнении буфера, при использовании endl или flush, при завершении программы).

Связь с cin: По умолчанию, cout связан с cin. При попытке чтения из cin, буфер cout автоматически сбрасывается.

Манипуляторы: Вместе с cout часто используются манипуляторы потока (например, endl, setw, fixed, scientific) для форматирования вывода.

Пример использования:

Основы языка:

Типы данных (встроенные, пользовательские).

Операторы и выражения.

Управляющие структуры (ветвления, циклы).

Функции и их перегрузка.

Указатели и ссылки.

Динамическое выделение памяти (new, delete).

Пространства имен.

Указатели и ссылки.

Пространства имен.

Объектно-ориентированное программирование (ООП):

Классы и объекты.

Инкапсуляция, наследование, полиморфизм.

Конструкторы и деструкторы.

Виртуальные функции и абстрактные классы.

Шаблоны классов и функций.

Умные указатели (std::unique_ptr, std::shared_ptr, std::weak_ptr).

Классы и объекты.

Стандартная библиотека C++ (STL):

Контейнеры (векторы, списки, мапы, сеты и др.).

Алгоритмы (сортировка, поиск, преобразования и др.).

Итераторы.

Функциональные объекты (лямбда-выражения, функторы).

Потоки ввода-вывода (iostream).

Итераторы.

Работа с памятью:

Стек и куча.

Управление ресурсами через RAII (Resource Acquisition Is Initialization).

Стек и куча.

Обработка исключений:

try, catch, throw.

try, catch, throw.

Многопоточность:

std::thread.

Мьютексы (std::mutex).

Условные переменные (std::condition_variable).

std::thread.

Новые стандарты C++ (C++11/14/17/20):

Автоматический вывод типов (auto).

Rvalue ссылки и семантика перемещения.

Лямбда-выражения.

Диапазонные циклы for.

Параметры шаблонов переменной длины (variadic templates).

Сопрограммы (coroutines) (C++20).

Лямбда-выражения.

Системное программирование:

Работа с файлами.

Сетевое взаимодействие (основы).

Работа с файлами.

Инструменты разработки:

Системы сборки (CMake).

Системы контроля версий (Git).

Отладчики (GDB).

Отладчики (GDB).

Производительность:

Оптимизация кода.

Профилирование.

Работа с низкоуровневыми деталями (при необходимости).

Оптимизация кода.

Профилирование.

Дополнительно знаком с принципами TDD (Test Driven Development) и Unit Testing.

Гарантии безопасности исключений определяют поведение функции в случае возникновения исключения. Выделяют четыре уровня гарантий:

Базовая гарантия (Basic guarantee): Если функция выбрасывает исключение, программа остается в валидном состоянии. Ресурсы не утекают (например, память освобождается), но точное состояние объектов может быть неизвестно. Сохраняется возможность дальше работать с приложением.

// Пример базовой гарантии

void basic_guarantee_function(std::vector<int>& vec, int value) {

// Могут возникнуть исключения при вставке

vec.push_back(value);

// Если исключение произошло, vec может быть в непредсказуемом состоянии (частично изменено),

// но память, выделенная для vec, будет корректно освобождена при выходе из области видимости.

}

Строгая гарантия (Strong guarantee): Если функция выбрасывает исключение, состояние программы остается идентичным тому, которое было до вызова функции. Все изменения откатываются.

// Пример строгой гарантии

// Используем Copy-and-Swap идиому для обеспечения строгой гарантии

class Resource {

int* data;

size_t size;

public:

Resource(size_t s) : size(s), data(new int[s]) {}

~Resource() { delete[] data; }

Resource(const Resource& other) : size(other.size), data(new int[other.size]) {

std::copy(other.data, other.data + size, data); // Может выбросить исключение

}

Resource& operator=(Resource other) // Передача по значению вызывает копирование

{

swap(*this, other); // Не выбрасывает исключений

return *this;

}

friend void swap(Resource& first, Resource& second) noexcept {

using std::swap;

swap(first.data, second.data);

swap(first.size, second.size);

}

// ... другие члены

};

void strong_guarantee_function(Resource& res, size_t new_size) {

Resource temp(new_size); // Если здесь исключение, res не изменится

res = temp; // Используем перегруженный оператор =, который обеспечивает строгую гарантию

}

Гарантия отсутствия исключений (No-throw guarantee): Функция гарантированно не выбрасывает исключений. Такие функции помечаются спецификатором noexcept.

// Пример отсутствия исключений

void no_throw_function() noexcept {

// Нет операций, которые могут выбросить исключение

int a = 5;

int b = 10;

int c = a + b;

}

Гарантия сбоя (Failure guarantee): В контексте безопасности исключений иногда упоминается этот уровень, означающий, что функция может оставить программу в неопределенном состоянии, возможна утечка ресурсов или сбои. Это фактически отсутствие гарантий. Следует избегать такого поведения.

Важность гарантий безопасности исключений заключается в создании надежного и предсказуемого кода, который корректно обрабатывает ошибки и управляет ресурсами даже при возникновении исключений. Выбор уровня гарантии зависит от критичности операции, стоимости отката и вероятности возникновения исключения.

Таблица сравнения гарантий:

Глобальные переменные

Время существования: С момента запуска программы до ее завершения.

Место хранения: Обычно в статической области памяти (.data или .bss секции исполняемого файла).

Инициализация:

Нестатические глобальные переменные, определенные на уровне файла, с нетривиальными конструкторами инициализируются до вызова main().

Глобальные переменные с нулевой инициализацией (например, int i = 0;) также инициализируются до main().

Переменные, определенные с const или имеющие статический срок хранения, инициализируются при загрузке программы.

Доступность: Видны во всех функциях в пределах области видимости.

Локальные переменные

Время существования: С момента определения переменной внутри блока кода (функции, цикла, условного оператора и т.д.) до выхода из этого блока.

Место хранения: Обычно на стеке (для автоматических переменных). Статические локальные переменные хранятся в статической области памяти.

Инициализация:

Автоматические локальные переменные не инициализируются по умолчанию (содержат "мусор"). Требуют явной инициализации.

Статические локальные переменные инициализируются один раз при первом достижении их определения.

Доступность: Видны только внутри блока кода, где они определены.

Примеры

Таблица сравнения

std::list и std::deque.

std::list:

Двусвязный список.

Добавление в начало (push_front) и конец (push_back) за константное время O(1).

Вставка и удаление элементов в любом месте также за константное время (при наличии итератора на нужный элемент).

Не обеспечивает произвольный доступ по индексу O(1).

Больше накладные расходы на хранение по сравнению с std::vector.

std::deque:

Двусторонняя очередь.

Позволяет быстро (за константное время O(1)) добавлять и удалять элементы как в начале (push_front, pop_front), так и в конце (push_back, pop_back).

Обеспечивает произвольный доступ по индексу за константное время O(1).

Внутренне реализован как набор блоков, что может привести к фрагментации памяти и медленному доступу по индексу по сравнению с std::vector (хотя асимптотика такая же).

Пример использования push_front:

weak_ptr используется для устранения циклических зависимостей между объектами, управляемыми shared_ptr. Он не увеличивает счетчик ссылок объекта и не предотвращает его уничтожение. Позволяет безопасно проверить, существует ли объект, на который он ссылается, и получить временный shared_ptr (с помощью lock()) для безопасного доступа.

Пример использования:

Основные сценарии применения:

Циклические ссылки: Разрыв циклов владения между объектами, управляемыми shared_ptr.

Кэширование: Ссылка на объекты, которые могут быть удалены из кэша из-за нехватки памяти, без предотвращения их удаления.

Родитель-Потомок связи: Потомок может иметь weak_ptr на родителя, чтобы избежать циклической ссылки.

Сравнение shared_ptr и weak_ptr:

Разница между созданием объекта через конструктор shared_ptr и функцией make_shared заключается в механизме выделения памяти и эффективности.

Конструктор shared_ptr: Выделяет память для объекта и управляющего блока (счетчики ссылок) отдельно.#include <memory>

class MyClass {

public:

int value;

MyClass(int v) : value(v) {}

};

int main() {

// Выделение памяти для MyClass и управляющего блока происходит отдельно

std::shared_ptr<MyClass> ptr1(new MyClass(10));

return 0;

}

make_shared: Выделяет память для объекта и управляющего блока одним блоком памяти.#include <memory>

class MyClass {

public:

int value;

};

int main() {

// Выделение памяти для MyClass и управляющего блока происходит одним блоком

std::shared_ptr<MyClass> ptr2 = std::make_shared<MyClass>(20);

return 0;

}

Использование make_shared предпочтительнее, если нет специфических причин использовать конструктор, так как это более эффективно и безопасно с точки зрения исключений. Конструктор может потребоваться, например, при создании shared_ptr из уже существующего "сырого" указателя.

Чтобы установить конкретный бит в числе в 1 с помощью побитовой операции ИЛИ (OR), нужно использовать маску, у которой в нужной позиции стоит 1, а остальные биты — 0.

Например, чтобы установить бит с номером n (нумерация с 0, начиная с младшего бита), делаем так:

Здесь 1 << n сдвигает единицу в позицию n, а операция | устанавливает этот бит в числе num в 1, не изменяя остальные биты.

Пример:

Оператор delete освобождает память, ранее выделенную оператором new или new[].

Работа оператора delete включает в себя следующее:

Вызов деструктора: Если удаляется объект класса, оператор delete сначала вызывает его деструктор для выполнения необходимых операций очистки или освобождения ресурсов, связанных с объектом. Для POD-типов или встроенных типов деструктор не вызывается.

Освобождение памяти: После вызова деструктора (если применимо), оператор delete возвращает память обратно в пул свободной памяти (обычно через вызов специфичной для платформы функции, например, free для malloc).

Синтаксис:

Важно помнить:

Вызов delete для указателя, не указывающего на память, выделенную new или new[], или для уже освобожденной памяти приводит к неопределенному поведению.

Использование delete и delete[] должно соответствовать способу выделения памяти (new -> delete, new[] -> delete[]). Несоответствие также приводит к неопределенному поведению.

После вызова delete указатель становится "висячим" (dangling pointer) и должен быть присвоен nullptr, чтобы избежать случайного использования.

Пример:

Процесс — это активная сущность, выполняемая программа со своим адресным пространством, файловыми дескрипторами, стеком, регистровым контекстом и другими ресурсами. Процессы изолированы друг от друга.

Поток (thread) — это наименьшая единица, которой может быть выделено процессорное время. Находится внутри процесса, разделяет его ресурсы (адресное пространство, файловые дескрипторы). У каждого потока свой стек, Program Counter, регистровый контекст. Потоки внутри одного процесса могут взаимодействовать напрямую, что требует синхронизации.

Основные отличия:

Отложенная инициализация (lazy initialization) — это шаблон проектирования, при котором инициализация объекта, переменной или значения происходит только при первом обращении к нему.

Преимущества:

Экономия памяти: Объекты создаются только тогда, когда они действительно нужны.

Ускорение запуска: Инициализация ресурсоемких объектов переносится на более позднее время.

Обработка циклических зависимостей: Позволяет инициализировать объекты, которые зависят друг от друга.

Недостатки:

Сложность в многопоточной среде: Требует синхронизации для безопасного доступа.

Нагрузка при первом доступе: Первое обращение может быть медленнее из-за инициализации.

Пример в C++:

В C++ манглинг имен (name mangling) используется компилятором для кодирования информации о типе, пространстве имен и других атрибутах функции или переменной в ее символьном имени. Это необходимо для поддержки перегрузки функций и других возможностей C++.

В языке C манглинг не используется. Поэтому:

Для C кода: Манглинг отключен по умолчанию.

Для C++ кода, при взаимодействии с C кодом: Используется спецификатор компоновки extern "C".

Этот спецификатор указывает компилятору C++, что символы (функции или переменные), объявленные внутри блока или непосредственно с ним, должны использовать соглашение о именах и вызовах, соответствующее C. Это позволяет компоновщику C++ находить функции и переменные, экспортированные из библиотек или объектов, скомпилированных C компилятором, или наоборот.

Примеры применения extern "C":

Обычно extern "C" используется в заголовочных файлах совместно с макросом __cplusplus для условного включения спецификатора компоновки только при компиляции кода как C++.

Это позволяет использовать my_c_header.h как в C, так и в C++ проектах без ошибок компоновки. При компиляции C++ компилятор увидит extern "C", а при компиляции C — нет, что соответствует стандартному поведению C.

virtual в C++ используется для объявления виртуальных функций в базовом классе. Это позволяет реализовать полиморфизм во время выполнения (runtime polymorphism).

Применение:

Определяет функцию в базовом классе, которую можно переопределить (override) в производных классах.

При вызове виртуальной функции через указатель или ссылку на базовый класс, фактически вызывается реализация функции в объекте производного класса (если она там переопределена). Это решение происходит во время выполнения через таблицу виртуальных функций (vtable).

Основные моменты:

Виртуальными могут быть только функции-члены класса. Глобальные функции, статические функции-члены и конструкторы не могут быть виртуальными.

Деструкторы могут и часто должны быть виртуальными, чтобы избежать утечек памяти при удалении объектов производных классов через указатель на базовый класс.

Если функция объявлена virtual в базовом классе, она автоматически остается виртуальной во всех производных классах, даже если ключевое слово virtual там не используется (хотя явно указывать virtual и override в производных классах рекомендуется для ясности).

Чисто виртуальные функции объявляются с = 0 и делают класс абстрактным. Такой класс нельзя инстанцировать напрямую.

Пример:

Связанные понятия:

vtable (таблица виртуальных функций): Таблица указателей на виртуальные функции класса. Каждый объект с виртуальными функциями содержит невидимый указатель (vptr) на vtable своего класса.

vptr (указатель на vtable): Необходим для определения вызываемой функции во время выполнения.

= 0 (чисто виртуальная функция): Означает, что у функции нет реализации в данном классе, делая его абстрактным.

override (спецификатор переопределения): Явно указывает, что функция в производном классе переопределяет виртуальную функцию базового. Помогает компилятору найти ошибки, если сигнатура функции не совпадает.

final (спецификатор): Запрещает дальнейшее переопределение виртуальной функции или наследование от класса.

Кратко: virtual — это основной механизм для реализации полиморфизма через наследование и динамическое связывание функций в C++.

Вызов исключения в конструкторе может привести к утечке ресурсов, если часть объектов или ресурсов внутри конструктора была успешно выделена до момента выброса исключения, а соответствующий деструктор при этом не будет вызван. Для предотвращения утечек рекомендуется использовать RAII (Resource Acquisition Is Initialization), например с помощью умных указателей или классов-оберток, которые гарантируют освобождение ресурсов при выходе из области видимости.

Вызов исключения в деструкторе приводит к неопределенному поведению. Если деструктор вызван в результате другого исключения (stack unwinding), и при этом сам выбросит необработанное исключение, программа завершится вызовом std::terminate. Согласно стандарту C++, деструкторы должны быть noexcept.

Сводная таблица:

Сортировка выбором (Selection Sort): Находит минимальный элемент из несортированной части массива и помещает его в начало.

Сортировка вставками (Insertion Sort): Постепенно строит отсортированный массив, вставляя каждый элемент из несортированной части на свое место.

Пузырьковая сортировка (Bubble Sort): Многократно обходит массив, сравнивая соседние элементы и меняя их местами, если они расположены в неправильном порядке.

Сортировка слиянием (Merge Sort): Рекурсивно делит массив на две половины, сортирует каждую половину, а затем объединяет (сливает) отсортированные половины.

Быстрая сортировка (Quick Sort): Выбирает опорный элемент (pivot) и разбивает другие элементы на две подмассива: те, что меньше опорного, и те, что больше. Затем рекурсивно сортирует эти подмассива.

Сортировка кучей (Heap Sort): Использует структуру данных "куча" (heap). Строит из массива максимальную кучу, а затем многократно извлекает из кучи максимальный элемент и помещает его в конец отсортированной части массива.

Сортировка Шелла (Shell Sort): Улучшение сортировки вставками. Сортирует элементы, разделенные определенным интервалом, затем уменьшает интервал и повторяет процесс.

Сортировка подсчетом (Counting Sort): Используется для сортировки целочисленных данных в определенном диапазоне. Считает количество вхождений каждого элемента и использует эту информацию для построения отсортированного массива.

Поразрядная сортировка (Radix Sort): Сортирует числа, обрабатывая их по разрядам (единицы, десятки, сотни и т.д.), используя вспомогательный алгоритм сортировки (например, сортировка подсчетом).

Основные характеристики некоторых сортировок:

где n — размер массива, k — диапазон значений (для Counting Sort) или количество цифр (для Radix Sort).

В C++ конструктор не может быть виртуальным методом. Виртуальные функции предназначены для обеспечения полиморфизма через механизм позднего связывания, а конструкторы вызываются при создании объекта и не могут быть виртуальными.

Причины:

Во время вызова конструктора объект ещё не полностью создан, и виртуальная таблица (vtable) ещё не инициализирована, поэтому виртуальный вызов невозможен.

Для обеспечения правильной инициализации базовых классов конструкторы вызываются в фиксированном порядке, без полиморфизма.

Если нужна виртуальная инициализация, обычно используют паттерн "виртуальный конструктор" через фабричные методы или виртуальные функции, вызываемые после создания объекта.

Временная сложность вставки в список зависит от конкретной реализации списка:

Односвязный список:

Вставка в начало: O(1) - нужно только изменить указатель заголовка.

Вставка в конец: O(N) - нужно пройти весь список, чтобы найти последний элемент.

Вставка по индексу или после определенного элемента: O(N) - нужно пройти часть списка.

Односвязный список:

Двухсвязный список:

Вставка в начало: O(1) - нужно изменить указатели заголовка и первого элемента.

Вставка в конец: O(1) - если есть указатель на хвост, или O(N), если нет.

Вставка по индексу или после определенного элемента: O(N) - нужно пройти часть списка. Но если есть ссылка на предыдущий элемент, можно перейти к нужному месту быстрее, чем в односвязном списке.

Двухсвязный список:

Динамический массив (например, std::vector):

Вставка в конец: В среднем O(1) - если есть свободное место, или O(N) - при реаллокации.

Вставка в середину или начало: O(N) - элементы после места вставки нужно сдвинуть.

Таким образом, общая временная сложность вставки в список в худшем случае часто равна O(N), но может быть O(1) для определенных операций и реализаций.

Механизм переопределения методов (override) в C++ позволяет дочернему классу предоставить свою собственную реализацию метода, который уже объявлен в его родительском классе. Это ключевой элемент полиморфизма времени выполнения.

Для успешного переопределения должны быть соблюдены следующие условия:

Метод в базовом классе должен быть объявлен как виртуальный (virtual).

Имя метода в производном классе должно совпадать с именем метода в базовом классе.

Список аргументов методов должен быть идентичен (включая константность).

Тип возвращаемого значения должен быть либо тем же самым, либо ковариантным (для указателей или ссылок).

Квалификаторы (например, const, volatile) должны совпадать.

Обе функции должны принадлежать к одной иерархии классов (базовый и производный).

Доступность метода (public, protected, private) может быть изменена, но, как правило, остается такой же или более свободной.

Ключевое слово override (доступно с C++11) не является обязательным для переопределения, но его использование настоятельно рекомендуется, так как оно заставляет компилятор проверить, действительно ли метод в производном классе переопределяет виртуальный метод базового класса. Это помогает выявить ошибки, такие как опечатки в имени метода или несовпадение сигнатуры.

Пример:

В данном примере, при вызове display() через указатель на базовый класс, указывающий на объект производного класса, вызывается переопределенная версия из класса Derived.

Выбрасывание исключений из конструктора приводит к тому, что объект не будет полностью создан. В этом случае автоматически вызываются деструкторы для всех уже проинициализированных подобъектов (членов-данных и базовых классов).

Выбрасывание исключений из деструктора крайне нежелательно. В случае, если исключение выбрасывается во время обработки другого активного исключения (например, при раскрутке стека), программа завершится вызовом std::terminate. Даже в отсутствие другого активного исключения, выброс исключения из деструктора может нарушить ожидаемый поток выполнения и сделать код непредсказуемым. Рекомендуется обрабатывать все исключения внутри деструктора или проектировать код так, чтобы деструктор не мог выбросить исключение.

Да, знаю.

В контексте стандартных контейнеров C++ (например, std::vector) методы resize и reserve используются для управления размером и емкостью контейнера:

reserve(n): Запрашивает, чтобы емкость вектора была не меньше, чем n. Если n больше текущей емкости, выделяется новое хранилище, и элементы перемещаются туда. Емкость может увеличиться, но размер вектора (количество элементов) не меняется. reserve используется для избежания многократных перевыделений памяти при добавлении элементов.

#include <vector>

int main() {

std::vector<int> v;

v.reserve(100); // Гарантируем емкость не менее 100

// Размер v все еще 0

// Добавление элементов до 100-го, скорее всего, не вызовет перевыделения

return 0;

}

resize(n): Изменяет размер вектора до n.

Если n меньше текущего размера, элементы после n-го удаляются.

Если n больше текущего размера:

Новые элементы добавляются в конец.

Если вызывается resize(n, value), новые элементы инициализируются значением value.

Если вызывается resize(n), новые элементы инициализируются значением по умолчанию для типа элемента (путем вызова конструктора по умолчанию).

Емкость вектора может увеличиться для размещения новых элементов, если это необходимо.

#include <vector>

int main() {

std::vector<int> v = {10, 20, 30};

v.resize(5); // Размер становится 5. Два новых элемента добавлены (инициализированы 0 для int).

// v теперь {10, 20, 30, 0, 0}

v.resize(2); // Размер становится 2. Элементы 30, 0, 0 удалены.

// v теперь {10, 20}

v.resize(6, 99); // Размер становится 6. Четыре новых элемента добавлены (инициализированы 99).

// v теперь {10, 20, 99, 99, 99, 99}

return 0;

}

Ключевые отличия:

Лямбда-функции в C++ являются синтаксическим сахаром для функциональных объектов (functors). Поля функциональных объектов соответствуют захваченным переменным из внешней области видимости лямбды.

Захват переменных необходим для следующих целей:

Передача состояния: Лямбда может использовать и изменять значения переменных, существовавших на момент ее создания. Это позволяет лямбде сохранять состояние между вызовами или получать доступ к данным, необходимым для ее работы.

int offset = 5;

auto add_offset = [offset](int x) {

return x + offset; // Захват переменной offset

};

// add_offset теперь использует сохраненное значение offset

Изменение внешней области видимости: При захвате по ссылке ([&], [&var]), лямбда может изменять значение переменной из внешней области видимости.

int counter = 0;

auto increment = [&counter]() { // Захват по ссылке

counter++;

};

increment(); // counter теперь равен 1

Использование в алгоритмах: Лямбды с захватом часто используются в алгоритмах стандартной библиотеки (например, std::for_each, std::sort), где им требуется доступ к данным, определяющим их поведение.

std::vector<int> numbers = {1, 5, 2, 8, 3};

int max_value = 7;

// Фильтруем числа, превышающие max_value

numbers.erase(std::remove_if(numbers.begin(), numbers.end(),

[&max_value](int x) { // Захват max_value

return x > max_value;

}),

numbers.end());

Передача объектов с состоянием: Поля функционального объекта позволяют лямбде сохранять и использовать экземпляры классов или структур.

struct Config {

int threshold;

};

Config config = {10};

auto is_above_threshold = [config](int value) { // Захват объекта Config

return value > config.threshold;

};

Таким образом, поля функциональных объектов, реализуемые через механизм захвата лямбд, служат для инкапсуляции состояния и данных, необходимых лямбде для выполнения ее функций, связывая ее с контекстом, в котором она была создана.

Отличия выброса исключения от аборта программы

Выброс исключения (exception) и аборт программы (abort) – это два разных механизма обработки нежелательных или ошибочных ситуаций в программе на C/C++.

Основное различие заключается в их предназначении, способе реагирования и влиянии на выполнение программы.

Выброс исключения

При выбросе исключения программа переходит в режим поиска обработчика, который может "поймать" это исключение. Если такой обработчик найден, управление передается ему, что позволяет выполнить логику восстановления или обработки ошибки. Происходит автоматическая раскрутка стека, гарантирующая вызов деструкторов локальных объектов, что помогает избежать утечек ресурсов.

Аборт программы

Аборт программы, вызванный функцией abort(), приводит к немедленному и принудительному завершению выполнения. Этот механизм используется для обозначения критических, необрабатываемых ошибок, когда продолжение работы программы небезопасно или невозможно. Раскрутка стека не происходит, и деструкторы локальных объектов не вызываются. Это может привести к утечкам ресурсов, если программа полагается на деструкторы для их освобождения.

В целом, исключения используются для обработки ошибок, которые могут быть предвидены и потенциально исправлены, в то время как аборт указывает на фатальную ошибку, которая требует немедленного завершения.

Открытая адресация и метод цепочек.

Открытая адресация:

Линейное зондирование: Поиск свободной ячейки происходит последовательно (H + 1, H + 2, ...).

Квадратичное зондирование: Поиск свободной ячейки происходит по квадратичной зависимости (H + 1², H + 2², ...).

Двойное хеширование: Используется вторая хеш-функция для определения шага при поиске (H + H2, H + 2*H2, ...).

Пример линейного зондирования:

Метод цепочек:

Каждая ячейка хеш-таблицы содержит указатель на связный список (или другой контейнер), где хранятся элементы, коллизировавшие по данному индексу.

Пример метода цепочек с использованием std::list:

Краткое сравнение:

Поскольку double является примитивным типом, операции с ним не генерируют исключений C++. Однако, могут возникать особые значения, такие как:

NaN (Not a Number): результат некорректной математической операции (например, деление нуля на ноль).

Infinity: результат деления ненулевого числа на ноль.

Для их проверки используются функции из <cmath> (или <math.h> в C):

isnan(x): возвращает true, если x является NaN.

isinf(x): возвращает true, если x является бесконечностью.

isfinite(x): возвращает true, если x не является NaN и не является бесконечностью.

Пример использования:

Да, shared_ptr потокобезопасен для операций инкремента/декремента счетчика ссылок. Это означает, что множество потоков могут одновременно копировать или уничтожать shared_ptr к одному и тому же ресурсу без гонки данных на самом счетчике.

Однако доступ к управляемому объекту данных через несколько shared_ptr из разных потоков не является потокобезопасным по умолчанию. Если разные потоки читают и/или модифицируют этот объект одновременно, вам необходимо использовать дополнительные механизмы синхронизации (например, мьютексы) для защиты самого объекта.

Использование shared_ptr в многопоточном контексте возможно и часто необходимо, но требует понимания того, какая часть shared_ptr (счетчик) потокобезопасна, а какая (доступ к данным) — нет.

std::optional в C++17 используется для представления значения, которое может присутствовать или отсутствовать.

Основные применения:

Возврат из функций, которые могут завершиться неудачей: Вместо возврата специального "нулевого" значения или использования выходных параметров, функция может вернуть std::optional<T>, где T — тип успешного результата. Если операция успешна, optional содержит значение; если нет – он пуст.#include <optional>

#include <string>

std::optional<std::string> find_user_by_id(int id) {

if (id == 123) {

return "Alice"; // Пользователь найден

} else {

return std::nullopt; // Пользователь не найден

}

}

Передача опциональных аргументов функциям: Функция может принимать std::optional<T> в качестве параметра, указывая, что значение этого параметра может быть предоставлено или нет.#include <optional>

void process_data(int value, std::optional<int> optional_config = std::nullopt) {

if (optional_config) {

// Используем опциональную конфигурацию

int config = optional_config.value();

// ...

} else {

// Используем значение по умолчанию или другой путь

// ...

}

}

Представление отсутствующих состояний в структурах данных: В полях структур или классов, где значение может быть неизвестно или не применимо.#include <optional>

#include <string>

struct UserDetails {

std::string name;

std::optional<int> age; // Возраст может быть неизвестен

};

Отличие от указателей на nullptr: std::optional явно выражает семантику опционального значения, в то время как указатель может использоваться для владения или ссылки на объект, а nullptr – лишь один из случаев. std::optional также избегает накладных расходов, связанных с динамическим выделением памяти, если объект создается прямо внутри него.

Ключевые особенности:

Явность: Явно указывает, что значение может отсутствовать.

Безопасность: Методы доступа (например, .value()) могут генерировать исключение, если значение отсутствует, предотвращая неопределенное поведение. Безопаснее использовать методы .has_value() или операторы * и -> после проверки.

Эффективность: Значение хранится непосредственно в optional (малое оптимизирование на уровне стека/владения) или рядом с ним, без накладных расходов на динамическое выделение памяти для самого значения, если только оно не является очень большим объектом или не требуется его полиморфное поведение.

Пример использования:

Использование std::string_view для передачи строк без копирования данных.

Применение алгоритмов из <string> и <algorithm> (например, find, search) вместо ручной итерации.

Предварительное выделение памяти с помощью reserve для уменьшения количества переаллокаций при наращивании строки.

Использование маленького буфера строки (Small String Optimization - SSO) в std::string (если реализовано компилятором).

При необходимости быстрой конкатенации большого количества строк использовать std::stringstream или поэлементно добавлять к одной результирующей строке, предварительно зарезервировав место.

Применения специальных библиотек для работы со строками, оптимизированных под конкретные задачи (например, регулярные выражения).

Размещение строк в статической памяти или пуле для избежания динамических выделений при работе с постоянными строками.

Использование низкоуровневых функций C-стиля (memcpy, memmove) для копирования больших объемов данных, если это безопасно и оправдано.

Пример использования std::string_view:

Пример использования reserve:

Сравнение конкатенации:

(Примечание: Производительность этих методов может варьироваться в зависимости от компилятора и стандартной библиотеки).

std::map (Красно-черное дерево)

Вставка, удаление, поиск: O(log N) в среднем и в худшем случае. N — количество элементов.

Доступ по ключу с помощью operator[] или метода at(): O(log N).

Получение итератора на начало/конец: O(1).

Итерация по всем элементам: O(N).

Память: O(N).

std::unordered_map (Хеш-таблица)

Вставка, удаление, поиск: O(1) в среднем случае. O(N) в худшем случае (при сильных коллизиях хеша). N — количество элементов.

Доступ по ключу с помощью operator[] или метода at(): O(1) в среднем случае. O(N) в худшем случае.

Итерация по всем элементам: O(N) в среднем. Порядок итерации не гарантирован.

Память: O(N). Зависит от коэффициента загрузки и реализации хеш-таблицы.

Сравнение:

std::unordered_map обычно быстрее для одиночных операций (вставка, поиск, удаление) благодаря O(1) в среднем, но требует хорошей хеш-функции и чувствительна к коллизиям. std::map гарантирует логарифмическую сложность независимо от данных, сохраняет элементы в отсортированном порядке и не требует хеш-функции для типа ключа.

Полиморфизм — это свойство объектов иметь множество форм или представлять несколько типов в иерархии наследования. Позволяет работать с объектами различных классов через общий интерфейс базового класса.

В C++ полиморфизм реализуется двумя основными способами:

Полиморфизм времени компиляции (статический, Ad-hoc полиморфизм):

Реализуется с помощью перегрузки функций и перегрузки операторов.

Выбор конкретной реализующей функции или оператора происходит на этапе компиляции.

// Пример перегрузки функций

void print(int a) { /* ... */ }

void print(double b) { /* ... */ }

Полиморфизм времени выполнения (динамический, Subtype полиморфизм):

Реализуется с помощью виртуальных функций и указателей/ссылок на базовый класс.

Выбор конкретной реализующей функции происходит на этапе выполнения программы.

Требует наличия хотя бы одной виртуальной функции в базовом классе.

Использует таблицу виртуальных функций (vtable).

// Пример динамического полиморфизма

class Base {

public:

virtual void show() { /* Реализация в базовом классе */ }

};

class Derived : public Base {

public:

void show() override { /* Переопределенная реализация */ }

};

// Использование

Base* ptr = new Derived();

ptr->show(); // Вызывается Derived::show()

Полиморфизм позволяет писать более гибкий и расширяемый код, так как новые классы могут быть добавлены без изменения существующего кода, работающего с базовым классом.

Счетчик ссылок в std::shared_ptr хранится в отдельном объекте — блоке управления (control block).

Блок управления создается:

При первом создании std::shared_ptr из указателя.

При создании std::shared_ptr с пользовательским удалителем или аллокатором.

При использовании std::make_shared или std::allocate_shared.

Этот блок управления содержит как минимум два счетчика:

Счетчик сильных ссылок (strong count): Увеличивается при создании или копировании std::shared_ptr. Уменьшается при уничтожении std::shared_ptr. Когда этот счетчик становится нулем, освобождается управляемый объект.

Счетчик слабых ссылок (weak count): Увеличивается при создании std::weak_ptr из std::shared_ptr. Уменьшается при уничтожении std::weak_ptr. Блок управления освобождается, когда оба счетчика — сильных и слабых ссылок — становятся нулем.

Использование std::make_shared предпочтительнее прямого создания из new, так как оно может аллоцировать объект и блок управления одним блоком памяти, что улучшает производительность и уменьшает фрагментацию.

Счетчики атомарны, что делает std::shared_ptr безопасным для использования в многопоточных сценариях, хотя доступ к самому управляемому объекту не синхронизирован по умолчанию.

Пример:

1 байт (при условии, что у класса нет виртуальных функций).

Это связано с тем, что каждый объект в C++ должен иметь уникальный адрес в памяти для корректной работы таких механизмов, как указатели. Выделение 1 байта обеспечивает это требование.

Если класс имеет хотя бы одну виртуальную функцию, объект будет занимать больше места (как минимум размер указателя на таблицу виртуальных функций vtable), даже если других членов данных нет.

std::map:

Хранит элементы в отсортированном порядке по ключам.

Реализована на основе красно-черного дерева.

Время доступа, вставки и удаления в среднем логарифмическое: O(log n).

Ключи должны иметь оператор <.

Подходит, когда важен упорядоченный обход элементов или частые операции поиска диапазона.

std::unordered_map:

Не хранит элементы в каком-либо определенном порядке.

Реализована на основе хэш-таблицы.

Среднее время доступа, вставки и удаления: O(1). В худшем случае (при плохом хэшировании и большом количестве коллизий): O(n).

Ключи должны быть хэшируемыми (предоставлять хэш-функцию) и иметь оператор ==.

Подходит, когда главный приоритет — быстрое присутствие (lookup) элементов по ключу и порядок неважен.

Сравнительная таблица:

Пример использования std::map:

Пример использования std::unordered_map:

Двойное исключение (double exception) в C++ возникает, когда при обработке исключения (внутри блока catch) возникает новое исключение до того, как обработка предыдущего завершена. Это приводит к вызову std::terminate.

Примеры ситуаций:

Выброс нового исключения в блоке catch:

#include <iostream>

#include <stdexcept>

void func() {

throw std::runtime_error("Первое исключение");

}

int main() {

try {

func();

} catch (const std::runtime_error& e) {

std::cerr << "Поймано: " << e.what() << std::endl;

throw std::logic_error("Второе исключение в catch"); // Возникновение нового исключения

}

return 0;

}

Здесь в блоке catch, который обрабатывает std::runtime_error, выбрасывается новое исключение std::logic_error.

Исключение во время очистки ресурсов в деструкторе в контексте обработки другого исключения:

#include <iostream>

#include <vector>

struct Resource {

~Resource() noexcept(false) { // Деструктор может выбросить исключение

std::cerr << "Деструктор Resource вызван" << std::endl;

if (true) { // Условие, вызывающее исключение

throw std::runtime_error("Исключение в деструкторе Resource");

}

}

};

void risky_func() {

Resource r; // Создается объект, деструктор которого может выбросить

throw std::runtime_error("Исключение из risky_func"); // Первое исключение

}

int main() {

try {

risky_func();

// Здесь может произойти исключение в деструкторе 'r', пока мы обрабатываем первое исключение

}

return 0;

}

Когда risky_func выбрасывает исключение, происходит раскрутка стека. В процессе раскрутки будет вызван деструктор объекта r. Если этот деструктор сам выбросит исключение, пока происходит обработка первого исключения, возникнет двойное исключение.

Исключение в обработчике исключения из-за внутренних ошибок или вызовов функций, выбрасывающих исключения:

#include <iostream>

#include <vector>

void process_error(const std::exception& e) {

std::cerr << "Обработка ошибки: " << e.what() << std::endl;

// Предположим, эта функция может выбросить исключение при определенных условиях

if (e.what() && std::string(e.what()).length() > 20) {

throw std::logic_error("Ошибка при обработке слишком длинного сообщения"); // Второе исключение

}

}

int main() {

try {

throw std::runtime_error("Это довольно длинное сообщение об ошибке"); // Первое исключение

} catch (const std::exception& e) {

process_error(e); // Этот вызов может выбросить второе исключение

}

return 0;

}

В этом случае, функция process_error, вызванная в блоке catch для обработки первого исключения, сама инициирует второе исключение.

Во всех этих сценариях, поскольку второе исключение возникает до завершения обработки первого (или во время раскрутки стека из-за первого), стандарт C++ требует вызова std::terminate. std::terminate по умолчанию вызывает abort().

Рекурсивный (или реентерабельный) мьютекс — это тип примитива синхронизации, который позволяет потоку, уже владеющему мьютексом, снова заблокировать его, не вызывая взаимоблокировку.

Основные особенности:

Многократная блокировка: Один и тот же поток может вызвать lock() (или эквивалентную функцию) на рекурсивном мьютексе несколько раз.

Счетчик владения: Мьютекс хранит счетчик, который увеличивается при каждом повторном захвате потоком-владельцем.

Разблокировка: Мьютекс становится полностью свободным только после того, как поток-владелец вызовет unlock() (или эквивалентную функцию) столько же раз, сколько было сделано блокировок.

Неблокирующие потоки: Другие потоки, пытающиеся захватить рекурсивный мьютекс, пока он занят, будут заблокированы до тех пор, пока владеющий поток не освободит его полностью.

Использование:

Полезен в рекурсивных функциях или при вызове методов одного объекта из другого, где оба метода требуют блокировки одного и того же мьютекса.

Позволяет избежать взаимной блокировки в сценариях, когда поток уже держит блокировку и пытается получить ее еще раз.

Пример в C++11 с std::recursive_mutex:

std::deque (double-ended queue) — это последовательный контейнер, который позволяет эффективно добавлять и удалять элементы как в начале, так и в конце. Он реализуется как последовательность блоков памяти, что обеспечивает быстрый доступ к любому элементу по индексу (почти как в std::vector), но без необходимости перемещать существующие элементы при вставке/удалении в начале.

Основные характеристики:

Произвольный доступ: Элементы доступны по индексу со сложностью O(1).

Вставка/удаление в конце: Осуществляется со сложностью O(1).

Вставка/удаление в начале: Осуществляется со сложностью O(1).

Вставка/удаление в середине: Осуществляется со сложностью O(n), где n — количество элементов между точкой вставки/удаления и ближайшим концом.

Непрерывное хранение: Элементы хранятся в нескольких непрерывных блоках памяти, но не обязательно в одном большом блоке, как std::vector.

Итераторы: Итераторы std::deque не гарантируют оставаться действительными после вставок/удалений, кроме как в конце (после push_back/pop_back) и начале (после push_front/pop_front), если вставляемый/удаляемый элемент не приводил к перераспределению всех блоков.

Используется, когда требуется частая вставка или удаление элементов с обоих концов, а также произвольный доступ.

Пример использования:

Сравнение с std::vector:

Хеш-таблица (hash table) — это структура данных, реализующая ассоциативный массив.

Принцип работы:

Хеширование. Для каждого ключа (key) вычисляется хеш-код (hash code) с помощью хеш-функции (hash function). Хеш-код — это целое число.

Индексирование. Хеш-код используется для определения индекса (index) в массиве (или векторе) внутренней структуры хеш-таблицы. Обычно индекс вычисляется как hash_code % array_size, где array_size — размер массива.

Хранение. По найденному индексу в массиве хранится связанное с ключом значение (value).

Проблемы и их решения:

Коллизии. Различные ключи могут давать одинаковый хеш-код, и, следовательно, один и тот же индекс в массиве. Это называется коллизией.

Методы разрешения коллизий:

Метод цепочек (Separate Chaining): В каждой ячейке массива хранится список (список, вектор и т.п.) пар "ключ-значение". При коллизии новая пара добавляется в этот список. При поиске по индексу просматривается соответствующий список для нахождения нужного ключа.

Метод открытой адресации (Open Addressing): При коллизии ищется другая свободная ячейка в массиве по определенному правилу (пробирование).

Линейное пробирование (Linear Probing): Последовательно проверяются ячейки index + 1, index + 2, и т.д. по модулю размера массива.

Квадратичное пробирование (Quadratic Probing): Проверяются ячейки index + 1^2, index + 2^2, и т.д. по модулю размера массива.

Двойное хеширование (Double Hashing): Используется вторая хеш-функция для определения шага пробирования.

Преимущества:

В среднем, операции вставки, удаления и поиска выполняются со сложностью O(1).

Недостатки:

В худшем случае (например, при большом количестве коллизий или плохой хеш-функции) сложность операций может достигать O(n), где n — количество элементов.

Требует дополнительной памяти (например, для списков при методе цепочек или для пробирования при открытой адресации).

Пример использования в C++ (std::unordered_map):

В C++ стандартная библиотека предоставляет несколько контейнеров, которые позволяют эффективно вставлять элементы в начало:

std::deque (double-ended queue): Этот контейнер оптимизирован для вставки и удаления элементов как в начале, так и в конце. Вставка в начало имеет амортизированную постоянную сложность O(1).

std::list (doubly linked list): Связный список. Вставка в начало осуществляется путем изменения указателей головы списка. Сложность вставки в начало постоянная - O(1).

std::forward_list (singly linked list): Односвязный список. Вставка в начало (с помощью push_front или emplace_front) также имеет постоянную сложность O(1).

std::vector: Хотя технически можно вставить элемент в начало std::vector с использованием insert(begin(), value), это не эффективно. Вставка в начало std::vector требует сдвига всех существующих элементов вправо, что приводит к линейной сложности O(n), где n — количество элементов в векторе.

Краткое сравнение эффективности вставки в начало:

Для частых операций вставки в начало предпочтительнее использовать std::deque, std::list или std::forward_list по сравнению с std::vector. Выбор между ними зависит от других необходимых операций (например, доступа по индексу или частых вставок/удалений в середине контейнера).

Время доступа по индексу: O(1)

Вставка в конец (при наличии свободной емкости): O(1)

Вставка в конец (при перераспределении): O(N)

Вставка в начало: O(N)

Вставка в середину: O(N)

Удаление с конца: O(1)

Удаление с начала: O(N)

Удаление из середины: O(N)

У класса могут быть следующие виды конструкторов:

Конструктор по умолчанию (Default Constructor):

Не принимает аргументов. Если явно не объявлен, компилятор может сгенерировать его автоматически, если класс не содержит пользовательских конструкторов и не наследуется от класса с пользовательским конструктором.

class MyClass {

public:

MyClass() {

// Инициализация по умолчанию

}

};

Конструктор копирования (Copy Constructor):

Принимает константную ссылку на объект того же класса. Используется для создания нового объекта как копии существующего.

class MyClass {

public:

MyClass(const MyClass& other) {

// Копирование данных из 'other'

}

};

Конструктор перемещения (Move Constructor):

Принимает rvalue-ссылку на объект того же класса. Используется для создания нового объекта путем "перемещения" ресурсов (например, владения памятью) из временного объекта, оставляя временный объект в валидном, но неопределенном состоянии. Появился в C++11.

class MyClass {

public:

MyClass(MyClass&& other) noexcept {

// Перемещение ресурсов из 'other'

}

};

Параметризованные конструкторы (Parameterized Constructors):

Принимают один или несколько аргументов для инициализации объекта.

class MyClass {

public:

MyClass(int value) {

// Инициализация с использованием 'value'

}

MyClass(int value1, double value2) {

// Инициализация с использованием 'value1' и 'value2'

}

};

При удалении объекта базового класса через указатель на базовый класс, который фактически указывает на объект производного класса, виртуальный деструктор гарантирует вызов деструктора производного класса перед вызовом деструктора базового класса. Это необходимо для корректного освобождения ресурсов, выделенных производным классом. Если деструктор базового класса не виртуальный, будет вызван только деструктор базового класса, что приведет к утечке ресурсов.

Без виртуального деструктора вывод был бы таким:

При наличии виртуального деструктора вывод:

А если вопрос прозвучит не так, как вы готовились?

Так бывает чаще всего. ИзиСобес слышит вопрос интервьюера и подсказывает ответ прямо во время разговора — его не видно ни в Zoom, ни при демонстрации экрана.

Посмотреть ИзиСобес