Priprema za Junior C++

Junior C++ Backend interview pitanja

15 izabranih Junior C++ interview pitanja za backend developere koji treba jasno da objasne osnove jezika i osnovni ownership.

Započni Junior C++ AI intervjuKreditna kartica nije potrebna. Dostupna je 1 besplatna sesija.
Vežba tehničkog intervjua na engleskomRežim u kome kandidati kojima engleski nije maternji mogu da vežbaju tehničke intervjue.

Resource Management

1Objasnite RAII (Resource Acquisition Is Initialization) i kako on utiče na upravljanje resursima bezbedno u odnosu na izuzetke u C++ backend servisima.

RAII (Resource Acquisition Is Initialization) označava koncept u kome C++ objekat poseduje resurs i oslobađa ga u svom destruktoru. Pošto se lokalni objekti automatski uništavaju kada se njihov životni vek ili opseg vidljivosti završi, uključujući i tokom odmotavanja steka pri izuzecima (stack unwinding), RAII omogućava determinističko čišćenje resursa i čini grane za obradu grešaka bezbednim u odnosu na izuzetke. U backend servisima se ovo ne odnosi samo na memoriju već i na deskriptore fajlova, sokete, zaključavanja muteksa (mutex locks), ručke baza podataka (database handles), transakcije i druge resurse operativnog sistema ili aplikacije.

class Fd {
public:
    explicit Fd(int fd = -1) noexcept : fd_(fd) {}
    ~Fd() { if (fd_ != -1) ::close(fd_); }

    Fd(const Fd&) = delete;
    Fd& operator=(const Fd&) = delete;

    Fd(Fd&& other) noexcept : fd_(std::exchange(other.fd_, -1)) {}
    Fd& operator=(Fd&& other) noexcept {
        if (this != &other) {
            if (fd_ != -1) ::close(fd_);
            fd_ = std::exchange(other.fd_, -1);
        }
        return *this;
    }

    int get() const noexcept { return fd_; }

private:
    int fd_;
};

void handle_request() {
    Fd sock(::accept(...));
    if (sock.get() == -1) throw std::runtime_error("accept failed");
    // If any later operation throws, sock is still closed by ~Fd().
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

Upravljanje memorijom

2Uporedite std::unique_ptr i std::shared_ptr i opišite kada je svaki od njih prikladan u backend API (Application Programming Interface) rešenjima.

`std::unique_ptr` predstavlja ekskluzivno vlasništvo: ima minimalne troškove, može se premeštati ali ne i kopirati, i pogodan je za jednog vlasnika ili za API funkcije koje prenose vlasništvo. `std::shared_ptr` predstavlja deljeno vlasništvo: može se kopirati i održava objekat u životu pomoću brojanja referenci sve dok ga poslednji jaki vlasnik ne oslobodi. U backend API-jima, koristite `unique_ptr` kada se vlasništvo prenosi, `shared_ptr` samo kada više nezavisnih vlasnika mora produžiti životni vek objekta, a reference ili sirove pokazivače za pristup bez vlasništva. `std::make_shared` se obično preporučuje pri kreiranju `shared_ptr` objekata jer je efikasan i bezbedan u odnosu na izuzetke.

struct Session {};

std::unique_ptr<Session> create_session();      // caller receives ownership
void process(Session& s);                       // borrows, does not own
void maybe_process(const Session* s);           // nullable borrow

class Dispatcher {
public:
    void add(std::shared_ptr<Session> s) {      // Dispatcher shares lifetime
        sessions_.push_back(std::move(s));
    }
private:
    std::vector<std::shared_ptr<Session>> sessions_;
};
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

Sistem tipova

3Šta je semantika premeštanja (move semantics) i kako se implementiraju ispravan konstruktor premeštanja i operator dodele premeštanjem?

Semantika premeštanja omogućava jeziku C++ da preuzme resurse iz privremenih ili na drugi način potrošnih objekata umesto da ih kopira. Ona koristi r-vrednosne reference kao što su T&& i std::move, što je kastovanje koje omogućava izbor preopterećenja za premeštanje; sam std::move ne premešta ništa. Ispravan konstruktor premeštanja inicijalizuje novi objekat preuzimanjem resursa izvornog objekta i ostavlja izvorni objekat u validnom stanju koje se može uništiti i kome se može dodeliti nova vrednost. Ispravan operator dodele premeštanjem vrši prenos u postojeći objekat, obrađuje ili toleriše samododelu, oslobađa ili ponovo koristi trenutni resurs odredišta, preuzima izvorni resurs i ostavlja izvor bezbednim. Operacije premeštanja bi često trebalo da budu noexcept kako bi standardni kontejneri mogli da ih koriste tokom realokacije uz očuvanje garancija pri izuzecima.

class Buffer {
public:
    Buffer() = default;
    explicit Buffer(std::size_t n) : size_(n), data_(new char[n]) {}
    ~Buffer() { delete[] data_; }

    Buffer(const Buffer&) = delete;
    Buffer& operator=(const Buffer&) = delete;

    Buffer(Buffer&& other) noexcept
        : size_(std::exchange(other.size_, 0)),
          data_(std::exchange(other.data_, nullptr)) {}

    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            size_ = std::exchange(other.size_, 0);
            data_ = std::exchange(other.data_, nullptr);
        }
        return *this;
    }

