Middle-C++-Vorbereitung

Interviewfragen für Middle-C++-Backend-Entwickler

15 ausgewählte Fragen für Middle-C++-Entwickler, die Performance, Ressourcenverwaltung und technische Kompromisse besprechen müssen.

Middle-C++-KI-Interview startenKeine Kreditkarte erforderlich. 1 kostenlose Session verfügbar.
Technische Interviews auf Englisch übenEin Modus, in dem Nicht-Muttersprachler technische Interviews üben können.

Speicherverwaltung

1Wie 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.

Diese Frage mit einem KI-Coach beantworten

2Erklä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.

Diese Frage mit einem KI-Coach beantworten

Templates

3Erklä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.

Diese Frage mit einem KI-Coach beantworten

Typsystem

4Vergleichen 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.

Diese Frage mit einem KI-Coach beantworten

5Vergleichen Sie std::variant mit vererbungsbasiertem Polymorphismus zur Modellierung heterogener Nachrichten oder Events.

std::variant ist ein Werttyp, der genau eine Alternative aus einer festen, geschlossenen Menge von Typen hält und üblicherweise mit std::visit oder expliziten Typabfragen behandelt wird. Es ist nützlich für Protokollnachrichten oder Events, wenn die Menge der Nachrichtenarten bekannt ist und typsichere Behandlung ohne virtuellen Dispatch und oft ohne Heap-Allokation pro Objekt gewünscht ist. Vererbungsbasierter Polymorphismus nutzt eine Basisklasse und virtuelle Funktionen zur Dispatch über eine gemeinsame Schnittstelle; er ist besser, wenn die Menge abgeleiteter Nachrichtentypen offen, unabhängig erweiterbar, plugin-artig oder hinter einer stabilen Schnittstelle verborgen ist. Variant begünstigt geschlossene Summentypen, Lokalität und Compile-Time-Prüfung; Vererbung begünstigt Erweiterbarkeit, Laufzeitpolymorphismus und schnittstellenbasiertes Design.

Diese Frage mit einem KI-Coach beantworten

Sprachsemantik

6Was 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.

Diese Frage mit einem KI-Coach beantworten

7Unterscheiden Sie undefined, unspecified und implementation-defined behavior anhand backend-relevanter Beispiele.

Undefined behavior bedeutet, dass der C++-Standard keine Anforderungen stellt; mögliche Folgen sind Abstürze, Datenkorruption, Sicherheitslücken oder vom Optimizer abhängige Fehlkompilierung. Beispiele sind Out-of-Bounds-Zugriff, Use-after-free, signed Integer Overflow und Data Races. Unspecified behavior bedeutet, dass der Standard mehr als ein Ergebnis zulässt und die Implementierung nicht dokumentieren muss, welches gewählt wird; ein häufiges Beispiel ist die Auswertungsreihenfolge vieler Funktionsargumente, sodass Code nicht von Side Effects in einer bestimmten Reihenfolge abhängen sollte. Implementation-defined behavior bedeutet, dass die Implementierung das Verhalten wählen und dokumentieren muss; Beispiele sind die Signedness von plain `char`, Größen/Bereiche einiger fundamentaler Typen innerhalb der Standardgrenzen sowie einiges Verhalten bei signed Right Shift. Im Backend-Code ist UB ein Korrektheits-/Sicherheitsrisiko, während unspecified und implementation-defined behavior Portabilitätsrisiken sind, die in Protokoll-, Speicher- und plattformübergreifender Logik vermieden oder isoliert werden sollten.

Diese Frage mit einem KI-Coach beantworten

Fehlerbehandlung

8Erklären Sie die Stufen der Exception Safety und wie noexcept Move-Operationen, Container und Codegenerierung beeinflusst.

Exception-Safety-Stufen beschreiben, was gilt, wenn eine Operation wirft. Die basic guarantee bedeutet, dass Invarianten erhalten bleiben und Ressourcen nicht leaken, der Zustand sich aber geändert haben kann. Die strong guarantee bedeutet Commit-oder-Rollback-Semantik: bei Fehlschlag ist der beobachtbare Zustand unverändert. Die nothrow guarantee bedeutet, dass die Operation nicht wirft. Übliche Techniken sind RAII, Arbeit auf Temporaries, Copy-and-Swap und die Anordnung von Mutationen so, dass der Commit erst nach erfolgreicher werfender Arbeit erfolgt. `noexcept` ist ein Vertrag: wirft eine `noexcept`-Funktion, wird `std::terminate` aufgerufen. Es beeinflusst auch generischen Code und Container: etwa kann `std::vector` bei Reallocation Elemente verschieben, wenn der Move-Konstruktor `noexcept` ist; andernfalls wird oft kopiert, häufig über `std::move_if_noexcept`, um Exception-Garantien zu erhalten. `noexcept` kann auch Codegenerierung oder Optimierung erleichtern, indem benötigte Exception-Propagationspfade reduziert werden – aber nur, wenn das Versprechen korrekt ist.

Diese Frage mit einem KI-Coach beantworten

9Erklären Sie std::expected oder gleichwertige Outcome-Typen als Alternativen zu Exceptions für fallible Operationen.

`std::expected<T, E>` repräsentiert entweder einen erfolgreichen Wert `T` oder einen expliziten Fehler `E` und macht Fehlschlag zum Teil des Rückgabetyps der Funktion, statt auf Exception-Propagation zu setzen. Es eignet sich für fallible Operationen, bei denen Fehler erwartet werden und Aufrufer sie lokal behandeln sollen, etwa Parsing, Validierung, Netzwerkaufrufe, Storage-Lookups und retry-fähige Backend-Operationen. Der Fehlertyp sollte strukturierte Informationen tragen, etwa Fehlercode, Kategorie, Retryability, Nachricht oder HTTP/RPC-Mapping. Expected-/Outcome-Typen komponieren durch Prüfen und Propagieren von Fehlern; in C++23-artigen APIs können monadishe Operationen wie `and_then`, `transform` und `or_else` Schritte verketten, ohne tief verschachtelte Bedingungen. Im Vergleich zu Exceptions machen sie Kontrollfluss und API-Verträge explizit und funktionieren gut über exception-freie oder ABI-Grenzen hinweg; Exceptions können je nach Projektpolitik weiterhin für seltene, nicht-lokale oder wirklich außergewöhnliche Fehler geeignet sein.

Diese Frage mit einem KI-Coach beantworten

Objektlebensdauer

10Beschreiben Sie die Regeln zur Objektlebensdauer für automatische, dynamische, temporäre und asynchron referenzierte Objekte.

Automatische Objekte leben von der Konstruktion bis zum Ende ihres Gültigkeitsbereichs; Zeiger oder Referenzen darauf werden dangling, sobald dieser Scope verlassen wird. Dynamische Objekte leben von der Allokation/Konstruktion, bis sie explizit zerstört werden oder ein besitzendes Objekt sie zerstört; rohe Zeiger und Referenzen verlängern die Lebensdauer nicht. Temporäre leben üblicherweise bis zum Ende des full expression, mit spezifischen Regeln zur Lebensdauerverlängerung bei Bindung an Referenzen, aber nicht in jedem Anwendungsfall. Asynchrone Arbeit wie Callbacks, Threads, Timer oder Coroutinen kann laufen, nachdem das referenzierte Objekt zerstört wurde; Captures und gespeicherte Referenzen brauchen daher explizites Lifetime-Management, um dangling references und Use-after-free zu vermeiden.

Diese Frage mit einem KI-Coach beantworten

11Beschreiben Sie Probleme der statischen Initialisierungsreihenfolge und wie constinit, Magic Statics und Dependency Injection sie abmildern.

Probleme der statischen Initialisierungsreihenfolge entstehen, weil dynamisch initialisierte Objekte auf Namespace-Ebene oder static-Objekte in verschiedenen Übersetzungseinheiten eine unspezifizierte relative Initialisierungsreihenfolge haben. Ein Global kann ein anderes nutzen, bevor es konstruiert wurde; die Zerstörungsreihenfolge kann beim Herunterfahren ähnliche Probleme erzeugen. constinit erzwingt, dass eine static- oder thread_local-Variable statische/konstante Initialisierung hat, sonst ist das Programm ill-formed, und vermeidet so die Abhängigkeit von dynamischer Initialisierungsreihenfolge für diese Variable. Magic Statics bzw. funktionslokale Statics werden bei der ersten Verwendung initialisiert und sind seit C++11 threadsicher. Dependency Injection vermeidet versteckte globale Abhängigkeiten, indem Objekte in kontrollierter Reihenfolge konstruiert und Abhängigkeiten explizit übergeben werden.

