C++ — подготовка к экзамену
18 вопросов + практическое задание. В билете: 2 теории + 1 практика. Писать руками на бумаге.
Общая структура программы. inline. static_assert. namespace
▶Структура и сборка
Единица трансляции (TU) — один .cpp после подстановки всех #include. Компилятор обрабатывает каждую TU изолированно, не зная про остальные; связывает их линкер.
Этапы: препроцессор (текстовая подстановка) → компиляция (.cpp→.o) → линковка (.o + библиотеки → программа).
Объявление vs определение
extern int x; // объявление (имя есть, памяти нет) — можно много раз
int x = 5; // определение — ровно одно (ODR)
void f(); // объявление
void f() {} // определение
ODR (One Definition Rule): у каждой сущности ровно одно определение на программу. В разных TU допустимы дубликаты, только если они идентичны.
inline
Сегодня не про встраивание (это лишь слабая подсказка оптимизатору). Главное значение — разрешение линкеру: «у этой функции/переменной может быть несколько одинаковых определений в разных TU — не ругайся на дубликат». Поэтому тело функции/переменной в заголовке помечают inline.
// utils.h — можно инклудить в сто .cpp без multiple definition
inline int square(int x){ return x*x; }
inline int g_counter = 0; // inline-переменная (C++17)
Неявно inline: методы, определённые в теле класса; constexpr-функции.
static_assert
Проверка условия на этапе компиляции; при провале — ошибка компиляции с текстом. Условие должно быть constexpr.
static_assert(sizeof(void*) == 8, "только 64-бит");
static_assert(std::is_integral_v<T>); // C++17: сообщение необязательно
| static_assert | assert | |
|---|---|---|
| Когда | компиляция | runtime |
| При провале | ошибка компиляции | abort() |
| В release | работает всегда | отключается NDEBUG |
namespace
| Конструкция | Что делает |
|---|---|
namespace A{} | группирует имена против конфликтов |
A::name | явный доступ (всегда безопасно) |
using A::name; | декларация — тащит одно имя |
using namespace A; | директива — тащит всё; ❌ в заголовках |
namespace{} | анонимный — имена только в этой TU (замена static) |
namespace fs=std::filesystem; | псевдоним |
ADL (поиск Кёнига): при вызове f(x) без квалификатора компилятор ищет f ещё и в namespace типов аргументов. Благодаря ADL работают std::cout << x и идиома swap без явного std::. ADL не заменяет поиск, а расширяет список кандидатов; финал решает разрешение перегрузок.
В namespace std нельзя ничего добавлять, кроме специализаций шаблонов для своих типов (например std::hash<MyType>).
Указатели и ссылки
▶Указатель — переменная, хранящая адрес другого объекта (≈8 байт). &x — взять адрес, *p — разыменование.
int x = 42;
int* p = &x; // p хранит адрес x
*p = 100; // x == 100
const int* p; // указатель на const: *p менять нельзя, p можно
int* const p=&x; // const-указатель: p менять нельзя, *p можно
nullptr (C++11) — «не указывает никуда». Лучше NULL/0: имеет тип nullptr_t, не путает разрешение перегрузок. p[i] ≡ *(p+i); p+1 сдвигает на sizeof(T).
Ссылка — псевдоним (другое имя) существующего объекта. Обязана инициализироваться, нельзя переподвязать, не может быть пустой.
int& r = x; // r — другое имя для x
const int& cr = 42; // const-ссылка продлевает жизнь временного
| Указатель | Ссылка | |
|---|---|---|
| Может быть пустым | да (nullptr) | нет |
| Переподвязка | да | нет |
| Инициализация обязательна | нет | да |
| Арифметика | да | нет |
| Своя память | да (объект) | логически нет (алиас) |
return. Можно возвращать ссылку на член объекта, параметр или static.Умные указатели (владение)
| Владение | Копирование | Когда | |
|---|---|---|---|
T* | нет | да | наблюдатель |
unique_ptr | единоличное | только move | дефолт |
shared_ptr | разделяемое (счётчик) | да | несколько владельцев |
weak_ptr | нет (наблюдатель) | да | разорвать цикл shared |
weak_ptr нужен, чтобы разорвать циклические ссылки shared_ptr (иначе счётчики не дойдут до 0 → утечка). Используй make_unique/make_shared, а не new.
const int* vs int* const · разница 4 видов указателей.Управляющие конструкции: ветвление, выбор, циклы
▶Ветвление
if(auto it=m.find(key); it!=m.end()){...} // if с инициализатором (C++17)
if constexpr(std::is_pointer_v<T>) ... // выбор ветки в КОМПАЙЛ-ТАЙМ
if constexpr: невыбранная ветка не компилируется (в обычном if обе ветки обязаны компилироваться). Тернарный ?: — единственный тернарный оператор, это выражение.
switch
Только по целым/enum/char (не string/float). break обязателен, иначе «проваливание» (fall-through); намеренное — [[fallthrough]];. Часто компилируется в таблицу переходов.
switch(code){
case 1: a(); break;
case 2: [[fallthrough]];
case 3: bc(); break;
default: d();
}
Циклы
| Цикл | Проверка | Минимум выполнений |
|---|---|---|
while | до тела | 0 |
do...while | после тела | 1 |
for | init→cond→тело→step | 0 |
for(const auto& x : v) // range-for: чтение без копий — БЕРИ ПО УМОЛЧАНИЮ
for(auto& x : v) x*=2; // ссылка — менять элементы
for(auto x : v) // КОПИЯ каждого элемента (дорого)
Range-for под капотом = цикл по begin()/end(). break (выйти), continue (след. итерация), return (выйти из функции).
Функции. Параметры и возврат. Лямбды. Перегрузка. Default-параметры
▶Передача параметров
| Способ | Когда |
|---|---|
по значению T | маленькие типы (int, double, указатель) |
const T& | большой объект, только читаем (без копии) |
T& | большой объект, надо менять оригинал |
T* | аргумент необязателен (nullptr = «нет») |
T&& | забираем владение / move |
Массив распадается в указатель (теряет размер) → передавать указатель+размер, int(&)[N], std::array/vector/span.
Возврат
Возврат по значению дёшев из-за RVO/NRVO и move. Нельзя возвращать ссылку/указатель на локальную.
Параметры по умолчанию
Только справа; указываются в одном месте (обычно в объявлении в .h).
void connect(std::string host, int port=8080, bool ssl=false);
Перегрузка
Одно имя — разные параметры (число/типы/const). Нельзя перегружать только по типу возврата.
Перегрузка операторов
struct Complex{
double re,im;
Complex operator+(const Complex& o)const{ return {re+o.re, im+o.im}; }
};
// operator<< — свободная функция (левый операнд ostream), часто friend:
std::ostream& operator<<(std::ostream& os, const Complex& c){
return os << c.re << " + " << c.im << "i";
}
Нельзя перегружать :: . .* ?: sizeof. Симметрию (2+c) даёт свободная функция. C++20: operator<=> генерирует все сравнения.
Лямбды
Лямбда = безымянный класс с operator(); захваты = его поля; объект = замыкание.
[захват](параметры) mutable -> ret { тело }
auto add = [](int a,int b){ return a+b; };
[x] // по значению (копия, безопасно)
[&x] // по ссылке (актуально, но опасно при переживании области)
[=][&]// всё по значению / по ссылке
[y=x*2]// init-capture (C++14), можно move внутрь
mutable снимает const с operator() → можно менять копии. [](auto x){} — generic lambda. Без захвата → конвертируется в указатель на функцию.
Целые и вещественные типы. Комплексные числа. enum
▶Целые
Размеры не фиксированы стандартом (зависят от платформы), есть лишь минимумы и соотношение char≤short≤int≤long≤long long. Точные размеры — <cstdint> (int32_t, uint64_t…).
int i=-1; unsigned u=1; i<u → false (i становится огромным).Вещественные (IEEE 754)
float ~7 цифр, double ~15–16 (дефолт), long double.
0.1+0.2 == 0.3 → false. Сравнивать через epsilon: abs(a-b) < 1e-9. 1.0/0.0=inf, 0.0/0.0=NaN; NaN != NaN → проверка std::isnan.std::complex
std::complex<double> a(3,4); // 3+4i
a.real(); a.imag(); std::abs(a)// =5; std::conj(a);
auto c = a + b; std::cout << a; // (3,4)
enum vs enum class
| enum (C-стиль) | enum class (C++11) | |
|---|---|---|
| Имена | вытекают в область (конфликты) | в своей области (Color::RED) |
| Конверсия в int | неявная (опасно) | только явная (static_cast) |
enum class Status : uint8_t { OK=200, Fail=500 }; // базовый тип + значения
Вывод: по умолчанию enum class — нет загрязнения области и опасной неявной конверсии. Enum используют для состояний, режимов, кодов, категорий.
Неявные и явные преобразования типов
▶Неявные
Компилятор сам: integral promotion (char/short→int), арифметические преобразования (int→double), указатель→bool, пользовательские (через конструктор/operator T()).
Сужающие (narrowing)
Потеря данных: double→int, большой→меньший, signed↔unsigned. Списочная инициализация {} запрещает сужение (ошибка компиляции) — потому безопаснее.
int x = 3.99; // ок, x==3 (тихо)
int y{3.99}; // ❌ ошибка: narrowing
explicit
Запрещает использование конструктора/оператора для неявных преобразований. Ставить на одноаргументные конструкторы, кроме случаев, где неявное превращение естественно (const char*→String).
4 явных каста C++
| Каст | Для чего | Проверка | Опасность |
|---|---|---|---|
static_cast | связанные типы, числа, иерархия | компайл-тайм | средняя |
dynamic_cast | downcast полиморфных типов | runtime | низкая |
const_cast | снять/добавить const | нет | высокая |
reinterpret_cast | переинтерпретация битов | нет | очень высокая |
static vs dynamic: static не проверяет тип в рантайме (неверный downcast = UB); dynamic проверяет реальный тип объекта (нужен polymorphic/virtual), при ошибке → nullptr (указатель) или bad_cast (ссылка).
Именованные касты лучше C-style (int)x: видны/грепаются, уже по возможностям, компилятор ловит ошибки.
constexpr (vs const, enum, макросы). volatile. typedef/using. auto/decltype
▶constexpr vs const
const = «нельзя менять» (значение может быть рантайм). constexpr = «вычислимо в компайл-тайме». Любой constexpr — const, но не наоборот.
const int x = readInt(); // ✅ рантайм
constexpr int y = readInt(); // ❌ не compile-time
int arr[y]; // ✅ y — compile-time константа
| Когда | Тип | Область | Отладчик | |
|---|---|---|---|---|
#define | препроцессор | нет | глобальная | нет |
const | компайл/рантайм | есть | да | да |
constexpr | компайл | есть | да | да |
Макросы хуже: нет типа, нет области видимости, не видны отладчику, побочные эффекты — SQUARE(i++)→((i++)*(i++)) (UB).
volatile
«Не оптимизируй обращения — значение меняется вне программы» (регистры железа, signal handler). НЕ про многопоточность и НЕ атомарность (для потоков — std::atomic).
typedef vs using
using ulong = unsigned long; // читается слева-направо
template<class T> using Vec=std::vector<T>; // шаблонный псевдоним — typedef не может
auto vs decltype
Оба подставляют тип в компайл-тайме. Отличие: откуда (auto — из инициализатора, decltype — из выражения в скобках, без вычисления) и const/ссылки (auto отбрасывает → копия, decltype сохраняет).
const int& r = x;
auto a = r; // int (копия)
decltype(r) b = x; // const int& (как есть)
auto mul(int a,int b) -> decltype(a*b); // тип результата по параметрам
Динамическое выделение памяти. Сравнение с C
▶Стек — локальные, авто, быстро, ограничен. Куча — размер в рантайме, живёт сколько нужно, вручную.
int* p = new int(42); delete p;
int* a = new int[100]; delete[] a; // new[] ↔ delete[] !
new делает два действия: выделяет память и вызывает конструктор. delete: деструктор + освобождение. Это главное отличие от C.
| malloc/free (C) | new/delete (C++) | |
|---|---|---|
| Конструктор/деструктор | ❌ нет | ✅ вызывает |
| Возвращает | void* (каст) | типизир. T* |
| Размер | вручную | сам по типу |
| При нехватке | nullptr | бросает bad_alloc |
Смешивать malloc↔delete — UB. Ошибки: утечка (забыл delete), double free, dangling (после delete), утечка при исключении.
make_unique, vector) освобождают сами, даже при исключении.Классы. Доступ. Специальные функции. friend. explicit
▶class vs struct: разница только в доступе по умолчанию (class — private, struct — public).
| Спецификатор | Доступ |
|---|---|
| public | отовсюду |
| protected | класс + наследники |
| private | только сам класс (+ friend) |
6 специальных функций
Widget(); // 1 конструктор по умолчанию
Widget(const Widget&); // 2 копирующий конструктор
Widget& operator=(const Widget&); // 3 копирующее присваивание
Widget(Widget&&) noexcept; // 4 move-конструктор
Widget& operator=(Widget&&) noexcept;// 5 move-присваивание
~Widget(); // 6 деструктор
Список инициализации : x(a), y(b) лучше присваивания в теле (обязателен для const-полей/ссылок; эффективнее). Порядок инициализации — по порядку объявления полей.
Правило 3/5/0: нужен один из {деструктор, copy-ctor, copy-=} → нужны все три (правило трёх); +move = пять; идеал — правило нуля: ничего не писать, владеть ресурсами через умные указатели/контейнеры.
= default / = delete — явно сгенерировать / запретить. Деструктор полиморфной базы — virtual.
friend
Даёт внешней функции/классу полный доступ к private/protected (поля, методы, конструкторы). Не нарушает инкапсуляцию — это её явная часть. Дружба не взаимна и не наследуется. Главный кейс — operator<<.
explicit
Запрещает неявное преобразование через конструктор. Ставить, когда аргумент — настройка (размер/дескриптор), преобразование дорогое/опасное, или explicit operator bool() для проверки в if.
Copy и Move семантика. Perfect forwarding
▶lvalue — есть имя/адрес (переменные). rvalue — временное, вот-вот умрёт (литералы, x+y, f()). У rvalue можно безопасно «украсть» ресурсы.
Copy vs Move
Глубокое копирование — своя память + копия содержимого. Поверхностное — только указатель → double free. Move — «крадёт» указатель у источника, источник обнуляет (O(1) вместо O(N)).
Buffer(Buffer&& o) noexcept : data(o.data), size(o.size){
o.data = nullptr; o.size = 0; // украли и обнулили источник
}
&& — rvalue-ссылка (ловит только временные). noexcept у move важен: vector при росте переместит элементы, только если move noexcept, иначе копирует.
std::move vs std::forward
std::move ничего не двигает — это static_cast к rvalue, который разрешает выбрать move-перегрузку (безусловно). После move объект валиден, но в неопределённом состоянии.
std::forward<T> — условно сохраняет исходную категорию (lvalue→lvalue, rvalue→rvalue). Для perfect forwarding.
template<typename T>
void wrapper(T&& arg){ // forwarding reference (не rvalue-ссылка!)
process(std::forward<T>(arg)); // передаём с сохранением категории
}
Forwarding reference T&& при выводе типа ловит и lvalue, и rvalue благодаря reference collapsing (& &&→&, && &&→&&).
Наследование. Виртуальные функции. Перегрузка vs переопределение
▶Наследование = отношение «является» (is-a). public почти всегда. Конструирование: база→производный; разрушение — обратно. Не наследуются: конструкторы, деструктор, =, friend.
virtual и полиморфизм
Без virtual метод выбирается по типу указателя (раннее связывание). С virtual — по реальному типу объекта в рантайме (позднее связывание) = динамический полиморфизм.
struct Animal{ virtual void sound(); virtual ~Animal()=default; };
struct Dog:Animal{ void sound() override; };
Animal* a = new Dog; a->sound(); // Dog::sound (через vtable)
vtable / vptr
vtable — таблица указателей на виртуальные функции, одна на класс. vptr — скрытый указатель в каждом объекте на vtable его реального класса. Вызов: объект→vptr→vtable→функция. Цена: +указатель в объекте, косвенный вызов, нет инлайнинга.
Чисто виртуальная =0 → класс абстрактный (нельзя инстанцировать). override — компилятор проверит совпадение сигнатуры (всегда пиши!). final — запрет дальнейшего переопределения/наследования.
delete base_ptr на производном объекте не вызовет ~Derived() → утечка. Есть virtual-функция → деструктор тоже virtual.Перегрузка vs Переопределение
| Перегрузка (overload) | Переопределение (override) | |
|---|---|---|
| Где | один класс | база↔производный |
| Сигнатуры | разные | одинаковые |
| virtual | нет | да |
| Выбор | компайл-тайм | рантайм |
Срезка (slicing): Animal a = dog; — теряется производная часть. Полиморфизм работает только через ссылки/указатели. Скрытие имён: метод в производном скрывает все одноимённые базы (лечится using Base::f;).
Виртуальное наследование. Множественное. Взаимодействие классов
▶Множественное наследование
Класс наследует от нескольких баз. Проблемы: конфликт имён (c.A::f()) и ромб.
Ромбовидная проблема
Base
/ \
Left Right // оба наследуют Base
\ /
Bottom // Bottom : Left, Right → ДВЕ копии Base!
Bottom содержит две копии Base → b.data неоднозначно, Base() зовётся дважды.
Виртуальное наследование — решение
struct Left : virtual Base {};
struct Right : virtual Base {};
struct Bottom : Left, Right {
Bottom() : Base(3) {} // ВИРТУАЛЬНУЮ базу инициализирует самый производный класс
};
virtual → единственная общая копия базы. Вызовы Base() из промежуточных классов игнорируются. Цена — доступ через доп. указатель. Пример из STL: iostream (istream+ostream виртуально наследуют ios).
Взаимодействие классов
| Связь | Смысл | Пример |
|---|---|---|
| Наследование | «является» (is-a) | Dog is-a Animal |
| Композиция | «состоит из» (has-a), владеет | Car has-a Engine (поле) |
| Агрегация | «использует», не владеет | Department → Employee* |
| Ассоциация | знают друг о друге | Driver использует Car |
Шаблоны. constexpr и const
▶Шаблон — «рецепт» для генерации кода под тип. typename = class в параметрах.
template<typename T> T max(T a,T b){ return a>b?a:b; }
template<typename T> class Stack{ std::vector<T> d; };
Инстанцирование — компилятор генерирует конкретную функцию/класс под каждый используемый тип. Следствие: определение шаблона должно быть видно в точке использования → шаблоны живут в заголовках.
Параметры шаблона — и связь с constexpr/const
Non-type template parameter (NTTP) — параметр-значение:
template<typename T, size_t N> class Array{ T data[N]; }; // как std::array
constexpr int n=10; Array<int,n> a; // ✅ n — compile-time
int k=readInt(); Array<int,k> c; // ❌ k — рантайм
Аргумент NTTP обязан быть compile-time константой (constexpr или целочисленный const). Это прямая связка вопроса.
Специализация
Полная (template<> struct X<bool>) — для одного типа. Частичная (X<T*>) — для семейства; у функций частичной нет (используют перегрузку).
constexpr в шаблонах
constexpr int factorial(int n){ return n<=1?1:n*factorial(n-1); }
constexpr int f5 = factorial(5); // 120 в компайл-тайме
constexpr упростил то, что раньше делали через шаблонное метапрограммирование (рекурсивные шаблоны). if constexpr — выбор ветки в компайл-тайме (невыбранная не компилируется). Variadic template<typename...Args> — переменное число параметров.
SFINAE. Концепты. type traits
▶type traits
Шаблоны, отвечающие на вопросы о типах в компайл-тайме (<type_traits>). Реализованы через специализацию.
std::is_integral_v<int> // true (_v — значение)
std::is_pointer_v<int*>
std::remove_const_t<const int> // int (_t — тип)
std::is_same_v<T,U>
SFINAE
Substitution Failure Is Not An Error — неудачная подстановка типа в сигнатуре не ошибка, а тихое исключение шаблона из кандидатов. Позволяет включать/выключать перегрузки через enable_if.
template<typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
void f(T x); // только для целых; иначе перегрузка исчезает
Минусы SFINAE: чудовищный синтаксис, невнятные ошибки.
Концепты (C++20)
Именованное ограничение на тип — «человеческий SFINAE»: читаемо, внятные ошибки, переиспользуемо.
template<typename T> concept Addable = requires(T a,T b){ a+b; };
template<std::integral T> T add(T a,T b){ return a+b; } // готовый концепт
void g(std::integral auto x); // сокращённый синтаксис
Связь: traits (инструмент) → SFINAE (костыль на их основе) → концепты (чистое решение). Все ограничивают шаблон подходящими типами.
Исключения. Коды возврата. RAII
▶try{ throw std::runtime_error("oops"); }
catch(const std::exception& e){ std::cout << e.what(); }
Раскрутка стека (stack unwinding): при throw программа идёт вверх по вызовам в поиске catch, вызывая деструкторы всех локальных объектов. Ловить по const& (избегает срезки), специфичные catch — раньше общих. Иерархия от std::exception (logic_error, runtime_error, bad_alloc).
Исключения vs коды возврата
| Коды возврата | Исключения | |
|---|---|---|
| Игнорировать | легко забыть | нельзя не заметить |
| Код | проверка после каждого вызова | один try/catch |
| Через уровни | вручную | всплывает само |
| Производительность | дёшево всегда | дорого при броске |
| Конструкторы | не могут вернуть код | единственный способ |
Правило: исключения — для исключительного (файл не открылся, нет памяти); коды/optional/expected — для ожидаемого (не найдено, невалидный ввод).
RAII
Resource Acquisition Is Initialization: ресурс захватывается в конструкторе, освобождается в деструкторе → освобождение автоматически и безопасно при исключениях (деструкторы зовутся при раскрутке стека).
void good(){
auto p = std::make_unique<int[]>(100);
mayThrow(); // бросит → деструктор unique_ptr освободит → нет утечки
}
RAII в STL: unique_ptr, lock_guard, fstream, vector. Деструктор не должен бросать (иначе при раскрутке → terminate). noexcept — обещание не бросать.
Категории значений
▶Каждое выражение в C++ имеет тип и категорию значения. Категория определяет, есть ли у выражения идентичность (адрес) и можно ли у него «украсть» ресурсы (move).
Две базовые характеристики
- glvalue (generalized lvalue) — имеет идентичность (адрес/местоположение).
- rvalue — можно перемещать (украсть ресурсы).
Три «листовые» категории
выражение
/ \
glvalue rvalue
/ \ / \
lvalue xvalue prvalue
| Категория | Идентичность | Можно move | Пример |
|---|---|---|---|
| lvalue | да | нет | x, obj.field, *p, arr[i] |
| prvalue | нет | да | 42, x+y, f() (возврат по значению) |
| xvalue | да | да | std::move(x), f() (возврат T&&) |
- lvalue — «именованный объект»: есть имя, можно взять
&, живёт долго. Нельзя красть. - prvalue (pure rvalue) — «чистое значение»: временный результат, нет имени/адреса. Можно красть.
- xvalue (eXpiring value) — «истекающий»: объект с адресом, но который разрешено обворовать (результат
std::move). Это glvalue и rvalue одновременно.
int x = 5;
x; // lvalue (есть имя)
5; // prvalue
x + 1; // prvalue (временное)
std::move(x); // xvalue (есть адрес, но разрешено move)
Зачем это нужно
Категории — фундамент move-семантики и перегрузки: компилятор по категории выбирает copy (для lvalue) или move (для rvalue).
void f(const T&); // ловит lvalue → копирование
void f(T&&); // ловит rvalue (prvalue/xvalue) → перемещение
Историческая память: lvalue = «left value» (могло стоять слева от =), rvalue = «right value». В современном C++ деление точнее (5 категорий).
std::move(x) (xvalue) · как категория влияет на выбор copy/move · может ли rvalue иметь адрес (xvalue — да).STL. Контейнеры. Итераторы
▶STL = Standard Template Library: контейнеры + итераторы + алгоритмы + функторы. Связаны через итераторы: алгоритмы работают с любыми контейнерами через единый интерфейс итераторов.
Контейнеры
Последовательные
| Контейнер | Структура | Доступ / особенности |
|---|---|---|
vector | динамический массив | O(1) индекс, push_back амортиз. O(1); дефолт |
array | массив фикс. размера | O(1), размер в компайл-тайме |
deque | двусторонняя очередь | O(1) с обоих концов |
list | двусвязный список | O(1) вставка/удаление, нет индекса |
forward_list | односвязный | экономнее list |
Ассоциативные (упорядоченные, дерево, O(log n))
| Контейнер | Особенности |
|---|---|
set / multiset | уникальные/с повторами ключи, отсортированы |
map / multimap | ключ→значение, отсортированы по ключу |
Неупорядоченные (хеш-таблица, O(1) среднее)
unordered_set, unordered_map — быстрее, но без порядка; нужен std::hash для ключа.
Адаптеры
stack (LIFO), queue (FIFO), priority_queue (куча) — обёртки над другими контейнерами.
Итераторы
Обобщённый «указатель» на элемент. Единый интерфейс: begin() (первый), end() (за последним).
for(auto it=v.begin(); it!=v.end(); ++it) std::cout<<*it;
for(const auto& x : v) ... // range-for = то же через begin/end
| Категория | Возможности | Контейнеры |
|---|---|---|
| Input/Output | чтение/запись, один проход | потоки |
| Forward | многократный проход вперёд | forward_list |
| Bidirectional | ++ и -- | list, set, map |
| Random access | +n, [], сравнение | vector, deque, array |
| Contiguous (C++17) | непрерывная память | vector, array, string |
vector при реаллокации (push_back) инвалидирует все; list/map — только удалённый элемент. Использовать инвалидированный итератор — UB.Алгоритмы
std::sort(v.begin(), v.end(), [](int a,int b){ return a>b; });
std::find_if(v.begin(), v.end(), [](int x){ return x>0; });
std::count_if, std::transform, std::accumulate, std::for_each ...
Алгоритмы принимают диапазон итераторов + предикат/компаратор (лямбду) → работают с любым контейнером. C++20 — ranges (std::ranges::sort(v)).
Qt: управление ресурсами, система «сигнал-слот»
▶Управление ресурсами — дерево объектов (parent-child)
Qt-объекты наследуют QObject и образуют дерево владения: у объекта есть родитель (parent). При удалении родителя автоматически удаляются все его дети — не нужно вручную delete каждого.
QWidget* window = new QWidget;
QPushButton* btn = new QPushButton(window); // parent = window
delete window; // btn удалится автоматически (он ребёнок)
Это форма RAII на уровне иерархии объектов: владелец отвечает за жизнь подчинённых. Поэтому в Qt часто пишут new без явного delete — за память отвечает родитель.
Сигналы и слоты
Механизм связи объектов («наблюдатель»): объект испускает сигнал при событии, подключённые слоты (методы) вызываются в ответ. Объекты не знают друг о друге напрямую — слабая связанность.
class Counter : public QObject {
Q_OBJECT // макрос — обязателен для сигналов/слотов
signals:
void valueChanged(int v); // сигнал — только объявление, без тела
public slots:
void setValue(int v){ ... emit valueChanged(v); } // emit испускает
};
QObject::connect(&a, &Counter::valueChanged,
&b, &Counter::setValue); // сигнал a → слот b
- signal — объявляется, тело генерирует moc (meta-object compiler); испускается через
emit. - slot — обычный метод, который можно подключить к сигналу.
connect()связывает; один сигнал → много слотов, и наоборот.Q_OBJECT+ moc добавляют метаинформацию (рефлексию), на которой держится механизм.
Преимущества: слабая связанность (объекты независимы), типобезопасность (с синтаксисом указателей на функции, C++11+), автоматический разрыв связи при удалении объекта.
Практика: класс Complex (operator +, <<, перегрузки)
▶Типовое задание билета. Разбор полного класса комплексного числа — на нём проверяют билеты 4 и 9.
#include <iostream>
class Complex {
double re, im;
public:
// конструкторы (default-параметры → один покрывает все случаи)
Complex(double r = 0, double i = 0) : re(r), im(i) {}
// геттеры — const-методы (не меняют объект)
double real() const { return re; }
double imag() const { return im; }
// operator+ как метод (левый операнд = *this)
Complex operator+(const Complex& o) const {
return Complex(re + o.re, im + o.im);
}
// operator+= (меняет объект, возвращает ссылку для цепочек)
Complex& operator+=(const Complex& o) {
re += o.re; im += o.im;
return *this;
}
// сравнение (C++20: одной строкой все операторы)
bool operator==(const Complex&) const = default;
// operator<< — свободная friend-функция (левый операнд ostream)
friend std::ostream& operator<<(std::ostream& os, const Complex& c);
};
// вывод: учитываем знак мнимой части
std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << c.re;
if (c.im >= 0) os << " + " << c.im << "i";
else os << " - " << -c.im << "i";
return os; // возврат os → цепочки cout << a << b
}
// свободная функция для симметрии: 2.0 + c (число слева)
Complex operator+(double d, const Complex& c) {
return Complex(d) + c;
}
int main() {
Complex a(3, 4), b(1, -2);
std::cout << (a + b) << "\n"; // 4 + 2i
a += b;
std::cout << a << "\n"; // 4 + 2i
std::cout << (2.0 + b) << "\n"; // 3 - 2i
}
Ключевые моменты для защиты
- Почему
operator<<— свободная функция, а не метод? Левый операнд —std::ostream, неComplex. Метод сделал бы левым операндом*this→ пришлось бы писатьc << cout.friend— чтобы видеть privatere/im. - Почему возвращаем
os&? Для цепочекcout << a << b. - Почему
operator+возвращает по значению, аoperator+=— ссылку?+создаёт новый объект;+=меняет существующий и возвращает*thisдля цепочек. - Почему параметр
const Complex&? Без копии и без права менять (билет 4). - Почему методы
const? Обещают не менять объект, работают на const-объектах. - Свободный
operator+(double, Complex)— для симметрии2.0 + c(метод покрыл бы толькоc + 2.0через неявное преобразование).
#include <iostream>, ; после класса, const на геттерах и operator+, return *this; в +=, return os; в <<.