Подготовка к C++ интервью

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

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

Начать ИИ-собеседование по C++Банковская карта не нужна. Доступна 1 бесплатная сессия.
Практика технического интервью на английскомРежим, в котором не носители языка могут потренироваться проходить интервью.

Resource Management

1Объясните RAII и то, как этот принцип формирует безопасное при исключениях управление ресурсами в C++ backend-сервисах.

RAII (Resource Acquisition Is Initialization) означает, что объект C++ владеет ресурсом и освобождает его в своём деструкторе. Поскольку локальные объекты уничтожаются автоматически при завершении времени жизни/выходе из области видимости, в том числе при раскрутке стека из-за исключения, RAII обеспечивает детерминированную очистку и делает пути обработки ошибок безопасными при исключениях. В backend-сервисах это относится не только к памяти, но и к файловым дескрипторам, сокетам, блокировкам мьютексов, дескрипторам БД, транзакциям и другим ресурсам ОС или приложения.

Попробовать ответить на вопрос AI-тренеру

Управление памятью

2Сравните std::unique_ptr и std::shared_ptr и опишите, когда каждый из них уместен в backend API.

std::unique_ptr выражает исключительное владение: он дёшев, перемещаем, но не копируем, и подходит для единственного владельца или для API, которые передают владение. std::shared_ptr выражает совместное владение: он копируем и удерживает объект живым через подсчёт ссылок, пока последний сильный владелец его не отпустит. В backend API используйте unique_ptr, когда владение передаётся, shared_ptr — только когда несколько независимых владельцев должны продлевать время жизни, а ссылки или «сырые» указатели — для доступа без владения. std::make_shared обычно предпочтителен при создании объектов shared_ptr, потому что он эффективен и безопасен при исключениях.

Попробовать ответить на вопрос AI-тренеру

3Как работают пользовательские deleter в smart pointer и когда они полезны для взаимодействия C/C++?

Пользовательский deleter — это вызываемая логика очистки, которую smart pointer использует вместо операции delete по умолчанию, когда указатель освобождает принадлежащий ему ресурс. Он полезен для взаимодействия C/C++, когда ресурс нужно освобождать конкретной функцией, например fclose, close, curl_easy_очистка, SSL_free, free или destroy-функцией библиотеки. Для unique_ptr тип deleter входит в тип unique_ptr и может влиять на его размер; stateless deleter могут быть оптимизированы, а deleter в виде указателя на функцию или stateful deleter добавляют хранилище. Для shared_ptr deleter хранится в control block и выполняется, когда последний сильный владелец освобождает объект. Пользовательские deleter позволяют C-ресурсам безопасно участвовать в RAII.

Попробовать ответить на вопрос AI-тренеру

4Объясните механики weak_ptr и control block у shared_ptr, включая циклы, enable_shared_from_this и стоимость refcount.

shared_ptr управляет совместным владением через control block, содержащий сильный и слабый счётчики ссылок, а также информацию об очистке — например, deleter и allocator. Копирование или уничтожение объектов shared_ptr увеличивает или уменьшает сильный счётчик, обычно атомарными операциями, поэтому отдельные объекты shared_ptr можно безопасно использовать из разных потоков, но каждое обновление refcount имеет стоимость. Когда сильный счётчик достигает нуля, управляемый объект уничтожается; control block остаётся, пока не исчезнут и слабые ссылки. weak_ptr указывает на тот же control block, не продлевая время жизни объекта; lock() возвращает shared_ptr, если объект ещё жив, и пустой shared_ptr в противном случае. Циклы только из shared_ptr приводят к утечкам, потому что сильные счётчики никогда не достигают нуля, поэтому weak_ptr используют для обратных указателей или observer-связей. enable_shared_from_this позволяет объекту, которым уже владеет shared_ptr, создать новый shared_ptr на себя, используя существующий control block, избегая опасных отдельных control block. Стоимость refcount включает атомарные increment/decrement, contention кэша, аллокацию control block и накладные расходы в горячих путях.

Попробовать ответить на вопрос AI-тренеру

Система типов

5Что такое move-семантика и как корректно реализовать move-конструктор и move-присваивание?

Move-семантика позволяет C++ передавать ресурсы от временных или иным образом «расходуемых» объектов вместо их копирования. Она использует rvalue-ссылки вида T&& и std::move, который является приведением типа, позволяющим выбрать move-перегрузки; сам по себе std::move ничего не перемещает. Корректный move-конструктор инициализирует новый объект, забирая ресурс у исходного и оставляя исходный валидным, уничтожаемым и присваиваемым. Корректный оператор move-присваивания переносит ресурс в уже существующий объект, обрабатывает или допускает self-assignment, освобождает или повторно использует текущий ресурс назначения, забирает ресурс источника и оставляет источник в безопасном состоянии. Move-операции часто следует помечать noexcept, чтобы стандартные контейнеры могли использовать их при реаллокации, сохраняя гарантии исключений.

