Подготовка Junior C++

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

15 выбранных вопросов для Junior C++-разработчиков, которым нужно уверенно объяснять основы языка и базовое владение памятью.

Начать ИИ-собеседование Junior 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Что такое move-семантика и как корректно реализовать move-конструктор и move-присваивание?

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

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

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

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

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

5Что такое std::byte и как безопасно представлять сырые двоичные буферы в C++?

std::byte — отдельный тип для представления сырых двоичных данных как байтов, а не символов или арифметических целых. Он повышает типобезопасность, потому что байтовые буферы случайно не воспринимаются как текст или числовые значения, при этом по-прежнему поддерживаются побитовые операции. Сырые двоичные буферы обычно следует представлять байт-ориентированным хранилищем вроде std::vector<std::byte> или std::array<std::byte, N> и передавать через невладеющие API как std::span<std::byte> или std::span<const std::byte>. Код сериализации должен явно кодировать и декодировать значения, а не полагаться на произвольную раскладку объектов в памяти.

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

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

6Опишите 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-тренеру

Обработка ошибок

7Объясните обработку исключений в C++, раскрутку стека, взаимодействие с деструкторами и границы исключений в сервисе.

Исключения в C++ передают управление от выражения `throw` к ближайшему подходящему `catch`. Во время распространения исключения раскрутка стека уничтожает полностью сконструированные автоматические объекты в обратном порядке, поэтому очистка через RAII выполняется автоматически. Деструкторы в общем случае не должны бросать исключения; если исключение покидает деструктор `noexcept` или другое исключение покидает область во время активной раскрутки, программа вызывает `std::terminate`. Исключения обычно следует перехватывать по ссылке, чаще `const&`, чтобы избежать slicing и лишних копий. Backend-сервисы должны определять границы исключений — например, обработчики запросов, точки входа worker-потоков, callback'и RPC/HTTP-фреймворка и `main` — где исключения логируются, преобразуются в ответы об ошибке или коды статуса и не допускаются к выходу в неуместные контексты: C API, деструкторы, потоки или функции `noexcept`.

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

8Что происходит, если деструктор бросает исключение, и как backend-типам следует сообщать об ошибках очистки?

Деструкторы в обычных случаях неявно `noexcept(true)`, поэтому если исключение покидает такой деструктор, вызывается `std::terminate`. Даже если деструктор явно объявлен `noexcept(false)`, выброс во время раскрутки стека опасен, потому что второе покидающее исключение при уже активном другом исключении также завершает программу. Поэтому деструкторы должны выполнять очистку по мере возможности и не позволять исключениям покидать их. Backend-типы должны сообщать об ошибках очистки через явные операции вроде `close()`, `flush()`, `commit()`, `stop()` или `shutdown()`, которые возвращают ошибку/`expected` или бросают до уничтожения. Деструктор может логировать, отдавать метрики, подавлять ошибки или выполнять безопасную запасную очистку, но не должен быть основным каналом сообщения о сбоях, требующих действий.

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

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

9Как std::vector управляет capacity, ростом, перевыделением и стабильностью итераторов?

std::vector хранит элементы непрерывно и отслеживает и size, и capacity. size — число сконструированных элементов; capacity — объём выделенного хранилища элементов, доступного до следующего выделения. Когда добавление элементов превысило бы capacity, vector выделяет больший блок, обычно используя определяемую реализацией стратегию геометрического роста, перемещает или копирует существующие элементы, уничтожает старые и освобождает старое хранилище. reserve(n) увеличивает capacity, не меняя size, а resize(n) меняет size, конструируя или уничтожая элементы. Перевыделение инвалидирует все итераторы, ссылки и указатели на элементы; даже без перевыделения операции вроде insert и erase могут инвалидировать позиции в точке изменения и после неё.

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

10Какие правила инвалидации нужно знать для непрерывных и node-based стандартных контейнеров?

Правила инвалидации зависят от контейнера и операции. Непрерывные контейнеры вроде vector и string имеют хрупкую стабильность итераторов/ссылок: рост может вызвать перевыделение и инвалидировать все итераторы, ссылки и указатели, а insert/erase могут сдвигать элементы и инвалидировать позиции в точке изменения и после неё даже без перевыделения. Упорядоченные node-based контейнеры вроде list, map, set и их multi-вариантов обычно сохраняют итераторы и ссылки на существующие неудалённые элементы при insert; удаление элемента инвалидирует итератор/ссылку на этот удалённый элемент. Неупорядоченные контейнеры тоже хранят элементы в узлах, поэтому ссылки и указатели на элементы в общем случае стабильны при rehash, но rehash инвалидирует итераторы. deque имеет особые правила сегментированного хранения. На практике перед сохранением итераторов или ссылок через модификации нужно проверять конкретный контейнер и операцию.

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