private:
    std::size_t size_ = 0;
    char* data_ = nullptr;
};
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

4Opišite const-korektnost (const-correctness) u C++ API-jima (Application Programming Interface) i kako efikasno dizajnirati const članske funkcije.

Const-korektnost podrazumeva izražavanje kroz sistem tipova koje operacije ne menjaju uočljivo ili logičko stanje objekta. `const` članska funkcija ima `const`-kvalifikovan `this` objekat, tako da ne može menjati članove podataka koji nisu označeni kao `mutable`, niti pozivati ne-`const` članske funkcije nad istim objektom. Dobar dizajn API-ja označava funkcije za čitanje kao `const`, vraća vrednosti ili `const` reference/pokazivače kada je to prikladno i izbegava izlaganje promenljivog unutrašnjeg stanja iz `const` funkcija. Ključna reč `mutable` treba da bude rezervisana za detalje implementacije koji ne menjaju logičko stanje, kao što su keševi, lenjo izračunate vrednosti, metrike ili muteksi. `const` je ugovorna obaveza na nivou API-ja o promenama stanja, a ne automatska garancija bezbednosti niti; garancije konkurentnosti zahtevaju zasebnu implementaciju i dokumentaciju.

#include <mutex>
#include <optional>
#include <string>

class UserProfile {
public:
    const std::string& id() const { return id_; }

    std::string displayName() const {
        std::lock_guard<std::mutex> lock(mu_);
        if (!cached_display_name_) {
            cached_display_name_ = computeDisplayName();
        }
        return *cached_display_name_;
    }

private:
    std::string computeDisplayName() const { return id_; }

    std::string id_;
    mutable std::mutex mu_;
    mutable std::optional<std::string> cached_display_name_;
};
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

5Šta je std::byte i kako treba bezbedno predstavljati sirove binarne bafere u C++ jeziku?

std::byte je zaseban tip za predstavljanje sirovih binarnih podataka kao bajtova, a ne kao znakova ili aritmetičkih celih brojeva. On poboljšava tipsku bezbednost jer se bajt baferi ne tretiraju slučajno kao tekst ili numeričke vrednosti, dok i dalje podržava bitske operacije. Sirove binarne bafere obično treba predstavljati skladištem orijentisanim na bajtove, kao što su std::vector<std::byte> ili std::array<std::byte, N>, i prosleđivati kroz API (Application Programming Interface) strukture koje ne preuzimaju vlasništvo kao std::span<std::byte> ili std::span<const std::byte>. Kôd za serijalizaciju treba eksplicitno da kodira i dekodira vrednosti umesto da se oslanja na proizvoljan raspored memorije objekata.

void write_frame(std::span<const std::byte> payload);

std::vector<std::byte> buf;
buf.push_back(std::byte{0x01});
buf.push_back(std::byte{0xFF});
write_frame(buf);
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

Životni vek objekta

6Opišite Pravilo nule (Rule of Zero), Pravilo trojke (Rule of Three) i Pravilo petice (Rule of Five) i kada se koje primenjuje.

Pravilo nule: dajte prednost klasama koje ne deklarišu prilagođeni destruktor, operacije kopiranja ili premeštanja; prepustite članovima koji koriste RAII (Resource Acquisition Is Initialization), kao što su `std::string`, `std::vector`, `std::unique_ptr`, omotači za datoteke/sokete itd., da upravljaju resursima. Pravilo trojke: ako klasa ručno upravlja resursom i zahteva prilagođeni destruktor, konstruktor kopiranja ili operator dodele kopiranjem, obično su joj potrebna sva tri kako bi se definisalo ispravno ponašanje kopiranja i prenosa vlasništva. Pravilo petice: u standardu C++11 i novijim, takvi tipovi bi takođe trebalo da razmotre konstruktor premeštanja i operator dodele premeštanjem. Koristite Pravilo nule za većinu aplikativnih tipova; koristite Pravilo trojke/petice kada tip direktno poseduje resurs ili ima netrivijalnu semantiku vlasništva/životnog veka.

