Przygotowanie dla Junior C++

Pytania rekrutacyjne Junior C++ Backend

15 wybranych pytań rekrutacyjnych dla młodszych programistów (Junior C++) backend, którzy muszą jasno wytłumaczyć podstawy języka i prawo własności.

Rozpocznij próbną rozmowę AI dla Junior C++Karta płatnicza nie jest wymagana. Dostępna 1 darmowa sesja.
Ćwiczenie rozmów technicznych po angielskuTryb dla osób, dla których angielski nie jest językiem ojczystym, do ćwiczenia rozmów technicznych.

Resource Management

1Wyjaśnij paradygmat RAII (Resource Acquisition Is Initialization) i sposób, w jaki kształtuje on bezpieczne ze względu na wyjątki zarządzanie zasobami w usługach backendowych pisanych w C++.

RAII (Resource Acquisition Is Initialization) oznacza, że obiekt C++ posiada prawo własności do zasobu i zwalnia ten zasób w swoim destruktorze. Ponieważ obiekty lokalne są niszczone automatycznie w momencie, gdy kończy się ich czas życia (lifetime) i zakres, co ma również miejsce podczas zwijania stosu przy wywołaniu wyjątku (stack unwinding), RAII zapewnia deterministyczne zwalnianie zasobów i sprawia, że ścieżki błędów stają się bezpieczne w kontekście wyjątków (exception-safe). W usługach backendowych dotyczy to nie tylko pamięci, ale także deskryptorów plików, gniazd (sockets), blokad muteksów, uchwytów do baz danych, transakcji oraz innych zasobów operacyjnych lub systemowych aplikacji.

Odpowiedz na to pytanie z trenerem AI

Zarządzanie pamięcią

2Porównaj `std::unique_ptr` i `std::shared_ptr` oraz opisz, kiedy zastosowanie każdego z nich jest odpowiednie w backendowym API (Application Programming Interface).

`std::unique_ptr` reprezentuje wyłączną własność: jest mało kosztowny, można go przenosić, ale nie kopiować, i jest odpowiedni w przypadku pojedynczego właściciela lub dla API przenoszących prawo własności. `std::shared_ptr` reprezentuje współdzieloną własność: jest kopiowalny i utrzymuje obiekt przy życiu poprzez zliczanie referencji, dopóki nie zwolni go ostatni silny właściciel. W backendowym API używaj `unique_ptr` w przypadku przeniesienia własności, `shared_ptr` tylko wtedy, gdy wielu niezależnych właścicieli musi przedłużyć czas życia, a referencji lub surowych wskaźników do dostępu bez przekazania prawa własności. `std::make_shared` jest zazwyczaj preferowane przy tworzeniu obiektów `shared_ptr`, ponieważ jest wydajne i odporne na wyjątki (exception-safe).

Odpowiedz na to pytanie z trenerem AI

System typów

3Czym jest semantyka przeniesienia (move semantics) i w jaki sposób implementuje się poprawny konstruktor przenoszący oraz przenoszący operator przypisania?

Semantyka przeniesienia (move semantics) pozwala w C++ przetransferować zasoby z obiektów tymczasowych lub w inny sposób zbędnych zamiast ich kopiowania. Wykorzystuje referencje do r-wartości, takie jak `T&&`, oraz `std::move`, co jest po prostu rzutowaniem, które pozwala wybrać przeciążenia dla przenoszenia; `std::move` samo w sobie niczego nie przenosi. Poprawny konstruktor przenoszący inicjuje nowy obiekt, przejmując zasób obiektu źródłowego i pozostawiając obiekt źródłowy w stanie ważnym, pozwalającym na zniszczenie i przypisanie. Poprawny przenoszący operator przypisania transferuje dane do już istniejącego obiektu, obsługuje lub toleruje przypisanie do samego siebie, zwalnia lub ponownie używa bieżącego zasobu obiektu docelowego, przejmuje zasób z obiektu źródłowego i pozostawia obiekt źródłowy w bezpiecznym stanie. Operacje przenoszenia powinny być często oznaczane jako `noexcept`, aby kontenery standardowe mogły ich używać podczas realokacji z zachowaniem gwarancji wyjątków.

Odpowiedz na to pytanie z trenerem AI

4Opisz poprawność użycia const (const-correctness) w interfejsach API (Application Programming Interface) C++ i jak skutecznie projektować stałe funkcje składowe (const member functions).

Poprawność użycia const (const-correctness) polega na zadeklarowaniu poprzez system typów, które operacje nie modyfikują obserwowalnego ani logicznego stanu obiektu. Stała funkcja składowa korzysta z obiektu `this` ograniczonego modyfikatorem `const`, zatem nie może modyfikować niemutowalnych pól w klasie ani wywoływać niestałych (non-const) funkcji składowych na tym samym obiekcie. Prawidłowe podejście do projektowania API narzuca oznaczanie zapytań tylko do odczytu słowem `const`, zwracanie wartości po stałych referencjach/wskaźnikach w stosownych sytuacjach i unikanie eksponowania mutowalnego, wewnętrznego stanu z funkcji ujętych jako `const`. Słowo kluczowe `mutable` powinno być rezerwowane dla detali implementacyjnych niezmieniających stanu logicznego – do cache'owania, wartości leniwie inicjowanych (lazy values), metryk lub muteksów. Zastosowanie modyfikatora `const` stwarza kontrakt określający mutowalność, a nie zautomatyzowaną gwarancję bezpieczeństwa dla wątków; te ostatnie wymagają odrębnej implementacji i odpowiedniej dokumentacji.

Odpowiedz na to pytanie z trenerem AI

5Czym jest `std::byte` i jak powinno się bezpiecznie reprezentować surowe bufory binarne w C++?

`std::byte` jest odrębnym typem służącym do reprezentowania surowych danych binarnych jako bajtów, a nie znaków czy liczb całkowitych. Poprawia on bezpieczeństwo typów (type safety), ponieważ bufory bajtów nie są przypadkowo traktowane jako tekst czy wartości liczbowe, jednocześnie wciąż obsługując operacje bitowe. Surowe bufory binarne powinny zazwyczaj być reprezentowane przez struktury przechowujące bajty, takie jak `std::vector<std::byte>` lub `std::array<std::byte, N>`, i przekazywane do interfejsów API nieprzejmujących własności (non-owning APIs) jako `std::span<std::byte>` lub `std::span<const std::byte>`. Kod do serializacji powinien wprost kodować i dekodować wartości, zamiast polegać na zależnym od implementacji układzie obiektów w pamięci (object layout).

Odpowiedz na to pytanie z trenerem AI

Czas życia obiektów

6Opisz Zasadę Zera, Zasadę Trzech i Zasadę Pięciu, a także określ, kiedy każda z nich ma zastosowanie.

Zasada Zera (Rule of Zero): preferuj klasy, które nie deklarują niestandardowego destruktora ani operacji kopiowania/przenoszenia; pozwól członkom wykorzystującym idiom RAII (Resource Acquisition Is Initialization), takim jak `std::string`, `std::vector`, `std::unique_ptr`, opakowaniom plików/gniazd itp., zarządzać zasobami. Zasada Trzech (Rule of Three): jeśli klasa samodzielnie zarządza zasobem i potrzebuje własnego destruktora, konstruktora kopiującego lub kopiującego operatora przypisania, zazwyczaj potrzebuje wszystkich trzech, aby zdefiniować poprawne zachowanie podczas kopiowania oraz prawo własności. Zasada Pięciu (Rule of Five): w języku C++11 i nowszych, takie typy powinny również brać pod uwagę konstruktor przenoszący oraz przenoszący operator przypisania. Używaj Zasady Zera w przypadku większości typów aplikacyjnych; używaj Zasady Trzech/Pięciu, gdy typ jest bezpośrednim właścicielem zasobu lub posiada nietrywialną semantykę własności/czasu życia.

Odpowiedz na to pytanie z trenerem AI

Obsługa błędów

7Wyjaśnij sposób obsługi wyjątków w C++, rozwijanie stosu (stack unwinding), interakcje z destruktorami oraz pojęcie granic obsługi wyjątków (exception boundaries) w usługach.

Wyjątki w C++ przekazują sterowanie z wyrażenia `throw` do najbliższego pasującego bloku `catch`. Podczas propagacji mechanizm rozwijania stosu (stack unwinding) niszczy w pełni skonstruowane obiekty o czasie życia automatycznym w odwrotnej kolejności, dzięki czemu czyszczenie zasobów za pomocą techniki RAII (Resource Acquisition Is Initialization) przebiega w sposób automatyczny. Destruktory na ogół nie powinny rzucać wyjątków; jeśli wyjątek wydostanie się z destruktora oznaczonego jako `noexcept` lub zrzucony zostanie kolejny wyjątek w trakcie aktywnego rozwijania stosu, program wywoła `std::terminate`. Wyjątki zazwyczaj powinno się przechwytywać przez referencję, najczęściej `const&`, aby uniknąć problemu ucięcia obiektu (object slicing) i tworzenia zbędnych kopii. Usługi backendowe powinny mieć wyznaczone granice obsługi wyjątków (exception boundaries) – takie jak procedury obsługi żądań (request handlers), punkty wejścia dla wątków roboczych, wywołania zwrotne frameworków RPC/HTTP oraz funkcja `main` – gdzie wyjątki są logowane, konwertowane na odpowiedzi z błędami lub kody statusu i bezwzględnie blokowane przed wyciekiem do niewłaściwych kontekstów, takich jak API (Application Programming Interface) w języku C, destruktory, wątki zewnętrzne czy funkcje `noexcept`.

Odpowiedz na to pytanie z trenerem AI

8Co się stanie, jeśli destruktor rzuci wyjątkiem, i w jaki sposób typy backendowe powinny zgłaszać niepowodzenia czyszczenia zasobów?

Destruktory są w normalnych przypadkach niejawnie zadeklarowane jako `noexcept(true)`, więc jeśli wyjątek wydostanie się z takiego destruktora, zostanie wywołana funkcja `std::terminate`. Nawet jeśli destruktor jest jawnie zadeklarowany jako `noexcept(false)`, rzucanie wyjątków podczas zwijania stosu (stack unwinding) jest niebezpieczne, ponieważ drugi wydostający się wyjątek w sytuacji, gdy inny jest już aktywny, również powoduje zakończenie programu. Dlatego destruktory powinny przeprowadzać czyszczenie zasobów w miarę najlepszych możliwości (best-effort) i nie pozwalać wyjątkom na wydostanie się na zewnątrz. Typy backendowe powinny zgłaszać niepowodzenia czyszczenia poprzez jawne operacje, takie jak `close()`, `flush()`, `commit()`, `stop()` lub `shutdown()`, które zwracają błąd lub wartość `expected`, ewentualnie rzucają wyjątkiem przed destrukcją. Destruktor może logować zdarzenia, emitować metryki, tłumić błędy lub wykonywać bezpieczne czyszczenie awaryjne, ale nie powinien być głównym kanałem zgłaszania błędów wymagających reakcji.

Odpowiedz na to pytanie z trenerem AI

Biblioteka standardowa

9Jak `std::vector` zarządza pojemnością, powiększaniem, realokacją oraz stabilnością iteratorów?

`std::vector` przechowuje elementy w sposób ciągły (contiguously) i śledzi zarówno rozmiar (`size`), jak i pojemność (`capacity`). Rozmiar to liczba skonstruowanych elementów; pojemność to ilość zaalokowanej pamięci dostępnej na elementy przed koniecznością kolejnej alokacji. Gdy dodanie elementów przekroczyłoby pojemność, wektor alokuje większy blok pamięci, zazwyczaj używając zdefiniowanej przez implementację strategii wzrostu geometrycznego, przenosi lub kopiuje istniejące elementy, niszczy stare i zwalnia starą pamięć. Metoda `reserve(n)` zwiększa pojemność bez zmiany rozmiaru, podczas gdy `resize(n)` zmienia rozmiar poprzez konstruowanie lub niszczenie elementów. Realokacja unieważnia wszystkie iteratory, referencje i wskaźniki do elementów; nawet bez realokacji, operacje takie jak `insert` i `erase` mogą unieważnić pozycje w miejscu modyfikacji lub za nim.

Odpowiedz na to pytanie z trenerem AI

10Jakie zasady unieważniania (invalidation rules) należy znać w przypadku kontenerów standardowych o ciągłym ułożeniu w pamięci (contiguous) oraz tych opartych na węzłach (node-based)?

Zasady unieważniania zależą od użytego kontenera oraz samej operacji. Kontenery o ciągłym ułożeniu w pamięci, takie jak `vector` i `string`, mają wysoce wrażliwą stabilność iteratorów/referencji: zwiększenie rozmiaru może wymusić realokację, unieważniając tym samym wszystkie iteratory, referencje i wskaźniki, a operacje wstawiania i usuwania (insert/erase) mogą przesunąć elementy i unieważnić pozycje, począwszy od miejsca zmiany, nawet bez realokacji. Uporządkowane kontenery oparte na węzłach (node-based), takie jak `list`, `map`, `set` i ich warianty typu multi, podczas wstawiania zazwyczaj utrzymują stabilność iteratorów i referencji do istniejących, nieusuniętych elementów; z kolei usunięcie elementu unieważnia wyłącznie iterator i referencję do tego usuniętego elementu. Kontenery nieuporządkowane (unordered containers) również przechowują elementy w węzłach, więc referencje i wskaźniki na poszczególne elementy pozostają przeważnie stabilne w trakcie operacji `rehash`, lecz operacja ta unieważnia iteratory. Kontener `deque` posiada natomiast dedykowane reguły dla pamięci przechowywanej w segmentach. W praktyce należy zawsze sprawdzić zasady konkretnego kontenera i operacji przed przechowywaniem iteratorów lub referencji w obliczu zbliżających się modyfikacji.

Odpowiedz na to pytanie z trenerem AI

11Porównaj std::map, std::unordered_map oraz kontenery w stylu flat-map w kontekście tablic wyszukiwania (lookup tables) na backendzie.

std::map to uporządkowany kontener asocjacyjny, zazwyczaj oparty na drzewie, z logarytmicznym czasem wyszukiwania, wstawiania i usuwania; jest przydatny, gdy ważna jest iteracja po posortowanych elementach, zapytania o przedziały lub gwarancje dotyczące kolejności. std::unordered_map opiera się na tablicy mieszającej (hash table), oferując średnio stały czas operacji dla konkretnych kluczy, bez zachowania ich kolejności; często jest to dobry wybór domyślny dla dużych, modyfikowalnych tablic wyszukiwania, jeśli funkcja mieszająca jest odpowiednia. Kontener w stylu flat-map przechowuje posortowane pary klucz-wartość w sposób ciągły w pamięci, zapewniając dobrą lokalność pamięci podręcznej oraz szybką iterację i wyszukiwanie binarne, jednak czas wstawiania i usuwania elementów wewnątrz bywa liniowy. W przypadku tablic wyszukiwania na backendzie, wyboru należy dokonać na podstawie tego, czy obciążenie wymaga zachowania kolejności lub przedziałów, opiera się głównie na dokładnym wyszukiwaniu, wymaga częstych modyfikacji, przewidywalnych opóźnień (latency), w zależności od narzutu na pamięć oraz zachowania względem pamięci podręcznej (cache).

Odpowiedz na to pytanie z trenerem AI

12Wyjaśnij `std::optional` i typowe przypadki użycia w backendzie do reprezentowania braku wartości.

`std::optional<T>` reprezentuje albo zawartą wartość typu `T`, albo jej brak. Pusty stan jest reprezentowany przez `std::nullopt`; kod może sprawdzić jego obecność za pomocą `has_value()` lub użyć opcji w kontekście logicznym (boolean), uzyskać dostęp do wartości za pomocą `*` lub `value()` i dostarczyć wartość domyślną za pomocą `value_or()`. W kodzie backendowym jest to przydatne dla pól bazy danych mogących przyjmować wartość null, opcjonalnych pól w żądaniach i konfiguracji, braków trafień w pamięci podręcznej (cache miss) lub repozytorium, gdzie taki brak jest oczekiwany, oraz stanów domenowych, w których wartość strażnicza (sentinel) taka jak -1 lub pusty ciąg znaków byłaby niejednoznaczna. Modeluje to brak wartości, a nie polimorfizm czy rozbudowane informacje o błędach.

Odpowiedz na to pytanie z trenerem AI

13Czym są std::string_view i std::span oraz jakie zagrożenia dla czasu życia obiektów wprowadzają widoki nieprzejmujące własności (non-owning views)?

std::string_view to nieprzejmujący własności (non-owning) widok na ciągłą sekwencję znaków; std::span<T> to nieprzejmujący własności widok na ciągłą sekwencję elementów typu T. Są one użyteczne w przypadku parametrów i interfejsów API buforów bez kopiowania (zero-copy), ponieważ przechowują wskaźnik i długość bez alokacji czy przejmowania własności pamięci. Głównym zagrożeniem jest czas życia: wskazywany obszar pamięci musi żyć dłużej niż sam widok i nie może zostać unieważniony podczas jego używania. Zwrócenie lub zapisanie widoku odnoszącego się do obiektu tymczasowego, lokalnego, zniszczonego lub przealokowanego kontenera może doprowadzić do powstania wiszącego widoku (dangling view).

Odpowiedz na to pytanie z trenerem AI

Współbieżność

14Czym różnią się od siebie `std::mutex`, `std::shared_mutex` i `std::recursive_mutex` oraz w jakich przypadkach użyłbyś każdego z nich?

`std::mutex` zapewnia wyłączne blokowanie (exclusive locking): tylko jeden wątek może go posiadać, więc jest to domyślny wybór do ochrony współdzielonego, modyfikowalnego stanu. `std::shared_mutex` obsługuje współdzielone blokady dla czytelników i wyłączne blokady dla pisarzy: wielu czytelników może mieć nałożoną blokadę jednocześnie, ale pisarze wymagają wyłącznego dostępu. Wybieraj go dla danych głównie do odczytu, gdy współbieżność czytelników jest warta narzutu wydajnościowego i gdy sprawiedliwość dostępu / zagłodzenie pisarzy (writer fairness/starvation) są akceptowalne lub obsłużone. `std::recursive_mutex` to wyłączny muteks, który ten sam wątek może zablokować wiele razy i musi odblokować taką samą liczbę razy; używaj go rzadko, głównie dla starszego (legacy) lub wielobieżnego (re-entrant) kodu, ponieważ może ukrywać zły projekt użycia blokad.

Odpowiedz na to pytanie z trenerem AI

Funkcje języka

15Wyjaśnij tryby przechwytywania zmiennych w wyrażeniach lambda i to, w jaki sposób oddziałują one z czasem życia obiektów w wywołaniach zwrotnych (callbacks).

Lambda może przechwytywać zmienne przez wartość (`[x]` lub `[=]`), przez referencję (`[&x]` lub `[&]`), przechwytywać wskaźnik `this`, lub używać przechwytywania z inicjalizacją (init-capture), na przykład `[p = std::move(ptr)]`. Przechwytywanie przez wartość fizycznie kopiuje wartości przechwyconego obiektu do wnętrza domknięcia (closure) w momencie tworzenia lambdy. Natomiast referencje po prostu wskazują na istniejące obiekty, więc obiekty źródłowe muszą przetrwać dłużej (mieć dłuższy czas życia) niż wszystkie późniejsze inwokacje tej lambdy. W przypadku wywołań zwrotnych (callbacks), zapisanych lambd lub pracy asynchronicznej, przechwytywania przez referencję oraz `this` potrafią być bardzo niebezpieczne – zmienne lokalne lub sam obiekt mogą zostać zniszczone jeszcze przed faktycznym wywołaniem. Dlatego w takich przypadkach preferowane jest przechwytywanie niezbędnych danych przez wartość, przeniesienie ich i nadanie własności (move/init-capture) albo celowe wdrażanie wzorców z użyciem `shared_ptr` / `weak_ptr`, by jednoznacznie określić czas życia niezbędnego obiektu.

Odpowiedz na to pytanie z trenerem AI