Junior C++ -valmistautuminen

Junior C++ Backend -haastattelukysymykset

15 valikoitua Junior-tason C++-haastattelukysymystä backend-kehittäjille, joiden on osattava selittää kielen perusteet ja omistajuuden peruskäsitteet selkeästi.

Aloita tekoälyavusteinen Junior C++ -haastatteluEi vaadi luottokorttia. 1 ilmainen sessio saatavilla.
Teknisten haastattelujen harjoittelu englanniksiTila, jossa ei-äidinkieliset puhujat voivat harjoitella teknisen haastattelun läpäisemistä.

Resource Management

1Selitä RAII (Resource Acquisition Is Initialization) ja miten se tukee poikkeusturvallista resurssienhallintaa C++-taustapalveluissa.

RAII (Resource Acquisition Is Initialization) tarkoittaa, että C++-olio omistaa resurssin ja vapauttaa sen destruktorissaan. Koska lokaalit oliot tuhotaan automaattisesti, kun niiden elinkaari tai näkyvyysalue päättyy – myös silloin, kun poikkeuksia käsitellään ja pinoa puretaan (stack unwinding) – RAII tarjoaa deterministisen siivouksen ja tekee virhepoluista poikkeusturvallisia. Taustapalveluissa tämä ei koske vain muistia, vaan myös tiedostokahvoja (file descriptors), verkkosocketeita, mutex-lukkoja, tietokantakahvoja, transaktioita ja muita käyttöjärjestelmän tai sovelluksen resursseja.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Muistinhallinta

2Vertaile osoittimia `std::unique_ptr` ja `std::shared_ptr` ja kuvaile, milloin kumpaakin on asianmukaista käyttää taustajärjestelmien API-rajapinnoissa (Application Programming Interface).

`std::unique_ptr` edustaa yksinomaista omistajuutta: se on kevyt, siirrettävä mutta ei kopioitava, ja se sopii yksittäiselle omistajalle tai API-rajapinnoille, jotka siirtävät omistajuuden. `std::shared_ptr` edustaa jaettua omistajuutta: se on kopioitava ja pitää olion elossa viitelaskennan avulla, kunnes viimeinen vahva omistaja vapauttaa sen. Taustajärjestelmien rajapinnoissa kannattaa käyttää `unique_ptr`-osoitinta, kun omistajuus siirretään, `shared_ptr`-osoitinta vain silloin, kun useiden itsenäisten omistajien on pidennettävä elinkaarta, ja viittauksia tai paljaita osoittimia (raw pointer) sellaiseen käyttöön, johon ei liity omistajuutta. Funktiota `std::make_shared` suositaan yleensä `shared_ptr`-olioita luotaessa, koska se on tehokas ja poikkeusturvallinen.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Tyyppijärjestelmä

3Mitä tarkoittaa siirtosemantiikka (move semantics), ja kuinka toteutat oikeaoppisen siirtokonstruktorin ja siirtosijoitusoperaattorin?

Siirtosemantiikka sallii C++:n siirtää resursseja väliaikaisista tai muutoin tarpeettomista olioista niiden kopioimisen sijaan. Se hyödyntää rvalue-viittauksia, kuten `T&&`, sekä `std::move`-funktiota, joka on tyyppimuunnos, joka mahdollistaa siirtoylikuormitusten valinnan; `std::move` itsessään ei siirrä mitään. Oikeaoppinen siirtokonstruktori alustaa uuden olion ottamalla lähtöolion resurssin ja jättämällä lähtöolion kelvolliseen, purettavissa (destructible) ja sijoitettavissa olevaan tilaan. Oikeaoppinen siirtosijoitusoperaattori siirtää resurssin olemassa olevaan olioon, käsittelee tai sietää itsesijoituksen, vapauttaa tai käyttää uudelleen kohteen nykyisen resurssin, ottaa haltuunsa lähteen resurssin ja jättää lähteen turvalliseen tilaan. Siirto-operaatioiden tulisi usein olla `noexcept`-määriteltyjä, jotta standardikirjaston säiliöt voivat käyttää niitä uudelleenvarauksen aikana säilyttäen poikkeustakuut.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

