C++ — подготовка к экзамену

18 вопросов + практическое задание. В билете: 2 теории + 1 практика. Писать руками на бумаге.

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_assertassert
Когдакомпиляция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>).

Спросят
Объявление vs определение · что такое ODR и причём inline · гарантирует ли inline встраивание (нет) · зачем анонимный namespace · что такое ADL.
2

Указатели и ссылки

Указатель — переменная, хранящая адрес другого объекта (≈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)нет
Переподвязкаданет
Инициализация обязательнанетда
Арифметикаданет
Своя памятьда (объект)логически нет (алиас)
Висячие (dangling)
Никогда не возвращай ссылку/указатель на локальную переменную функции — она умрёт на return. Можно возвращать ссылку на член объекта, параметр или static.

Умные указатели (владение)

ВладениеКопированиеКогда
T*нетданаблюдатель
unique_ptrединоличноетолько moveдефолт
shared_ptrразделяемое (счётчик)данесколько владельцев
weak_ptrнет (наблюдатель)даразорвать цикл shared

weak_ptr нужен, чтобы разорвать циклические ссылки shared_ptr (иначе счётчики не дойдут до 0 → утечка). Используй make_unique/make_shared, а не new.

Спросят
Ссылка vs указатель (таблица) · висячий указатель · nullptr vs NULL · const int* vs int* const · разница 4 видов указателей.
3

Управляющие конструкции: ветвление, выбор, циклы

Ветвление

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
forinit→cond→тело→step0
for(const auto& x : v)  // range-for: чтение без копий — БЕРИ ПО УМОЛЧАНИЮ
for(auto& x : v) x*=2;  // ссылка — менять элементы
for(auto x : v)         // КОПИЯ каждого элемента (дорого)

Range-for под капотом = цикл по begin()/end(). break (выйти), continue (след. итерация), return (выйти из функции).

goto
Избегать: делает поток управления невидимым (спагетти-код), может перепрыгнуть инициализацию, плохо дружит с RAII/деструкторами. Структурные конструкции делают границы потока явными.
Спросят
while vs do-while · зачем break в switch · по каким типам switch · if constexpr vs if · range-for: auto/auto&/const auto&.
4

Функции. Параметры и возврат. Лямбды. Перегрузка. 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. Без захвата → конвертируется в указатель на функцию.

Спросят
3 способа передачи · const T& зачем · нельзя возвращать ссылку на локальную · перегрузка по возврату (нельзя) · захват =/& · что лямбда под капотом · operator+/<< (практика!).
5

Целые и вещественные типы. Комплексные числа. enum

Целые

Размеры не фиксированы стандартом (зависят от платформы), есть лишь минимумы и соотношение char≤short≤int≤long≤long long. Точные размеры — <cstdint> (int32_t, uint64_t…).

Грабли
Переполнение signed — UB; unsigned — wrap-around (по модулю 2ⁿ). Сравнение signed/unsigned: int i=-1; unsigned u=1; i<ufalse (i становится огромным).

Вещественные (IEEE 754)

float ~7 цифр, double ~15–16 (дефолт), long double.

Грабли
0.1+0.2 == 0.3false. Сравнивать через 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 используют для состояний, режимов, кодов, категорий.

Спросят
Фиксированы ли размеры · переполнение signed/unsigned · почему double нельзя через == · NaN · enum vs enum class (2 отличия).
6

Неявные и явные преобразования типов

Неявные

Компилятор сам: 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_castdowncast полиморфных типовruntimeнизкая
const_castснять/добавить constнетвысокая
reinterpret_castпереинтерпретация битовнеточень высокая

static vs dynamic: static не проверяет тип в рантайме (неверный downcast = UB); dynamic проверяет реальный тип объекта (нужен polymorphic/virtual), при ошибке → nullptr (указатель) или bad_cast (ссылка).

const_cast → UB
Изменять через снятый const изначально const-объект — UB (он может лежать в read-only памяти). Легально, только если объект изначально не const, а const добавился на уровне указателя.

Именованные касты лучше C-style (int)x: видны/грепаются, уже по возможностям, компилятор ловит ошибки.

Спросят
narrowing и почему {} ловит · зачем explicit · 4 каста · static vs dynamic · когда const_cast = UB.
7

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);  // тип результата по параметрам
Спросят
constexpr vs const · почему макросы хуже (SQUARE(i++)) · volatile ≠ многопоточность · using vs typedef · auto отбрасывает const, decltype сохраняет.
8

