Preparazione C++ Junior

Domande di Colloquio per Backend C++ Junior

15 domande di colloquio sul C++ selezionate per sviluppatori backend Junior che devono spiegare chiaramente i fondamenti del linguaggio e le basi della gestione della proprietà.

Inizia un Colloquio AI in C++ JuniorNessuna carta di credito richiesta. 1 sessione gratuita disponibile.
Pratica per colloqui tecnici in ingleseUna modalità in cui chi non è madrelingua può esercitarsi a superare i colloqui tecnici.

Resource Management

1Spiega RAII (Resource Acquisition Is Initialization) e come modella la gestione delle risorse sicura rispetto alle eccezioni nei servizi di backend in C++.

RAII (Resource Acquisition Is Initialization) significa che un oggetto C++ possiede una risorsa e la rilascia tramite il proprio distruttore. Poiché gli oggetti locali vengono distrutti automaticamente quando termina il loro ciclo di vita o ambito di visibilità, incluso durante lo srotolamento dello stack (stack unwinding) in caso di eccezioni, RAII garantisce una pulizia deterministica e rende i percorsi di errore sicuri rispetto alle eccezioni. Nei servizi di backend questo si applica non solo alla memoria, ma anche a descrittori di file, socket, lock di mutex, handle di database, transazioni e altre risorse del sistema operativo o dell'applicazione.

Prova a rispondere a questa domanda con un coach IA

Gestione della Memoria

2Confronta `std::unique_ptr` e `std::shared_ptr` e descrivi quando ciascuno è appropriato nelle API di backend.

`std::unique_ptr` rappresenta la proprietà esclusiva: è leggero, supporta lo spostamento ma non la copia, ed è appropriato per un singolo proprietario o per le API che trasferiscono la proprietà. `std::shared_ptr` rappresenta la proprietà condivisa: è copiabile e mantiene in vita un oggetto tramite il conteggio dei riferimenti finché l'ultimo proprietario forte non lo rilascia. Nelle API di backend, usa `unique_ptr` quando la proprietà viene trasferita, `shared_ptr` solo quando più proprietari indipendenti devono estenderne il ciclo di vita, e i riferimenti o i puntatori grezzi per l'accesso senza acquisirne la proprietà. L'uso di `std::make_shared` è generalmente preferito quando si creano oggetti `shared_ptr` perché è efficiente e sicuro rispetto alle eccezioni.

Prova a rispondere a questa domanda con un coach IA

Sistema dei Tipi

3Che cos'è la semantica di spostamento (move semantics) e come si implementano correttamente un costruttore di spostamento e un operatore di assegnazione per spostamento?

La semantica di spostamento permette a C++ di trasferire risorse da oggetti temporanei o altrimenti sacrificabili invece di copiarle. Utilizza i riferimenti rvalue come `T&&` e `std::move`, che è un cast che permette di selezionare gli overload di spostamento; `std::move` di per sé non sposta nulla. Un costruttore di spostamento (move constructor) corretto inizializza un nuovo oggetto prendendo le risorse dell'oggetto sorgente e lasciando quest'ultimo in uno stato valido, distruttibile e assegnabile. Un operatore di assegnazione per spostamento (move assignment operator) corretto trasferisce le risorse in un oggetto esistente, gestisce o tollera l'auto-assegnazione, rilascia o riutilizza la risorsa corrente della destinazione, acquisisce la risorsa sorgente e lascia il sorgente in uno stato sicuro. Le operazioni di spostamento dovrebbero spesso essere dichiarate `noexcept` affinché i container della libreria standard possano utilizzarle durante la riallocazione, preservando le garanzie di sicurezza rispetto alle eccezioni.

Prova a rispondere a questa domanda con un coach IA

4Descrivi la const-correctness nelle API C++ e come progettare in modo efficace le funzioni membro const.

La const-correctness significa esprimere attraverso il sistema dei tipi quali operazioni non modificano lo stato osservabile o logico di un oggetto. Una funzione membro const ha l'oggetto this qualificato come const, pertanto non può modificare i membri dati non mutabili o chiamare funzioni membro non const sullo stesso oggetto. Una buona progettazione delle API marca come const le interrogazioni di sola lettura, restituisce valori o riferimenti/puntatori const quando appropriato, ed evita di esporre lo stato interno mutabile dalle funzioni const. La parola chiave mutable dovrebbe essere riservata ai dettagli implementativi che non alterano lo stato logico, come cache, valori calcolati in modalità lazy, metriche o mutex. const rappresenta un contratto dell'API sulla mutazione, non una garanzia automatica di sicurezza nei thread (thread-safety); le garanzie di concorrenza richiedono un'implementazione e una documentazione separate.

Prova a rispondere a questa domanda con un coach IA

5Cos'è `std::byte` e come dovrebbero essere rappresentati in modo sicuro i buffer binari grezzi in C++?

`std::byte` è un tipo distinto per rappresentare dati binari grezzi sotto forma di byte, non di caratteri o interi aritmetici. Migliora la sicurezza dei tipi perché i buffer di byte non vengono trattati accidentalmente come testo o valori numerici, pur supportando le operazioni bit a bit. I buffer binari grezzi dovrebbero generalmente essere rappresentati con strutture di archiviazione orientate ai byte come `std::vector<std::byte>` o `std::array<std::byte, N>`, e passati attraverso API che non acquisiscono la proprietà (non-owning) come `std::span<std::byte>` o `std::span<const std::byte>`. Il codice di serializzazione dovrebbe codificare e decodificare esplicitamente i valori anziché affidarsi al layout arbitrario degli oggetti.

Prova a rispondere a questa domanda con un coach IA

Ciclo di Vita degli Oggetti

6Descrivi la Regola dello Zero, la Regola del Tre e la Regola del Cinque e quando si applica ciascuna di esse.

Regola dello Zero (Rule of Zero): preferire classi che non dichiarano operazioni personalizzate di distruzione, copia o spostamento; lasciare che i membri basati su RAII (Resource Acquisition Is Initialization), come `std::string`, `std::vector`, `std::unique_ptr`, e i wrapper di file/socket, gestiscano le risorse. Regola del Tre (Rule of Three): se una classe gestisce manualmente una risorsa e richiede un distruttore, un costruttore di copia o un operatore di assegnazione per copia personalizzati, di solito ha bisogno di tutti e tre per definire un comportamento corretto di copia e di proprietà (ownership). Regola del Cinque (Rule of Five): in C++11 e successivi, tali tipi dovrebbero considerare anche il costruttore di spostamento e l'operatore di assegnazione per spostamento. Utilizza la Regola dello Zero per la maggior parte dei tipi dell'applicazione; utilizza la Regola del Tre/Cinque quando il tipo possiede direttamente una risorsa o ha una semantica di proprietà e ciclo di vita non banale.

Prova a rispondere a questa domanda con un coach IA

Gestione degli Errori

7Spiega la gestione delle eccezioni in C++, lo stack unwinding, l'interazione con i distruttori e i limiti delle eccezioni nei servizi.

Le eccezioni in C++ trasferiscono il controllo da un'espressione `throw` al blocco `catch` corrispondente più vicino. Durante la propagazione, lo stack unwinding distrugge gli oggetti automatici completamente costruiti in ordine inverso, in modo che la pulizia basata su RAII (Resource Acquisition Is Initialization) avvenga automaticamente. I distruttori, in generale, non dovrebbero lanciare eccezioni; se un'eccezione fuoriesce da un distruttore `noexcept` o se un'altra eccezione viene generata durante uno stack unwinding attivo, il programma chiama `std::terminate`. Le eccezioni dovrebbero di solito essere catturate per riferimento, comunemente come `const&`, per evitare il problema dello slicing e copie non necessarie. I servizi di backend dovrebbero definire dei limiti per le eccezioni (exception boundaries), come i gestori delle richieste, i punti di ingresso dei worker thread, le funzioni di callback dei framework RPC/HTTP e il `main`, in cui le eccezioni vengono registrate, convertite in risposte di errore o codici di stato e bloccate prima di sfuggire verso contesti inappropriati come API C, distruttori, thread o funzioni `noexcept`.

Prova a rispondere a questa domanda con un coach IA

8Cosa succede se un distruttore lancia un'eccezione, e come dovrebbero i tipi di backend segnalare i fallimenti durante la pulizia?