4Kuvaile const-korrektiutta (const-correctness) C++:n rajapinnoissa ja miten suunnitella const-jäsenfunktioita tehokkaasti.

Const-korrektius tarkoittaa sen ilmaisemista tyyppijärjestelmän kautta, mitkä operaatiot eivät muokkaa olion havaittavaa tai loogista tilaa. `const`-jäsenfunktiolla on `const`-määreellä varustettu `this`-olio, joten se ei voi muokata jäsenmuuttujia, joilla ei ole `mutable`-määrettä, eikä kutsua saman olion ei-`const`-jäsenfunktioita. Hyvässä rajapintasuunnittelussa vain luku -kyselyt merkitään `const`-määreellä, palautetaan arvoja tai `const`-viittauksia/-osoittimia soveltuvin osin, ja vältetään sisäisen, muuttuvan tilan paljastamista `const`-funktioista. `mutable`-avainsana tulisi varata toteutusyksityiskohdille, jotka eivät muuta loogista tilaa, kuten välimuisteille, laiskasti laskettaville arvoille (lazy values), metriikalle tai mutexeille. `const` on rajapintasopimus muuttumattomuudesta, ei automaattinen tae säieturvallisuudesta; samanaikaisuustakuut (concurrency guarantees) vaativat erillisen toteutuksen ja dokumentaation.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

5Mikä on `std::byte` ja miten raaka binääridata tulisi esittää turvallisesti C++:ssa?

`std::byte` on erillinen tyyppi, joka on tarkoitettu raa'an binääridatan esittämiseen tavuina, ei merkkeinä tai aritmeettisina kokonaislukuina. Se parantaa tyyppiturvallisuutta, koska tavupuskureita ei käsitellä vahingossa tekstinä tai numeerisina arvoina, mutta se tukee silti bittitason operaatioita. Raa'at binääripuskurit tulisi yleensä esittää tavupohjaisilla säiliöillä, kuten `std::vector<std::byte>` tai `std::array<std::byte, N>`, ja välittää sellaisenaan API-rajapintojen läpi, jotka eivät ota omistajuutta, esimerkiksi tyyppeinä `std::span<std::byte>` tai `std::span<const std::byte>`. Serialisointikoodin tulisi koodata ja purkaa arvot eksplisiittisesti sen sijaan, että luotettaisiin olioiden mielivaltaisiin muistiasetteluihin (object layouts).

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Olioiden elinkaari

6Kuvaile nollan sääntö (Rule of Zero), kolmen sääntö (Rule of Three) ja viiden sääntö (Rule of Five) sekä milloin kutakin sovelletaan.

Nollan sääntö: suosi luokkia, jotka eivät määrittele omia destruktoreita, kopio- tai siirto-operaatioita; anna RAII (Resource Acquisition Is Initialization) -jäsenten, kuten `std::string`, `std::vector`, `std::unique_ptr` sekä tiedostojen tai verkkosocketien kääreiden, hallita resursseja. Kolmen sääntö: jos luokka hallitsee resurssia manuaalisesti ja tarvitsee kustomoidun destruktorin, kopiokonstruktorin tai kopiosijoitusoperaattorin, se yleensä tarvitsee kaikki kolme määrittääkseen oikeanlaisen kopiointi- ja omistajuuskäyttäytymisen. Viiden sääntö: C++11:ssä ja uudemmissa tällaisissa tyypeissä tulisi harkita myös siirtokonstruktoria ja siirtosijoitusoperaattoria. Käytä nollan sääntöä useimmille sovellustason tyypeille; käytä kolmen tai viiden sääntöä silloin, kun tyyppi omistaa resurssin suoraan tai kun sillä on epätriviaalia omistajuus- tai elinkaarisemantiikkaa.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Virheiden käsittely