Динамическое выделение памяти. Сравнение с 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

Смешивать mallocdelete — UB. Ошибки: утечка (забыл delete), double free, dangling (после delete), утечка при исключении.

Современный C++
Ручной new/delete почти не пишут — используют RAII: умные указатели и контейнеры (make_unique, vector) освобождают сами, даже при исключении.
Спросят
new vs malloc (таблица) · new[]↔delete[] · 2 действия new · что бросает при нехватке (bad_alloc) · утечка/double free/dangling · как решает RAII.
9

Классы. Доступ. Специальные функции. 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.

Спросят
class vs struct · 6 спец-функций · список инициализации и порядок полей · правило 3/5/0 · зачем virtual-деструктор · friend и инкапсуляция · explicit.
10

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 (& &&&, && &&&&).

Спросят
зачем move · глубокое vs поверхностное (double free) · что делает std::move (не двигает) · std::move vs std::forward (безусл./усл.) · forwarding reference · noexcept у move · состояние после move.
11

Наследование. Виртуальные функции. Перегрузка 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 — запрет дальнейшего переопределения/наследования.

virtual-деструктор
Без него delete base_ptr на производном объекте не вызовет ~Derived() → утечка. Есть virtual-функция → деструктор тоже virtual.

Перегрузка vs Переопределение

Перегрузка (overload)Переопределение (override)
Гдеодин классбаза↔производный
Сигнатурыразныеодинаковые
virtualнетда
Выборкомпайл-таймрантайм

Срезка (slicing): Animal a = dog; — теряется производная часть. Полиморфизм работает только через ссылки/указатели. Скрытие имён: метод в производном скрывает все одноимённые базы (лечится using Base::f;).

Спросят
перегрузка vs переопределение (таблица!) · раннее/позднее связывание · vtable/vptr · virtual-деструктор · абстрактный класс · срезка · скрытие имён.
12

Виртуальное наследование. Множественное. Взаимодействие классов

Множественное наследование

Класс наследует от нескольких баз. Проблемы: конфликт имён (c.A::f()) и ромб.

Ромбовидная проблема

     Base
     /  \
   Left Right     // оба наследуют Base
     \  /
    Bottom        // Bottom : Left, Right → ДВЕ копии Base!

Bottom содержит две копии Baseb.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
Принцип
Предпочитай композицию наследованию. Наследование — только для настоящего is-a с полиморфизмом (принцип подстановки Лисков).
Спросят
ромб (схема) · почему 2 копии · как virtual решает · кто инициализирует виртуальную базу · композиция vs наследование · виды связей.
13

Шаблоны. 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> — переменное число параметров.

Спросят
зачем шаблоны · инстанцирование (почему в заголовках) · NTTP и почему аргумент constexpr/const · полная vs частичная специализация · if constexpr.
14

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 (костыль на их основе) → концепты (чистое решение). Все ограничивают шаблон подходящими типами.

Спросят
что такое traits (_v/_t) · расшифровать SFINAE и принцип · enable_if · концепты и чем лучше SFINAE · requires-выражение · как связаны.
15

Исключения. Коды возврата. 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 — обещание не бросать.

Спросят
раскрутка стека · исключения vs коды (когда что) · почему конструктор только исключением · RAII и безопасность при исключениях · почему деструктор не бросает · ловить по const&.
16

Категории значений

Каждое выражение в 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 категорий).

Спросят
lvalue vs rvalue · что такое xvalue/prvalue/glvalue · к какой категории std::move(x) (xvalue) · как категория влияет на выбор copy/move · может ли rvalue иметь адрес (xvalue — да).
17

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)).

Спросят
vector vs list (когда что) · map vs unordered_map (дерево O(log n) vs хеш O(1)) · что такое итератор и категории · begin/end · инвалидация итераторов · как алгоритмы связаны с контейнерами.
18

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+), автоматический разрыв связи при удалении объекта.

Спросят
как Qt управляет памятью (дерево parent-child, удаление родителя) · что такое сигнал/слот · зачем Q_OBJECT и moc · как connect · преимущество (слабая связанность) · паттерн «наблюдатель».

Практика: класс 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 — чтобы видеть private re/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; в <<.