🤔 Что такое рекурсивный mutex?
Рекурсивный mutex — это тип мьютекса, который позволяет одному и тому же потоку захватывать его несколько раз без блокировки. Это полезно, когда одна и та же функция вызывает себя рекурсивно или косвенно через другие функции и при этом требуется захватить один и тот же мьютекс. В отличие от обычного мьютекса, рекурсивный мьютекс учитывает количество захватов и освобождается только после того, как поток выполнит соответствующее количество вызовов unlock(). Это предотвращает взаимоблокировки при рекурсивных вызовах.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
🤔 Какая сложность поиска в set и unordered_set?
Контейнеры set и unordered_set представляют собой различные структуры данных, каждая из которых имеет свои особенности по скорости выполнения основных операций, включая поиск. Вот как работают эти контейнеры и какова сложность их операций поиска:
🚩set
Реализуется как сбалансированное двоичное дерево поиска, обычно как красно-черное дерево. Он хранит элементы в отсортированном порядке, что позволяет выполнять двоичный поиск. Сложность поиска: Поиск в нем выполняется за логарифмическое время, \(O(\log n)\), где \(n\) — количество элементов в set. Эта эффективность достигается за счёт использования структуры сбалансированного дерева, которое позволяет быстро делить данные на меньшие сегменты.
🚩unordered_set
Реализуется с использованием хеш-таблицы. Это позволяет, при идеальных условиях, выполнять поиск за константное время. Сложность поиска: В среднем, поиск в нем занимает константное время \(O(1)\). Однако в худшем случае, например, при неудачной работе хеш-функции или при большом количестве коллизий, поиск может деградировать до \(O(n)\). В таких ситуациях все ключи могут оказаться в одной "корзине" или "ведре" (bucket), и для нахождения правильного элемента потребуется просмотреть все элементы в этом ведре.
Для set
#include <iostream>
#include <set>
int main() {
std::set<int> mySet = {5, 3, 9, 1};
auto search = mySet.find(3);
if (search != mySet.end()) {
std::cout << "Found " << *search << std::endl;
} else {
std::cout << "Not found" << std::endl;
}
return 0;
}
Для unordered_set
#include <iostream>
#include <unordered_set>
int main() {
std::unordered_set<int> mySet = {5, 3, 9, 1};
auto search = mySet.find(3);
if (search != mySet.end()) {
std::cout << "Found " << *search << std::endl;
} else {
std::cout << "Not found" << std::endl;
}
return 0;
}
Ставь 👍 и забирай 📚 Базу знаний
🤔 Хорошо ли хранить числа с плавающей запятой в качестве ключа контейнера?
Нет, хранить числа с плавающей запятой как ключи не рекомендуется.
1. Числа с плавающей запятой могут иметь проблемы с точностью, что приведёт к некорректному сравнению ключей.
2. Лучше использовать целые числа или строки в качестве ключей, а дробные значения преобразовывать к целым или строковым представлениям.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
🤔 Что будет если для беззнаковой переменной равной 0 сделать декремент?
Если вы сделаете декремент (--) для беззнаковой переменной, значение которой равно 0, это приведет к переполнению, и переменная примет максимальное значение, которое может быть представлено этим типом.
Рассмотрим, что произойдет с переменной типа unsigned int, если её значение равно 0 и мы применим к ней декремент:
#include <iostream>
int main() {
unsigned int x = 0;
std::cout << "x before decrement: " << x << std::endl;
x--; // Декрементирование беззнаковой переменной 0
std::cout << "x after decrement: " << x << std::endl; // Переполнение
return 0;
}
Для беззнаковой переменной unsigned int: Диапазон значений: от 0 до 4294967295 (для 32-битной реализации).
Когда переменная x равна 0 и мы выполняем x--, происходит следующее:
🟠Значение 0 уменьшается на 1.
🟠Поскольку unsigned int не может представлять отрицательные значения, происходит переполнение.
🟠Переполнение для беззнакового типа приводит к "оборачиванию" значения через максимальное значение типа.
🟠Максимальное значение для unsigned int (при 32-битной реализации) равно 4294967295.
Таким образом, после выполнения x--, значение x становится 4294967295.
Результат выполнения приведенного выше кода будет следующим:
x before decrement: 0
x after decrement: 4294967295
Для беззнаковых типов в C++ (например, unsigned char, unsigned short, unsigned int, unsigned long): Если значение переменной равно 0 и к ней применяется декремент, результатом будет максимальное значение этого типа.
Ставь 👍 и забирай 📚 Базу знаний
🤔 Что можешь рассказать о little-endian и big-endian?
Little-endian и big-endian — это порядки байтов в мультибайтовых данных, используемые в компьютерной архитектуре для хранения данных. В little-endian младший байт сохраняется по низкому адресу, а старший байт по высокому. В big-endian старший байт сохраняется по низкому адресу, а младший по высокому. Выбор порядка байтов влияет на совместимость и производительность системы в различных сценариях использования.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
🤔 Почему не сделать стек вызова очень большим?
Может показаться логичным способом избежать переполнения стека, но в реальности это решение приводит к проблемам
🟠Неэффективное использование памяти
Если стек слишком велик, это может привести к нерациональному использованию памяти. Память, выделенная под стек, может оставаться неиспользованной, что уменьшает доступное пространство для других частей программы и данных.
🟠Ограниченные системные ресурсы
Системы имеют ограниченное количество оперативной памяти. Выделение слишком большого размера стека для каждого потока может привести к исчерпанию доступной памяти, особенно в многопоточных приложениях, где каждый поток имеет свой стек.
🟠Сложности с отладкой
Большие стеки могут затруднить обнаружение ошибок и утечек памяти. Например, если стек слишком велик, ошибка переполнения стека может не проявиться сразу, что усложняет диагностику проблемы.
🟠Влияние на производительность
Большие стеки могут повлиять на производительность системы из-за увеличения времени переключения контекста (context switching) и необходимости резервирования большого объема памяти при каждом запуске потока.
🚩Как оптимально настроить?
1⃣Анализ программы
Оцените потребности вашей программы в памяти. Измерьте, сколько памяти реально требуется для выполнения типичных задач.
2⃣Тестирование
Проведите тесты с различными размерами стека, чтобы определить оптимальный баланс между достаточным размером стека и эффективным использованием памяти.
3⃣Использование рекурсии с осторожностью
Если ваш код активно использует рекурсию, убедитесь, что глубина рекурсии контролируется и не приводит к переполнению стека.
#include <pthread.h>
#include <stdio.h>
void* myThreadFunction(void* arg) {
// Код потока
return NULL;
}
int main() {
pthread_t thread;
pthread_attr_t attr;
size_t stacksize;
// Инициализация атрибутов
pthread_attr_init(&attr);
// Установка размера стека
stacksize = 16 * 1024 * 1024; // 16 МБ
pthread_attr_se
🤔 В каком стандарте появился override?
Ключевое слово override было введено в стандарт C++11. До его появления разработчики не могли явно указывать, что метод должен переопределять виртуальный метод базового класса. Это приводило к ошибкам, если сигнатура метода в базовом классе менялась или имя метода было неправильно написано.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
🤔 В каком порядке элементы списка инициализируются в конструкторе после двоеточия?
Порядок инициализации элементов списка инициализации конструктора (initializer list) определяется порядком объявления членов класса, а не порядком, указанным в списке инициализации. Это важно понимать, так как неправильный порядок может привести к неожиданным ошибкам, особенно при инициализации зависимых членов.
#include <iostream>
class MyClass {
private:
int a;
int b;
int c;
public:
MyClass(int x, int y, int z) : c(z), b(y), a(x) {
std::cout << "a: " << a << ", b: " << b << ", c: " << c << std::endl;
}
};
int main() {
MyClass obj(1, 2, 3);
return 0;
}
🚩Список инициализации конструктора
В конструкторе MyClass список инициализации указан в порядке c(z), b(y), a(x). Однако это не влияет на порядок инициализации членов класса. Члены класса будут инициализированы в порядке их объявления: a, затем b, затем c.
🚩Порядок инициализации
1⃣a инициализируется первым (значение x).
2⃣b инициализируется вторым (значение y).
3⃣c инициализируется третьим (значение z).
🚩Вывод программы
Программа выведет
a: 1, b: 2, c: 3
🚩Почему это важно
Неправильный порядок в списке инициализации не приведет к ошибке компиляции, но может вызвать логические ошибки, особенно если один член зависит от другого. Рассмотрим пример с зависимыми членами:
class MyClass {
private:
int a;
int& ref;
public:
MyClass(int x) : ref(a), a(x) { // Неправильный порядок: ref инициализируется до a
std::cout << "a: " << a << ", ref: " << ref << std::endl;
}
};
Ставь 👍 и забирай 📚 Базу знаний
🤔 Принцип Open/Closed (открытости/закрытости)?
Принцип открытости/закрытости гласит, что классы должны быть открыты для расширения, но закрыты для модификации. Это достигается за счёт использования абстракций, чтобы добавлять функциональность через наследование или композицию, не изменяя исходный код.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
🤔 Как можно взвести бит используя побитовую операцию ИЛИ?
Чтобы взвести (установить) бит в определённое значение (1) в числе, можно использовать побитовую операцию ИЛИ (|). Эта операция позволяет установить конкретный бит в 1, не изменяя остальные биты числа.
🚩Как это работает
Операция ИЛИ (|) сравнивает каждый бит двух чисел. Если хотя бы один из битов в соответствующей позиции равен 1, результат в этой позиции будет 1. Иначе, результат будет 0.
🚩Шаги
1⃣Создаём маску, которая имеет единицу в позиции n и нули в остальных позициях. Это можно сделать с помощью выражения 1 << n.
2⃣Применяем операцию ИЛИ между числом x и маской.
#include <iostream>
int main() {
int x = 0b00001010; // Число, с которым работаем (10 в десятичной системе)
int n = 3; // Позиция бита, которую хотим установить (начиная с 0)
// Создаём маску с единицей в позиции n
int mask = 1 << n;
// Устанавливаем бит в позиции n
x = x | mask;
// Вывод результата
std::cout << "Результат: " << std::bitset<8>(x) << std::endl; // Двоичный вывод
std::cout << "Результат: " << x << std::endl; // Десятичный вывод
return 0;
}
1 << n создает маску, сдвигая 1 влево на n позиций. Например, если n = 3, результат будет 0b00001000. Операция x | mask устанавливает бит в x на позиции n в 1. Если бит в x на позиции n уже был 1, он останется 1, если он был 0, то станет 1.
🚩Пример вывода
Если x было 0b00001010 и n = 3, результат будет:
Маска: 0b00001000
x | mask: 0b00001010 | 0b00001000 = 0b00001010
В результате бит в позиции 3 установлен в 1, и итоговое значение числа в двоичном формате 0b00001010 (десятичное 10).
Ставь 👍 и забирай 📚 Базу знаний
🤔 Какое присваивание разрешает unique_ptr?
unique_ptr поддерживает только перемещение (move assignment), чтобы гарантировать уникальное владение объектом, но не копирование.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Переходи в скоростной режим карьеры
⚡Учись у тех, кто прошел путь от джуна до топа. В Мини-СЕО ты попадешь в команду топ-менеджера Т-Банка и сможешь:
— исследовать экосистемы и находить наиболее перспективные точки роста;
— развивать сегмент автолюбителей в Т-Банке;
— заниматься региональной экспансией банка;
— вести стратегический план развития 3P, развивать AI-продукты;
— участвовать в создании B2B-маркетплейса;
— разрабатывать эффективные методологии.
Программа длится шесть месяцев и подойдет студентам и выпускникам, которые уже умеют в математику и аналитику.
Подай заявку до 25 сентября!
Зарегистрироваться
#реклама 16+
t-miniceo.ru
О рекламодателе
🤔 Расскажи о истории умных указателей
История умных указателей (smart pointers) в C++ связана с необходимостью управления динамической памятью и улучшением безопасности работы с указателями. Они появились как способ автоматизировать управление памятью и избежать ошибок, таких как утечки памяти, двойное освобождение или использование освобожденной памяти.
🚩Основные этапы развития умных указателей
🟠Классические проблемы обычных указателей
В языке C++ динамическая память выделяется с помощью new и освобождается с помощью delete. Однако ручное управление памятью приводит к ошибкам:
Утечки памяти: забыли вызвать delete, и память остаётся занятой.
Двойное освобождение: дважды вызвали delete на одном указателе.
Использование освобождённой памяти: доступ к указателю после delete.
Эти проблемы стали мотиватором для появления решений, которые бы автоматически управляли памятью.
🟠Ручная реализация умных указателей в C++98
До стандартизации разработчики часто писали свои собственные классы для управления динамической памятью. Такой класс оборачивал указатель и вызывал delete в своём деструкторе:
class SmartPointer {
int* ptr;
public:
explicit SmartPointer(int* p = nullptr) : ptr(p) {}
~SmartPointer() { delete ptr; }
int& operator*() { return *ptr; }
int* operator->() { return ptr; }
};
🟠Boost и развитие библиотеки умных указателей
В библиотеке Boost появились первые стандартизированные умные указатели:
boost::shared_ptr: реализует совместное владение указателем (reference counting).
boost::scoped_ptr: уникальное владение (аналог будущего std::unique_ptr).
boost::weak_ptr: слабая ссылка для работы с циклическими зависимостями.
Boost сыграл огромную роль в стандартизации умных указателей, так как многие из них легли в основу C++11.
🟠C++11: стандартные умные указатели
Стандарт C++11 ввёл библиотеку <memory>, которая предоставляет три типа умных указателей:
std::unique_ptr:
Гарантирует уникальное владение ресурсом.
Замена для обычных указателей с более с
🤔 Что известно об умных указателях?
Обёртки над обычными указателями, автоматически управляющие временем жизни объекта.
- std::unique_ptr — единственный владелец, нельзя копировать.
- std::shared_ptr — подсчёт ссылок, разделённое владение.
- std::weak_ptr — не влияет на счётчик shared_ptr, нужен для избежания циклов.
Плюсы:
- Безопасное управление памятью.
- Нет утечек при правильном использовании.
- Легче соблюдать RAII.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
🤔 Какие есть тонкости при вызове виртуальной функции из конструктора или деструктора?
Вызов виртуальных функций из конструктора или деструктора в C++ может привести к неожиданным результатам, поскольку полиморфизм в этих контекстах работает иначе. Рассмотрим эти тонкости подробнее.
🚩Поведение виртуальных функций в конструкторах и деструкторах
🟠Конструкторы
Виртуальные функции в конструкторах вызываются в контексте текущего класса, а не в контексте производного класса. Когда конструктор базового класса вызывает виртуальную функцию, будет вызвана версия этой функции из самого базового класса, даже если объект принадлежит производному классу. Это связано с тем, что в момент вызова конструктора объект еще не полностью сконструирован как объект производного класса.
🟠Деструкторы
Виртуальные функции в деструкторах также вызываются в контексте текущего класса. Когда деструктор базового класса вызывает виртуальную функцию, будет вызвана версия этой функции из самого базового класса, поскольку деструкторы вызываются в порядке от производного к базовому, и в момент вызова деструктора базового класса объект уже не является объектом производного класса.
🚩Пример для иллюстрации
#include <iostream>
class Base {
public:
Base() {
std::cout << "Base constructor\n";
// Вызов виртуальной функции из конструктора
this->virtualFunction();
}
virtual ~Base() {
std::cout << "Base destructor\n";
// Вызов виртуальной функции из деструктора
this->virtualFunction();
}
virtual void virtualFunction() {
std::cout << "Base virtualFunction\n";
}
};
class Derived : public Base {
public:
Derived() {
std::cout << "Derived constructor\n";
}
~Derived() override {
std::cout << "Derived destructor\n";
}
void virtualFunction() override {
std::cout << "Derived virtualFunction\n";
}
};
int main() {
Derived d;
return 0;
}
🤔 Вывод программы:
Base constructor
Base
🤔 Как вы будете реализовывать Singletone ?
Это шаблон проектирования, который гарантирует, что класс имеет только один экземпляр, и предоставляет глобальную точку доступа к этому экземпляру. Существует несколько способов реализации Singleton, включая ленивую инициализацию, использование static и многопоточную безопасность.
🚩Основные методы реализации
🟠Ленивая инициализация (Lazy Initialization)
При ленивой инициализации объект создается только при первом обращении к нему.
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
// Удаляем конструкторы копирования и присваивания
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {} // Приватный конструктор
};
🟠Инициализация при запуске (Eager Initialization)
При инициализации при запуске объект создается сразу при загрузке программы.
class Singleton {
public:
static Singleton& getInstance() {
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {}
static Singleton instance; // Статический экземпляр
};
Singleton Singleton::instance;
🟠Многопоточная безопасность (Thread-safe Singleton)
Используя std::call_once и std::once_flag, можно обеспечить безопасность при доступе из нескольких потоков.
#include <mutex>
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initInstanceFlag, &Singleton::initSingleton);
return *instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {}
static void initSingleton() {
instance = new Singleton();
}
static Singleton* instance;
static std::once_flag initInstanceFlag;
};
Singleton* Singleton::instance = nullptr;
std::once_flag Singleton::initInstanceFlag;
🚩Плюсы
➕Ленивая инициализаци
🤔 Что известно о истории умных указателей?
- Первым в стандарт попал auto_ptr в C++98 — оказался неудобным и небезопасным.
- Затем Boost предложил shared_ptr, scoped_ptr, weak_ptr.
- С выходом C++11 появились современные unique_ptr, shared_ptr, weak_ptr.
- auto_ptr был объявлен устаревшим и позже удалён (в C++17).
- Современные умные указатели активно используются для автоматического управления памятью и соблюдения RAII.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
🤔 Как работает range based?
Range-based for loop – это упрощённый цикл for, который позволяет перебирать элементы контейнера (std::vector, std::array, std::map, std::set и т. д.) без индексов и итераторов.
for (auto element : container) {
// Действие с element
}
🚩Как это работает внутри?
Простой пример с std::vector
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
for (int x : v) { // Перебираем все элементы вектора
std::cout << x << " ";
}
}
Вывод
1 2 3 4 5
Как работает этот цикл?
Компилятор превращает его в обычный for с итератором:
for (auto it = v.begin(); it != v.end(); ++it) {
int x = *it; // Копируем элемент
std::cout << x << " ";
}
🚩Передача по ссылке (`&`) и по значению (`=`)
Передача по значению (=) – создаёт копию элемента
for (int x : v) { // x - копия элемента
x = 100; // НЕ изменит вектор!
}
Передача по ссылке (&) – изменяет оригинал
for (int& x : v) { // x - ссылка на элемент
x *= 2; // Изменит оригинальный вектор!
}
🚩Работает со всеми контейнерами STL
С std::map
std::map<int, std::string> m = {{1, "one"}, {2, "two"}};
for (const auto& [key, value] : m) { // structured binding (C++17)
std::cout << key << " -> " << value << "\n";
}
С std::set
std::set<int> s = {1, 2, 3, 4};
for (int x : s) { std::cout << x << " "; }
🚩Работает с `std::initializer_list`
for (int x : {10, 20, 30}) {
std::cout << x << " ";
}
Вывод
10 20 30
🚩Как работает range-based for с обычными массивами?
int arr[] = {1, 2, 3};
for (int x : arr) { std::cout << x << " "; }
Работает так же, как и с `std::vector`!
Работает с пользовательскими классами (если есть begin() и end())
class MyContainer {
int data[3] = {10, 20, 30};
public:
int* begin() { return data; }
int* end() { return data + 3; }
};
int main() {
MyContainer c;
for (int x : c) { std::cout << x << " "; }
}
Ставь 👍 и забирай 📚 Базу знаний
🤔 В каких STL контейнерах внутри находится хеш таблица?
🚩В стандартной библиотеке шаблонов (STL) C++ хеш-таблица используется для реализации следующих контейнеров
🟠std::unordered_map
Ассоциативный контейнер, который хранит пары ключ-значение, с уникальными ключами. Обеспечивает амортизированное среднее время доступа, вставки и удаления за O(1).
🟠std::unordered_multimap
Ассоциативный контейнер, который хранит пары ключ-значение, где ключи могут повторяться. Обеспечивает амортизированное среднее время для основных операций за O(1), несмотря на дублирование ключей.
🟠std::unordered_set
Ассоциативный контейнер, который хранит уникальные элементы, неупорядоченные. Обеспечивает амортизированное среднее время для основных операций за O(1).
🟠std::unordered_multiset
Ассоциативный контейнер, который хранит элементы, где значения могут повторяться, неупорядоченные. Обеспечивает амортизированное среднее время для основных операций за O(1), несмотря на дублирование элементов.
Ставь 👍 и забирай 📚 Базу знаний
🤔 Что такое placement new?
placement new – это специальная форма оператора new, которая размещает объект в уже выделенной памяти вместо того, чтобы выделять её заново.
placement new
НЕ выделяет память (мы сами передаём адрес).
Просто вызывает конструктор в указанном месте.
void* ptr = malloc(sizeof(int)); // Выделяем память вручную
int* num = new(ptr) int(42); // Размещаем объект в этой памяти
Синтаксис placement new
new (адрес) Type(аргументы конструктора);
адрес – указатель на уже выделенную память.
Type(аргументы конструктора) – объект создаётся в этом месте.
#include <iostream>
int main() {
char buffer[sizeof(int)]; // Буфер памяти (размер как у int)
int* p = new (buffer) int(123); // Размещаем int в этом буфере
std::cout << "Значение: " << *p << std::endl; // 123
}
🚩Когда используется `placement new`?
🟠Оптимизация выделения памяти
Например, если у нас часто создаются и удаляются объекты, но память уже есть. Не нужно каждый раз вызывать new и delete.
🟠Размещение объектов в особых областях памяти
Например, на стеке (char buffer[100]), в shared memory, или в выделенной заранее области (malloc).
🟠Создание объекта в заранее выделенном массиве
Стандартный std::vector использует placement new внутри себя!
🟠Кастомные аллокаторы (`std::allocator`)
Например, в играх и в embedded-разработке, где управление памятью критично.
🚩`placement new` и классы
Если объект – это класс, placement new вызывает его конструктор в выделенной памяти.
#include <iostream>
#include <cstdlib> // Для malloc/free
class MyClass {
public:
int x;
MyClass(int val) : x(val) { std::cout << "Конструктор!\n"; }
~MyClass() { std::cout << "Деструктор!\n"; }
};
int main() {
void* buffer = malloc(sizeof(MyClass)); // Выделяем сырую память
MyClass* obj = new (buffer) MyClass(42); // Размещаем объект
std::cout << "Значение: " << obj->x << std::endl;
obj->~MyClass(); // Ручной вызов деструктора!
free(bu
🤔 Что такое наследование?
Наследование — это механизм ООП, позволяющий одному классу (наследнику) унаследовать свойства и методы другого класса (родителя) для повторного использования и расширения.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
🤔 Что такое deque?
Deque (double-ended queue, двусторонняя очередь) — это последовательный контейнер в C++, который позволяет добавлять и удалять элементы как с начала, так и с конца очереди с постоянной временной сложностью O(1). Контейнер deque является частью стандартной библиотеки шаблонов (STL) и объявляется в заголовочном файле <deque>.
🚩Основные характеристики:
🟠Быстрый доступ к элементам:
deque обеспечивает доступ к элементам по индексу с амортизированной временной сложностью O(1).
🟠Двусторонняя очередь:
Позволяет эффективно добавлять и удалять элементы как с начала, так и с конца очереди.
🟠Динамический размер:
deque автоматически изменяет свой размер по мере добавления или удаления элементов, подобно vector.
🟠Не требует смежного хранения:
В отличие от vector, элементы deque могут быть размещены в различных сегментах памяти, что делает его более гибким для частых операций вставки и удаления.
🚩Основные операции:
🟠Добавление элементов:
push_back(), push_front()
🟠Удаление элементов:
pop_back(), pop_front()
🟠Доступ к элементам:
operator[], at(), front(), back()
🟠Размер и емкость:
size(), empty(), resize()
🟠Итераторы:
Поддержка итераторов для прохода по элементам
#include <iostream>
#include <deque>
int main() {
std::deque<int> dq;
// Добавление элементов в конец
dq.push_back(1);
dq.push_back(2);
dq.push_back(3);
// Добавление элементов в начало
dq.push_front(0);
std::cout << "Deque elements: ";
for (int elem : dq) {
std::cout << elem << " ";
}
std::cout << std::endl;
// Удаление элемента с конца
dq.pop_back();
// Удаление элемента с начала
dq.pop_front();
std::cout << "Deque elements after pop operations: ";
for (int elem : dq) {
std::cout << elem << " ";
}
std::cout << std::endl;
return 0;
}
Ставь 👍 и забирай 📚 Базу знаний
🤔 Когда нужно объявлять деструктор виртуальным?
Виртуальный деструктор нужен всегда, когда класс предполагается использовать в качестве базового и поддерживает полиморфизм (наследование и динамическое связывание).
🚩Зачем нужен виртуальный деструктор?
Если у базового класса деструктор не виртуальный, а удаление происходит через указатель на базовый класс, деструктор производного класса не вызовется! Это приведёт к утечке памяти или некорректному освобождению ресурсов.
Пример проблемы без виртуального деструктора
#include <iostream>
class Base {
public:
~Base() { std::cout << "Деструктор Base\n"; } // НЕ виртуальный!
};
class Derived : public Base {
public:
~Derived() { std::cout << "Деструктор Derived\n"; }
};
int main() {
Base* obj = new Derived();
delete obj; // Проблема! Деструктор Derived НЕ вызывается!
}
Вывод
Деструктор Base
🚩Решение: сделать деструктор виртуальным
Если объявить деструктор базового класса виртуальным (virtual), то при удалении через указатель на базовый класс будет вызван полный цепной деструктор. Используем virtual
class Base {
public:
virtual ~Base() { std::cout << "Деструктор Base\n"; } // Виртуальный!
};
class Derived : public Base {
public:
~Derived() { std::cout << "Деструктор Derived\n"; }
};
int main() {
Base* obj = new Derived();
delete obj; // Теперь вызываются оба деструктора
}
Вывод
Деструктор Derived
Деструктор Base
🚩Когда не нужно делать деструктор виртуальным?
Когда класс не предназначен для наследования
Например, std::vector, std::string, std::unique_ptr – у них нет виртуального деструктора, так как они не предполагают полиморфное использование
Когда удаление всегда происходит по ссылке/указателю на сам класс, а не на базовый
Derived* obj = new Derived();
delete obj; // В любом случае вызовет правильный деструктор
Если класс "заморожен" (final)
class FinalClass final {
public:
~FinalClass() { std::cout << "Деструктор\n"; }
};
Ставь 👍 и забирай 📚 Базу зн
🤔 Что знаешь про гарантии безопасности исключений?
Гарантии безопасности исключений (Exception Safety Guarantees) — это концепция, связанная с корректным поведением программы при возникновении исключений. Она определяет, насколько безопасно может завершиться выполнение функции или блока кода в случае выбрасывания исключения.
🚩Никаких гарантий (No Guarantee)
Этот уровень означает, что при возникновении исключения состояние программы может быть непредсказуемым. Объекты могут остаться в недопустимом состоянии, и поведение программы после выброса исключения неопределено.
void unsafeFunction(std::vector<int>& vec, int value) {
vec.push_back(value); // Если здесь выбросится исключение, состояние vec не определено
// ...
}
🚩Базовая гарантия (Basic Guarantee)
Этот уровень гарантирует, что не произойдёт утечек ресурсов или нарушений инвариантов объектов. После выброса исключения все объекты остаются в допустимом состоянии, однако состояние программы может быть частично изменено.
void safeFunction(std::vector<int>& vec, int value) {
try {
vec.push_back(value); // Если исключение, состояние vec остаётся корректным
} catch (...) {
// Обработка исключения
std::cerr << "Ошибка при добавлении элемента!" << std::endl;
}
}
🚩Сильная гарантия (Strong Guarantee)
Этот уровень гарантирует, что в случае возникновения исключения программа останется в исходном состоянии, как будто вызов функции никогда не происходил. Состояние откатывается до того, что было до вызова функции.
void addValue(std::vector<int>& vec, int value) {
std::vector<int> temp = vec; // Создаём копию
temp.push_back(value); // Работаем с копией
vec = temp; // Замена содержимого
}
🚩Гарантия отсутствия исключений (No-Throw Guarantee)
Этот уровень гарантирует, что функция никогда не выбрасывает исключений. Обычно применяется к ключевым операциям (например, деструкторам, перемещениям).
void safeSwap(std::vector<int>& a, std::vector<int>&