Підготовка для Junior C++

Питання для співбесіди Junior C++ Backend-розробника

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

Почати AI-співбесіду для Junior C++Банківська картка не потрібна. Доступна 1 безкоштовна сесія.
Практика технічних співбесід англійськоюРежим для тих, для кого англійська не є рідною, щоб тренувати технічні співбесіди.

Resource Management

1Поясніть RAII (Resource Acquisition Is Initialization) і як цей підхід формує безпечне щодо винятків керування ресурсами в backend-сервісах на C++.

RAII (Resource Acquisition Is Initialization) означає, що об'єкт C++ володіє ресурсом і звільняє його у своєму деструкторі. Оскільки локальні об'єкти знищуються автоматично, коли закінчується їхній час життя або область видимості, зокрема під час розкручування стека при винятках, RAII забезпечує детерміноване очищення і робить шляхи обробки помилок безпечними щодо винятків. У backend-сервісах це стосується не лише пам'яті, а й дескрипторів файлів, сокетів, блокувань м'ютексів, підключень до баз даних, транзакцій та інших ресурсів ОС або застосунку.

Відповісти на це запитання з ШІ-тренером

Керування пам'яттю

2Порівняйте std::unique_ptr і std::shared_ptr та опишіть, коли кожен із них доречний у backend API (Application Programming Interface).

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

Відповісти на це запитання з ШІ-тренером

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

3Що таке семантика переміщення (move semantics) і як правильно реалізувати конструктор переміщення та оператор присвоєння переміщенням?

Семантика переміщення дозволяє C++ передавати ресурси від тимчасових або інших об'єктів, якими можна пожертвувати, замість їх копіювання. Вона використовує `rvalue`-посилання, такі як `T&&`, та `std::move`, який є приведенням типу, що дозволяє вибирати перевантаження для переміщення; сам по собі `std::move` нічого не переміщує. Правильний конструктор переміщення ініціалізує новий об'єкт, забираючи ресурс вихідного об'єкта та залишаючи вихідний об'єкт у валідному стані, придатному для знищення та присвоєння. Правильний оператор присвоєння переміщенням передає дані в наявний об'єкт, обробляє або допускає самоприсвоєння, вивільняє або повторно використовує поточний ресурс цільового об'єкта, забирає ресурс вихідного об'єкта та залишає вихідний об'єкт у безпечному стані. Операції переміщення часто мають бути `noexcept`, щоб стандартні контейнери могли використовувати їх під час перерозподілу пам'яті, зберігаючи гарантії безпеки винятків.

Відповісти на це запитання з ШІ-тренером

4Опишіть константну коректність (const-correctness) в C++ API (Application Programming Interface) та як ефективно проектувати константні функції-члени.

Константна коректність означає вираження через систему типів того, які операції не змінюють спостережуваний або логічний стан об'єкта. Константна функція-член має об'єкт this із кваліфікатором const, тому вона не може змінювати дані-члени, що не є mutable, або викликати неконстантні функції-члени для того самого об'єкта. Хороший дизайн API позначає запити лише для читання як const, повертає значення або константні посилання/вказівники, коли це доцільно, і уникає розкриття змінного (mutable) внутрішнього стану з константних функцій. Ключове слово mutable слід зарезервувати для деталей реалізації, які не змінюють логічний стан, таких як кеші, відкладені (lazy) обчислення, метрики або м'ютекси. const — це контракт API щодо мутацій, а не автоматична гарантія потокобезпеки; гарантії багатопотоковості потребують окремої реалізації та документації.

Відповісти на це запитання з ШІ-тренером

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