I distruttori sono implicitamente `noexcept(true)` in casi normali, quindi se un'eccezione fuoriesce da un tale distruttore, viene chiamato `std::terminate`. Anche se un distruttore è esplicitamente dichiarato `noexcept(false)`, lanciare un'eccezione durante lo svolgimento dello stack (stack unwinding) è pericoloso perché una seconda eccezione che sfugge mentre un'altra è attiva termina anch'essa il programma. Pertanto, i distruttori dovrebbero eseguire una pulizia di tipo best-effort e non dovrebbero mai lasciare sfuggire le eccezioni. I tipi di backend dovrebbero segnalare i fallimenti di pulizia tramite operazioni esplicite come `close()`, `flush()`, `commit()`, `stop()` o `shutdown()`, che restituiscono un errore o un tipo `expected`, o lanciano un'eccezione prima della distruzione. Il distruttore può registrare log, emettere metriche, sopprimere gli errori o eseguire una pulizia di ripiego sicura, ma non dovrebbe essere il canale principale di segnalazione degli errori per i fallimenti su cui è possibile intervenire.

Prova a rispondere a questa domanda con un coach IA

Libreria Standard

9In che modo `std::vector` gestisce la capacità, la crescita, la riallocazione e la stabilità degli iteratori?

`std::vector` memorizza gli elementi in modo contiguo e tiene traccia sia della dimensione (`size`) che della capacità (`capacity`). La dimensione è il numero di elementi costruiti; la capacità è la quantità di memoria allocata per gli elementi disponibile prima che sia necessaria un'altra allocazione. Quando l'aggiunta di elementi supererebbe la capacità, il vettore alloca un blocco più grande, tipicamente utilizzando una strategia di crescita geometrica definita dall'implementazione, sposta o copia gli elementi esistenti, distrugge quelli vecchi e rilascia la vecchia memoria. `reserve(n)` aumenta la capacità senza modificare la dimensione, mentre `resize(n)` modifica la dimensione costruendo o distruggendo elementi. La riallocazione invalida tutti gli iteratori, i riferimenti e i puntatori agli elementi; anche senza riallocazione, operazioni come `insert` ed `erase` possono invalidare le posizioni in corrispondenza o successive al punto di modifica.

Prova a rispondere a questa domanda con un coach IA

10Quali regole di invalidazione dovresti conoscere per i container standard contigui e basati su nodi?

Le regole di invalidazione dipendono dal container e dall'operazione. I container contigui come vector e string hanno una stabilità fragile degli iteratori e dei riferimenti: l'aumento delle dimensioni può comportare una riallocazione e invalidare tutti gli iteratori, riferimenti e puntatori, e l'inserimento/rimozione (insert/erase) può spostare gli elementi e invalidare le posizioni in corrispondenza o dopo la modifica, anche senza riallocazione. I container ordinati basati su nodi come list, map, set e le loro varianti multi generalmente mantengono stabili iteratori e riferimenti agli elementi esistenti non rimossi durante l'inserimento; la rimozione di un elemento invalida l'iteratore/riferimento a quell'elemento rimosso. Anche i container non ordinati memorizzano gli elementi in nodi, quindi i riferimenti e i puntatori agli elementi sono generalmente stabili durante un rehash, ma il rehash invalida gli iteratori. deque ha regole speciali per l'archiviazione segmentata. In pratica, è consigliabile verificare lo specifico container e la specifica operazione prima di memorizzare iteratori o riferimenti durante le modifiche.

Prova a rispondere a questa domanda con un coach IA

11Confronta std::map, std::unordered_map e i contenitori in stile flat-map per le tabelle di ricerca nel backend.

std::map è un contenitore associativo ordinato, solitamente basato su alberi, con tempi logaritmici per ricerca, inserimento ed eliminazione; è utile quando contano l'iterazione ordinata, le query per intervalli o le garanzie di ordinamento. std::unordered_map è basato su tabelle hash con operazioni a tempo costante medio per chiavi esatte e nessun ordinamento delle chiavi; spesso rappresenta un'ottima scelta predefinita per tabelle di ricerca di grandi dimensioni e mutevoli, a patto che la funzione di hash sia efficiente. Un contenitore in stile flat-map archivia le coppie chiave/valore ordinate in modo contiguo, offrendo una buona località della cache e un'iterazione o ricerca binaria veloce, ma l'inserimento e l'eliminazione nel mezzo richiedono un tempo lineare. Per le tabelle di ricerca nel backend, la scelta dipende dalla necessità di ordinamento o query per intervalli, dalla predominanza di ricerche esatte, dalla frequenza delle mutazioni, dalla prevedibilità della latenza, dall'overhead di memoria e dal comportamento della cache.

