55 вопросов с ответами, которые покрывают базу: от компиляции и типов до ООП, STL и качества кода. Открывай ответ под вопросом и проверяй себя.
Как текст превращается в программу и что делает препроцессор.
Компиляция — перевод исходного кода в машинный до запуска. C++ компилируемый: получаем исполняемый файл, который работает без компилятора. Интерпретируемые языки выполняют код построчно во время запуска. Плюсы C++: скорость и контроль; минусы: дольше цикл «правка → запуск».
#include отличается от using namespace std;?Препроцессор обрабатывает директивы # до компиляции. #include буквально вставляет содержимое файла. using namespace std; — это уже язык C++: он позволяет писать cout вместо std::cout. Это разные этапы и разные задачи.
main возвращает int?Точка входа — int main(), с неё начинается программа. Возвращаемое значение — код завершения для ОС: 0 — успех, ненулевое — ошибка. void main() не соответствует стандарту.
Объявление сообщает компилятору что существует (имя, тип). Определение создаёт объект или тело функции. Заголовки (.h/.hpp) содержат объявления и подключаются через #include, чтобы разные .cpp видели общие имена. Include guards или #pragma once защищают от повторного включения.
Что хранить, как выбирать тип и какие ловушки поджидают.
int отличается от double? Когда какой использовать?int — целые (обычно 4 байта), double — дробные (8 байт). Счётчики, индексы, количество — int. Дробные величины (рост, температура, средний балл) — double. Деление int на int даёт целое.
char, std::string и const char*?char — один символ. std::string — современная строка с размером и удобными методами. const char* — указатель на массив символов (C-строка), требует ручного контроля. В новом коде для текста — std::string.
const? Чем он отличается от constexpr?const запрещает изменение после инициализации. constexpr — значение известно на этапе компиляции. Любой constexpr является const, но не наоборот.
auto и когда его уместно использовать?auto выводит тип из инициализатора. Удобно для длинных типов (итераторы, лямбды). Тип остаётся строгим. Не стоит злоупотреблять там, где тип важен для читаемости.
double в int? Почему 0.1 + 0.2 != 0.3?Дробная часть отбрасывается, округления нет: 3.9 → 3. Дробные числа хранятся в двоичном формате приближённо, поэтому 0.1 + 0.2 может дать 0.30000000000000004. Сравнивать double на равенство нельзя — только с допуском.
Выход за диапазон типа. Для signed int это неопределённое поведение: значение может стать отрицательным или «сломаться». Для unsigned — циклический переход. Решение: long long, проверки диапазона.
Операторы, условия и циклы — то, из чего состоит логика.
Арифметические + - * / %, сравнения == != < > <= >=, логические && || !, присваивания = += -=, инкремент/декремент, тернарный ?:, побитовые. Приоритет: * / % выше + -, сравнения ниже арифметики, && выше ||, присваивание — самый низкий. Скобки меняют порядок.
++i отличается от постфиксного i++?++i сначала увеличивает, потом возвращает новое значение. i++ возвращает старое, потом увеличивает. В отдельном выражении разницы нет, в составном — есть.
if отличается от switch и когда что выбирать?if — любые условия, диапазоны, логика. switch — выбор по целому значению из набора констант, читается лучше при многих вариантах. Не забывай break, иначе выполнение «провалится» в следующий case.
while отличается от do-while?while проверяет условие до тела — тело может не выполниться ни разу. do-while проверяет после — тело выполнится хотя бы один раз. Удобно для ввода с проверкой.
break и continue? Можно ли выйти сразу из двух вложенных циклов?break выходит из ближайшего цикла или switch. continue переходит к следующей итерации. Выйти сразу из двух вложенных циклов через break нельзя — нужен флаг или goto/функция.
условие ? A : B — возвращает A, если условие истинно, иначе B. Уместен для коротких выборов значений. Для сложной логики лучше if.
Как разбивать программу на понятные части.
Объявление (прототип): int add(int, int); — сообщает компилятору сигнатуру. Определение содержит тело. Прототип нужен, если функция используется раньше, чем определена.
По значению — копия, изменения не видны снаружи. По ссылке T& — работаем с оригиналом, синтаксис как у обычной переменной. По указателю T* — передаём адрес, нужна проверка на nullptr. Для больших объектов — ссылка, для изменения — ссылка или указатель.
Несколько функций с одним именем и разными параметрами. Компилятор выбирает по числу и типам аргументов. Возвращаемый тип не участвует в выборе.
Параметр может иметь значение по умолчанию: void f(int x = 0);. Аргументы по умолчанию должны идти последними. Задаются один раз (обычно в объявлении).
Функция вызывает сама себя. Нужны: базовый случай (условие остановки) и шаг к нему. Риски: переполнение стека при большой глубине, лишние вычисления. Иногда лучше цикл.
Где живут данные и как не сломать программу.
* и & отличаются в объявлении и в выражении?Указатель хранит адрес. В объявлении int* p — тип «указатель на int». В выражении *p — разыменование, &x — взятие адреса. То есть * и & многозначны в зависимости от контекста.
nullptr и почему не NULL?nullptr — типизированный нулевой указатель (C++11). NULL — макрос, часто равный 0, что создаёт неоднозначности при перегрузке. В новом коде — только nullptr.
Ссылка T& — псевдоним существующего объекта. Должна быть инициализирована, не может быть nullptr и не переприсваивается. Указатель можно менять и обнулять. Ссылка безопаснее, указатель гибче.
Стек — быстрый, автоматически очищается при выходе из блока, небольшой. Куча — динамическая память через new/delete, живёт до освобождения. Локальные переменные — на стеке; объекты из new — в куче.
Утечка — память из new не освобождена delete. Висячий указатель — указывает на уже удалённый объект. Обращение к нему — неопределённое поведение. Решение: умные указатели и RAII.
new/delete?unique_ptr — единственный владелец, shared_ptr — общий с подсчётом ссылок. Они автоматически освобождают память и корректно работают с исключениями. В современном C++ «голый» new почти не нужен.
Как хранить наборы данных и не изобретать велосипед.
std::vector?Массив фиксированного размера, не знает свою длину, не проверяет границы. vector динамический, знает размер, умеет добавлять элементы, безопаснее. В 95% задач — vector.
Объект, который «указывает» на элемент контейнера и умеет двигаться к следующему. Единый способ обхода для разных контейнеров и мост к алгоритмам STL.
std::map отличается от std::unordered_map?map хранит ключи упорядоченно (дерево), операции O(log n). unordered_map — хеш-таблица, в среднем O(1), но без порядка. Нужен порядок — map, нужна скорость — unordered_map.
vector — по умолчанию. list — частые вставки/удаления в середине. map/set — упорядоченный поиск по ключу. unordered_* — быстрый поиск без порядка. stack/queue — специфические дисциплины.
for и какие у него ограничения?for (auto x : v) — обход элементов контейнера. По умолчанию копирует элементы; для изменения нужен auto&, для чтения больших объектов — const auto&. Не подходит, если во время обхода меняется размер контейнера.
Классы, наследование, полиморфизм — и зачем это всё.
struct отличается от class в C++?Только уровнем доступа по умолчанию: у struct члены public, у class — private. В остальном они равнозначны.
private/public/protected?Скрытие внутреннего устройства класса. private — только внутри класса. protected — плюс производные классы. public — всем. Инкапсуляция защищает инварианты и упрощает изменение кода.
Конструктор инициализирует объект при создании, деструктор освобождает ресурсы при уничтожении. Конструктор — имя класса, деструктор — ~Имя(). Вызываются автоматически.
Конструктор копирования создаёт новый объект как копию. Операция присваивания копирует в уже существующий. Оба генерируются компилятором, но при владении ресурсами их нужно писать вручную.
Если классу нужен деструктор, конструктор копирования или операция присваивания — скорее всего, нужны все три (правило трёх). В C++11 добавляются конструктор и оператор перемещения (правило пяти).
public-наследование отличается от private?Производный класс получает члены базового. public-наследование — отношение «является» (is-a). private — деталь реализации, внешний код не видит базовый тип. На практике чаще нужно public.
Виртуальная функция выбирается по фактическому типу объекта во время выполнения. Это полиморфизм. Виртуальный деструктор нужен, чтобы при удалении через указатель на базовый класс вызывался деструктор производного.
Чисто виртуальная функция — virtual void f() = 0;, без реализации в базовом классе. Класс с хотя бы одной такой функцией — абстрактный, объекты не создаются. Задаёт интерфейс для наследников.
this и когда он нужен?Указатель на текущий объект внутри метода. Нужен при совпадении имён параметра и поля, для возврата *this, для передачи себя в другие функции.
Как писать код, который работает с разными типами.
Шаблон — «заготовка», по которой компилятор создаёт конкретные версии для нужных типов. template<class T> T max(T a, T b); — функция. template<class T> class Box {...}; — класс. Это основа STL.
Standard Template Library: контейнеры (vector, map, set), алгоритмы (sort, find), итераторы, функциональные объекты. Всё шаблонное и работает с разными типами.
[capture](args){ body } — анонимная функция. Захват: [] — ничего, [x] — по значению, [&x] — по ссылке, [=]/[&] — всё по значению/ссылке. Удобно для алгоритмов.
std::sort, std::find, std::accumulate?sort сортирует диапазон, find ищет значение и возвращает итератор, accumulate сворачивает диапазон в одно значение (сумма и не только). Все принимают итераторы начала и конца.
Как реагировать на ошибки и общаться с пользователем.
try/catch/throw? Когда использовать исключения, а когда коды возврата?throw бросает исключение, try оборачивает код, catch ловит по типу. Исключения — для исключительных ситуаций (нет файла, деление на ноль в логике). Для ожидаемых ошибок иногда лучше коды возврата или std::optional.
Resource Acquisition Is Initialization: ресурс захватывается в конструкторе и освобождается в деструкторе. При исключении деструкторы всё равно вызываются — ресурсы не утекают. На этом построены умные указатели и файловые потоки.
std::cin/std::cout отличаются от scanf/printf?cin/cout — типобезопасные потоки C++, понимают пользовательские типы через перегрузку << и >>. scanf/printf — из C, быстрее в некоторых случаях, но требуют строк формата и не проверяют типы.
>> отличается от getline?cin >> s читает до пробела. getline(cin, s) читает всю строку до \n. Если после >> использовать getline, нужно убрать оставшийся \n через cin.ignore().
У потока есть флаги: good, eof, fail, bad. Проверка if (!cin) или cin.fail() позволяет понять, что ввод не удался, и корректно обработать ошибку, а не работать с мусором.
Как писать так, чтобы код не ломался и его можно было поддерживать.
Ситуация, когда стандарт не гарантирует ничего: программа может работать, падать или делать что угодно. Примеры: выход за границы массива, разыменование nullptr, переполнение signed, чтение неинициализированной переменной. UB нужно избегать любой ценой.
-Wall -Wextra и предупреждения компилятора?Предупреждения ловят подозрительный код до запуска: неиспользуемые переменные, сравнения разных типов, потенциальные ошибки. Включать всегда — дешевле, чем отлаживать.
Инструмент для выполнения программы по шагам. Базовое: точки останова, шаг с заходом/без, просмотр переменных, стек вызовов. Позволяет найти причину ошибки, а не гадать.
Описывает, как собрать проект: файлы, зависимости, флаги, библиотеки. CMake генерирует файлы для конкретной среды (Make, Visual Studio, Ninja). Без неё большие проекты собирать вручную неудобно.
Автоматические проверки отдельных функций/классов. Ловят регрессии при изменениях, документируют поведение, заставляют писать код с понятными интерфейсами. Даже 5–10 тестов сильно экономят время.
Если ты отвечаешь на большинство вопросов без подглядывания — база у тебя есть. Дальше — практика: маленькие проекты, алгоритмы, чтение чужого кода и разбор ошибок.
Возвращайся к разделам, где были сомнения, и закрепляй практикой.