`std::byte` — це окремий тип для представлення сирих бінарних даних у вигляді байтів, а не символів чи арифметичних цілих чисел. Він покращує безпеку типів, оскільки байтові буфери випадково не обробляються як текст або числові значення, і водночас підтримує побітові операції. Сирі бінарні буфери зазвичай слід представляти за допомогою байт-орієнтованих сховищ, таких як `std::vector<std::byte>` або `std::array<std::byte, N>`, і передавати через API (Application Programming Interface), що не передбачають передавання володіння, у вигляді `std::span<std::byte>` або `std::span<const std::byte>`. Код серіалізації повинен явно кодувати та декодувати значення, а не покладатися на довільне розташування об'єктів у пам'яті.

Відповісти на це запитання з ШІ-тренером

Час життя об'єктів

6Опишіть Правило нуля (Rule of Zero), Правило трьох (Rule of Three) та Правило п'яти (Rule of Five), а також випадки застосування кожного з них.

Правило нуля: віддавайте перевагу класам, які не оголошують користувацькі операції деструктора, копіювання чи переміщення; дозвольте членам класу, що реалізують RAII (Resource Acquisition Is Initialization), таким як `std::string`, `std::vector`, `std::unique_ptr`, обгортки файлів/сокетів тощо, керувати ресурсами. Правило трьох: якщо клас самостійно керує ресурсом і потребує користувацького деструктора, конструктора копіювання або оператора присвоєння копіюванням, йому зазвичай потрібні всі три для визначення правильної поведінки копіювання та володіння. Правило п'яти: у C++11 і пізніших версіях для таких типів також слід розглядати конструктор переміщення та оператор присвоєння переміщенням. Використовуйте Правило нуля для більшості прикладних типів; використовуйте Правило трьох/п'яти, коли тип безпосередньо володіє ресурсом або має нетривіальну семантику володіння/часу життя.

Відповісти на це запитання з ШІ-тренером

Обробка помилок

7Поясніть обробку винятків у C++, розгортання стека (stack unwinding), взаємодію з деструкторами та межі обробки винятків (exception boundaries) у сервісах.

Винятки у C++ передають керування від виразу `throw` до найближчого відповідного блоку `catch`. Під час поширення винятку відбувається розгортання стека (stack unwinding), яке знищує повністю сконструйовані автоматичні об'єкти у зворотному порядку, тому очищення за принципом RAII (Resource Acquisition Is Initialization) виконується автоматично. Деструктори зазвичай не повинні кидати винятки; якщо виняток виходить за межі деструктора з позначкою `noexcept` або інший виняток виникає під час активного розгортання стека, програма викликає `std::terminate`. Винятки зазвичай слід перехоплювати за посиланням, найчастіше `const&`, щоб уникнути зрізання типів (object slicing) та зайвих копій. Серверні сервіси повинні визначати межі обробки винятків (exception boundaries), такі як обробники запитів, точки входу робочих потоків, зворотні виклики (callbacks) фреймворків RPC (Remote Procedure Call) / HTTP (Hypertext Transfer Protocol) та функція `main`. На цих межах винятки логуються, перетворюються на відповіді з помилкою або коди стану, що запобігає їх потраплянню в невідповідні контексти, такі як C API (Application Programming Interface), деструктори, потоки або функції `noexcept`.

Відповісти на це запитання з ШІ-тренером

8Що відбувається, якщо деструктор викидає виняток, і як бекенд-типи повинні повідомляти про помилки очищення ресурсів?

У звичайних випадках деструктори неявно мають специфікатор `noexcept(true)`, тому, якщо виняток виходить за межі такого деструктора, викликається `std::terminate`. Навіть якщо деструктор явно оголошено як `noexcept(false)`, генерування винятків під час розкручування стека (stack unwinding) є небезпечним, оскільки другий виняток, що виходить назовні, коли інший виняток уже є активним, також завершує програму. Тому деструктори повинні виконувати очищення настільки добре, наскільки це можливо (best-effort cleanup), і не дозволяти виняткам виходити за їхні межі. Бекенд-типи повинні повідомляти про помилки очищення через явні операції, такі як `close()`, `flush()`, `commit()`, `stop()` або `shutdown()`, які повертають помилку/`expected` або викидають виняток до знищення об'єкта. Деструктор може логувати події, надсилати метрики, пригнічувати помилки або виконувати безпечне резервне очищення, але він не повинен бути основним каналом повідомлення про збої, які потребують втручання (actionable failures).