Попробовать ответить на вопрос AI-тренеру

6Опишите const-correctness в API на C++ и как эффективно проектировать const-функции-члены.

Const-correctness означает выражение через систему типов того, какие операции не изменяют наблюдаемое или логическое состояние объекта. У const-функции-члена объект this квалифицирован как const, поэтому она не может изменять не-mutable члены данных и вызывать не-const функции-члены того же объекта. Хороший дизайн API помечает только-для-чтения запросы как const, возвращает значения или const-ссылки/указатели, когда это уместно, и не раскрывает изменяемое внутреннее состояние из const-функций. mutable следует резервировать для деталей реализации, не меняющих логическое состояние: кэшей, ленивых значений, метрик или мьютексов. const — это контракт API о мутациях, а не автоматическая гарантия потокобезопасности; гарантии параллелизма требуют отдельной реализации и документации.

Попробовать ответить на вопрос AI-тренеру

7Что такое move-only типы и как они влияют на границы API для сокетов, файловых дескрипторов/хендлов и блокировок?

Move-only типы — это типы, которые нельзя копировать, но можно перемещать. Они типичны для уникального владения ресурсами вроде сокетов, файловых дескрипторов, file handles, блокировок и `std::unique_ptr`. Операции копирования удалены, чтобы избежать дублированного владения и двойного освобождения; move-операции передают владение и оставляют источник валидным, но обычно пустым/не владеющим. API должны явно выражать границы владения: фабрики могут возвращать move-only объекты по значению, функции, забирающие владение, — принимать по значению или rvalue reference, а функции только для просмотра — по ссылке, указателю или другим borrowing-хендлам. Контейнеры могут хранить move-only значения, когда элементы вставляются или перемещаются через move.

Попробовать ответить на вопрос AI-тренеру

8Сравните auto, decltype, decltype(auto) и вывод аргументов шаблона в типичном backend-коде.

auto использует вывод, похожий на шаблонный, для переменных: обычный auto обычно отбрасывает ссылки и cv-квалификаторы верхнего уровня (top-level const), если объявление об этом не просит, например auto&, const auto& или auto&&. decltype(expr) более точно исследует объявленный тип или тип выражения: не взятое в скобки id-expression даёт объявленный тип, тогда как другие lvalue-выражения дают T&, xvalue — T&&, а prvalue — T. decltype(auto) выводит тип по правилам decltype, часто для возвращаемых типов, когда нужно сохранить ссылки. Вывод аргументов шаблона похож на auto, но зависит от формы параметра — T, T&, const T& или T&& — и имеет собственные правила. Braced initializer — частый источник различий: auto x = {1,2} выводит std::initializer_list<int>, тогда как обычный параметр шаблона, как правило, не может вывести T из «голого» braced initializer, если параметр не ожидает initializer_list или другой подходящий тип.

Попробовать ответить на вопрос AI-тренеру

Время жизни объектов

9Опишите Rule of Zero, Rule of Three и Rule of Five и когда применяется каждое из них.

Rule of Zero: предпочитайте классы, которые не объявляют пользовательские деструктор/операции копирования/перемещения; пусть ресурсами управляют RAII-члены вроде std::string, std::vector, std::unique_ptr, обёрток файлов/сокетов и т.п. Rule of Three: если класс вручную управляет ресурсом и ему нужен пользовательский деструктор, конструктор копирования или оператор присваивания копированием, обычно нужны все три, чтобы корректно определить поведение копирования/владения. Rule of Five: в C++11 и новее такие типы также должны учитывать конструктор перемещения и оператор присваивания перемещением. Используйте Rule of Zero для большинства прикладных типов; используйте Rule of Three/Five, когда тип напрямую владеет ресурсом или имеет нетривиальную семантику владения/времени жизни.

Попробовать ответить на вопрос AI-тренеру

Семантика языка

10Опишите формы инициализации в C++ и типичные ловушки, включая initializer_list и aggregates.

В C++ есть несколько форм инициализации. Default initialization, например `T x;`, вызывает конструктор по умолчанию для классовых типов, но оставляет автоматические fundamental-переменные неинициализированными. Value initialization, например `T x{};` или `T()`, при необходимости zero-инициализирует до инициализации конструктором/членами. List initialization использует фигурные скобки, отклоняет сужающие преобразования и имеет особые правила разрешения перегрузок, включая сильное предпочтение жизнеспособных конструкторов `std::initializer_list`. Aggregate initialization инициализирует члены aggregate напрямую фигурными скобками; C++20 также поддерживает designated initializers для aggregates в порядке объявления. Типичные ловушки: неинициализированные локальные скаляры, неожиданный выбор перегрузки `initializer_list`, ошибки сужения с фигурными скобками, most-vexing parse со скобками и изменение поведения, когда тип перестаёт быть aggregate.