11Сравните std::map, std::unordered_map и контейнеры в стиле flat-map для lookup-таблиц в бэкенде.

std::map — упорядоченный, обычно древовидный ассоциативный контейнер с логарифмическими lookup/insert/erase; он полезен, когда важны отсортированная итерация, range-запросы или гарантии порядка. std::unordered_map основан на хеш-таблице со средней константной сложностью операций по точному ключу и без упорядоченности ключей; часто это хороший выбор по умолчанию для больших изменяемых lookup-таблиц при качественном хешировании. Контейнер в стиле flat-map хранит отсортированные пары ключ/значение непрерывно, обеспечивая хорошую локальность кэша и быструю итерацию/поиск двоичным поиском, но вставка и удаление в середине линейны. Для lookup-таблиц в бэкенде выбор зависит от того, нужны ли упорядоченность/диапазоны, в основном точные lookup, частые мутации, предсказуемая задержка, накладные расходы по памяти и поведение кэша.

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

12Объясните std::optional и типичные бэкенд-сценарии представления отсутствующих значений.

std::optional<T> представляет либо содержащееся значение T, либо отсутствие значения. Пустое состояние задаётся std::nullopt; код может проверять has_value() или использовать optional в булевом контексте, получать значение через * или value() и подставлять значение по умолчанию через value_or(). В бэкенд-коде это полезно для nullable-полей БД, необязательных полей запроса/конфигурации, промахов кэша или репозитория, где отсутствие ожидаемо, и доменных состояний, где sentinel вроде -1 или пустой строки был бы неоднозначен. Он моделирует отсутствие значения, а не полиморфизм и не богатую информацию об ошибке.

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

13Что такое std::string_view и std::span, и какие риски времени жизни создают невладеющие представления (views)?

std::string_view — это невладеющее представление непрерывной последовательности символов; std::span<T> — невладеющее представление непрерывной последовательности элементов типа T. Они полезны для параметров и API буферов без копирования (zero-copy), потому что хранят указатель и длину, не выделяя и не владея памятью. Главная опасность — время жизни: хранилище, на которое ссылается view, должно переживать сам view и не должно инвалидироваться, пока view используется. Возврат или сохранение view на временный объект, локальную переменную, уничтоженный объект или перевыделенный контейнер может оставить «висячий» (dangling) view.

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

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

14Чем отличаются std::mutex, std::shared_mutex и std::recursive_mutex, и когда какой из них выбирать?

std::mutex обеспечивает исключительную блокировку: его может удерживать только один поток, поэтому это выбор по умолчанию для защиты разделяемого изменяемого состояния. std::shared_mutex поддерживает разделяемые блокировки читателей и исключительные блокировки писателей: много читателей могут удерживать блокировку одновременно, но писателям нужен исключительный доступ. Его выбирают для данных, которые в основном читают, когда параллелизм читателей оправдывает накладные расходы и когда справедливость/голодание писателей приемлемы или обрабатываются. std::recursive_mutex — исключительный мьютекс, который один и тот же поток может захватить несколько раз и должен столько же раз отпустить; используйте его редко, в основном для унаследованного или реентерабельного кода, потому что он может маскировать плохой дизайн блокировок.

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

Возможности языка

15Объясните режимы захвата лямбд (lambda capture) и то, как захваты взаимодействуют с временем жизни объектов в колбэках.

Лямбда может захватывать переменные по значению (`[x]` или `[=]`), по ссылке (`[&x]` или `[&]`), захватывать `this` или использовать init-capture, например `[p = std::move(ptr)]`. Value capture копирует захваченный объект в замыкание в момент создания лямбды; reference capture хранит ссылки, поэтому исходные объекты должны переживать все вызовы лямбды. В колбэках, сохранённых лямбдах или асинхронной работе reference capture и захват `this` опасны, потому что локальные переменные или объект могут быть уничтожены до вызова. Предпочитайте value capture для нужных данных, move/init-capture для владения или осознанные паттерны `shared_ptr`/`weak_ptr`, когда время жизни объекта нужно продлить или проверить.

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