Préparation Junior C++

Questions d'entretien Junior pour développeur backend C++

15 questions sélectionnées pour les développeurs Junior C++ qui doivent expliquer clairement les fondamentaux du langage et la gestion de base des ressources.

Commencer un entretien IA Junior C++Aucune carte bancaire requise. 1 session gratuite disponible.
Pratique d'entretien technique en anglaisUn mode où les non-natifs peuvent s'entraîner à passer des entretiens techniques.

Resource Management

1Expliquez RAII et la manière dont ce principe façonne la gestion des ressources sûre vis-à-vis des exceptions dans les services backend en C++.

RAII (Resource Acquisition Is Initialization) signifie qu’un objet C++ possède une ressource et la libère dans son destructeur. Comme les objets locaux sont détruits automatiquement lorsque leur durée de vie/portée se termine, y compris pendant le déroulement de pile dû à une exception, RAII fournit un nettoyage déterministe et rend les chemins d’erreur sûrs vis-à-vis des exceptions. Dans les services backend, cela s’applique non seulement à la mémoire, mais aussi aux descripteurs de fichiers, sockets, verrous de mutex, handles de base de données, transactions et autres ressources de l’OS ou de l’application.

Essayer de répondre à cette question avec un coach IA

Gestion mémoire

2Comparez std::unique_ptr et std::shared_ptr et décrivez quand chacun est approprié dans les API backend.

std::unique_ptr représente une propriété exclusive : il est peu coûteux, déplaçable mais non copiable, et convient à un propriétaire unique ou aux API qui transfèrent la propriété. std::shared_ptr représente une propriété partagée : il est copiable et maintient un objet en vie par comptage de références jusqu’à ce que le dernier propriétaire fort le libère. Dans les API backend, utilisez unique_ptr lorsque la propriété est transférée, shared_ptr uniquement lorsque plusieurs propriétaires indépendants doivent prolonger la durée de vie, et des références ou des pointeurs bruts pour les accès non propriétaires. std::make_shared est couramment préféré lors de la création d’objets shared_ptr, car il est efficace et sûr vis-à-vis des exceptions.

Essayer de répondre à cette question avec un coach IA

Système de types

3Qu’est-ce que la sémantique de déplacement, et comment implémente-t-on correctement la construction par déplacement et l’affectation par déplacement ?

La sémantique de déplacement permet à C++ de transférer des ressources depuis des objets temporaires ou autrement sacrifiables au lieu de les copier. Elle utilise des références rvalue comme T&& et std::move, qui est un cast permettant de sélectionner les surcharges de déplacement ; std::move ne déplace rien par lui-même. Un constructeur par déplacement correct initialise un nouvel objet en prenant la ressource de l’objet source et en laissant la source valide, destructible et assignable. Un opérateur d’affectation par déplacement correct transfère vers un objet existant, gère ou tolère l’auto-affectation, libère ou réutilise la ressource courante de la destination, prend la ressource de la source et laisse la source dans un état sûr. Les opérations de déplacement doivent souvent être noexcept afin que les conteneurs standard puissent les utiliser lors des réallocations tout en préservant les garanties vis-à-vis des exceptions.

Essayer de répondre à cette question avec un coach IA

4Décrivez la const-correctness dans les API C++ et comment concevoir efficacement des fonctions membres const.

La const-correctness consiste à exprimer via le système de types quelles opérations ne modifient pas l’état observable ou logique d’un objet. Une fonction membre const possède un objet this qualifié const, elle ne peut donc pas modifier les membres de données non mutable ni appeler des fonctions membres non const sur le même objet. Une bonne conception d’API marque les requêtes en lecture seule comme const, renvoie des valeurs ou des références/pointeurs const lorsque c’est approprié, et évite d’exposer un état interne mutable depuis des fonctions const. mutable doit être réservé aux détails d’implémentation qui ne changent pas l’état logique, comme les caches, les valeurs calculées paresseusement, les métriques ou les mutex. const est un contrat d’API sur la mutation, pas une garantie automatique de sûreté vis-à-vis des threads ; les garanties de concurrence nécessitent une implémentation et une documentation séparées.

Essayer de répondre à cette question avec un coach IA

5Qu’est-ce que std::byte et comment représenter de manière sûre les tampons binaires bruts en C++ ?

std::byte est un type distinct pour représenter des données binaires brutes sous forme d’octets, et non des caractères ou des entiers arithmétiques. Il améliore la sûreté de type, car les tampons d’octets ne sont pas accidentellement traités comme du texte ou comme des valeurs numériques, tout en prenant en charge les opérations bit à bit. Les tampons binaires bruts devraient généralement être représentés avec un stockage orienté octets, comme std::vector<std::byte> ou std::array<std::byte, N>, et passés à des API non propriétaires sous forme de std::span<std::byte> ou std::span<const std::byte>. Le code de sérialisation doit encoder et décoder explicitement les valeurs au lieu de s’appuyer sur des dispositions arbitraires d’objets en mémoire.

Essayer de répondre à cette question avec un coach IA

Durée de vie des objets