7Selitä C++:n poikkeusten käsittely, pinon purku (stack unwinding), vuorovaikutus destruktorien kanssa sekä palveluiden poikkeusrajat (exception boundaries).

C++-poikkeukset siirtävät ohjauksen `throw`-lausekkeesta lähimpään vastaavaan `catch`-lohkoon. Poikkeuksen etenemisen aikana tapahtuva pinon purku tuhoaa täysin alustetut automaattiset oliot käänteisessä järjestyksessä, jolloin RAII (Resource Acquisition Is Initialization) -mallin mukainen resurssien vapautus tapahtuu automaattisesti. Destruktorit eivät yleensä saisi heittää poikkeuksia; jos poikkeus karkaa `noexcept`-destruktorista tai toinen poikkeus karkaa aktiivisen pinon purun aikana, ohjelma kutsuu `std::terminate`-funktiota. Poikkeukset tulisi yleensä ottaa kiinni viitteenä, tyypillisesti muodossa `const&`, jotta vältetään olioiden typistyminen (slicing) ja tarpeettomat kopiot. Taustapalveluiden tulisi määritellä poikkeusrajat – esimerkiksi pyyntöjen käsittelijöihin, työsäikeiden aloituskohtiin, RPC (Remote Procedure Call)- ja HTTP (Hypertext Transfer Protocol) -viitekehysten takaisinkutsuihin ja `main`-funktioon. Näissä rajoissa poikkeukset lokitetaan, muunnetaan virhevastauksiksi tai tilakoodeiksi ja estetään karkaamasta epäsopiviin konteksteihin, kuten C-rajapintoihin, destruktoreihin, säikeisiin tai `noexcept`-funktioihin.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

8Mitä tapahtuu, jos destruktori heittää poikkeuksen, ja miten taustajärjestelmän tyyppien tulisi raportoida siivouksen epäonnistumisista?

Destruktorit ovat normaaleissa tapauksissa implisiittisesti `noexcept(true)`, joten jos poikkeus karkaa tällaisesta destruktorista, kutsutaan `std::terminate`-funktiota. Vaikka destruktori olisi eksplisiittisesti määritelty `noexcept(false)`, poikkeuksen heittäminen pinon purkamisen (stack unwinding) aikana on vaarallista, koska toinen karkaava poikkeus toisen poikkeuksen ollessa aktiivinen päättää myös ohjelman suorituksen. Siksi destruktorien tulisi suorittaa parhaan kykynsä mukainen siivous, eivätkä ne saa antaa poikkeuksien karata. Taustajärjestelmien tyyppien tulisi raportoida siivouksen epäonnistumisista eksplisiittisillä toimenpiteillä, kuten `close()`, `flush()`, `commit()`, `stop()` tai `shutdown()`, jotka palauttavat virheen tai `expected`-arvon tai heittävät poikkeuksen ennen olion tuhoamista. Destruktori voi lokittaa, lähettää metriikkaa, vaimentaa virheitä tai suorittaa turvallisen varajärjestelyn mukaisen siivouksen, mutta sen ei pitäisi olla ensisijainen virheraportointikanava toimenpiteitä vaativille virheille.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Standardikirjasto

9Miten `std::vector` hallitsee kapasiteettia, kasvamista, muistin uudelleenvarointia ja iteraattorien stabiiliutta?

`std::vector` tallentaa alkiot peräkkäin muistiin ja seuraa sekä kokoa (size) että kapasiteettia (capacity). Koko on luotujen alkioiden määrä; kapasiteetti on varatun muistin määrä, johon voidaan tallentaa alkioita ennen kuin uusi muistinvaraus on tarpeen. Kun alkioiden lisääminen ylittäisi kapasiteetin, `vector` varaa suuremman muistilohkon – yleensä käyttäen toteutuskohtaista geometristä kasvustrategiaa – siirtää tai kopioi olemassa olevat alkiot, tuhoaa vanhat ja vapauttaa vanhan muistin. `reserve(n)` kasvattaa kapasiteettia muuttamatta kokoa, kun taas `resize(n)` muuttaa kokoa luomalla tai tuhoamalla alkioita. Muistin uudelleenvaraus mitätöi kaikki alkioihin osoittavat iteraattorit, viittaukset ja osoittimet; jopa ilman muistin uudelleenvarausta operaatiot, kuten `insert` ja `erase`, voivat mitätöidä muutoskohdassa tai sen jälkeen sijaitsevat positiot.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

