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

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

Избрани въпроси за интервю за C++ бекенд разработчици, групирани по теми и генерирани от същия каталог с въпроси, който задвижва платформата за практика EngineerSpeak.

Започнете интервю с ИИ за 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Как работят потребителските функции за изтриване (custom deleters) в интелигентните указатели и кога са полезни за съвместимостта между C и C++?

Потребителската функция за изтриване е извикваема логика за почистване, която се използва от интелигентния указател вместо операцията delete по подразбиране, когато указателят освобождава притежавания ресурс. Тя е полезна за съвместимостта между C и C++, когато даден ресурс трябва да бъде освободен със специфична функция като fclose, close, curl_easy_cleanup, SSL_free, free или функция за унищожаване от библиотека. За unique_ptr типът на функцията за изтриване е част от типа unique_ptr и може да повлияе на неговия размер; функциите за изтриване без състояние могат да бъдат оптимизирани и премахнати, докато указателите към функции или функциите за изтриване със състояние добавят допълнителна памет. За shared_ptr функцията за изтриване се съхранява в контролния блок и се изпълнява, когато последният силен собственик освободи обекта. Потребителските функции за изтриване позволяват на ресурси от C да участват безопасно в RAII (Resource Acquisition Is Initialization).

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

4Обяснете работата на контролния блок при `weak_ptr` и `shared_ptr`, включително цикли, `enable_shared_from_this` и цената за поддържане на броячите на препратки.

Един `shared_ptr` управлява споделената собственост чрез контролен блок, съдържащ броячи на силните и слабите препратки, плюс информация за изчистване, като например обекти за изтриване (`deleter`) и заделяне на памет (`allocator`). Копирането или унищожаването на `shared_ptr` обекти увеличава или намалява брояча на силните препратки, обикновено с атомарни операции, така че отделните `shared_ptr` обекти да могат да се манипулират безопасно от различни нишки, но всяко обновяване на брояча има цена. Когато броят на силните препратки достигне нула, управляваният обект се унищожава; контролният блок остава, докато не изчезнат и слабите препратки. Типът `weak_ptr` сочи към същия контролен блок, без да удължава времето на живот на обекта; методът `lock()` връща `shared_ptr`, ако обектът е все още жив, и празен `shared_ptr` в противен случай. Циклите, съставени само от `shared_ptr`, предизвикват изтичане на памет, тъй като броячите на силните препратки никога не достигат нула, затова `weak_ptr` се използва за обратни указатели или връзки при шаблона наблюдател. Класът `enable_shared_from_this` позволява на обект, който вече е притежаван от `shared_ptr`, да създаде нов `shared_ptr` към себе си, използвайки съществуващия контролен блок, избягвайки опасни отделни контролни блокове. Разходите, свързани с броячите на препратки, включват атомарни увеличения/намаления, конкуренция за кеша, заделяне на памет за контролния блок и допълнително натоварване в критичните за производителността части от кода.

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

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

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

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

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

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

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

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

7Какво представляват типовете, позволяващи само преместване (move-only types), и как те влияят на границите на API интерфейсите за сокети, файлови манипулатори и ключалки?

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

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

8Сравнете `auto`, `decltype`, `decltype(auto)` и извличането на аргументи на шаблон (template argument deduction) в често срещан бекенд код.

`auto` използва извличане, подобно на шаблоните, за променливи: обикновеният `auto` обикновено премахва референциите и модификаторите `const` на най-високо ниво, освен ако декларацията не ги изисква изрично, като например `auto&`, `const auto&` или `auto&&`. `decltype(expr)` проверява декларирания тип или типа на израза по-точно: идентификатор без скоби връща декларирания тип, докато други lvalue изрази дават `T&`, xvalue изразите дават `T&&`, а prvalue изразите дават `T`. `decltype(auto)` извлича типа, използвайки правилата на `decltype`, често за връщани типове, когато референциите трябва да бъдат запазени. Извличането на аргументи на шаблон е подобно на `auto`, но зависи от формата на параметъра, като `T`, `T&`, `const T&` или `T&&`, и има свои собствени правила. Инициализаторите във фигурни скоби са често срещана разлика: `auto x = {1,2}` извлича `std::initializer_list<int>`, докато обикновен параметър на шаблон обикновено не може да извлече `T` от чист инициализатор във фигурни скоби, освен ако параметърът не очаква `initializer_list` или друг подходящ тип.

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

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