Попробовать ответить на вопрос AI-тренеру

11Что такое неопределённое поведение (undefined behavior) в C++ и как оно может проявляться в производственных инцидентах backend?

Неопределённое поведение (undefined behavior, UB) — это поведение, для которого стандарт C++ не накладывает никаких требований после недопустимой операции. Программа может казаться рабочей, падать, портить данные, открывать уязвимости безопасности или быть оптимизированной до неожиданного поведения. Компиляторы предполагают, что UB не происходит, и оптимизируют исходя из этого, поэтому проблемы могут проявляться только в release-сборках или под производственной нагрузкой. Backend-инциденты могут возникать из-за висячих указателей/ссылок, use-after-free, нарушений времени жизни объектов, выхода за границы, переполнения знаковых целых, гонок данных, некорректных приведений, чтения неинициализированных данных, double free или нарушений strict aliasing. Меры смягчения включают RAII и ясный дизайн владения/времени жизни, более безопасные абстракции и проверки границ, тестирование/фаззинг, code review, статический анализ и санитайзеры вроде ASan, UBSan и TSan.

Попробовать ответить на вопрос AI-тренеру

Object Model

12Объясните виртуальные функции, vtable, стоимость dynamic dispatch, object slicing и опасности виртуального деструктора.

Виртуальная функция обеспечивает runtime-полиморфизм: при вызове через указатель или ссылку на базовый тип выбирается реализация для dynamic type объекта. Большинство реализаций хранят скрытый vptr в каждом полиморфном объекте, указывающий на vtable адресов виртуальных функций для этого dynamic type. Типичная стоимость — дополнительный указатель в объекте, косвенный вызов, возможное влияние на кэш/предсказание ветвлений и меньшие возможности инлайнинга, хотя компиляторы иногда могут devirtualize. Object slicing происходит, когда derived-объект копируется или сохраняется by value как base-объект, теряя derived-часть и динамическое поведение. Если базовый класс предназначен для удаления через указатель на базу, его деструктор должен быть виртуальным; иначе удаление derived-объекта через такой base-указатель даёт неопределённое поведение.

Попробовать ответить на вопрос AI-тренеру

Стандартная библиотека

13Опишите часы, точки времени и длительности в std::chrono для таймаутов сервисов, метрик и временных меток.

`std::chrono` моделирует время с помощью часов (clocks), `time_point` и `duration`. `duration` — это интервал с единицей измерения, например миллисекунды или секунды. `time_point` — это точка на шкале времени конкретных часов. `steady_clock` монотонны и должны использоваться для измерения прошедшего времени, таймаутов сервисов, дедлайнов и задержек (latency), потому что на них не влияют изменения системных (wall-clock) часов. `system_clock` представляют гражданское/настенное время и подходят для временных меток, логирования, персистентности и календарных преобразований, но могут «прыгать», когда системное время корректируют. Таймауты обычно следует строить как `steady_clock::now() + duration`; машинные временные метки должны использовать явно задокументированный формат настенного времени, как правило UTC на границах API/хранения.

Попробовать ответить на вопрос AI-тренеру

14Опишите std::format и современные средства форматирования в сравнении с iostreams и printf-style API.

`std::format` — это type-safe средство форматирования C++20, вдохновлённое fmtlib. Оно использует поля подстановки `{}` и спецификации формата для получения отформатированного текста без stateful-синтаксиса вставки iostreams и без C varargs в стиле `printf`. По сравнению с `printf` оно избегает многих проблем несоответствия формата и типа; по сравнению с iostreams часто понятнее и проще для композиции. fmtlib — широко используемая библиотека, которая предшествовала `std::format` и повлияла на него, и может давать более широкую поддержку или более новые возможности. Пользовательские типы можно форматировать через поддержку custom formatter. В high-volume логировании производительность зависит от избежания ненужного форматирования, преобразований и аллокаций, особенно для отключённых уровней логов; предпочтительны API, которые откладывают форматирование или сначала проверяют уровень лога.

Попробовать ответить на вопрос AI-тренеру

Шаблоны

15Объясните категории значений (value categories) в C++ и perfect forwarding, а также почему это важно для эффективных обобщённых API.