6Décrivez la règle de zéro, la règle de trois et la règle de cinq, ainsi que les situations où chacune s’applique.

Règle de zéro : préférer les classes qui ne déclarent pas d’opérations personnalisées de destruction/copie/déplacement ; laisser les membres RAII tels que std::string, std::vector, std::unique_ptr, les wrappers de fichiers/sockets, etc., gérer les ressources. Règle de trois : si une classe gère manuellement une ressource et a besoin d’un destructeur personnalisé, d’un constructeur de copie ou d’un opérateur d’affectation par copie, elle a généralement besoin des trois pour définir un comportement correct de copie/propriété. Règle de cinq : en C++11 et versions ultérieures, ces types doivent aussi considérer le constructeur de déplacement et l’opérateur d’affectation par déplacement. Utilisez la règle de zéro pour la plupart des types applicatifs ; utilisez la règle de trois/cinq lorsque le type possède directement une ressource ou a une sémantique non triviale de propriété/durée de vie.

Essayer de répondre à cette question avec un coach IA

Gestion des erreurs

7Expliquez la gestion des exceptions en C++, le déroulement de pile, l’interaction avec les destructeurs et les frontières d’exceptions dans un service.

Les exceptions C++ transfèrent le contrôle d’une expression `throw` vers le `catch` correspondant le plus proche. Pendant la propagation, le déroulement de pile détruit les objets automatiques entièrement construits dans l’ordre inverse, de sorte que le nettoyage RAII s’exécute automatiquement. Les destructeurs ne doivent généralement pas lancer d’exception ; si une exception s’échappe d’un destructeur `noexcept` ou si une autre exception s’échappe pendant un déroulement déjà actif, le programme appelle `std::terminate`. Les exceptions doivent généralement être capturées par référence, souvent `const&`, afin d’éviter le slicing et les copies inutiles. Les services backend doivent définir des frontières d’exceptions, comme les gestionnaires de requêtes, les points d’entrée de threads de travail, les callbacks de frameworks RPC/HTTP et `main`, où les exceptions sont journalisées, converties en réponses d’erreur ou codes de statut, et empêchées de s’échapper vers des contextes inappropriés comme les API C, les destructeurs, les threads ou les fonctions `noexcept`.

Essayer de répondre à cette question avec un coach IA

8Que se passe-t-il si un destructeur lance une exception, et comment les types backend doivent-ils signaler les échecs de nettoyage ?

Les destructeurs sont implicitement `noexcept(true)` dans les cas normaux ; ainsi, si une exception s’échappe d’un tel destructeur, `std::terminate` est appelé. Même si un destructeur est explicitement déclaré `noexcept(false)`, lancer pendant le déroulement de pile est dangereux, car une seconde exception qui s’échappe alors qu’une autre exception est active termine également le programme. Par conséquent, les destructeurs doivent effectuer un nettoyage au mieux et ne doivent pas laisser les exceptions s’échapper. Les types backend doivent signaler les échecs de nettoyage via des opérations explicites telles que `close()`, `flush()`, `commit()`, `stop()` ou `shutdown()`, qui retournent une erreur/`expected` ou lancent avant la destruction. Le destructeur peut journaliser, émettre des métriques, supprimer les erreurs ou effectuer un nettoyage de repli sûr, mais ne doit pas être le canal principal de signalement d’erreurs nécessitant une action.

Essayer de répondre à cette question avec un coach IA

Bibliothèque standard

9Comment std::vector gère-t-il la capacité, la croissance, la réallocation et la stabilité des itérateurs ?

std::vector stocke les éléments de manière contiguë et suit à la fois la taille et la capacité. size est le nombre d’éléments construits ; capacity est la quantité de stockage d’éléments allouée disponible avant qu’une autre allocation soit nécessaire. Lorsque l’ajout d’éléments dépasserait la capacité, vector alloue un bloc plus grand, typiquement avec une stratégie de croissance géométrique définie par l’implémentation, déplace ou copie les éléments existants, détruit les anciens et libère l’ancien stockage. reserve(n) augmente la capacité sans changer la taille, tandis que resize(n) change la taille en construisant ou détruisant des éléments. Une réallocation invalide tous les itérateurs, références et pointeurs vers les éléments ; même sans réallocation, des opérations comme insert et erase peuvent invalider les positions au niveau du point de modification ou après celui-ci.

Essayer de répondre à cette question avec un coach IA

10Quelles règles d’invalidation faut-il connaître pour les conteneurs standard contigus et à nœuds ?

Les règles d’invalidation dépendent du conteneur et de l’opération. Les conteneurs contigus comme vector et string ont une stabilité fragile des itérateurs/références : la croissance peut réallouer et invalider tous les itérateurs, références et pointeurs, et insert/erase peuvent décaler des éléments et invalider les positions au niveau du changement ou après celui-ci même sans réallocation. Les conteneurs ordonnés à nœuds comme list, map, set et leurs variantes multi conservent généralement les itérateurs et références vers les éléments existants non effacés lors d’une insertion ; effacer un élément invalide l’itérateur/la référence vers cet élément effacé. Les conteneurs unordered stockent également les éléments dans des nœuds, donc les références et pointeurs vers les éléments restent généralement stables lors d’un rehash, mais un rehash invalide les itérateurs. deque a des règles spéciales de stockage segmenté. En pratique, vérifiez le conteneur et l’opération spécifiques avant de conserver des itérateurs ou références à travers des modifications.

Essayer de répondre à cette question avec un coach IA

11Comparez std::map, std::unordered_map et les conteneurs de style flat-map pour des tables de recherche backend.

std::map est un conteneur associatif ordonné, généralement basé sur un arbre, avec des opérations de recherche/insertion/suppression logarithmiques ; il est utile lorsque l’itération triée, les requêtes par intervalle ou les garanties d’ordre sont importantes. std::unordered_map est basé sur une table de hachage, avec des opérations par clé exacte en temps constant en moyenne et sans ordre des clés ; c’est souvent un bon choix par défaut pour de grandes tables de recherche mutables lorsque le hachage est de bonne qualité. Un conteneur de style flat-map stocke des paires clé/valeur triées de manière contiguë, ce qui donne une bonne localité de cache ainsi qu’une itération et une recherche par dichotomie rapides, mais l’insertion et la suppression au milieu sont linéaires. Pour des tables de recherche backend, le choix dépend du besoin d’ordre ou d’intervalles, de recherches principalement exactes, de mutations fréquentes, d’une latence prévisible, du surcoût mémoire et du comportement vis-à-vis du cache.

Essayer de répondre à cette question avec un coach IA

12Expliquez std::optional et ses cas d’utilisation backend typiques pour représenter des valeurs absentes.

std::optional<T> représente soit une valeur T contenue, soit l’absence de valeur. L’état vide est représenté par std::nullopt ; le code peut vérifier has_value() ou utiliser l’optional dans un contexte booléen, accéder à la valeur avec * ou value(), et fournir une valeur par défaut avec value_or(). Dans du code backend, il est utile pour les champs de base de données nullable, les champs de requête ou de configuration optionnels, les absences en cache ou en dépôt lorsque l’absence est attendue, et les états métier où une sentinelle comme -1 ou une chaîne vide serait ambiguë. Il modélise l’absence d’une valeur, pas le polymorphisme ni des informations d’erreur riches.

Essayer de répondre à cette question avec un coach IA

13Que sont std::string_view et std::span, et quels dangers de durée de vie les vues non propriétaires introduisent-elles ?

std::string_view est une vue non propriétaire sur une séquence contiguë de caractères ; std::span<T> est une vue non propriétaire sur une séquence contiguë de T. Elles sont utiles pour les paramètres sans copie et les API de tampons, car elles transportent un pointeur et une longueur sans allouer ni posséder la mémoire. Le principal danger est la durée de vie : le stockage référencé doit vivre plus longtemps que la vue et ne doit pas être invalidé pendant l’utilisation de la vue. Retourner ou stocker une vue vers un temporaire, un objet local, un objet détruit ou un conteneur réalloué peut laisser une vue pendante.

Essayer de répondre à cette question avec un coach IA

Concurrence

14En quoi std::mutex, std::shared_mutex et std::recursive_mutex diffèrent-ils, et quand choisir chacun d’eux ?

std::mutex fournit un verrouillage exclusif : un seul thread peut le détenir, c’est donc le choix par défaut pour protéger un état mutable partagé. std::shared_mutex prend en charge des verrous lecteurs partagés et des verrous écrivains exclusifs : plusieurs lecteurs peuvent détenir le verrou simultanément, mais les écrivains ont besoin d’un accès exclusif. Choisissez-le pour des données majoritairement lues lorsque la concurrence entre lecteurs justifie le surcoût et lorsque l’équité envers les écrivains/le risque de famine est acceptable ou géré. std::recursive_mutex est un mutex exclusif que le même thread peut verrouiller plusieurs fois et doit déverrouiller le même nombre de fois ; utilisez-le rarement, surtout pour du code hérité ou réentrant, car il peut masquer une mauvaise conception du verrouillage.

Essayer de répondre à cette question avec un coach IA

Fonctionnalités du langage

15Expliquez les modes de capture des lambdas et la façon dont les captures interagissent avec les durées de vie des objets dans les callbacks.

Une lambda peut capturer des variables par valeur (`[x]` ou `[=]`), par référence (`[&x]` ou `[&]`), capturer `this`, ou utiliser une init-capture comme `[p = std::move(ptr)]`. Les captures par valeur copient l’objet capturé dans la closure au moment où la lambda est créée ; les captures par référence stockent des références, donc les objets originaux doivent survivre à toutes les invocations de la lambda. Dans les callbacks, les lambdas stockées ou le travail asynchrone, les captures par référence et les captures de `this` sont dangereuses, car les variables locales ou l’objet peuvent être détruits avant l’invocation. Préférez les captures par valeur pour les données nécessaires, les captures par déplacement/init-capture pour la propriété, ou des schémas délibérés avec `shared_ptr`/`weak_ptr` lorsque la durée de vie de l’objet doit être prolongée ou vérifiée.

Essayer de répondre à cette question avec un coach IA