10Mitä mitätöitymissääntöjä tulisi tietää peräkkäisrakenteisten ja solmupohjaisten standardisäiliöiden osalta?

Mitätöitymissäännöt riippuvat säiliöstä ja operaatiosta. Peräkkäisrakenteisilla säiliöillä, kuten vector ja string, iteraattoreiden ja viitteiden pysyvyys on herkkä: kasvu voi aiheuttaa muistin uudelleenvarauksen ja mitätöidä kaikki iteraattorit, viitteet ja osoittimet. Lisäksi alkioiden lisääminen tai poistaminen voi siirtää alkioita ja mitätöidä muutoskohdan ja sitä seuraavat sijainnit, vaikka muistia ei varattaisi uudelleen. Solmupohjaiset järjestetyt säiliöt, kuten list, map, set ja niiden multi-versiot, pitävät olemassa olevien, poistamattomien alkioiden iteraattorit ja viitteet yleensä voimassa lisäyksien yli; alkion poistaminen mitätöi kyseiseen poistettuun alkioon osoittavan iteraattorin tai viitteen. Myös järjestämättömät säiliöt tallentavat alkionsa solmuihin, joten viitteet ja osoittimet alkioihin pysyvät yleensä voimassa uudelleenhajautuksen (rehash) aikana, mutta uudelleenhajautus mitätöi iteraattorit. deque-säiliöllä on omat segmentoitua tallennustilaa koskevat erityissääntönsä. Käytännössä tarkista aina tietty säiliö ja operaatio ennen kuin tallennat iteraattoreita tai viitteitä muutosten yli.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

11Vertaile `std::map`-, `std::unordered_map`- ja flat-map-tyyppisiä säiliöitä taustajärjestelmien hakutauluina.

`std::map` on järjestetty, yleensä puurakenteeseen perustuva assosiatiivinen säiliö, jolla on logaritminen haku-, lisäys- ja poistoaika; se on hyödyllinen, kun lajiteltu läpikäynti, välikyselyt tai järjestystakuut ovat tärkeitä. `std::unordered_map` perustuu hajautustauluun ja tarjoaa keskimäärin vakioaikaiset tarkan avaimen operaatiot ilman avainten järjestystä; se on usein hyvä oletusvalinta suuriin muuttuviin hakutauluihin, kun hajautus toimii hyvin. Flat-map-tyyppinen säiliö tallentaa lajitellut avain-arvoparit peräkkäin muistiin, mikä tarjoaa hyvän välimuistipaikallisuuden ja nopean läpikäynnin tai puolitushaun, mutta lisäys ja poisto rakenteen keskeltä ovat lineaarisia operaatioita. Taustajärjestelmien hakutaulujen valinta tulisi tehdä sen perusteella, vaatiiko työkuorma järjestystä tai välikyselyitä, pääasiassa tarkkoja hakuja, usein tapahtuvaa muokkausta, ennustettavaa latenssia, muistin lisäkuormaa ja välimuistin toimintaa.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

12Selitä `std::optional` ja tyypilliset taustajärjestelmien käyttötapaukset puuttuvien arvojen esittämiseen.

`std::optional<T>` edustaa joko sen sisältämää `T`-arvoa tai arvon puuttumista. Tyhjää tilaa edustaa `std::nullopt`. Koodissa voidaan tarkistaa `has_value()` tai käyttää optionalia totuusarvoyhteydessä, päästä arvoon käsiksi `*`-operaattorilla tai `value()`-metodilla, ja antaa oletusarvo `value_or()`-metodilla. Taustajärjestelmäkoodissa se on hyödyllinen tietokannan kentille, jotka voivat olla tyhjiä (nullable), valinnaisille pyyntö- tai konfiguraatiokentille, välimuistin tai tietovaraston hudeille, joissa arvon puuttumista odotetaan, ja toimialueen tiloille, joissa -1:n tai tyhjän merkkijonon kaltainen erikoisarvo olisi moniselitteinen. Se mallintaa arvon puuttumista, ei polymorfismia tai rikasta virhetietoa.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