Diese Frage mit einem KI-Coach beantworten

12Was sind Copy Elision, NRVO und RVO, und wann kann man sich darauf verlassen?

Copy Elision bedeutet, ein Objekt direkt an seinem endgültigen Ziel zu konstruieren, statt ein separates Temporäres zu erzeugen und es zu kopieren oder zu moven. RVO bezieht sich üblicherweise auf die Rückgabe eines unbenannten Temporären oder prvalue wie return T{}; in C++17 und später sind viele dieser Fälle verpflichtend, weil der prvalue das Ergebnisobjekt direkt initialisiert. NRVO ist die Rückgabe eines benannten lokalen Objekts wie return x; der Compiler darf dieses Local direkt im Return-Slot des Aufrufers konstruieren, aber das ist nicht garantiert. Man kann sich auf verpflichtende C++17-prvalue-Elision in den spezifizierten Fällen verlassen, nicht aber darauf, dass NRVO immer stattfindet; vermeiden Sie std::move auf einem benannten lokalen Rückgabewert, weil das NRVO verhindern kann.

Diese Frage mit einem KI-Coach beantworten

Standardbibliothek

13Wie beeinflussen die Anforderungen an Comparator, Equality und Hasher die Korrektheit assoziativer Container?

Assoziative Container stützen sich auf Vergleich, Equality und Hashing, um Schlüsselidentität zu definieren und ihre internen Invarianten zu wahren. Geordnete Container wie std::map erfordern, dass der Comparator eine strikte schwache Ordnung (strict weak ordering) auferlegt; Schlüssel gelten als äquivalent, wenn keiner kleiner als der andere ist, nicht zwingend über operator==. Ungeordnete Container erfordern, dass Equality eine Äquivalenzrelation ist, und je zwei als gleich betrachtete Schlüssel denselben Hash-Wert erzeugen müssen. Schlüssel dürfen im gespeicherten Zustand nicht so mutiert werden, dass sich ihre Ordnung, Equality oder ihr Hash ändert, weil Lookup, erase und Eindeutigkeitsverhalten sonst inkorrekt werden können.

Diese Frage mit einem KI-Coach beantworten

14Erklären Sie heterogenen Lookup in geordneten und ungeordneten assoziativen Containern.

Heterogener Lookup erlaubt es, einen assoziativen Container mit einem anderen Typ als dem Schlüsseltyp zu durchsuchen und so den Aufbau temporärer Schlüssel zu vermeiden. In geordneten Containern erfordert das einen transparenten Comparator, zum Beispiel std::less<> oder einen benutzerdefinierten Comparator mit is_transparent-Marker. In ungeordneten Containern sind sowohl transparente Hash- als auch Equality-Funktoren nötig, die Schlüsseltyp und Lookup-Typ konsistent behandeln. Ein häufiges Backend-Beispiel ist ein mit std::string geschlüsselter Container, der mit std::string_view oder const char* durchsucht werden kann, ohne ein temporäres std::string zu allokieren.

Diese Frage mit einem KI-Coach beantworten

Nebenläufigkeit

15Erklären Sie das C++-Speichermodell: Data Races, Happens-before und Synchronisationsgarantien.

Das C++-Speichermodell legt fest, wann Operationen in verschiedenen Threads geordnet sind und wann Schreibvorgänge sichtbar werden. Ein Data Race entsteht, wenn zwei Threads gleichzeitig auf dieselbe Speicherstelle zugreifen, mindestens ein Zugriff ein Schreibzugriff ist und die Zugriffe nicht durch Happens-before geordnet oder anderweitig durch atomare Operationen abgesichert sind; ein Data Race führt zu undefiniertem Verhalten. Happens-before ist die Ordnungsrelation, die frühere Seiteneffekte für spätere Operationen sichtbar macht. Synchronisationsoperationen erzeugen diese Ordnung: Zum Beispiel synchronisiert das Unlock eines Mutex mit einem späteren erfolgreichen Lock desselben Mutex (synchronizes-with), und geeignete atomare Release/Acquire-Operationen können zwischen Threads synchronisieren. Korrekte Programme nutzen Mutexes, Atomics oder andere Synchronisation, um Happens-before für gemeinsamen Zustand herzustellen.

Diese Frage mit einem KI-Coach beantworten