9Опишете Правилото на нулата (Rule of Zero), Правилото на трите (Rule of Three) и Правилото на петте (Rule of Five) и кога се прилага всяко от тях.

Правило на нулата: предпочитайте класове, които не декларират потребителски деструктор, операции за копиране или преместване; оставете членовете, използващи RAII (Resource Acquisition Is Initialization), като `std::string`, `std::vector`, `std::unique_ptr`, обвивки на файлове/сокети и други, да управляват ресурсите. Правило на трите: ако даден клас ръчно управлява ресурс и се нуждае от потребителски деструктор, конструктор за копиране или оператор за присвояване чрез копиране, той обикновено се нуждае и от трите, за да дефинира правилно поведение за копиране и управление на собствеността. Правило на петте: в C++11 и по-нови версии, такива типове трябва да обмислят и добавянето на конструктор за преместване и оператор за присвояване чрез преместване. Използвайте Правилото на нулата за повечето типове в приложението; използвайте Правилото на трите/петте, когато типът директно притежава ресурс или има нетривиална семантика на собственост и време на живот.

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

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

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

C++ има няколко форми на инициализация. Инициализацията по подразбиране, като например `T x;`, извиква конструктор по подразбиране за класови типове, но оставя автоматичните променливи от основни типове неинициализирани. Инициализацията със стойност, като например `T x{};` или `T()`, инициализира с нули там, където е приложимо, преди инициализацията на конструктора или членовете. Списъчната инициализация използва фигурни скоби, отхвърля стесняващи преобразувания и има специални правила за разрешаване на претоварването, включително силно предпочитание към валидни `std::initializer_list` конструктори. Агрегатната инициализация инициализира членовете на агрегата директно с фигурни скоби; C++20 също така поддържа обозначени инициализатори за агрегати, в реда на тяхното деклариране. Често срещаните капани включват неинициализирани локални скалари, изненадващ избор при претоварването на `initializer_list`, грешки при стесняване с фигурни скоби, най-объркващия синтактичен анализ (most vexing parse) при използване на кръгли скоби и промени в поведението, когато даден тип престане да бъде агрегат.

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

11Какво е недефинирано поведение в C++ и как може да се прояви в инциденти в продукционна бекенд среда?

Недефинираното поведение е поведение, за което стандартът на C++ не налага никакви изисквания след възникване на невалидна операция. Програмата може да изглежда, че работи, да се срине, да повреди данни, да разкрие уязвимости в сигурността или да бъде оптимизирана в изненадващо поведение. Компилаторите предполагат, че недефинирано поведение (UB) не се случва, и оптимизират въз основа на това предположение, така че проблемите могат да се появят само в release компилации или под продукционен трафик. Инцидентите в сървърната част могат да произтичат от висящи указатели или референции, използване след освобождаване на паметта, нарушения на времето на живот на обектите, достъп извън границите на масива, препълване на целочислени типове със знак, състезания за данни, невалидни преобразувания на типове, четения на неинициализирани стойности, двойно освобождаване на памет или нарушения на правилата за стриктно припокриване на типовете. Смекчаването на тези проблеми включва RAII (Resource Acquisition Is Initialization) и ясен дизайн на притежанието и времето на живот, по-безопасни абстракции и проверки на границите, тестване чрез фъзинг, преглед на кода, статичен анализ и инструменти за динамична проверка като ASan, UBSan и TSan.

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

Object Model

12Обяснете виртуалните функции, таблиците на виртуалните функции (vtables), цената на динамичното диспечериране, изрязването на обектите и опасностите при виртуалните деструктори.

Виртуалната функция позволява полиморфизъм по време на изпълнение: при извикване чрез указател към базов клас или референция, се избира имплементацията за динамичния тип на обекта. Повечето имплементации съхраняват скрит указател (`vptr`) във всеки полиморфен обект, който сочи към таблица с адресите на виртуалните функции (`vtable`) за съответния динамичен тип. Обичайната цена се изразява в допълнителен указател в обекта, непряко извикване, възможно влияние върху кеша и предвиждането на разклоненията, както и в намалени възможности за вграждане на кода, въпреки че компилаторите понякога могат да девиртуализират извикванията. Изрязване на обектите възниква, когато производен обект бъде копиран или съхранен по стойност като базов обект, при което се губи производната част и динамичното поведение. Ако базов клас е предназначен да бъде изтриван чрез базов указател, неговият деструктор трябва да бъде виртуален; в противен случай изтриването на производен обект чрез този базов указател води до недефинирано поведение.

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

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

13Опишете часовниците, времевите точки и продължителностите в `std::chrono` и тяхното приложение за таймаути на услуги, метрики и времеви отпечатъци.

`std::chrono` моделира времето чрез часовници, времеви точки (`time_point`) и продължителности (`duration`). Продължителността е интервал с определена мерна единица, като например милисекунди или секунди. Времевата точка представлява конкретен момент от времевата линия на даден часовник. `steady_clock` е монотонно нарастващ и трябва да се използва за измерване на изминало време, таймаути на услуги, крайни срокове и измервания на закъснението (латентността), тъй като не се влияе от промени в реалното системно време. `system_clock` представя гражданското време и е подходящ за времеви отпечатъци, журналиране, трайно съхранение на данни и преобразуване на дати, но стойността му може да прескочи напред или назад при коригиране на системното време. Таймаутите обикновено трябва да се изчисляват чрез `steady_clock::now() + duration`; машинните времеви отпечатъци трябва да използват ясно документиран формат за реално време, най-често UTC на границите на програмните интерфейси и при съхранение на данните.

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

14Опишете `std::format` и съвременните средства за форматиране в сравнение с `iostreams` и програмните интерфейси в стил `printf`.

`std::format` е типово безопасно средство за форматиране в C++20, вдъхновено от fmtlib. То използва полета за заместване `{}` и спецификации за форматиране, за да генерира форматиран текст без синтаксиса за вмъкване със състояния на `iostreams` или C varargs в стил `printf`. В сравнение с `printf`, то избягва много проблеми с несъответствието на формат и тип; в сравнение с `iostreams`, често е по-ясно и по-лесно за комбиниране. fmtlib е широко използваната библиотека, която предшества и повлиява `std::format` и може да предостави по-широка поддръжка или по-нови функции. Потребителски дефинираните типове могат да бъдат форматирани с поддръжка за персонализирани форматиращи класове. При интензивно логиране, производителността зависи от избягването на ненужно форматиране, преобразувания и заделяне на памет, особено за деактивирани нива на логиране; за предпочитане са програмни интерфейси, които отлагат форматирането или първо проверяват нивото на логиране.

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

Шаблони

15Обяснете категориите на стойностите и перфектното препращане (perfect forwarding) в C++, и защо те са важни за създаването на ефективни обобщени програмни интерфейси.

Категориите на стойностите в C++ описват изразите: `lvalues` имат идентичност и към тях може да се реферира след израза; `prvalues` са чисти `rvalues`, като например много временни или изчислени стойности; `xvalues` са обекти с изтичащ живот, чиито ресурси могат да бъдат преизползвани. Перфектното препращане е шаблонна техника, при която се приема препращаща референция, обикновено `T&&`, където `T` се извежда, и се препраща чрез `std::forward<T>(arg)`, така че категорията на стойността на извикващия код да се запази: `lvalues` остават `lvalues`, а `rvalues` остават `rvalues`. Това е възможно благодарение на правилата за свиване на референциите. Този механизъм е от значение за обобщените бекенд програмни интерфейси, защото обвиващи класове, фабрики, диспечери и функции от тип `emplace` могат да избегнат ненужни копия и да запазят избора на презаредена функция и поведението при преместване.

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

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

16Опишете стратегиите за предотвратяване на взаимно блокиране и заключване на множество мютекси с помощта на std::scoped_lock и std::lock.

