Ausgewählte C++-Interviewfragen für Backend-Entwickler, nach Themen gruppiert und aus demselben Fragenkatalog gerendert, der auch die EngineerSpeak-Praxis antreibt.
1Erklären Sie RAII und wie es ausnahmesichere Ressourcenverwaltung in C++-Backend-Services prägt.
RAII (Resource Acquisition Is Initialization) bedeutet, dass ein C++-Objekt eine Ressource besitzt und sie in seinem Destruktor freigibt. Da lokale Objekte automatisch zerstört werden, wenn ihre Lebensdauer bzw. ihr Scope endet, auch während des Stack-Unwindings bei Exceptions, ermöglicht RAII deterministisches Aufräumen und macht Fehlerpfade ausnahmesicher. In Backend-Services gilt dies nicht nur für Speicher, sondern auch für Dateideskriptoren, Sockets, Mutex-Locks, Datenbank-Handles, Transaktionen und andere Betriebssystem- oder Anwendungsressourcen.
2Vergleichen Sie std::unique_ptr und std::shared_ptr und beschreiben Sie, wann welcher in Backend-APIs geeignet ist.
std::unique_ptr repräsentiert exklusiven Besitz: er ist leichtgewichtig, movable, aber nicht kopierbar, und eignet sich für einen einzelnen Besitzer oder für APIs, die Besitz übertragen. std::shared_ptr repräsentiert geteilten Besitz: er ist kopierbar und hält ein Objekt über Reference Counting am Leben, bis der letzte starke Besitzer es freigibt. In Backend-APIs verwendet man unique_ptr, wenn Besitz übertragen wird, shared_ptr nur dann, wenn mehrere unabhängige Besitzer die Lebensdauer verlängern müssen, und Referenzen oder rohe Pointer für nicht besitzenden Zugriff. std::make_shared wird beim Erzeugen von shared_ptr-Objekten häufig bevorzugt, weil es effizient und ausnahmesicher ist.
3Wie funktionieren Custom Deleter in Smart Pointern, und wann sind sie für C/C++-Interoperabilität nützlich?
Ein Custom Deleter ist aufrufbare Aufräumlogik, die ein Smart Pointer anstelle der standardmäßigen delete-Operation verwendet, wenn der Pointer seine besessene Ressource freigibt. Er ist für C/C++-Interoperabilität nützlich, wenn eine Ressource mit einer bestimmten Funktion freigegeben werden muss, etwa fclose, close, curl_easy_Aufräumen, SSL_free, free oder einer Destroy-Funktion einer Bibliothek. Bei unique_ptr ist der Deleter-Typ Teil des unique_ptr-Typs und kann dessen Größe beeinflussen; zustandslose Deleter können wegoptimiert werden, während Funktionspointer- oder zustandsbehaftete Deleter Speicherplatz hinzufügen. Bei shared_ptr wird der Deleter im Control Block gespeichert und ausgeführt, wenn der letzte starke Besitzer das Objekt freigibt. Custom Deleter ermöglichen, dass C-Ressourcen sicher an RAII teilnehmen.
4Erklären Sie weak_ptr und die Control-Block-Mechanik von shared_ptr, einschließlich Zyklen, enable_shared_from_this und Refcount-Kosten.
Ein shared_ptr verwaltet geteilten Besitz über einen Control Block, der starke und schwache Reference Counts sowie Aufräuminformationen wie Deleter und Allocator enthält. Das Kopieren oder Zerstören von shared_ptr-Objekten inkrementiert bzw. dekrementiert den starken Count, typischerweise mit atomaren Operationen, sodass separate shared_ptr-Objekte sicher über Threads hinweg manipuliert werden können, aber jedes Refcount-Update Kosten verursacht. Wenn der starke Count null erreicht, wird das verwaltete Objekt zerstört; der Control Block bleibt bestehen, bis auch schwache Referenzen verschwunden sind. weak_ptr zeigt auf denselben Control Block, ohne die Objektlebensdauer zu verlängern; lock() gibt einen shared_ptr zurück, wenn das Objekt noch lebt, andernfalls einen leeren shared_ptr. Zyklen, die nur aus shared_ptr bestehen, leaken, weil starke Counts nie null erreichen; daher wird weak_ptr für Rückzeiger oder Observer-Links verwendet. enable_shared_from_this erlaubt einem Objekt, das bereits von shared_ptr besessen wird, einen neuen shared_ptr auf sich selbst unter Verwendung des bestehenden Control Blocks zu erzeugen und vermeidet gefährliche separate Control Blocks. Refcount-Kosten umfassen atomare Inkremente/Dekremente, Cache-Contention, Control-Block-Allokation und Overhead in Hot Paths.
5Was sind Move Semantics, und wie implementiert man korrekte Move-Konstruktion und Move-Zuweisung?
Move Semantics erlauben C++, Ressourcen aus temporären oder anderweitig entbehrlichen Objekten zu übertragen, statt sie zu kopieren. Sie verwenden Rvalue-Referenzen wie T&& und std::move; std::move ist ein Cast, der ermöglicht, dass Move-Overloads ausgewählt werden; std::move selbst bewegt nichts. Ein korrekter Move-Konstruktor initialisiert ein neues Objekt, indem er die Ressource des Quellobjekts übernimmt und die Quelle gültig, zerstörbar und zuweisbar zurücklässt. Ein korrekter Move-Zuweisungsoperator überträgt in ein bestehendes Objekt, behandelt oder toleriert Selbstzuweisung, gibt die aktuelle Ressource des Ziels frei oder verwendet sie wieder, übernimmt die Ressource der Quelle und lässt die Quelle sicher zurück. Move-Operationen sollten oft noexcept sein, damit Standardcontainer sie während Reallokationen verwenden können und dabei Exception-Garantien erhalten bleiben.
6Beschreiben Sie const-Korrektheit in C++-APIs und wie man const-Memberfunktionen wirksam entwirft.
Const-Korrektheit bedeutet, über das Typsystem auszudrücken, welche Operationen den beobachtbaren oder logischen Zustand eines Objekts nicht verändern. Eine const-Memberfunktion hat ein const-qualifiziertes this-Objekt, daher kann sie keine nicht-mutable-Datenmember verändern und keine nicht-const-Memberfunktionen auf demselben Objekt aufrufen. Gutes API-Design markiert schreibgeschützte Abfragen als const, gibt Werte oder const-Referenzen/-Zeiger zurück, wo es angemessen ist, und vermeidet, veränderbaren internen Zustand aus const-Funktionen heraus offenzulegen. mutable sollte für Implementierungsdetails reserviert sein, die den logischen Zustand nicht ändern, etwa Caches, lazy Werte, Metriken oder Mutexe. const ist ein API-Vertrag über Mutation, keine automatische Garantie für Thread-Sicherheit; Nebenläufigkeitsgarantien benötigen eigene Implementierung und Dokumentation.
7Was sind move-only types und wie beeinflussen sie API-Grenzen für Sockets, File Handles und Locks?
Move-only types sind Typen, die nicht kopiert, aber bewegt werden können. Sie sind üblich für eindeutigen Besitz von Ressourcen wie Sockets, File Descriptors, File Handles, Locks und `std::unique_ptr`. Copy-Operationen sind gelöscht, um doppelten Besitz und doppeltes Freigeben zu vermeiden; Move-Operationen übertragen den Besitz und hinterlassen die Quelle gültig, aber üblicherweise leer/nicht besitzend. APIs sollten Besitzgrenzen explizit machen: Factories können move-only-Objekte per Wert zurückgeben, Funktionen, die Besitz übernehmen, können per Wert oder rvalue reference akzeptieren, und Funktionen, die nur inspizieren, sollten Referenzen, Zeiger oder andere Borrowing-Handles nehmen. Container können move-only-Werte speichern, wenn Elemente per Move eingefügt oder verschoben werden.
8Vergleichen Sie auto, decltype, decltype(auto) und Template-Argumentdeduktion in gängigem Backend-Code.
auto verwendet für Variablen eine templateähnliche Deduktion: einfaches auto entfernt normalerweise Referenzen und top-level const, sofern die Deklaration sie nicht anfordert, etwa auto&, const auto& oder auto&&. decltype(expr) untersucht den deklarierten Typ oder Ausdruckstyp genauer: ein nicht geklammerter id-expression liefert den deklarierten Typ, während andere lvalue-Ausdrücke T& ergeben, xvalues T&& ergeben und prvalues T ergeben. decltype(auto) deduziert nach decltype-Regeln, oft für Rückgabetypen, wenn Referenzen erhalten bleiben müssen. Template-Argumentdeduktion ist ähnlich wie auto, hängt aber von der Parameterform ab, etwa T, T&, const T& oder T&&, und hat eigene Regeln. Initialisierung mit geschweiften Klammern ist ein häufiger Unterschied: auto x = {1,2} deduziert std::initializer_list<int>, während ein einfacher Template-Parameter T im Allgemeinen nicht aus einem bloßen braced initializer deduziert werden kann, sofern der Parameter nicht eine initializer_list oder einen anderen passenden Typ erwartet.
9Beschreiben Sie die Rule of Zero, die Rule of Three und die Rule of Five und wann sie jeweils gelten.
Rule of Zero: Bevorzugen Sie Klassen, die keine eigenen Destruktor-/Kopier-/Verschiebeoperationen deklarieren; lassen Sie RAII-Member wie std::string, std::vector, std::unique_ptr, Datei-/Socket-Wrapper usw. Ressourcen verwalten. Rule of Three: Wenn eine Klasse eine Ressource manuell verwaltet und einen eigenen Destruktor, Kopierkonstruktor oder Kopierzuweisungsoperator benötigt, benötigt sie üblicherweise alle drei, um korrektes Kopier- und Besitzverhalten zu definieren. Rule of Five: In C++11 und später sollten solche Typen zusätzlich Verschiebekonstruktor und Verschiebezuweisungsoperator berücksichtigen. Verwenden Sie die Rule of Zero für die meisten Anwendungstypen; verwenden Sie die Rule of Three/Five, wenn der Typ direkt eine Ressource besitzt oder nichttriviale Besitz-/Lebenszeitsemantik hat.
10Beschreiben Sie C++-Initialisierungsformen und häufige Fallstricke, einschließlich initializer_list und Aggregaten.
C++ kennt mehrere Initialisierungsformen. Default-Initialisierung, etwa `T x;`, ruft bei Klassentypen einen Default-Konstruktor auf, lässt automatische fundamentale Variablen jedoch uninitialisiert. Value-Initialisierung, etwa `T x{};` oder `T()`, zero-initialisiert wo anwendbar vor der Konstruktor-/Member-Initialisierung. Listeninitialisierung verwendet geschweifte Klammern, lehnt Narrowing-Konvertierungen ab und hat besondere Regeln zur Überladungsauflösung, einschließlich einer starken Präferenz für viable `std::initializer_list`-Konstruktoren. Aggregatinitialisierung initialisiert Aggregatmember direkt mit geschweiften Klammern; C++20 unterstützt außerdem Designated Initializers für Aggregate, in Deklarationsreihenfolge. Häufige Fallstricke sind uninitialisierte lokale Skalare, überraschende Auswahl von `initializer_list`-Überladungen, Narrowing-Fehler mit geschweiften Klammern, das Most Vexing Parse mit runden Klammern und Verhaltensänderungen, wenn ein Typ aufhört, ein Aggregat zu sein.
11Was ist undefiniertes Verhalten in C++, und wie kann es in Produktionsvorfällen im Backend auftreten?
Undefiniertes Verhalten ist Verhalten, für das der C++-Standard nach dem Auftreten einer ungültigen Operation keine Anforderungen stellt. Das Programm kann scheinbar funktionieren, abstürzen, Daten beschädigen, Sicherheitslücken offenlegen oder zu überraschendem Verhalten wegoptimiert werden. Compiler nehmen an, dass UB nicht passiert, und optimieren auf Grundlage dieser Annahme; daher können Probleme nur in Release-Builds oder unter Produktionsverkehr auftreten. Backend-Vorfälle können durch hängende Zeiger/Referenzen, use-after-free, Verletzungen der Objektlebenszeit, Zugriff außerhalb von Grenzen, Überlauf vorzeichenbehafteter Ganzzahlen, Data Races, ungültige Casts, Lesen uninitialisierter Werte, doppelte Freigaben oder strict-aliasing-Verletzungen entstehen. Gegenmaßnahmen umfassen RAII und klares Besitz-/Lebenszeitdesign, sicherere Abstraktionen und Bounds-Checks, Tests/Fuzzing, Code-Review, statische Analyse und Sanitizers wie ASan, UBSan und TSan.
12Erklären Sie virtuelle Funktionen, vtables, Kosten des dynamischen Dispatch, Object Slicing und Gefahren virtueller Destruktoren.
Eine virtuelle Funktion ermöglicht Laufzeit-Polymorphie: Wird sie über einen Basiszeiger oder eine Basisreferenz aufgerufen, wird die Implementierung des dynamischen Typs des Objekts gewählt. Die meisten Implementierungen speichern in jedem polymorphen Objekt einen versteckten vptr, der auf eine vtable mit Adressen der virtuellen Funktionen dieses dynamischen Typs zeigt. Die typischen Kosten sind ein zusätzlicher Zeiger im Objekt, ein indirekter Aufruf, mögliche Cache-/Branch-Prediction-Auswirkungen und weniger Inlining-Möglichkeiten, obwohl Compiler manchmal devirtualisieren können. Object Slicing entsteht, wenn ein abgeleitetes Objekt by value als Basisobjekt kopiert oder gespeichert wird und dabei der abgeleitete Teil und das dynamische Verhalten verloren gehen. Soll eine Basisklasse über einen Basiszeiger gelöscht werden, muss ihr Destruktor virtuell sein; andernfalls hat das Löschen eines abgeleiteten Objekts über diesen Basiszeiger undefiniertes Verhalten.
13Beschreiben Sie Uhren, Zeitpunkte und Zeitdauern in std::chrono für Service-Timeouts, Metriken und Zeitstempel.
`std::chrono` modelliert Zeit mit Uhren, `time_point`s und `duration`s. Eine `duration` ist ein Intervall mit einer Einheit, etwa Millisekunden oder Sekunden. Ein `time_point` ist ein Punkt auf der Zeitachse einer bestimmten Uhr. `steady_clock` ist monoton und sollte für verstrichene Zeit, Service-Timeouts, Deadlines und Latenzmessungen verwendet werden, weil sie von Änderungen der Wanduhrzeit nicht beeinflusst wird. `system_clock` repräsentiert zivile/Wanduhrzeit und eignet sich für Zeitstempel, Logging, Persistenz und Kalenderumrechnungen, kann aber springen, wenn die Systemzeit angepasst wird. Timeouts sollten in der Regel auf `steady_clock::now() + duration` basieren; Maschinen-Zeitstempel sollten ein klar dokumentiertes Wanduhr-Format verwenden, typischerweise UTC an API-/Speichergrenzen.
14Beschreiben Sie std::format und moderne Formatierungs-Facilities im Vergleich zu Iostreams und printf-artigen APIs.
`std::format` ist die typsichere Formatierungs-Facility aus C++20, inspiriert von fmtlib. Sie nutzt `{}`-Ersetzungsfelder und Formatspezifikationen, um formatierten Text zu erzeugen – ohne die zustandsbehaftete Insertions-Syntax von Iostreams oder C-Varargs im `printf`-Stil. Gegenüber `printf` vermeidet sie viele Format-/Typ-Mismatch-Probleme; gegenüber Iostreams ist sie oft klarer und leichter zu komponieren. fmtlib ist die weit verbreitete Library, die `std::format` voranging und beeinflusste und möglicherweise breitere Unterstützung oder neuere Features bietet. Benutzerdefinierte Typen können über Custom-Formatter-Unterstützung formatiert werden. Beim High-Volume-Logging hängt die Performance davon ab, unnötige Formatierung, Konvertierungen und Allokationen zu vermeiden, besonders bei deaktivierten Log-Levels; APIs, die Formatierung verzögern oder zuerst den Log-Level prüfen, sind vorzuziehen.
15Erklären Sie C++-Wertkategorien und perfektes Forwarding und warum sie für effiziente generische APIs wichtig sind.
C++-Wertkategorien beschreiben Ausdrücke: lvalues haben Identität und können nach dem Ausdruck weiter referenziert werden; prvalues sind reine rvalues wie viele temporäre oder berechnete Werte; xvalues sind auslaufende Objekte, deren Ressourcen wiederverwendet werden können. Perfektes Forwarding ist die Template-Technik, eine Forwarding-Referenz zu nehmen, typischerweise T&&, wobei T deduziert wird, und mit std::forward<T>(arg) weiterzuleiten, sodass die Wertkategorie des Aufrufers erhalten bleibt: lvalues bleiben lvalues und rvalues bleiben rvalues. Referenzkollapsregeln machen dies möglich. Das ist für generische Backend-APIs wichtig, weil Wrapper, Fabriken, Dispatcher und Funktionen im emplace-Stil unnötige Kopien vermeiden sowie Überladungsauswahl und Verschiebeverhalten erhalten können.
16Beschreiben Sie Deadlock-Vermeidung und Strategien zum Sperren mehrerer Mutexes mit std::scoped_lock und std::lock.
Deadlock wird vermieden, indem zirkuläres Warten verhindert wird: Locks nach Möglichkeit in einer konsistenten globalen Reihenfolge erwerben, oder mehrere Mutexes mit `std::lock`/`std::scoped_lock` erwerben, die einen Deadlock-Vermeidungsalgorithmus nutzen. `std::scoped_lock lock(a, b, ...)` ist die einfachste RAII-Form zum Sperren mehrerer Mutexes und entsperrt sie automatisch beim Verlassen des Scopes. Mit `std::lock` zuerst die Mutexes sperren, dann RAII-Wrapper mit `std::adopt_lock` anbinden, oder `std::unique_lock` mit `std::defer_lock` verwenden. Kritische Abschnitte kurz halten und blockierende Operationen oder unbekannte Callbacks während gehaltenen Locks vermeiden; Deadlock-Vermeidung garantiert nicht automatisch Fairness und verhindert nicht alle Livelock-/Starvation-Szenarien.
17Erklären Sie Nutzungsmuster von condition_variable beim Warten auf Zustandsänderungen.
Verwenden Sie `std::condition_variable`, um auf die Änderung eines durch einen Mutex geschützten Zustandsprädikats zu warten. Der gemeinsame Zustand wird unter demselben Mutex geändert, anschließend weckt `notify_one` oder `notify_all` wartende Threads. Wartende sollten `cv.wait(lock, predicate)` oder eine gleichwertige Schleife verwenden, weil Aufweckungen spurios sein können und Benachrichtigungen nicht unabhängig vom Prädikatszustand gemerkt werden. `notify_one` weckt einen Wartenden; `notify_all` weckt alle Wartenden und eignet sich für Broadcast-Zustandsänderungen wie Shutdown.
18Entwerfen Sie eine threadsichere Bounded Queue mit Standard-C++-Primitiven und definieren Sie die API-Garantien.
Eine begrenzte threadsichere Queue lässt sich mit einem `std::mutex`, Condition Variables, einem Container mit fester Kapazität und einem Shutdown-/Closed-Flag bauen. `push` blockiert, timed out oder schlägt fehl, wenn die Queue voll ist, und erzeugt so Backpressure; `pop` blockiert, wenn sie leer ist. Beide Operationen sollten auf Prädikaten wie `size < capacity || closed` und `!empty || closed` warten. Bei Shutdown/Close werden blockierte Produzenten und Konsumenten aufgeweckt; neue Pushes werden abgelehnt, und es ist festzulegen, ob Konsumenten vorhandene Elemente drainen oder sofort stoppen. Die API sollte Blockierverhalten, Close-Semantik, Rückgabewerte und Nebenläufigkeitsgarantien spezifizieren.
19Wie führst du eine Schema-Migration für einen C++-Service ohne Downtime durch?
Verwende eine gestufte Expand-Contract-Migration. Füge zuerst rückwärtskompatible Schema-Erweiterungen hinzu, etwa nullable Columns oder neue Tabellen, die den aktuell laufenden C++-Service nicht brechen. Deploye kompatiblen Code, der sowohl alte als auch neue Repräsentationen verarbeiten kann, fülle bestehende Daten in kleinen gedrosselten Batches nach, validiere die Konsistenz, schalte Reads auf das neue Schema um und entferne alte Columns oder Code erst später, nachdem alle deployten Versionen nicht mehr davon abhängen. Vermeide bei großen Tabellen lange blockierende DDL, nutze Online-/Concurrent-Operationen, wo unterstützt, überwache Locks/Replikation/Ressourcen und halte Rollback-Pfade bereit, die mit teilweise deploytem Code und teilweise migrierten Daten funktionieren.
20Wie sollte ein C++-Service aus Datenbank-Replikas lesen und dabei veraltete Reads sowie Read-your-writes-Garantien managen?
Behandle Replica-Reads als expliziten Konsistenz-Trade-off. Reads, die Staleness tolerieren können, dürfen an gesunde Replikas gehen, aber Read-your-writes-, transaktionale oder freshness-kritische Reads sollten an den Primary gehen, sofern nicht bekannt ist, dass die gewählte Replica die relevante Schreibposition wie LSN, Timestamp oder Version bereits nachgespielt hat. Verfolge Replica-Lag und Health, kodiere die erforderliche Konsistenz pro Endpoint oder Request und falle auf den Primary zurück, warte auf Catch-up oder schlage fehl, wenn Replikas die erlaubte Staleness überschreiten. Dokumentiere das Konsistenzmodell, damit Aufrufer wissen, welche Reads veraltet sein können.