Content from Programmer Lao Liao:
https://space.bilibili.com/3494351095204205
1.Please explain which memory area each variable in the following code is stored in and why.【Tencent – Backend Development】
#include <iostream>const int g_const = 10; // Constant areaint g_var = 20; // .data sectionstatic int s_var = 30; // .data sectionchar* p_str = "Hello"; // p_str in .data section, "Hello" in constant areavoid memory_layout_demo() { static int local_s_var = 40; // .data section int local_var = 50; // Stack const int local_const = 60; // Stack int* heap_var = new int(70); // heap_var in stack, pointing to heap memory char arr[] = "World"; // Stack (array allocated on stack) std::cout << "g_const: " << &g_const << std::endl; std::cout << "g_var: " << &g_var << std::endl; std::cout << "s_var: " << &s_var << std::endl; std::cout << "p_str: " << &p_str << " -> " << (void*)p_str << std::endl; std::cout << "local_s_var: " << &local_s_var << std::endl; std::cout << "local_var: " << &local_var << std::endl; std::cout << "local_const: " << &local_const << std::endl; std::cout << "heap_var: " << &heap_var << " -> " << heap_var << std::endl; std::cout << "arr: " << &arr << std::endl; delete heap_var;}int main() { memory_layout_demo(); return 0;}// Compile and run: g++ -std=c++11 memory_layout.cpp -o memory_layout
Reference Answer::
-
g_const:Stored in the constant area, as it is a const global constant.
-
g_var:Stored in the .data section, initialized global variable.
-
s_var:Stored in the .data section, static global variable.
-
p_str:Pointer itself in the .data section, points to the string “Hello” in the constant area.
-
local_s_var:Stored in the .data section, static local variable.
-
local_var:Stored in the stack, normal local variable.
-
local_const:Stored in the stack, const local variable.
-
heap_var:Pointer itself in the stack, points to the memory address in the heap.
-
arr:Stored in the stack, array allocated on the stack.
2. Why does calling virtual functions in constructors and destructors not result in polymorphism? Please explain from the perspective of vptr initialization timing.
Reference Answer::
In the constructor, vptr initialization occurs before the constructor body executes. When the base class constructor executes, vptr points to the base class’s virtual function table, so the called virtual function is the base class’s version. Even though subsequent derived class constructors will reset vptr to point to the derived class’s virtual function table, the polymorphic mechanism has not been fully established during the execution of the base class constructor.
Similarly, in the destructor, when the derived class’s destructor has finished executing, vptr is reset to point to the base class’s virtual function table, so when calling virtual functions in the base class destructor, only the base class’s version can be called.
This is a safety mechanism to ensure that during the incomplete state of object construction and destruction, members of the derived class that have not yet been initialized or have already been destroyed are not called.
3. Please implement a simple RAII wrapper class to manage memory allocated with malloc, ensuring that memory does not leak.
【Baidu – Intelligent Driving】
#include <iostream>#include <cstdlib>class MallocRAII {public: // Constructor, allocates memory of specified size explicit MallocRAII(size_t size) : ptr_(malloc(size)) { if (!ptr_) { throw std::bad_alloc(); } std::cout << "Allocated " << size << " bytes at " << ptr_ << std::endl; } // Get raw pointer void* get() const { return ptr_; } // Overload -> operator for easy access void* operator->() const { return ptr_; } // Disable copy MallocRAII(const MallocRAII&) = delete; MallocRAII& operator=(const MallocRAII&) = delete; // Allow move MallocRAII(MallocRAII&& other) noexcept : ptr_(other.ptr_) { other.ptr_ = nullptr; } MallocRAII& operator=(MallocRAII&& other) noexcept { if (this != &other) { free(ptr_); ptr_ = other.ptr_; other.ptr_ = nullptr; } return *this; } // Destructor, releases memory ~MallocRAII() { if (ptr_) { std::cout << "Freeing memory at " << ptr_ << std::endl; free(ptr_); } }private: void* ptr_;};void malloc_raii_demo() { try { MallocRAII memory(100); // Allocate 100 bytes // Use memory int* data = static_cast<int*>(memory.get()); data[0] = 42; std::cout << "Data: " << data[0] << std::endl; // Memory will be automatically released when going out of scope } catch (const std::bad_alloc& e) { std::cerr << "Memory allocation failed: " << e.what() << std::endl; }}int main() { malloc_raii_demo(); return 0;}// Compile and run: g++ -std=c++11 malloc_raii.cpp -o malloc_raii
Scoring Points::
-
Allocate memory in the constructor, release in the destructor
-
Handle allocation failure (throw exception)
-
Disable copy construction and copy assignment (avoid double free)
-
Provide move semantics support
-
Provide a method to access the raw pointer
-
Exception safety guarantee
4. Please explain the difference between const and constexpr, and explain when to use constexpr instead of const.
【Tencent – WeChat Backend】
Reference Answer::
Difference::
-
Semantic difference::constmeans “read-only”, whileconstexprmeans “compile-time constant”.
-
Calculation timing::constcan be calculated at runtime, whileconstexprmust be calculated at compile time.
-
Application scope::constcan modify variables, function parameters, member functions, etc., whileconstexprmainly modifies variables and functions.
-
C++ version::constcomes from C language, whileconstexprwas introduced in C++11.
When to use constexpr::
-
When compile-time constants are needed (array size, template parameters, case labels, etc.)
-
To define mathematical constants that can be computed at compile time
-
To construct objects known at compile time
-
For metaprogramming and template calculations
When to use const::
-
Runtime constants
-
Function parameter protection
-
const member functions
-
Constantity modifiers for pointers and references
5. Please explain the difference between volatile and atomic, and explain when to use volatile instead of atomic.
Reference Answer::
Difference::
-
Semantic difference::volatileprevents compiler optimization, ensuring every access reads and writes from memory; whileatomicensures atomicity of operations.
-
Thread safety::volatiledoes not guarantee atomicity, whileatomicguarantees atomicity.
-
Memory order::volatiledoes not guarantee memory order, whileatomicprovides memory order control.
-
Applicable scenarios::volatileis used for hardware registers, memory-mapped IO, etc.; whileatomicis used for multithreaded synchronization.
When to use volatile::
-
Accessing memory-mapped hardware registers
-
Variables modified by signal handlers
-
Shared memory modified by other CPUs in a multicore system
-
Preventing the compiler from optimizing away “ineffective” code
When to use atomic::
-
Data sharing and synchronization between threads
-
Counters, flags, etc., that require atomic operations
-
Implementing lock-free data structures
-
Scenarios requiring memory order control
Important Note: In modern C++ multithreaded programming, prefer usingatomicinstead ofvolatileto ensure thread safety.
6. Please explain the difference between std::uncaught_exceptions() and std::uncaught_exception(), and explain how to use them in destructors to determine the exception exit status.
Reference Answer::
Difference Analysis:
-
std::uncaught_exception(): Introduced in C++98, returns a bool indicating whether there are any uncaught exceptions.
-
std::uncaught_exceptions(): Introduced in C++17, returns an int indicating the number of currently uncaught exceptions.
Application in Destructors:
class SmartResource {public: ~SmartResource() noexcept { const int uncaught_count = std::uncaught_exceptions(); const bool normal_exit = (uncaught_count == 0); try { if (normal_exit) { // Normal exit: perform full cleanup complete_cleanup(); } else { // Exception exit: perform minimal safe cleanup minimal_cleanup(); } } catch (...) { // Log but never rethrow std::cerr << "Cleanup operation failed" << std::endl; } }private: void complete_cleanup() { std::cout << "Performing complete resource cleanup" << std::endl; } void minimal_cleanup() noexcept { std::cout << "Performing minimal safe cleanup" << std::endl; }};// Usage Examplevoid exception_aware_demo() { try { SmartResource resource; throw std::runtime_error("Test exception"); } catch (const std::exception& e) { std::cout << "Caught exception: " << e.what() << std::endl; }}
Best Practices:
-
Use std::uncaught_exceptions() (C++17+) to get the exact number of exceptions.
-
Choose different cleanup strategies based on exception status in destructors.
-
Perform full cleanup on normal exit and minimal safe cleanup on exception exit.
-
All cleanup operations should be in a try-catch block to ensure no exceptions are thrown.
7. Please explain the priority order of C++ function overload resolution and explain in what cases ambiguity in overload resolution may occur.
Reference Answer::
Priority Order of Overload Resolution:
-
Exact match: Parameters are exactly the same.
-
Type promotion: char→int, float→double, etc.
-
Standard conversion: int→double, derived class→base class, etc.
-
User-defined conversion: through conversion constructors or conversion operators.
-
Variadic parameters: worst match.
Ambiguity Scenarios:
void ambiguous(int a, double b) {}void ambiguous(double a, int b) {}void test() { ambiguous(1, 2); // Ambiguous: both functions require one conversion}class ConversionAmbiguity {public: operator int() const { return 42; } operator double() const { return 3.14; }};void process(int x) {}void process(double x) {}void test2() { ConversionAmbiguity obj; process(obj); // Ambiguous: both conversion paths have the same priority}
Resolution Strategies::
-
Explicit type conversion: ambiguous(1, static_cast<double>(2))
-
Use static_cast to select the conversion path.
-
Redesign function signatures to avoid ambiguity.
8. Please explain the process of template instantiation in C++, and the difference between explicit instantiation and implicit instantiation.
Reference Answer::
Template Instantiation Process:
-
Syntax check: Check the correctness of the template syntax.
-
Parameter deduction: Deduce template parameters based on the call.
-
Code generation: Generate code by replacing template parameters with concrete types.
-
Compilation optimization: Optimize the generated code.
Explicit Instantiation vs Implicit Instantiation:
// Template definitiontemplate<typename T>class DataProcessor {public: void process(T data) { std::cout << "Processing: " << data << std::endl; }};// Explicit instantiation (in header file)extern template class DataProcessor<int>; // Declarationextern template class DataProcessor<double>; // Declaration// In source filetemplate class DataProcessor<int>; // Explicit instantiation definitiontemplate class DataProcessor<double>; // Explicit instantiation definitionvoid usage_example() { DataProcessor<int> processor1; // Using explicit instantiation DataProcessor<double> processor2; // Using explicit instantiation DataProcessor<std::string> processor3; // Implicit instantiation}
Comparison::
-
Implicit instantiation: The compiler automatically instantiates as needed, which may lead to code bloat.
-
Explicit instantiation: The programmer explicitly specifies instantiation, reducing compilation time and controlling code generation.
9. Please explain the difference between lvalues, rvalues, and xvalues in C++, and give examples of how to determine the value category of an expression.【Tencent – WeChat Backend】
Reference Answer::
Value Category Differences:
-
Lvalue: An expression with an identifier, can take an address, persists. Examples: variable names, string literals, function calls returning lvalue references.
-
Rvalue: Temporary objects, literals (except string literals), function calls returning non-reference types.
-
Xvalue: An expression with an identifier that is about to be moved, a subset of rvalues.
Determination Methods:
// 1. Address testint x = 42;&x; // Valid → Lvalue// &100; // Invalid → Rvalue// 2. Assignment testx = 100; // Valid → Lvalue// 100 = x; // Invalid → Rvalue// 3. std::move teststd::move(x); // Xvalue// 4. Function overload determinationvoid func(int&); // Accepts Lvaluevoid func(int&&); // Accepts Rvalueint y = 10;func(y); // Calls Lvalue versionfunc(10); // Calls Rvalue version
10. Please explain why move constructors and move assignment operators need to be marked as noexcept, and explain the consequences of not marking them.
Reference Answer::
Importance of noexcept:
-
Standard library optimization: If the move operation is noexcept, containers like std::vector and std::deque will use move instead of copy when reallocating memory.
-
Exception safety: Move operations should generally not fail, marking noexcept provides compile-time guarantees.
-
Performance guarantee: Avoids the overhead of exception checks during move operations.
Consequences of not marking noexcept:
std::vector<MyClass> vec;// ...vec.push_back(MyClass()); // May need to expand// If MyClass's move constructor is not noexcept:// 1. vector will use copy constructor instead of move constructor// 2. Performance drops, especially for large objects// 3. May lose the advantages of move semantics
Correct Practice:
class MyClass {public: // Move constructor must be noexcept MyClass(MyClass&& other) noexcept : data_(std::move(other.data_)) { other.data_ = nullptr; } // Move assignment operator must be noexcept MyClass& operator=(MyClass&& other) noexcept { if (this != &other) { delete[] data_; data_ = other.data_; other.data_ = nullptr; } return *this; }private: char* data_;};
11. Please implement a simplified version of std::move and explain its working principle.
Reference Answer::
#include <type_traits>// Simplified std::move implementationtemplate<typename T>constexpr typename std::remove_reference<T>::type&& my_move(T&& arg) noexcept { // 1. Use std::remove_reference to remove reference modifiers // 2. Add && to indicate rvalue reference // 3. Use static_cast for unconditional conversion // 4. noexcept guarantee not to throw exceptions return static_cast<typename std::remove_reference<T>::type&&>(arg);}// Usage Examplevoid my_move_demo() { int x = 42; const int cx = 100; // Test various cases int&& r1 = my_move(x); // T = int& → int&& int&& r2 = my_move(123); // T = int → int&& const int&& r3 = my_move(cx); // T = const int& → const int&& // Verify effect std::cout << "x: " << x << std::endl; // Still 42 std::cout << "r1: " << r1 << std::endl; // 42}
Working Principle:
-
Template parameter deduction: T&& is a universal reference, deducing the type based on the passed parameter.
-
Remove reference: std::remove_reference<T> removes all reference modifiers.
-
Add rvalue reference: type&& ensures the return type is an rvalue reference type.
-
Static cast: performs a safe type conversion.
12. Please design a test case to demonstrate the performance advantages of move semantics in std::vector, and explain why move semantics can improve performance.
Reference Answer::
Test Case Design::
#include <vector>#include <chrono>#include <iostream>class LargeObject {public: LargeObject() : data_(new int[1000]) { for (int i = 0; i < 1000; ++i) { data_[i] = i; } } // Move constructor LargeObject(LargeObject&& other) noexcept : data_(other.data_) { other.data_ = nullptr; } ~LargeObject() { delete[] data_; } // Disable copy LargeObject(const LargeObject&) = delete; LargeObject& operator=(const LargeObject&) = delete;private: int* data_;};void vector_move_performance_test() { const int iterations = 10000; // Test move semantics auto start_move = std::chrono::high_resolution_clock::now(); std::vector<LargeObject> move_vec; move_vec.reserve(iterations); for (int i = 0; i < iterations; ++i) { LargeObject obj; move_vec.push_back(std::move(obj)); // Move construction } auto end_move = std::chrono::high_resolution_clock::now(); auto move_time = std::chrono::duration_cast<std::chrono::milliseconds>( end_move - start_move); std::cout << "Move semantics took: " << move_time.count() << "ms" << std::endl; std::cout << "Final size of vector: " << move_vec.size() << std::endl;}// Without move semantics, vector needs to frequently reallocate memory and copy objects// Move semantics avoids these overheads by transferring ownership of resources
Performance Improvement Reasons:
-
Avoid deep copies: Move operations only transfer pointers, not copy data.
-
Reduce memory allocations: Avoid repeated memory allocations and releases.
-
Optimize container operations: std::vector uses move instead of copy during expansion.
-
Better cache locality: Reduces memory operations, improving cache hit rates.
13. Please explain how std::unique_ptr implements exclusive ownership and why it is safer than std::auto_ptr.
Reference Answer::
Exclusive Ownership Implementation:
-
Delete copy operations: Copy constructor and copy assignment operator are marked as= delete
-
Support move semantics: Transfer ownership through move constructor and move assignment operator.
-
Explicit ownership transfer: Ownership transfer must be explicitly done usingstd::move to indicate intent.
Advantages over auto_ptr:
// Dangerous behavior of auto_ptr (C++98/03, deprecated)std::auto_ptr<int> ap1(new int(42));std::auto_ptr<int> ap2 = ap1; // Ownership transfer, ap1 becomes null// *ap1; // Runtime error: dereferencing null pointer// Safe behavior of unique_ptrstd::unique_ptr<int> up1(new int(42));// std::unique_ptr<int> up2 = up1; // Compile error: copy constructor deletedstd::unique_ptr<int> up2 = std::move(up1); // Explicit ownership transfer
Safety Features:
-
Compile-time checks: Copy operations are prohibited at compile time.
-
Explicit ownership transfer: Must usestd::move to clarify intent.
-
Better compatibility: Supports array types and custom deleters.
-
Clearer semantics: Clearly indicates exclusive ownership.
14. Please explain the underlying mechanism of C++20 coroutines, including the roles of coroutine handles, promise types, and awaiters.
Reference Answer::
Coroutine Underlying Mechanism:
// 1. Coroutine handle (coroutine_handle)struct CoroutineHandleDemo { std::coroutine_handle<> handle; // Type-erased coroutine handle void resume() { if (handle &&& !handle.done()) { handle.resume(); // Resume coroutine execution } } void destroy() { if (handle) { handle.destroy(); // Destroy coroutine frame } }};// 2. Promise type (promise_type)struct MyPromise { int result_value; // Required interfaces std::suspend_always initial_suspend() { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void unhandled_exception() { std::terminate(); } void return_value(int value) { result_value = value; } // Get return object MyCoroutine get_return_object() { return MyCoroutine{std::coroutine_handle<MyPromise>::from_promise(*this)}; }};// 3. Awaiter (Awaiter)struct MyAwaiter { bool await_ready() const noexcept { return false; } // Is ready void await_suspend(std::coroutine_handle<>) const noexcept {} // Operations on suspend int await_resume() const noexcept { return 42; } // Return value on resume};
Role Explanation:
-
Coroutine handle: Controls coroutine lifecycle (resume, destroy).
-
Promise type: Defines coroutine behavior (initial/final suspension, return value handling).
-
Awaiter: Controls suspension and resumption logic (readiness check, suspend operation, resume value).