Взаимното блокиране се предотвратява чрез избягване на кръгови изчаквания: придобивайте ключалките в последователен глобален ред, когато е възможно, или придобивайте множество мютекси чрез std::lock/std::scoped_lock, които използват алгоритъм за избягване на взаимно блокиране. std::scoped_lock lock(a, b, ...) е най-простата форма на RAII (Resource Acquisition Is Initialization) за заключване на няколко мютекса и тяхното автоматично отключване при излизане от областта на видимост. При използване на std::lock, първо заключете мютексите, след което прикачете RAII обвивки с помощта на std::adopt_lock, или използвайте std::unique_lock с std::defer_lock. Поддържайте критичните секции кратки и избягвайте блокиращи операции или неизвестни обратни функции, докато държите ключалки; избягването на взаимно блокиране не гарантира автоматично справедливост и не предотвратява всички сценарии на активно блокиране или гладуване.

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

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

Използвайте `std::condition_variable`, за да изчакате промяна на предикат за състояние, защитен с мютекс. Споделеното състояние се променя, докато се държи същият мютекс, след което `notify_one` или `notify_all` събужда чакащите нишки. Чакащите нишки трябва да използват `cv.wait(lock, predicate)` или еквивалентен цикъл, тъй като събужданията могат да бъдат фалшиви, а известията не се запазват независимо от състоянието на предиката. `notify_one` събужда една чакаща нишка; `notify_all` събужда всички чакащи нишки и е подходящо за масово оповестяване на промени в състоянието, като например при спиране на работа.

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

18Проектирайте безопасна за нишките ограничена опашка, използвайки стандартни C++ примитиви, и дефинирайте гаранциите на нейния API.

Ограничена и безопасна за нишките опашка може да бъде изградена с помощта на `std::mutex`, условни променливи, контейнер с фиксиран капацитет и флаг за спиране/затваряне. `push` блокира, изтича по време или се проваля, когато опашката е пълна, което осигурява противоналягане; `pop` блокира, когато опашката е празна. И двете операции трябва да изчакват въз основа на предикати като `size < capacity || closed` и `!empty || closed`. При спиране или затваряне трябва да се събудят блокираните производители и потребители; новите `push` заявки трябва да бъдат отхвърлени и да се определи дали потребителите ще извлекат оставащите елементи или ще спрат незабавно. Програмният интерфейс трябва да специфицира блокиращото поведение, семантиката при затваряне, върнатите стойности и гаранциите за паралелност.

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

Database

19Как се изпълнява миграция на схема за C++ услуга без прекъсване на работата?

Използвайте поетапна миграция от типа „разширяване и свиване“ (expand-contract). Първо направете обратносъвместими допълнения към схемата, като например колони, позволяващи null стойности, или нови таблици, които не нарушават работата на текущо работещата C++ услуга. Внедрете съвместим код, който може да обработва както старите, така и новите представяния, попълнете съществуващите данни в малки, контролирани партиди, валидирайте консистентността, превключете четенията към новата схема и едва по-късно премахнете старите колони или код, след като всички внедрени версии вече не зависят от тях. За големи таблици избягвайте дълги блокиращи DDL (Data Definition Language) операции, използвайте онлайн/паралелни операции там, където се поддържат, наблюдавайте заключванията, репликацията и ресурсите, и поддържайте пътища за отмяна, които работят с частично внедрен код и частично мигрирани данни.

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

20Как една C++ услуга трябва да чете от реплики на база данни, като същевременно управлява четенията на остарели данни и гаранциите за четене на собствените записи?

Разглеждайте четенията от реплики като изричен компромис с консистентността. Четения, които могат да толерират остарели данни, могат да се насочват към здрави реплики, но четения с изискване за прочитане на собствените записи, транзакционни четения или такива, критични за актуалността на данните, трябва да се насочват към главния възел, освен ако не е известно, че избраната реплика вече е възпроизвела съответната позиция на запис (като например LSN, времеви печат или версия). Проследявайте изоставането и състоянието на репликите, кодирайте необходимата консистентност за всяка крайна точка или заявка и при нужда преминавайте към главния сървър, изчаквайте наваксването или връщайте грешка, когато репликите надвишат допустимото изоставане. Документирайте модела на консистентност, за да знаят извикващите услуги кои четения могат да върнат остарели данни.

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