13Mitä ovat `std::string_view` ja `std::span`, ja millaisia elinkaareen liittyviä riskejä omistamattomat näkymät (non-owning views) tuovat mukanaan?

`std::string_view` on omistamaton näkymä yhtenäiseen merkkisarjaan; `std::span<T>` on omistamaton näkymä yhtenäiseen `T`-alkioiden sarjaan. Ne ovat hyödyllisiä nollakopioinnin (zero-copy) parametreille ja puskurien API-rajapinnoille, koska ne pitävät sisällään osoittimen ja pituuden varaamatta tai omistamatta muistia. Suurin riski liittyy elinkaareen: viitatun tallennustilan on oltava olemassa kauemmin kuin näkymän, eikä sitä saa mitätöidä näkymän käytön aikana. Näkymän palauttaminen tai tallentaminen viittaamaan väliaikaiseen objektiin, paikalliseen objektiin, tuhottuun objektiin tai uudelleenvarattuun säiliöön voi jättää jälkeensä roikkuvan näkymän (dangling view).

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Rinnakkaisuus

14Miten `std::mutex`, `std::shared_mutex` ja `std::recursive_mutex` eroavat toisistaan, ja milloin valitsisit kunkin niistä?

`std::mutex` tarjoaa yksinomaisen lukituksen: vain yksi säie voi pitää sitä hallussaan, joten se on oletusvalinta jaetun muuttuvan tilan suojaamiseen. `std::shared_mutex` tukee jaettuja lukijalukkoja ja yksinomaisia kirjoittajalukkoja: useat lukijat voivat pitää lukkoa samanaikaisesti, mutta kirjoittajat tarvitsevat yksinomaisen pääsyn. Valitse se enimmäkseen luettavalle tiedolle silloin, kun lukijoiden samanaikaisuus on lukituksen aiheuttaman lisäkulun arvoista ja kun kirjoittajien reiluus tai nälkiintyminen (starvation) on hyväksyttävää tai huomioitu koodissa. `std::recursive_mutex` on yksinomainen lukko, jonka sama säie voi lukita useita kertoja ja joka on vapautettava yhtä monta kertaa; käytä sitä harvoin, pääasiassa vanhassa tai uudelleenkutsuttavassa (re-entrant) koodissa, koska se voi piilottaa huonon lukitusten suunnittelun.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Kielen ominaisuudet

15Selitä lambdan kaappaustavat (capture) ja miten kaappaukset vaikuttavat olioiden elinkaareen takaisinkutsuissa.

Lambda voi kaapata muuttujia arvon perusteella (`[x]` tai `[=]`), viitteen perusteella (`[&x]` tai `[&]`), kaapata `this`-osoittimen tai käyttää alustuskaappausta, kuten `[p = std::move(ptr)]`. Arvokaappaukset kopioivat kaapatun olion sulkeumaan (closure), kun lambda luodaan. Viitekaappaukset tallentavat viitteitä, joten alkuperäisten olioiden elinkaaren on kestettävä kaikkien lambdan kutsujen yli. Takaisinkutsuissa, tallennetuissa lambdoissa tai asynkronisessa työssä viitekaappaukset ja `this`-kaappaukset ovat vaarallisia, koska paikalliset muuttujat tai itse olio saatetaan tuhota ennen kutsua. Suosi arvokaappauksia tarvittavalle tiedolle, siirto-/alustuskaappausta omistajuuden siirtämiseen, tai tarkoituksellisia `shared_ptr`- / `weak_ptr` -malleja, kun olion elinkaarta on pidennettävä tai tarkistettava.

Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa