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

Въпроси за интервю за Junior C++ бекенд разработчици

15 избрани въпроса за интервю за Junior C++ бекенд разработчици, които трябва да обяснят ясно основите на езика и базовото управление на собствеността.

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

Resource Management

1Обяснете RAII (Resource Acquisition Is Initialization) и как този принцип оформя управлението на ресурси с гаранции за безопасност при изключения в бекенд услуги на C++.

RAII (Resource Acquisition Is Initialization) означава, че един обект в C++ притежава даден ресурс и го освобождава в своя деструктор. Тъй като локалните обекти се унищожават автоматично, когато техният живот или област на видимост приключи, включително по време на размотаването на стека при възникване на изключение, RAII осигурява детерминирано почистване и прави пътищата за грешки безопасни спрямо изключения. В бекенд услугите това се прилага не само за паметта, но и за файлови дескриптори, сокети, мютекси, манипулатори за бази данни, транзакции и други ресурси на операционната система или приложението.

Опитайте да отговорите на този въпрос с треньор с ИИ

Управление на паметта

2Сравнете `std::unique_ptr` и `std::shared_ptr` и опишете кога всеки от тях е подходящ в бекенд API интерфейсите.

`std::unique_ptr` представлява изключително притежание: той има ниска цена за производителността, може да бъде преместван, но не и копиран, и е подходящ за единствен собственик или за API интерфейси, които прехвърлят собствеността. `std::shared_ptr` представлява споделено притежание: той може да бъде копиран и поддържа обекта жив чрез броене на референции, докато последният силен собственик не го освободи. В бекенд API интерфейсите използвайте `unique_ptr`, когато се прехвърля собственост, `shared_ptr` – само когато множество независими собственици трябва да удължат времето на живот, и референции или сурови указатели за достъп без притежание. `std::make_shared` обикновено се предпочита при създаването на `shared_ptr` обекти, защото е ефикасен и безопасен спрямо изключения.

Опитайте да отговорите на този въпрос с треньор с ИИ

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

3Какво представлява семантиката на преместване (move semantics) и как се реализират правилно конструктор за преместване и оператор за присвояване чрез преместване?

Семантиката на преместване позволява на C++ да прехвърля ресурси от временни или други обекти, които вече не са нужни, вместо да ги копира. Тя използва `rvalue` референции като `T&&` и `std::move`, което представлява преобразуване на типа, позволяващо избора на презаредени функции за преместване; самото извикване на `std::move` не премества нищо. Правилният конструктор за преместване инициализира нов обект, като взима ресурса на обекта източник и оставя източника валиден, готов за унищожаване и позволяващ ново присвояване. Правилният оператор за присвояване чрез преместване прехвърля ресурса в съществуващ обект, обработва или толерира присвояване на самия себе си, освобождава или преизползва текущия ресурс на целта, взима ресурса на източника и оставя източника в безопасно състояние. Операциите по преместване често трябва да бъдат `noexcept`, за да могат стандартните контейнери да ги използват при презаделяне на памет, запазвайки гаранциите за безопасност при изключения.

Опитайте да отговорите на този въпрос с треньор с ИИ

4Опишете коректността спрямо `const` в програмните интерфейси на C++ и как ефективно да проектирате `const` функции-членове.

Коректността спрямо `const` означава да се изрази чрез системата от типове кои операции не променят видимото или логическото състояние на обекта. Една `const` функция-член има `this` обект с квалификатор `const`, така че тя не може да променя данни-членове, освен ако нямат квалификатор `mutable`, или да извиква функции-членове без `const` на същия обект. Добрият дизайн на програмни интерфейси маркира заявките само за четене като `const`, връща стойности или `const` референции/указатели, когато е подходящо, и избягва разкриването на изменяемо вътрешно състояние от `const` функции. Ключовата дума `mutable` трябва да се запази за детайли на имплементацията, които не променят логическото състояние, като кешове, отложено изчисляване, метрики или мютекси. Квалификаторът `const` е договор на програмния интерфейс относно промяната на състоянието, а не автоматична гаранция за безопасност при многопоточност; гаранциите за паралелност изискват отделна имплементация и документация.

Опитайте да отговорите на този въпрос с треньор с ИИ

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>`. Кодът за сериализация трябва изрично да кодира и декодира стойностите, вместо да разчита на произволно разположение на обектите в паметта.

Опитайте да отговорите на този въпрос с треньор с ИИ

Време на живот на обектите

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

Изключенията в C++ прехвърлят управлението от израз `throw` към най-близкия съвпадащ `catch`. По време на разпространението им, разгъването на стека унищожава напълно конструираните автоматични обекти в обратен ред, така че почистването чрез RAII (Resource Acquisition Is Initialization) се изпълнява автоматично. Деструкторите по правило не трябва да хвърлят изключения; ако дадено изключение излезе извън деструктор, маркиран с `noexcept`, или друго изключение излезе по време на активно разгъване на стека, програмата извиква `std::terminate`. Изключенията обикновено трябва да се прихващат по референция, най-често чрез `const&`, за да се избегне изрязване на обекта (slicing) и ненужни копия. Бекенд услугите трябва да дефинират граници на изключенията, като например обработчици на заявки, входни точки на работните нишки, обратни функции в RPC (Remote Procedure Call) или HTTP рамки, както и функцията `main`. На тези места изключенията се записват в журнал, преобразуват се в отговори за грешка или кодове на състояния и се предотвратява изтичането им в неподходящи контексти като C API интерфейси, деструктори, нишки или функции, маркирани с `noexcept`.

Опитайте да отговорите на този въпрос с треньор с ИИ

8Какво се случва, ако деструктор хвърли изключение, и как бекенд типовете трябва да отчитат неуспешно изчистване на ресурси?

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

Опитайте да отговорите на този въпрос с треньор с ИИ

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

9Как `std::vector` управлява капацитета, нарастването, преразпределянето на паметта и стабилността на итераторите?

`std::vector` съхранява елементите в последователна памет и проследява както размера (`size`), така и капацитета (`capacity`). Размерът е броят на конструираните елементи, а капацитетът е количеството заделена памет, достъпно преди да се наложи ново заделяне. Когато добавянето на елементи надхвърли капацитета, векторът заделя по-голям блок памет, обикновено използвайки специфична за имплементацията стратегия за геометрично нарастване, премества или копира съществуващите елементи, унищожава старите и освобождава старата памет. Методът `reserve(n)` увеличава капацитета, без да променя размера, докато `resize(n)` променя размера чрез конструиране или унищожаване на елементи. Преразпределянето на паметта инвалидира всички итератори, референции и указатели към елементите. Дори и без преразпределяне, операции като `insert` и `erase` могат да инвалидират позициите в или след точката на модификация.

Опитайте да отговорите на този въпрос с треньор с ИИ

10Какви правила за инвалидиране трябва да знаете за непрекъснатите и базираните на възли стандартни контейнери?

Правилата за инвалидиране зависят от контейнера и операцията. Непрекъснатите контейнери като vector и string имат крехка стабилност на итераторите и референциите: нарастването им може да доведе до презаделяне на памет и да инвалидира всички итератори, референции и указатели, а операциите за вмъкване и изтриване могат да изместят елементи и да инвалидират позициите на или след промяната, дори и без презаделяне. Базираните на възли подредени контейнери като list, map, set и техните multi варианти обикновено запазват итераторите и референциите към съществуващите неизтрити елементи стабилни при вмъкване; изтриването на елемент инвалидира само итератора или референцията към този изтрит елемент. Неподредените контейнери също съхраняват елементите във възли, така че референциите и указателите към елементите обикновено са стабилни при прехеширане, но прехеширането инвалидира итераторите. Контейнерът deque има специални правила за сегментирано съхранение. На практика, проверявайте конкретния контейнер и операция, преди да съхранявате итератори или референции между модификациите.

Опитайте да отговорите на този въпрос с треньор с ИИ

11Сравнете `std::map`, `std::unordered_map` и контейнерите от тип flat-map за справочни таблици в бекенда.

`std::map` е подреден, обикновено базиран на дърво асоциативен контейнер с логаритмично време за търсене, вмъкване и изтриване; той е полезен, когато са важни сортираното обхождане, заявките в диапазон или гаранциите за подредба. `std::unordered_map` е базиран на хеш таблица със средно константно време за операции с точен ключ и без подредба на ключовете; често е добър избор по подразбиране за големи изменяеми справочни таблици, когато хеширането е ефективно. Контейнерът от тип flat-map съхранява сортирани двойки ключ/стойност последователно в паметта, което осигурява добра локалност на кеша и бързо обхождане и двоично търсене, но вмъкването и изтриването в средата отнемат линейно време. За справочни таблици в бекенда, избирайте въз основа на това дали работното натоварване изисква подредба или диапазони, предимно точни търсения, чести промени, предвидима латентност, режийни разходи на памет и поведение на кеша.

Опитайте да отговорите на този въпрос с треньор с ИИ

12Обяснете `std::optional` и типичните случаи на употреба в бекенда за представяне на липсващи стойности.

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

Опитайте да отговорите на този въпрос с треньор с ИИ

13Какво представляват `std::string_view` и `std::span` и какви рискове за времето на живот на обектите въвеждат непритежаващите изгледи?

`std::string_view` е непритежаващ изглед към непрекъсната последователност от символи; `std::span<T>` е непритежаващ изглед към непрекъсната последователност от `T`. Те са полезни за параметри без копиране (zero-copy) и буферни API интерфейси, тъй като съдържат указател и дължина, без да заделят или притежават памет. Основният риск е времето на живот: реферираната памет трябва да надживее изгледа и не трябва да бъде инвалидирана, докато той се използва. Връщането или съхраняването на изглед към временен, локален или унищожен обект, или към преалокиран контейнер, може да доведе до висящ изглед (dangling view).

Опитайте да отговорите на този въпрос с треньор с ИИ

Паралелно изпълнение

14С какво се различават `std::mutex`, `std::shared_mutex` и `std::recursive_mutex` и кога бихте избрали всеки от тях?

`std::mutex` осигурява ексклузивно заключване: само една нишка може да го държи, така че той е изборът по подразбиране за защита на споделено изменяемо състояние. `std::shared_mutex` поддържа споделени ключалки за четене и ексклузивни ключалки за запис: много четци могат да държат ключалката едновременно, но писачите се нуждаят от ексклузивен достъп. Изберете го за данни, които се четат предимно, когато конкурентността на четците си струва режийните разходи и когато справедливостта към писачите или тяхното гладуване за ресурси е приемливо или се обработва. `std::recursive_mutex` е ексклузивен мютекс, който една и съща нишка може да заключи многократно и трябва да отключи същия брой пъти; използвайте го рядко, главно за наследен код или реентрантен код, тъй като може да прикрие лош дизайн на заключването.

Опитайте да отговорите на този въпрос с треньор с ИИ

Възможности на езика

15Обяснете режимите на прихващане при ламбда функциите и как те взаимодействат с времето на живот на обектите при обратни извиквания (callbacks).

Една ламбда функция може да прихваща променливи по стойност (`[x]` или `[=]`), по референция (`[&x]` или `[&]`), да прихваща `this` или да използва инициализиращо прихващане (init-capture) като `[p = std::move(ptr)]`. Прихващането по стойност копира прихванатия обект в създадения ламбда обект (closure), когато функцията се създава; прихващането по референция съхранява референции, така че оригиналните обекти трябва да съществуват по-дълго от всички извиквания на ламбда функцията. При обратни извиквания, съхранени ламбда функции или асинхронни задачи, прихващането по референция и прихващането на `this` са опасни, защото локалните променливи или самият обект могат да бъдат унищожени преди извикването. Предпочитайте прихващане по стойност за необходимите данни, move/init-capture за управление на собствеността, или съзнателно използване на шаблони с `shared_ptr`/`weak_ptr`, когато времето на живот на обекта трябва да бъде удължено или проверено.

Опитайте да отговорите на този въпрос с треньор с ИИ