Prova a rispondere a questa domanda con un coach IA

12Spiega std::optional e i tipici casi d'uso nel backend per rappresentare valori assenti.

std::optional<T> rappresenta un valore T contenuto al suo interno oppure nessun valore. Lo stato vuoto è rappresentato da std::nullopt; il codice può verificare la presenza del valore con has_value() o usando l'optional in un contesto booleano, accedere al valore tramite * o value(), e fornire un valore predefinito con value_or(). Nel codice di backend è utile per campi di database che ammettono valori nulli, campi opzionali di configurazione o di richiesta, mancate corrispondenze (miss) nella cache o nel repository quando l'assenza è prevista, e stati di dominio in cui un valore sentinella come -1 o una stringa vuota risulterebbe ambiguo. Modella l'assenza di un valore, non il polimorfismo o informazioni dettagliate sugli errori.

Prova a rispondere a questa domanda con un coach IA

13Cosa sono std::string_view e std::span, e quali rischi legati al ciclo di vita introducono le viste non proprietarie (non-owning views)?

std::string_view è una vista non proprietaria di una sequenza contigua di caratteri; std::span<T> è una vista non proprietaria di una sequenza contigua di T. Sono utili per parametri a zero copie e per le API basate su buffer perché trasportano un puntatore e una lunghezza senza allocare o possedere la memoria. Il rischio principale riguarda il ciclo di vita (lifetime): la memoria referenziata deve sopravvivere alla vista e non deve essere invalidata mentre la vista è in uso. Restituire o memorizzare una vista riferita a un oggetto temporaneo, a un oggetto locale, a un oggetto distrutto o a un contenitore riallocato può lasciare una vista penzolante (dangling view).

Prova a rispondere a questa domanda con un coach IA

Concorrenza

14In cosa differiscono `std::mutex`, `std::shared_mutex` e `std::recursive_mutex`, e quando sceglieresti ciascuno di essi?

`std::mutex` fornisce un locking esclusivo: solo un thread può acquisirlo, quindi è la scelta predefinita per proteggere uno stato mutabile condiviso. `std::shared_mutex` supporta lock condivisi per i lettori e lock esclusivi per gli scrittori: molti lettori possono mantenere il lock contemporaneamente, ma gli scrittori necessitano di un accesso esclusivo. Sceglilo per dati letti di frequente, quando la concorrenza tra lettori vale l'overhead e quando l'equità (fairness) verso gli scrittori o il problema della starvation sono accettabili o gestiti. `std::recursive_mutex` è un mutex esclusivo che lo stesso thread può bloccare più volte e deve sbloccare lo stesso numero di volte; usalo raramente, principalmente per codice legacy o rientrante, perché può nascondere una progettazione carente del locking.

Prova a rispondere a questa domanda con un coach IA

Funzionalità del Linguaggio

15Spiega le modalità di cattura delle lambda e come queste interagiscono con il ciclo di vita degli oggetti nelle funzioni di callback.

Una lambda può catturare le variabili per valore (`[x]` o `[=]`), per riferimento (`[&x]` o `[&]`), catturare `this`, o usare una cattura con inizializzazione (init-capture) come `[p = std::move(ptr)]`. Le catture per valore copiano l'oggetto catturato nella closure al momento della creazione della lambda; le catture per riferimento memorizzano dei riferimenti, per cui gli oggetti originali devono sopravvivere a tutte le invocazioni della lambda. Nelle callback, nelle lambda memorizzate o nel lavoro asincrono, le catture per riferimento e le catture di `this` sono pericolose, poiché le variabili locali o l'oggetto potrebbero essere distrutti prima dell'invocazione. È preferibile usare le catture per valore per i dati necessari, le catture con spostamento o inizializzazione per gestire la proprietà, oppure pattern intenzionali con `shared_ptr` / `weak_ptr` quando il ciclo di vita dell'oggetto deve essere esteso o verificato.

Prova a rispondere a questa domanda con un coach IA