struct Session {
    std::string user_id;
    std::vector<std::string> roles;
    std::unique_ptr<Connection> conn;

    // No custom destructor/copy/move operations.
};
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

Obrada grešaka

7Objasnite obradu izuzetaka u jeziku C++, odmotavanje steka, interakciju sa destruktorima i granice izuzetaka u servisima.

C++ izuzeci prenose kontrolu sa `throw` izraza na najbliži odgovarajući `catch` blok. Tokom propagacije, odmotavanje steka uništava potpuno konstruisane automatske objekte obrnutim redosledom, pa se RAII (Resource Acquisition Is Initialization) čišćenje resursa izvršava automatski. Destruktori generalno ne bi trebalo da bacaju izuzetke; ako izuzetak napusti `noexcept` destruktor ili se pojavi novi izuzetak tokom aktivnog odmotavanja steka, program poziva `std::terminate`. Izuzetke obično treba hvatati po referenci, najčešće `const&`, kako bi se izbeglo odsecanje objekta i nepotrebno kopiranje. Backend servisi bi trebalo da definišu granice izuzetaka, kao što su funkcije za obradu zahteva, ulazne tačke radnih niti, povratni pozivi RPC (Remote Procedure Call) ili HTTP (Hypertext Transfer Protocol) radnih okvira i funkcija `main`, gde se izuzeci beleže u logove, prevode u odgovore sa greškom ili statusne kodove i gde se sprečava njihovo bekstvo u neodgovarajuće kontekste kao što su C API (Application Programming Interface) funkcije, destruktori, niti ili `noexcept` funkcije.

struct Conn {
    ~Conn() noexcept { /* close or return connection to pool */ }
};

void handle() {
    Conn c;
    throw std::runtime_error("db failed");
} // c is destroyed while the exception propagates
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

8Šta se dešava ako destruktor baci izuzetak i kako serverski tipovi treba da prijave greške pri čišćenju resursa?

Destruktori su u standardnim slučajevima implicitno `noexcept(true)`, pa ako izuzetak napusti takav destruktor, poziva se `std::terminate`. Čak i ako je destruktor eksplicitno deklarisan kao `noexcept(false)`, bacanje izuzetka tokom odmotavanja steka (engl. stack unwinding) je opasno jer drugi izuzetak koji pobegne dok je jedan izuzetak već aktivan takođe prekida izvršavanje programa. Zbog toga destruktori treba da izvrše čišćenje po principu najboljeg mogućeg pokušaja i ne smeju dozvoliti da izuzeci napuste destruktor. Serverski tipovi treba da prijavljuju greške pri čišćenju resursa kroz eksplicitne operacije kao što su `close()`, `flush()`, `commit()`, `stop()` ili `shutdown()`, koje vraćaju grešku/`expected` ili bacaju izuzetak pre samog uništenja objekta. Destruktor može da evidentira u logovima, šalje metrike, potisne greške ili izvrši bezbedno rezervno čišćenje, ali ne bi trebalo da bude primarni kanal za prijavljivanje grešaka na koje se može reagovati.

class FileWriter {
public:
    std::expected<void, Error> close() noexcept; // reports flush/close failure explicitly

    ~FileWriter() noexcept {
        if (!closed_) {
            auto r = close();
            if (!r) log_error(r.error()); // destructor does not throw
        }
    }
private:
    bool closed_ = false;
};
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

Standardna biblioteka

9Kako std::vector upravlja kapacitetom, rastom, realokacijom i stabilnošću iteratora?

std::vector skladišti elemente uzastopno u memoriji i prati i veličinu i kapacitet. size predstavlja broj konstruisanih elemenata, dok je capacity količina alocirane memorije za skladištenje elemenata pre nego što bude potrebna nova alokacija. Kada bi dodavanje elemenata premašilo kapacitet, vector alocira veći memorijski blok, obično koristeći strategiju geometrijskog rasta definisanu implementacijom, premešta ili kopira postojeće elemente, uništava stare i oslobađa prethodnu memoriju. reserve(n) povećava kapacitet bez promene veličine, dok resize(n) menja veličinu konstruisanjem ili uništavanjem elemenata. Realokacija poništava sve iteratore, reference i pokazivače na elemente; čak i bez realokacije, operacije kao što su insert i erase mogu poništiti pozicije na mestu modifikacije ili nakon njega.