Категории значений в C++ описывают выражения: lvalue имеют идентичность и к ним можно обращаться после выражения; prvalue — чистые rvalue, например многие временные/вычисленные значения; xvalue — истекающие объекты, чьи ресурсы можно повторно использовать. Perfect forwarding — это шаблонный приём: принять forwarding reference, обычно T&&, где T выводится, и переслать через std::forward<T>(arg), чтобы сохранить категорию значения вызывающего кода: lvalue остаются lvalue, а rvalue — rvalue. Это возможно благодаря правилам свёртки ссылок (reference collapsing). Это важно для обобщённых backend API, потому что обёртки, фабрики, диспетчеры и функции в стиле emplace могут избегать лишних копий и сохранять выбор перегрузок и поведение перемещения.

Попробовать ответить на вопрос AI-тренеру

Конкурентность

16Опишите предотвращение взаимных блокировок (deadlock) и стратегии захвата нескольких мьютексов с помощью std::scoped_lock и std::lock.

Deadlock предотвращается избеганием циклического ожидания: захватывайте блокировки в согласованном глобальном порядке, когда это возможно, либо захватывайте несколько мьютексов через `std::lock`/`std::scoped_lock`, которые используют алгоритм избегания deadlock. `std::scoped_lock lock(a, b, ...)` — простейшая RAII-форма для захвата нескольких мьютексов с автоматическим освобождением при выходе из области видимости. С `std::lock` сначала захватите мьютексы, затем прикрепите RAII-обёртки с `std::adopt_lock`, либо используйте `std::unique_lock` с `std::defer_lock`. Держите критические секции короткими и избегайте блокирующих операций или неизвестных callback'ов, удерживая блокировки; избегание deadlock само по себе не гарантирует справедливость и не предотвращает все сценарии livelock/starvation.

Попробовать ответить на вопрос AI-тренеру

17Объясните типичные паттерны использования condition_variable для ожидания изменения состояния.

Используйте `std::condition_variable`, чтобы ждать изменения предиката состояния, защищённого мьютексом. Общее состояние изменяют, удерживая тот же мьютекс, затем `notify_one` или `notify_all` будит ожидающих. Ожидающие должны использовать `cv.wait(lock, predicate)` или эквивалентный цикл, потому что пробуждения могут быть ложными (spurious), а уведомления сами по себе не запоминаются отдельно от состояния предиката. `notify_one` будит одного ожидающего; `notify_all` будит всех и подходит для широковещательных изменений состояния, например shutdown.

Попробовать ответить на вопрос AI-тренеру

18Спроектируйте потокобезопасную ограниченную очередь (bounded queue) на стандартных примитивах C++ и определите гарантии её API.

Ограниченную потокобезопасную очередь можно построить на `std::mutex`, condition variables, контейнере фиксированной ёмкости и флаге shutdown/closed. `push` блокируется, истекает по timeout или завершается с ошибкой, когда очередь полна — это обеспечивает backpressure; `pop` блокируется, когда очередь пуста. Обе операции должны ждать по предикатам вроде `size < capacity || closed` и `!empty || closed`. При shutdown/close нужно разбудить заблокированных производителей и потребителей; отклонять новые push и определить, дренируют ли потребители уже имеющиеся элементы или останавливаются сразу. API должен задавать блокирующее поведение, семантику close, возвращаемые значения и гарантии конкурентности.

Попробовать ответить на вопрос AI-тренеру

Database

19Как выполнить миграцию схемы для C++-сервиса без простоя?

Используйте поэтапную миграцию expand-contract. Сначала сделайте обратно совместимые дополнения схемы — например, nullable-столбцы или новые таблицы, — которые не ломают уже работающий C++-сервис. Задеплойте совместимый код, способный работать и со старым, и с новым представлением, выполните backfill существующих данных небольшими throttled-батчами, проверьте согласованность, переключите чтение на новую схему и только позже удалите старые столбцы или код, когда все задеплоенные версии больше от них не зависят. Для больших таблиц избегайте долгой блокирующей DDL, используйте online/concurrent-операции там, где они поддерживаются, мониторьте блокировки/репликацию/ресурсы и сохраняйте пути отката, работающие при частично задеплоенном коде и частично мигрированных данных.

Попробовать ответить на вопрос AI-тренеру

20Как C++-сервису следует читать из реплик базы данных, управляя устаревшими чтениями и гарантиями read-your-writes?

Относитесь к чтениям с реплик как к явному компромиссу по согласованности. Чтения, которые допускают устаревание, могут идти на здоровые реплики, но read-your-writes, транзакционные или критичные к свежести чтения должны идти на primary, если только выбранная реплика заведомо воспроизвела релевантную позицию записи — например LSN, timestamp или version. Отслеживайте лаг и здоровье реплик, кодируйте требуемую согласованность на уровне endpoint'а или запроса и делайте резервный переход на primary, ждите догона или завершайтесь ошибкой, когда реплики превышают допустимое устаревание. Документируйте модель согласованности, чтобы вызывающие стороны знали, какие чтения могут быть устаревшими.

Попробовать ответить на вопрос AI-тренеру