16. What is the difference between arrays and pointers?
- Definition and Essence: An array is a collection of elements of the same type, stored contiguously in memory; a pointer is a variable that stores the address of another variable.
- Memory Allocation: The memory allocation for an array is determined at compile time, with a fixed size; a pointer can point to different memory addresses at runtime, and its size is typically 4 bytes (32-bit system) or 8 bytes (64-bit system).
- Access Method: An array accesses elements via an index, such as
<span>arr[i]</span>; a pointer must use the dereference operator<span>*</span>to access the object it points to, such as<span>*ptr</span>. - Operations: Arrays can perform index operations and pointer arithmetic (e.g.,
<span>arr + 1</span>); pointers can perform arithmetic operations (e.g.,<span>ptr + 1</span>) and relational operations (e.g.,<span>ptr1 < ptr2</span>). - Lifecycle: The lifecycle of an array depends on its definition location; local arrays are destroyed at the end of the function, while global arrays are destroyed at the end of the program; the lifecycle of a pointer depends on its definition location and memory allocation method, with dynamically allocated memory needing to be manually released.
17. Discuss the differences between <span>strcpy</span>, <span>sprintf</span>, and <span>memcpy</span>.
- Functionality:
<span>strcpy</span>: Used to copy a string that ends with<span>'\0'</span>.<span>sprintf</span>: Used to format a string, converting various types of data into a string according to a specified format.<span>memcpy</span>: Used to copy a block of memory of any type, without regard to the contents of the memory block.
<span>strcpy</span>:<span>char* strcpy(char* dest, const char* src);</span><span>sprintf</span>:<span>int sprintf(char* str, const char* format,...);</span><span>memcpy</span>:<span>void* memcpy(void* dest, const void* src, size_t n);</span>
<span>strcpy</span>: Does not check the size of the destination buffer, which may lead to buffer overflow.<span>sprintf</span>: May encounter format errors or buffer overflow when formatting strings.<span>memcpy</span>: Requires manual specification of the number of bytes to copy; otherwise, it may lead to out-of-bounds memory access.
18. How are smart pointers implemented? How to use <span>weak_ptr</span>? How to convert <span>weak_ptr</span> to <span>shared_ptr</span>? How to solve the circular reference problem with smart pointers? Is <span>shared_ptr</span> thread-safe?
- Implementation Principles of Smart Pointers:
- std::unique_ptr: Implements exclusive ownership, meaning only one std::unique_ptr can point to a given object at any time. When this pointer is destroyed or reset, it automatically releases the managed object.
#include<memory>
std::unique_ptr<int> ptr = std::make_unique<int>(10);
std::shared_ptr: Implements shared ownership, allowing multiple std::shared_ptr to point to the same object. Through a reference counting mechanism, when the reference count reaches 0, the managed object is automatically released.<span><span>#</span><span><span>include</span></span><span><span><memory></span></span></span><span><span>std</span></span><span>::</span><span><span>shared_ptr</span></span><span><</span><span><span>int</span></span><span>> ptr1 = </span><span><span>std</span></span><span>::make_shared<</span><span><span>int</span></span><span>>(</span><span><span>20</span></span><span>);</span><span><span>std</span></span><span>::</span><span><span>shared_ptr</span></span><span><</span><span><span>int</span></span><span>> ptr2 = ptr1;</span>std::weak_ptr: A weak reference that does not affect the reference count of the object, primarily used to solve the circular reference problem of std::shared_ptr.<span><span>#</span><span><span>include</span></span><span><span><memory></span></span></span><span><span>std</span></span><span>::</span><span><span>shared_ptr</span></span><span><</span><span><span>int</span></span><span>> ptr = </span><span><span>std</span></span><span>::make_shared<</span><span><span>int</span></span><span>>(</span><span><span>30</span></span><span>);</span><span><span>std</span></span><span>::</span><span><span>weak_ptr<</span><span><span>int</span></span><span>> weakPtr = ptr;</span></span>
<span>weak_ptr</span>Usage: Typically used in conjunction with<span>shared_ptr</span>, it can be constructed from a<span>shared_ptr</span>or another<span>weak_ptr</span>, and attempts to obtain the pointed-to object’s<span>shared_ptr</span>using the<span>lock</span>method. If the object has been released,<span>lock</span>returns a null<span>shared_ptr</span>. For example:
std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp(sp);
std::shared_ptr<int> sp2 = wp.lock(); // If the object exists, sp2 points to it; otherwise, it is null.
<span>weak_ptr</span>Conversion to<span>shared_ptr</span>: Call the<span>lock</span>method of<span>weak_ptr</span>; if the object exists, it returns a valid<span>shared_ptr</span>; if the object has been released, it returns a null<span>shared_ptr</span>.- Solving Circular Reference Problems: In classes with circular references, define one of the class’s pointer members as
<span>weak_ptr</span>to break the circular reference, allowing the object reference count to correctly decrement to 0 and enabling normal object release. For example:
class B;
class A {
public:
std::weak_ptr<B> ptrB;
};
class B {
public:
std::shared_ptr<A> ptrA;
};
<span>shared_ptr</span>Thread Safety: The reference count update operation of<span>shared_ptr</span>is atomic, making it safe for multiple threads to access and modify the reference count of the same<span>shared_ptr</span>. However, simultaneous access and modification of the object pointed to by<span>shared_ptr</span>by multiple threads may lead to data races and undefined behavior, requiring additional synchronization mechanisms (such as mutexes) to ensure thread safety of object access.
19. What are the differences between <span>new</span> and <span>make_shared</span>?
- Memory Allocation Mechanism:
<span>new</span>: Involves two steps, first allocating raw uninitialized memory using<span>operator new</span>, then calling the object’s constructor to initialize it. For example,<span>int* p = new int(10);</span>, first allocates memory for an<span>int</span>, then writes the value 10; when allocating an array, additional handling for element construction and destruction is required.<span>make_shared</span>: Allocates memory in one step, simultaneously allocating memory for the object and the control block that stores reference counting and management information. It allocates a sufficiently large block of memory on the heap, part for the object and part for the control block data, reducing the number of memory allocations and overhead, such as<span>std::shared_ptr<int> sp = std::make_shared<int>(10);</span>.
<span>new</span>: When allocating memory and constructing an object, if the constructor throws an exception, the allocated object memory is automatically released (the compiler ensures that the destructor is called in case of construction failure to avoid memory leaks); if an exception occurs after memory allocation in<span>operator new</span>and before the constructor is called, there is no object created, so no destructor needs to be called.<span>make_shared</span>: If an exception is thrown during the object construction process after memory allocation, since the memory is allocated in one go (both object memory and control block memory together), memory release is more complex, posing a risk of memory leaks, although modern standard libraries usually handle this properly.
<span>new</span>: Two memory allocation operations incur additional overhead, including function call overhead and potential memory fragmentation issues, which can significantly impact performance when used frequently.<span>make_shared</span>: One memory allocation reduces overhead and time, with the object and control block in the same memory area, improving cache hit rates, enhancing performance, and reducing memory allocation frequency helps to lower memory fragmentation and improve memory usage efficiency.
<span>new</span>: Suitable for scenarios requiring precise control over memory allocation and release processes, such as custom memory management classes, or when there are special requirements for memory layout; when additional initialization operations that are not suitable for the constructor are needed after memory allocation,<span>new</span>is also a good choice.<span>make_shared</span>: Preferably used when creating objects managed by<span>std::shared_ptr</span>, especially in scenarios where dynamic objects are frequently created and destroyed (such as containers storing dynamically allocated objects), simplifying code, improving performance, and reducing memory management complexity.
20. What is the difference between <span>const</span> and <span>constexpr</span>?
- Constant Determination Time:
<span>const</span>constants have their values determined at compile or runtime, initialized before use;<span>constexpr</span>constants must have their values determined at compile time, used to define compile-time constants. - Scope of Use:
<span>const</span>can modify variables, function parameters, function return values, class member variables, etc.;<span>constexpr</span>can modify not only variables but also functions (<span>constexpr</span>functions), requiring the function to return a constant value at compile time. For example:
const int a = 10; // Runtime constant
constexpr int b = 20; // Compile-time constant
constexpr int add(int x, int y) {
return x + y;
}
- Compiler Optimization:
<span>constexpr</span>constants and functions can be optimized more by the compiler (e.g., replaced with specific values, reducing runtime computation overhead);<span>const</span>constants may have limited optimization in certain cases.
21. When is the copy constructor called?
- Object Initialization: Called when initializing a new object of the same type using an existing object, such as
<span>MyClass obj1; MyClass obj2(obj1);</span>or<span>MyClass obj2 = obj1;</span>(where<span>=</span>is copy initialization). - Function Parameters: Called when passing parameters by value, creating a copy of the actual parameter to pass to the function. For example,
<span>void func(MyClass obj);</span>, calling<span>func(obj1);</span>calls the copy constructor of<span>MyClass</span>. - Function Return Objects: Called when returning an object by value from a function, copying the internal object to the function call site. For example,
<span>MyClass func() { MyClass obj; return obj; }</span>, calling<span>func()</span>returns an object, invoking the copy constructor. - Container Initialization or Inserting Elements: May call the copy constructor when using an existing object to initialize container elements or inserting objects into a container. For example,
<span>std::vector<MyClass> vec; vec.push_back(obj1);</span>(if<span>MyClass</span>does not have a move constructor, or the compiler does not support move semantics optimization, the copy constructor will be called).
22. If there is an empty class, what functions will be added by default?
If a class is empty, the compiler will automatically add the following functions:
- Default Constructor: Used to create objects of the class; when no constructors are provided, the compiler generates a default constructor automatically.
class EmptyClass {
public:
// Default constructor
EmptyClass() {}
};
- Copy Constructor: Used to create a new object that is a copy of another object of the same type.
class EmptyClass {
public:
// Copy constructor
EmptyClass(const EmptyClass& other) {}
};
- Destructor: Called when an object is destroyed, used to release resources occupied by the object.
class EmptyClass {
public:
// Destructor
~EmptyClass() {}
};
- Assignment Operator Overload Function: Used to assign the value of one object to another object of the same type.
class EmptyClass {
public:
// Assignment operator overload function
EmptyClass& operator=(const EmptyClass& other) {
return *this;
}
};
23. Where is the virtual function table of the base class stored in memory, and when is the virtual table pointer <span>vptr</span> initialized?
- Storage Location of the Virtual Function Table: The virtual function table of the base class is stored in the read-only data segment (
<span>.rodata</span>), as the contents of the virtual function table remain fixed during program execution and belong to constant data. - Initialization Time of the Virtual Table Pointer: The
<span>vptr</span>is initialized during the execution of the object’s constructor, before entering the constructor body, making it point to the virtual function table of the class to which the object belongs, ensuring that after the object is constructed, virtual functions can be correctly called.
24. How are <span>new</span> and <span>delete</span> implemented?
<span>new</span>Implementation: Divided into two steps, first calling the<span>operator new</span>function to allocate memory (<span>operator new</span>typically calls<span>malloc</span>to actually allocate memory), then calling the object’s constructor to initialize the object. Example code is as follows:
void* operator new(size_t size) {
void* p = malloc(size); if (!p) { throw std::bad_alloc(); } return p;
}
class Example {
public:
Example() { std::cout << "Constructor called." << std::endl; }
};
int main() {
Example* e = new Example();
delete e;
return 0;
}
<span>delete</span>Implementation: Also divided into two steps, first calling the object’s destructor to clean up the object’s resources, then calling the<span>operator delete</span>function to release memory (<span>operator delete</span>typically calls<span>free</span>to actually release memory).
25. In which situations must the member initializer list be used?
- Constant Member Variables: Constant member variables cannot be modified after initialization and must be initialized in the member initializer list. For example:
class Example {
public:
const int value;
Example(int v) : value(v) {}
};
- Reference Member Variables: Reference member variables must be initialized at the time of definition and cannot refer to other objects afterward, requiring initialization in the member initializer list. For example:
class Example {
public:
int& ref;
Example(int& r) : ref(r) {}
};
- Class Members Without Default Constructors: If a class member is an object of a class that does not have a default constructor, it must be initialized in the member initializer list by calling that class’s constructor. For example:
class AnotherClass {
public:
AnotherClass(int v) {}
};
class Example {
public:
AnotherClass obj;
Example(int v) : obj(v) {}
};
26. How is polymorphism implemented in C++?
- Static Polymorphism (Compile-time Polymorphism):
- Function Overloading: In the same class, functions with the same name but different parameter lists are selected by the compiler based on the types and number of actual parameters at the time of the call.
- Templates: Achieve generic algorithms and data structures through template programming, with the compiler generating corresponding code based on template parameter types at compile time.
27. What are the functions of the <span>final</span> and <span>override</span> keywords?
<span>final</span>: When modifying a class, it indicates that the class cannot be inherited, such as<span>class FinalClass final {};</span>; when modifying a virtual function, it indicates that the virtual function cannot be overridden in derived classes, such as<span>virtual void func() final;</span>.<span>override</span>: Used in derived class virtual function declarations to explicitly indicate that the function overrides a base class virtual function. If the base class does not have a corresponding virtual function, the compiler will report an error, preventing failure to override due to spelling mistakes. For example:
class Base {
public:
virtual void func() {}
};
class Derived : public Base {
public:
void func() override {}
};
28. What are the uses of <span>auto</span>, <span>decltype</span>, and <span>decltype(auto)</span>?
<span>auto</span>: Used to automatically deduce variable types, with the compiler determining the variable type based on the initialization expression. For example:<span>auto num = 10;</span>, automatically deducing<span>num</span>as type<span>int</span>.<span>decltype</span>: Obtains the type of an expression without evaluating the expression’s value. For example:<span>int x = 5; decltype(x) y;</span>, where<span>y</span>is of type<span>int</span>.<span>decltype(auto)</span>: Combines the features of<span>decltype</span>and<span>auto</span>, automatically deducing the type based on the initialization expression, with deduction rules similar to<span>decltype</span>. For example:
int& func() {
static int num = 10;
return num;
}
decay_t<decltype(auto) <="" code="" int&.="" is="" of="" ref="" type=""></decltype(auto)>
29. What is the difference between shallow copy and deep copy?
- Shallow Copy: Simply copies the values of the object’s member variables; if the object contains pointer members, only the pointer values are copied, not the objects pointed to, leading to multiple objects sharing the same memory. When one of the objects releases memory, the other object’s pointer becomes a dangling pointer. For example:
class MyClass {
public:
int* data;
MyClass(int value) {
data = new int(value);
}
// Shallow copy constructor
MyClass(const MyClass& other) : data(other.data) {}
~MyClass() {
delete data;
}
};
- Deep Copy: Not only copies the values of the object’s member variables but also allocates new memory for pointer members and copies the objects pointed to into the new memory, ensuring each object has independent memory and does not affect each other. For example:
class MyClass {
public:
int* data;
MyClass(int value) {
data = new int(value);
}
// Deep copy constructor
MyClass(const MyClass& other) {
data = new int(*other.data);
}
~MyClass() {
delete data;
}
};
30. What is the difference between dynamic compilation and static compilation?
- Static Compilation: The compiler integrates the code of the library files that the program depends on into the executable file, generating an executable file that can run independently without external library files. However, the file size is larger, and when the library files are updated, the entire program must be recompiled.
- Dynamic Compilation: The generated executable file loads the required library files at runtime, resulting in a smaller file size. When library files are updated and the interface remains unchanged, the executable file does not need to be recompiled, but it must ensure that the system has the corresponding library files at runtime; otherwise, the program cannot run.
31. Can template functions be virtual functions?
Template functions cannot be directly declared as virtual functions. Template functions are instantiated into specific functions at compile time, while virtual functions implement runtime polymorphism, relying on the virtual function table and dynamic binding mechanism of the object. Template functions determine specific function instances at compile time, which is incompatible with the runtime characteristics of virtual functions. However, virtual member functions can be defined in class templates, and these virtual member functions can achieve polymorphic behavior after instantiation. For example:
template <typename T>
class MyClass {
public:
virtual void func() {
// Virtual member function
}
};
template class MyClass<int>;
template class MyClass<double>;