std::vector<int> v;
v.reserve(100);          // capacity >= 100, size == 0
v.push_back(1);          // size == 1
v.resize(10);            // size == 10, adds nine zero-initialized ints

std::cout << v.size() << " " << v.capacity() << "\n";
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

10Koja pravila o nevalidnosti iteratora i referenci treba poznavati za kontinualne kontejnere i kontejnere zasnovane na čvorovima u standardnoj biblioteci?

Pravila o invalidaciji zavise od kontejnera i operacije. Kontinualni kontejneri kao što su `vector` i `string` imaju krhku stabilnost iteratora i referenci: povećanje kapaciteta može dovesti do realokacije i učiniti nevažećim sve iteratore, reference i pokazivače, dok operacije `insert` i `erase` mogu pomeriti elemente i poništiti pozicije na mestu promene ili nakon njega, čak i bez realokacije. Uređeni kontejneri zasnovani na čvorovima kao što su `list`, `map`, `set` i njihove `multi` varijante uglavnom zadržavaju stabilnost iteratora i referenci na postojeće, neobrisane elemente prilikom umetanja (`insert`); brisanje elementa (`erase`) čini nevažećim iterator i referencu samo na taj obrisani element. Neuređeni kontejneri takođe skladište elemente u čvorovima, pa su reference i pokazivači na elemente uglavnom stabilni tokom ponovnog heširanja (`rehash`), ali `rehash` poništava iteratore. `deque` ima posebna pravila zbog segmentiranog skladištenja. U praksi, proverite specifičan kontejner i operaciju pre nego što čuvate iteratore ili reference tokom modifikacija.