Відповісти на це запитання з ШІ-тренером

Стандартна бібліотека

9Як std::vector керує місткістю, збільшенням розміру, перерозподілом пам'яті та стабільністю ітераторів?

std::vector зберігає елементи неперервно і відстежує як розмір (size), так і місткість (capacity). Розмір — це кількість сконструйованих елементів; місткість — це обсяг виділеної пам'яті для елементів, доступний до того, як знадобиться наступне виділення. Коли додавання елементів перевищує місткість, вектор виділяє більший блок пам'яті (зазвичай використовуючи стратегію геометричного зростання, яка визначається реалізацією), переміщує або копіює наявні елементи, знищує старі та звільняє стару пам'ять. Виклик reserve(n) збільшує місткість без зміни розміру, тоді як resize(n) змінює розмір, створюючи або знищуючи елементи. Перерозподіл пам'яті (reallocation) робить недійсними всі ітератори, посилання та вказівники на елементи; навіть без перерозподілу пам'яті такі операції, як insert та erase, можуть зробити недійсними позиції на місці модифікації або після нього.

Відповісти на це запитання з ШІ-тренером

10Які правила інвалідації ви повинні знати для неперервних (contiguous) та вузлових (node-based) стандартних контейнерів?

Правила інвалідації залежать від контейнера та операції. Неперервні контейнери, такі як `vector` та `string`, мають низьку стабільність ітераторів та посилань: розширення може спричинити перерозподіл пам'яті (reallocation) та зробити недійсними всі ітератори, посилання та вказівники. Крім того, вставка чи видалення (`insert`/`erase`) може зсунути елементи та інвалідувати позиції у місці зміни та після неї навіть без перерозподілу пам'яті. Вузлові впорядковані контейнери, такі як `list`, `map`, `set` та їхні `multi`-варіанти, як правило, зберігають ітератори та посилання на існуючі не видалені елементи стабільними під час вставки; видалення елемента інвалідує ітератор або посилання лише на цей видалений елемент. Невпорядковані контейнери (unordered containers) також зберігають елементи у вузлах, тому посилання та вказівники на елементи зазвичай залишаються стабільними під час повторного хешування (rehash), але повторне хешування інвалідує ітератори. Контейнер `deque` має особливі правила сегментованого зберігання. На практиці завжди перевіряйте документацію до конкретного контейнера та операції, перш ніж зберігати ітератори або посилання між модифікаціями.

Відповісти на це запитання з ШІ-тренером

11Порівняйте std::map, std::unordered_map та контейнери в стилі flat-map для таблиць пошуку в бекенді.

`std::map` — це впорядкований асоціативний контейнер, зазвичай на основі дерева, з логарифмічним часом виконання операцій пошуку, вставки та видалення; він корисний, коли важливі відсортована ітерація, запити за діапазоном (range queries) або гарантії збереження порядку. `std::unordered_map` базується на хеш-таблиці з усередненим константним часом операцій за точним ключем і без впорядкування ключів; він часто є хорошим вибором за замовчуванням для великих змінюваних таблиць пошуку за умови якісного хешування. Контейнер у стилі flat-map зберігає відсортовані пари ключ-значення у безперервній області пам'яті, що забезпечує хорошу локальність кешу та швидку ітерацію і пошук (через бінарний пошук), але операції вставки та видалення посередині виконуються за лінійний час. Для бекенд-таблиць пошуку вибір залежить від того, чи потрібні робочому навантаженню впорядкування та діапазони, чи переважають пошуки за точним збігом, а також від частоти змін, передбачуваності затримки (latency), накладних витрат пам'яті та поведінки кешу.