std::map<int, std::string> m = {{1,"a"}, {2,"b"}, {3,"c"}};
for (auto it = m.begin(); it != m.end(); ) {
    if (it->first % 2 == 1)
        it = m.erase(it); // returns next iterator
    else
        ++it;
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

11Uporedite `std::map`, `std::unordered_map` i kontejnere u stilu flat-map za tabele pretrage na backendu.

`std::map` je uređeni asocijativni kontejner, obično zasnovan na stablu, sa logaritamskom složenošću pretrage, umetanja i brisanja; koristan je kada su važni sortirana iteracija, upiti po opsegu ili garancije redosleda. `std::unordered_map` je zasnovan na heš tabeli sa prosečnim konstantnim vremenom za operacije sa tačnim ključem i bez redosleda ključeva; često je dobar podrazumevani izbor za velike promenljive tabele pretrage kada je heš funkcija dobra. Kontejner u stilu flat-map čuva sortirane parove ključ/vrednost u kontinualnoj memoriji, pružajući dobru lokalnost keša i brzu iteraciju/binarnu pretragu, ali su umetanje i brisanje u sredini linearni. Za tabele pretrage na backendu, izbor zavisi od toga da li opterećenje zahteva uređenost/opsege, uglavnom tačne pretrage, česte izmene, predvidivo kašnjenje, memorijsko zauzeće i ponašanje keša.

// Exact lookup, frequently updated:
std::unordered_map<std::string, User> users_by_id;

// Need sorted iteration or lower_bound/range queries:
std::map<std::string, User> users_by_id_ordered;

// Build once, query many times: vector sorted by key is a common flat-map style.
std::vector<std::pair<std::string, User>> users;
std::sort(users.begin(), users.end(), [](auto const& a, auto const& b) {
    return a.first < b.first;
});

auto it = std::lower_bound(users.begin(), users.end(), std::string_view{"u123"},
    [](auto const& p, std::string_view key) { return p.first < key; });
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

12Objasnite `std::optional` i tipične slučajeve upotrebe na serverskoj strani za predstavljanje nepostojećih vrednosti.

`std::optional<T>` predstavlja ili sadržanu vrednost tipa `T` ili odsustvo vrednosti. Prazno stanje je predstavljeno pomoću `std::nullopt`; u kodu se može proveriti `has_value()` ili koristiti `std::optional` u logičkom (boolean) kontekstu, pristupiti vrednosti preko operatora `*` ili metode `value()`, kao i proslediti podrazumevana vrednost pomoću `value_or()`. U serverskom kodu je koristan za kolone baze podataka koje mogu biti null (nullable), opciona polja u zahtevima ili konfiguraciji, promašaje u kešu ili repozitorijumu gde je odsustvo očekivano, kao i za domenska stanja gde bi kontrolna vrednost (sentinel) poput `-1` ili praznog stringa bila dvosmislena. On modeluje odsustvo vrednosti, a ne polimorfizam ili detaljne informacije o greškama.

struct UserProfile {
    std::string id;
    std::optional<std::string> display_name; // absent if user has not set it
};

std::string label(UserProfile const& u) {
    return u.display_name.value_or("anonymous");
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

13Šta su std::string_view i std::span, i kakve opasnosti po životni vek uvode pogledi bez vlasništva?

`std::string_view` je pogled bez vlasništva nad neprekidnim nizom karaktera; `std::span<T>` je pogled bez vlasništva nad neprekidnim nizom elemenata tipa T. Korisni su za parametre bez kopiranja (zero-copy) i API-je za bafere jer sadrže pokazivač i dužinu bez alokacije ili preuzimanja vlasništva nad memorijom. Glavna opasnost se odnosi na životni vek: memorija na koju pokazuju mora nadživeti sam pogled i ne sme postati nevažeća dok se pogled koristi. Vraćanje ili čuvanje pogleda koji pokazuje na privremeni objekat, lokalni objekat, uništeni objekat ili realocirani kontejner može ostaviti viseći pogled (dangling view).

std::string_view bad() {
    std::string s = "hello";
    return std::string_view{s}; // dangling after return
}

void ok(std::string_view name) {
    // safe only during this call if caller's data outlives the call
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

Konkurentnost

14Kako se razlikuju std::mutex, std::shared_mutex i std::recursive_mutex i kada biste izabrali koji od njih?

std::mutex pruža ekskluzivno zaključavanje: samo jedna nit može da ga drži, pa predstavlja podrazumevani izbor za zaštitu deljenog promenljivog stanja. std::shared_mutex podržava deljena zaključavanja za čitanje i ekskluzivna zaključavanja za pisanje: više čitalaca može istovremeno držati katanac, ali je piscima potreban ekskluzivan pristup. Birajte ga za podatke koji se uglavnom čitaju kada konkurentnost čitalaca opravdava dodatne režijske troškove i kada je pravednost raspodele za pisce ili izgladnjivanje prihvatljivo ili rešeno. std::recursive_mutex je ekskluzivni muteks koji ista nit može zaključati više puta i mora ga otključati isti broj puta; koristite ga retko, uglavnom za nasleđeni (legacy) ili reentrantni kod, jer može prikriti loš dizajn zaključavanja.

std::mutex m;
std::shared_mutex sm;
std::recursive_mutex rm;

// Default exclusive protection
{
    std::lock_guard<std::mutex> lk(m);
    // modify shared state
}

// Read-mostly protection
{
    std::shared_lock<std::shared_mutex> read_lk(sm); // many readers allowed
    // read shared state
}
{
    std::unique_lock<std::shared_mutex> write_lk(sm); // exclusive writer
    // modify shared state
}

// Recursive locking by same thread, usually avoid if possible
void legacy_api() {
    std::lock_guard<std::recursive_mutex> lk(rm);
    // may call another function that also locks rm
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom

Karakteristike jezika

15Objasnite režime hvatanja u lambda izrazima i kako hvatanje promenljivih utiče na životni vek objekata u povratnim pozivima (callbacks).

Lambda izraz može hvatati promenljive po vrednosti (`[x]` ili `[=]`), po referenci (`[&x]` ili `[&]`), može hvatati `this` ili koristiti inicijalizovano hvatanje kao što je `[p = std::move(ptr)]`. Hvatanje po vrednosti kopira uhvaćeni objekat u zatvorenje (closure) prilikom kreiranja lambde; hvatanje po referenci čuva reference, pa originalni objekti moraju nadživeti sva izvršavanja lambde. U povratnim pozivima, sačuvanim lambdama ili asinhronom radu, hvatanje po referenci i hvatanje pokazivača `this` su opasni jer lokalne promenljive ili objekat mogu biti uništeni pre poziva. Preporučuje se hvatanje po vrednosti za potrebne podatke, premeštanje/inicijalizovano hvatanje za prenos vlasništva ili pažljivo korišćenje `shared_ptr`/`weak_ptr` obrazaca kada je potrebno produžiti ili proveriti životni vek objekta.

std::function<void()> make_bad_callback() {
    std::string name = "job";
    return [&] {
        // BUG: name is destroyed when make_bad_callback returns.
        std::cout << name << '\n';
    };
}

std::function<void()> make_good_callback() {
    std::string name = "job";
    return [name] {
        std::cout << name << '\n';
    };
}
Pokušajte da odgovorite na ovo pitanje sa AI trenerom