Відповісти на це запитання з ШІ-тренером

12Поясніть призначення `std::optional` та типові варіанти використання в бекенді для представлення відсутніх значень.

`std::optional<T>` представляє або вміщене значення типу `T`, або відсутність значення. Порожній стан представлений через `std::nullopt`; код може перевірити `has_value()` або використати `optional` у булевому контексті, отримати доступ до значення за допомогою `*` або `value()`, а також надати значення за замовчуванням за допомогою `value_or()`. У серверному коді це корисно для полів бази даних, які можуть мати значення null, необов'язкових полів запитів/конфігурації, промахів кешу або репозиторію, де відсутність є очікуваною, а також станів предметної області, де маркерне значення (sentinel value), наприклад -1 або порожній рядок, було б неоднозначним. Він моделює відсутність значення, а не поліморфізм чи розширену інформацію про помилку.

Відповісти на це запитання з ШІ-тренером

13Що таке std::string_view та std::span, і які ризики щодо часу життя об'єктів створюють неволодіючі представлення?

std::string_view — це неволодіюче представлення безперервної послідовності символів; std::span<T> — це неволодіюче представлення безперервної послідовності елементів типу T. Вони корисні для параметрів із нульовим копіюванням (zero-copy) та буферних API (Application Programming Interface), оскільки містять вказівник і довжину, не виділяючи пам'яті та не володіючи нею. Основна небезпека пов'язана з часом життя: сховище даних, на яке є посилання, має жити довше, ніж представлення, і не повинно стати недійсним під час його використання. Повернення або збереження представлення на тимчасовий, локальний чи знищений об'єкт, а також на перерозподілений контейнер може призвести до появи посилання на недійсну пам'ять (dangling view).

Відповісти на це запитання з ШІ-тренером

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

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

`std::mutex` забезпечує ексклюзивне блокування: лише один потік може утримувати його, тому це типовий вибір для захисту спільного змінного стану. `std::shared_mutex` підтримує спільні блокування для читачів (reader locks) та ексклюзивні блокування для письменників (writer locks): багато читачів можуть утримувати блокування одночасно, але письменникам потрібен ексклюзивний доступ. Вибирайте його для даних, які переважно читаються, коли паралелізм читачів вартий додаткових витрат і коли справедливість/голодування письменників є прийнятними або обробляються. `std::recursive_mutex` — це ексклюзивний м'ютекс, який той самий потік може блокувати кілька разів і повинен розблокувати таку ж кількість разів; використовуйте його рідко, здебільшого для застарілого або реентерабельного коду (re-entrant code), оскільки він може приховувати поганий дизайн блокувань.

Відповісти на це запитання з ШІ-тренером

Можливості мови

15Поясніть режими захоплення змінних у лямбда-виразах і як захоплення взаємодіє з часом життя об'єктів у зворотних викликах (callbacks).

Лямбда-вираз може захоплювати змінні за значенням (`[x]` або `[=]`), за посиланням (`[&x]` або `[&]`), захоплювати `this`, або використовувати захоплення з ініціалізацією (init-capture), наприклад, `[p = std::move(ptr)]`. Захоплення за значенням копіює захоплений об'єкт у замикання під час створення лямбди; захоплення за посиланням зберігає посилання, тому оригінальні об'єкти повинні мати більший час життя, ніж усі виклики лямбди. У зворотних викликах, збережених лямбдах або під час асинхронної роботи захоплення за посиланням та захоплення `this` є небезпечними, оскільки локальні змінні або сам об'єкт можуть бути знищені до моменту виклику. Надавайте перевагу захопленню за значенням для необхідних даних, переміщенню/init-capture для передавання володіння, або свідомо використовуйте патерни `shared_ptr`/`weak_ptr`, коли час життя об'єкта потрібно подовжити або перевірити.

Відповісти на це запитання з ШІ-тренером