ref: https://github.com/0voice/cpp-learning-2025
I. Classes and Objects
1. Class vs. Struct
In C++, class and struct are the core constructs for object-oriented programming. Their only essential difference is the default access control, as their syntax is nearly identical:
| Feature | class |
struct |
|---|---|---|
| Default Access Control | private (Private) |
public (Public) |
| Default Inheritance Access | Private Inheritance | Public Inheritance |
| Primary Use Case | Encapsulate complex objects (focus on behavior + attributes) | Encapsulate simple data structures (focus on attributes) |
| Supported Features | Member variables, member functions, inheritance, polymorphism (identical to struct) |
Same as class |
-
Example:
classDefinition1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// Class definition (typically in a .h header file)
class Person {
// Access specifiers (can appear multiple times, effective until the next specifier)
private:
// Private members: accessible only within the class (core of encapsulation, hides implementation details)
std::string name;
int age;
public:
// Public members: accessible from outside the class (provides external interface)
// Member functions (behavior)
void setInfo(std::string n, int a) {
name = n;
age = a;
}
void showInfo() {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
}; -
Example:
structDefinition1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16struct Student {
// Default public: member variables can be accessed directly.
std::string id;
std::string name;
// Structs also support member functions
void show() {
std::cout << "ID: " << id << ", Name: " << name << std::endl;
}
};
// Direct access to struct members from outside (default public)
Student s;
s.id = "2024001";
s.name = "Tom";
s.show();
2. Member Variables and Member Functions
- Member Variables: Variables defined within a class/struct (store the object’s state).
- Member Functions: Functions defined within a class/struct (describe the object’s behavior).
- Access Control Rules (Core of encapsulation, enabling “data hiding”):
public: Public members, accessible within the class, from outside, and by derived classes (interface exposed to the outside world).private: Private members, accessible only within the class (hides internal implementation, cannot be directly modified from outside).protected: Protected members, accessible within the class and by derived classes, but not from outside (designed for inheritance).
- Interview Focus: The significance of encapsulation – hiding internal state via
privateand interacting only throughpublicinterfaces, ensuring data safety and code maintainability.
3. Object Creation: Stack vs. Heap
An object is an instance of a class. The creation method determines its memory location and lifetime management:
| Creation Method | Memory Location | Lifetime | Syntax Example | Access Method |
|---|---|---|---|---|
| Stack Allocation | Stack Memory | Automatically destroyed when it goes out of scope | Person p; (no new) |
Direct access with . |
| Heap Allocation | Heap Memory | Must be manually destroyed with delete |
Person* p = new Person; (uses new) |
Pointer access with -> |
-
Example: Stack Object
1
2
3
4
5void test() {
Person p; // Created on the stack, scope is within the test() function
p.setInfo("Jerry", 22); // Access members of stack object using '.'
p.showInfo();
} // Function ends, stack object automatically destroyed (destructor called) -
Example: Heap Object
1
2
3
4
5
6
7
8
9
10
11int main() {
// Create object on the heap, returns a pointer to the object
Person* p = new Person;
p->setInfo("Alice", 25); // Access members of heap object (pointer) using '->'
p->showInfo();
// Must manually delete to destroy, otherwise memory leak
delete p;
p = nullptr; // Avoid dangling pointer
return 0;
} -
Key Point: Objects created on the heap with
newmust be destroyed withdelete(matchingnew), otherwise it leads to a memory leak (heap memory is not automatically released).
II. Construction and Destruction
1. Constructor
- Primary Purpose: Automatically called when an object is created, used to initialize member variables (solves the problem of “member variables being uninitialized by default”).
- Key Characteristics:
- Function name is identical to the class name.
- No return type (not even
void). - Can be overloaded (multiple constructors with different parameter lists are supported).
- If not explicitly defined, the compiler generates a default constructor (no parameters, empty body).
(1) Default Constructor
- A constructor with no parameters (either explicitly defined or automatically generated by the compiler).
- Example:
1
2
3
4
5
6
7
8
9
10
11
12
13class Person {
private:
std::string name;
int age;
public:
// Explicitly defined default constructor
Person() {
name = "Unknown";
age = 0;
}
};
Person p; // Calls default constructor, name="Unknown", age=0
(2) Parameterized Constructor
- A constructor with parameters, used to assign values directly during initialization.
- Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Person {
private:
std::string name;
int age;
public:
// Parameterized constructor (overloaded)
Person(std::string n, int a) {
name = n;
age = a;
}
};
// Pass arguments when creating object, calls parameterized constructor
Person p("Bob", 28);
p.showInfo(); // Output: Name: Bob, Age: 28
(3) Copy Constructor
- Primary Purpose: Initializes a new object using an existing object (e.g., object assignment, pass-by-value in functions).
- Syntax:
ClassName(const ClassName& source_object)(parameter must be a const reference to avoid infinite recursion). - Default Behavior: If not explicitly defined, the compiler generates a shallow copy constructor (copies member variables byte-by-byte).
- Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Person {
private:
std::string name;
int age;
public:
// Explicitly defined copy constructor
Person(const Person& other) {
this->name = other.name; // `this` pointer: points to the current object itself
this->age = other.age;
std::cout << "Copy constructor called" << std::endl;
}
};
Person p1("Charlie", 30);
Person p2 = p1; // Calls copy constructor (initializes p2 with p1)
Person p3(p1); // Equivalent to p2, also calls copy constructor
(4) Move Constructor (C++11 and later)
- Background: Solves the performance waste caused by “copying temporary objects” (e.g., when a function returns a local object).
- Primary Purpose: “Steals” resources (like heap memory) from the source object instead of copying them, improving efficiency.
- Syntax:
ClassName(ClassName&& source_object)(parameter is an rvalue reference&&). - Default Behavior: Not automatically generated by the compiler if not explicitly defined (must be manually implemented).
- Example (with heap memory resource):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30class String {
private:
char* data; // Heap memory storing the string
public:
// Parameterized constructor (allocates heap memory)
String(const char* str) {
int len = std::strlen(str);
data = new char[len + 1];
std::strcpy(data, str);
}
// Move constructor (steals 'data' from 'other')
String(String&& other) noexcept { // `noexcept`: guarantees no exception is thrown
this->data = other.data; // Directly take over the resource
other.data = nullptr; // Nullify source object to avoid double deletion in destructor
std::cout << "Move constructor called" << std::endl;
}
// Destructor (releases heap memory)
~String() {
delete[] data;
}
};
// Function returning a local object (temporary object, rvalue)
String createString() {
return String("Hello");
}
String s = createString(); // Calls move constructor, not copy (no duplicate heap allocation)
2. Move Assignment Operator (C++11 and later)
- Primary Purpose: “Assigns” an rvalue object to the current object, also “stealing” resources instead of copying.
- Syntax:
ClassName& operator=(ClassName&& other) noexcept; - Example (continuing with the
Stringclass):1
2
3
4
5
6
7
8
9
10
11
12String& operator=(String&& other) noexcept {
if (this != &other) { // Avoid self-assignment
delete[] this->data; // Release current object's existing resource
this->data = other.data; // Take over source object's resource
other.data = nullptr; // Nullify source object
std::cout << "Move assignment operator called" << std::endl;
}
return *this;
}
String s1("World");
s1 = createString(); // Calls move assignment (steals temporary object's resources)
3. Destructor
- Primary Purpose: Automatically called when an object is destroyed, used to release resources (e.g., heap memory, file handles, network connections).
- Key Characteristics:
- Function name is
~followed by the class name. - No return value, no parameters (cannot be overloaded; only one destructor per class).
- If not explicitly defined, the compiler generates a default destructor (empty body).
- Function name is
- Example (releasing heap memory):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Person {
private:
std::string* name; // Pointer to a string on the heap
public:
// Constructor: allocates heap memory
Person(std::string n) {
name = new std::string(n);
}
// Destructor: releases heap memory (avoids memory leak)
~Person() {
delete name; // Releases heap memory pointed to by 'name'
std::cout << "Destructor called" << std::endl;
}
};
Person p("David"); // Heap memory allocated during construction
// Function ends, p is destroyed, destructor automatically called, releasing heap memory
4. Virtual Destructor (High-frequency Interview Topic)
-
The Core Problem: When a base class pointer points to a derived class object, if the base class destructor is not virtual, destroying the object only calls the base class destructor, and the derived class destructor is not executed, leading to resource leakage.
-
Primary Purpose: Ensures that when a derived class object is destroyed via a base class pointer, the derived class destructor is called first, followed by the base class destructor.
-
Syntax: Add
virtualbefore the base class destructor. -
Example (comparing non-virtual and virtual destructors):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// Base class
class Base {
public:
// Non-virtual destructor (incorrect example)
~Base() {
std::cout << "Base destructor" << std::endl;
}
};
// Derived class (with heap resource)
class Derived : public Base {
private:
int* data;
public:
Derived() {
data = new int(10);
}
~Derived() {
delete data; // Releases derived class's heap resource
std::cout << "Derived destructor" << std::endl;
}
};
// Test: Base class pointer pointing to derived class object
Base* ptr = new Derived;
delete ptr; // Only Base destructor is called, Derived destructor not executed → Memory leak! -
Corrected Version (virtual destructor):
1
2
3
4
5
6
7
8
9
10class Base {
public:
virtual ~Base() { // Base class destructor declared virtual
std::cout << "Base destructor" << std::endl;
}
};
Base* ptr = new Derived;
delete ptr;
// Correct execution order: Derived destructor → Base destructor (no memory leak) -
Interview Focus: The necessity of virtual destructors – if a class might be inherited and the derived class has heap resources, the base class destructor must be
virtual. Destructors of abstract classes (containing pure virtual functions) should also be virtual (can have a default implementation).
5. Initializer List
-
Primary Purpose: Directly initializes member variables before the constructor body executes (more efficient than assignment inside the constructor body, especially for
constmembers, reference members, and object members without default constructors). -
Syntax:
Constructor(parameter_list) : member1(value1), member2(value2), ... {} -
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Person {
private:
const std::string name; // const member (must be initialized, cannot be assigned)
int& age; // Reference member (must be initialized, cannot be rebound)
std::string addr;
public:
// Initialize members via initializer list (the only way to initialize const and references)
Person(std::string n, int& a, std::string ad) : name(n), age(a), addr(ad) {
// Constructor body: can perform other logic (e.g., parameter validation)
if (a < 0) throw std::invalid_argument("Age cannot be negative");
}
};
int age = 25;
Person p("Eve", age, "Beijing"); // Correctly initializes const, reference, and ordinary members -
Advantages:
- Higher efficiency: Direct initialization avoids the extra overhead of “default construction followed by assignment”.
- More powerful: Supports initialization of
constmembers, references, and members without default constructors.
-
Interview Focus: The execution order of the initializer list – follows the declaration order of member variables in the class, not the order in the initializer list.
6. RAII Principle (Resource Acquisition Is Initialization)
- Core Idea: “Resource acquisition is initialization” – binding the lifetime of a resource (heap memory, file handles, locks, etc.) to the lifetime of an object:
- Acquire the resource when the object is created (constructor).
- Automatically release the resource when the object is destroyed (destructor).
- Core Value: No need for manual resource management, fundamentally avoiding memory leaks and resource leaks.
- Typical Applications:
- Smart pointers (
unique_ptr/shared_ptr): Encapsulate heap memory, automaticallydeleteon destruction. - Lock management (
lock_guard): Lock during construction, automatically unlock on destruction.
- Smart pointers (
- Example (RAII wrapper for file handle):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class FileGuard {
private:
FILE* file; // Resource: file handle
public:
// Acquire resource during construction (open file)
FileGuard(const char* path, const char* mode) {
file = std::fopen(path, mode);
if (!file) throw std::runtime_error("Failed to open file");
}
// Release resource during destruction (close file)
~FileGuard() {
if (file) std::fclose(file);
}
// Provide access interface
FILE* getFile() const { return file; }
};
// Usage: No need to manually close the file
void readFile() {
FileGuard fg("test.txt", "r"); // Open file (acquire resource)
std::fread(..., fg.getFile()); // Operate on the file
} // fg destroyed, destructor automatically closes file (releases resource)
7. Introduction to Smart Pointers
Smart pointers are a classic implementation of the RAII principle, encapsulating raw pointers to automatically manage heap memory (automatically delete on destruction), avoiding dangling pointers and memory leaks:
| Smart Pointer Type | Core Feature | Use Case | Syntax Example |
|---|---|---|---|
unique_ptr |
Exclusive ownership (cannot be copied, only moved) | Single object exclusively owns a resource | std::unique_ptr<Person> p(new Person); |
shared_ptr |
Shared ownership (reference counting, released when count reaches 0) | Multiple objects share a resource | std::shared_ptr<Person> p = std::make_shared<Person>(); |
weak_ptr |
Weak reference (doesn’t increase reference count, solves circular reference with shared_ptr) |
Auxiliary to shared_ptr, avoids circular references |
std::weak_ptr<Person> wp = p; |
-
Example:
unique_ptr(Unique Pointer)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main() {
// Method 1: Create unique_ptr (exclusively owns Person object)
std::unique_ptr<Person> p1(new Person("Frank", 35));
p1->showInfo(); // Access members using ->
// Method 2: Recommended to use make_unique (safer, avoids memory leak issues)
auto p2 = std::make_unique<Person>("Grace", 32);
// Cannot be copied (exclusive ownership)
// std::unique_ptr<Person> p3 = p1; // Compilation error
// Can be moved (transfers ownership)
std::unique_ptr<Person> p3 = std::move(p1); // p1 loses ownership, becomes nullptr
p3->showInfo();
return 0;
} // p3, p2 destroyed, automatically calls delete to release heap memory -
Key Advantage: No need for manual
delete; even if the program throws an exception, objects will be destructed (destruction order of stack objects).
8. Rule of Five
Extended from the “Rule of Three” after C++11, the core rule is:
If a class explicitly defines any one of the following members, it must explicitly define the other four; otherwise, it may lead to resource management errors:
- Destructor (
~ClassName())- Copy Constructor (
ClassName(const ClassName&))- Copy Assignment Operator (
operator=(const ClassName&))- Move Constructor (
ClassName(ClassName&&))- Move Assignment Operator (
operator=(ClassName&&))
- Core Reason: Explicitly defining a destructor usually means the class manages resources (heap memory, etc.). The default copy/move semantics perform a “shallow copy,” which can lead to double deletion or memory leaks.
- Simplified Approach:
- If copying/moving is not needed: Explicitly delete (
= delete) the copy/move related functions. - If needed: Manually implement deep copy/move semantics.
- If copying/moving is not needed: Explicitly delete (
- Example (deleting copy, retaining move):
1
2
3
4
5
6
7
8
9
10
11
12
13class String {
public:
// Delete copy constructor and copy assignment (prohibit copying)
String(const String&) = delete;
String& operator=(const String&) = delete;
// Implement move constructor and move assignment (allow moving)
String(String&&) noexcept;
String& operator=(String&&) noexcept;
// Destructor
~String();
};
III. Inheritance and Polymorphism
1. Inheritance Syntax, Base Class, and Derived Class
- Core Idea: “Reuse attributes and behavior of existing classes,” while allowing derived classes to extend or modify base class functionality.
- Terminology:
- Base Class (Parent Class): The class being inherited from.
- Derived Class (Child Class): The class that inherits from the base class.
- Inheritance Syntax:
class DerivedClassName : access_specifier BaseClassName { ... }; - Inheritance Access Control:
| Inheritance Type | Base Class public Members | Base Class protected Members | Base Class private Members |
|---|---|---|---|
| public Inheritance | Become public in derived class | Become protected in derived class | Inaccessible |
| protected Inheritance | Become protected in derived class | Become protected in derived class | Inaccessible |
| private Inheritance | Become private in derived class | Become private in derived class | Inaccessible |
-
Example: Public Inheritance (Most Common)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36// Base class (Parent)
class Animal {
protected: // Protected members: accessible by derived classes
std::string name;
public:
Animal(std::string n) : name(n) {}
// Base class member function
void eat() {
std::cout << name << " is eating" << std::endl;
}
};
// Derived class (Child): publicly inherits from Animal
class Dog : public Animal {
private:
std::string breed; // New member in derived class
public:
// Derived class constructor: must first initialize the base class (using initializer list)
Dog(std::string n, std::string b) : Animal(n), breed(b) {}
// Derived class extends functionality
void bark() {
std::cout << name << " (" << breed << ") is barking: Woof Woof" << std::endl;
}
// Derived class overrides base class function (override)
void eat() override { // `override` keyword: ensures overriding a base class virtual function (C++11+)
std::cout << name << " is eating dog food" << std::endl;
}
};
// Using the derived class
Dog dog("Wangcai", "Golden Retriever");
dog.eat(); // Calls derived class's overridden eat() → Wangcai is eating dog food
dog.bark(); // Calls derived class's new bark() → Wangcai (Golden Retriever) is barking: Woof Woof -
Core Rule: The derived class constructor must first initialize the base class (via the initializer list). The destructor execution order is the reverse of construction (derived class destructor → base class destructor).
2. Virtual Functions (virtual)
-
Primary Purpose: The foundation for polymorphism – allows derived classes to override base class functions. When a base class pointer/reference points to a derived class object, the overridden version in the derived class is called.
-
Syntax: Add
virtualbefore the base class function. Derived classes can addoverridewhen overriding (optional but recommended to avoid overriding errors). -
Example: Virtual Functions Enabling Polymorphism
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32class Animal {
public:
virtual void makeSound() { // Base class virtual function
std::cout << "Animal makes a sound" << std::endl;
}
};
class Cat : public Animal {
public:
void makeSound() override { // Override virtual function
std::cout << "Cat says: Meow Meow" << std::endl;
}
};
class Dog : public Animal {
public:
void makeSound() override { // Override virtual function
std::cout << "Dog says: Woof Woof" << std::endl;
}
};
int main() {
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
animal1->makeSound(); // Calls Dog's makeSound() → Woof Woof (polymorphism)
animal2->makeSound(); // Calls Cat's makeSound() → Meow Meow (polymorphism)
delete animal1; // Virtual destructor ensures correct release
delete animal2;
return 0;
} -
Underlying Mechanism: When a base class contains virtual functions, the compiler generates a virtual function table (vtable) for the class, storing the addresses of its virtual functions. Each object contains a virtual table pointer (vptr), pointing to its class’s vtable. Calling a virtual function involves looking up the vtable via the vptr to find the actual function address (dynamic binding).
3. Pure Virtual Functions and Abstract Classes
-
Pure Virtual Function: A virtual function without a function body. Syntax:
virtual return_type function_name(parameter_list) = 0; -
Abstract Class: A class containing at least one pure virtual function (cannot be instantiated, can only be used as a base class).
-
Primary Purpose: Defines an interface specification, forcing derived classes to implement specific functionality.
-
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28// Abstract class (contains pure virtual functions)
class Shape {
public:
virtual double getArea() = 0; // Pure virtual function: calculate area (interface)
virtual double getPerimeter() = 0; // Pure virtual function: calculate perimeter (interface)
virtual ~Shape() {} // Virtual destructor (essential to avoid resource leaks in derived classes)
};
// Derived class must implement all pure virtual functions, otherwise it remains abstract
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double getArea() override {
return 3.14159 * radius * radius;
}
double getPerimeter() override {
return 2 * 3.14159 * radius;
}
};
// Using abstract class (base class pointer pointing to derived class object)
Shape* circle = new Circle(5.0);
std::cout << "Circle area: " << circle->getArea() << std::endl; // 196.349
std::cout << "Circle perimeter: " << circle->getPerimeter() << std::endl; // 31.4159 -
Interview Focus: Abstract classes cannot be instantiated, but pointers/references to them can be defined. Derived classes must implement all pure virtual functions to be instantiated.
4. Brief Introduction to vtable (Virtual Function Table)
-
Core Concept: The vtable is a static array generated by the compiler for a class containing virtual functions, storing the addresses of all virtual functions for that class.
-
Core Mechanism:
- Each object contains a
vptr(virtual table pointer, typically at the beginning of the object’s memory), pointing to its class’s vtable. - A base class has a vtable. If a derived class overrides a virtual function, it replaces the corresponding function address in the vtable. If it adds new virtual functions, they are appended to the vtable.
- Calling a virtual function involves finding the vtable via the
vptr, then locating the actual function address via the function index (dynamic binding, resolved at runtime).
- Each object contains a
-
Memory Structure Example (
Animalbase class,Dogderived class):1
2
3
4
5
6
7Animal class vtable:
[0] → Animal::makeSound()
[1] → Animal::~Animal()
Dog class vtable (overrides makeSound):
[0] → Dog::makeSound() (replaces base class function)
[1] → Animal::~Animal() (inherits base class virtual destructor) -
Interview Focus:
- The size of an object of a class with virtual functions increases (by the size of a vptr, 4 bytes on 32-bit systems, 8 bytes on 64-bit systems).
- Virtual function calls are slower than regular function calls (extra vptr lookup and indirect jump).
- Constructors cannot be virtual (vptr is not fully initialized while the object is being constructed).
- Destructors can (and often should) be virtual.
IV. Operator Overloading
1. Core Concept
- Purpose: Gives operators new meanings, allowing user-defined types (classes, structs) to support operator operations (e.g.,
+,==,<<). - Nature: A special kind of function, with the function name format
operator operator_symbol. - Rules:
- Operators that cannot be overloaded:
.,.*,::,?:,sizeof,typeid, etc. - Precedence, associativity, and number of operands of the operator remain unchanged after overloading.
- Must be related to a user-defined type (cannot overload operators for built-in types like
int+int).
- Operators that cannot be overloaded:
2. Assignment Operator Overloading (operator=)
- Primary Purpose: Customizes the logic for object assignment (default is shallow copy; manual implementation is needed for deep copy to avoid resource leaks).
- Syntax:
ClassName& operator=(const ClassName& source_object); - Important Notes:
- Must return
*this(to support chained assignment likea = b = c). - Must check for self-assignment (
this == &source_object) to avoid releasing resources prematurely. - Release current object’s resources before copying source object’s resources.
- Must return
- Example (Deep Copy Assignment):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32class String {
private:
char* data;
int length;
public:
// Assignment operator overloading (deep copy)
String& operator=(const String& other) {
// 1. Check for self-assignment
if (this == &other) return *this;
// 2. Release current object's resources
delete[] data;
// 3. Deep copy source object's resources
length = other.length;
data = new char[length + 1];
std::strcpy(data, other.data);
// 4. Return *this (for chained assignment)
return *this;
}
~String() {
delete[] data;
}
};
// Usage
String s1("Hello");
String s2("World");
s1 = s2; // Calls overloaded assignment operator (deep copy, no memory leak)
String s3 = s1 = s2; // Chained assignment (supported)
3. Arithmetic Operator Overloading (+, -, etc.)
- Typically overloaded as member functions or global functions (global functions are recommended for supporting commutativity, e.g.,
a + bandb + a). - Example: Overloading
+to add twoPointobjects (coordinate-wise addition)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class Point {
private:
int x, y;
public:
Point(int x_, int y_) : x(x_), y(y_) {}
// Method 1: Overload + as a member function (left operand is `this`)
Point operator+(const Point& other) const {
return Point(this->x + other.x, this->y + other.y);
}
// Friend function: allows global function to access private members (explained later)
friend std::ostream& operator<<(std::ostream& os, const Point& p);
};
// Method 2: Overload + as a global function (supports commutativity, e.g., 3 + point)
Point operator+(int val, const Point& p) {
return Point(p.x + val, p.y + val);
}
// Usage
Point p1(1, 2), p2(3, 4);
Point p3 = p1 + p2; // Calls member function operator+ → (4,6)
Point p4 = 5 + p1; // Calls global function operator+ → (6,7)
4. Relational Operator Overloading (==, !=, etc.)
- Typically overloaded as global functions, returning
bool. - Example: Overloading
==and!=to comparePointobjects for equality1
2
3
4
5
6
7
8
9
10
11
12
13
14bool operator==(const Point& p1, const Point& p2) {
return p1.x == p2.x && p1.y == p2.y;
}
bool operator!=(const Point& p1, const Point& p2) {
return !(p1 == p2); // Reuse == logic, avoid redundancy
}
// Usage
if (p1 == p2) {
std::cout << "p1 and p2 are equal" << std::endl;
} else {
std::cout << "p1 and p2 are not equal" << std::endl;
}
5. Output Stream Operator Overloading (operator<<)
coutis an object of classostream, and<<is a member function ofostream.- Overloading
<<for a user-defined type must be a global function (left operand isostream&, not the user-defined class object). - Requires
frienddeclaration to allow the function access to the user-defined class’s private members. - Example (continuing with
Pointclass):1
2
3
4
5
6
7
8
9// Global function + friend: overloads <<, supports cout << Point object
std::ostream& operator<<(std::ostream& os, const Point& p) {
os << "(" << p.x << ", " << p.y << ")"; // Accesses private members x, y of p
return os; // Return os to support chained output (e.g., cout << p1 << p2)
}
// Usage
std::cout << "p1: " << p1 << ", p2: " << p2 << std::endl;
// Output: p1: (1, 2), p2: (3, 4)
6. Deep Copy vs. Shallow Copy (Core Interview Topic)
-
Shallow Copy (default copy/assignment semantics):
- Copies only the “values” of member variables. If a member variable is a pointer (pointing to heap memory), after copying, both pointers point to the same memory location.
- Harm: Destructors will attempt to release the same memory block twice, causing a program crash.
-
Deep Copy (manually implemented copy/assignment):
- Not only copies the member variables’ values but also re-allocates heap memory for pointer members and copies the content.
- Effect: The pointer members of the two objects point to different heap memory; they are released separately during destruction without conflict.
-
Comparative Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45// Shallow copy (default behavior, incorrect example)
class StringShallow {
private:
char* data;
public:
StringShallow(const char* str) {
data = new char[std::strlen(str) + 1];
std::strcpy(data, str);
}
// No explicit copy constructor or assignment operator (defaults to shallow copy)
~StringShallow() { delete[] data; }
};
StringShallow s1("Hello");
StringShallow s2 = s1; // Shallow copy: s1.data and s2.data point to the same memory
// Function ends, s2 destructor releases data → s1 destructor attempts to release again → Crash!
// Deep copy (correct implementation)
class StringDeep {
private:
char* data;
public:
StringDeep(const char* str) {
data = new char[std::strlen(str) + 1];
std::strcpy(data, str);
}
// Deep copy constructor
StringDeep(const StringDeep& other) {
data = new char[std::strlen(other.data) + 1];
std::strcpy(data, other.data);
}
// Deep copy assignment
StringDeep& operator=(const StringDeep& other) {
if (this != &other) {
delete[] data;
data = new char[std::strlen(other.data) + 1];
std::strcpy(data, other.data);
}
return *this;
}
~StringDeep() { delete[] data; }
};
StringDeep s3("World");
StringDeep s4 = s3; // Deep copy: s3.data and s4.data point to different memory → No crash -
Interview Focus: The difference between deep and shallow copy, problems with default copy/assignment, and how to implement deep copy.
V. Template Basics
1. Function Template
-
Primary Purpose: Defines a “generic function” that works with multiple data types, avoiding the need to write identical logic for different types (e.g., sum for
int, sum fordouble). -
Syntax:
template <typename T>ortemplate <class T>(typenameandclassare equivalent here). -
Example: Generic sum function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// Function template: T is a template parameter (type placeholder)
template <typename T>
T add(T a, T b) {
return a + b;
}
int main() {
// Method 1: Compiler deduces the type automatically
int sum1 = add(1, 2); // T=int → returns 3
double sum2 = add(1.5, 2.5); // T=double → returns 4.0
// Method 2: Explicitly specify the type
int sum3 = add<int>(3, 4); // Explicitly specify T=int
return 0;
} -
Template Instantiation: When the compiler encounters
add(1,2), it automatically generates the specific implementationint add(int a, int b)(instantiation). The template itself does not generate code.
2. Class Template
-
Primary Purpose: Defines a “generic class” where member variables/functions can be of a template type.
-
Syntax: Declare with
template <typename T>; the class name can be followed by template parameters. -
Example: Generic stack class (supports elements of any type)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37template <typename T, int Size = 100> // Template parameters + default value
class Stack {
private:
T data[Size]; // Array of template type
int top;
public:
Stack() : top(-1) {}
void push(const T& val) {
if (top < Size - 1) data[++top] = val;
}
T pop() {
if (top >= 0) return data[top--];
throw std::runtime_error("Stack underflow");
}
bool isEmpty() const {
return top == -1;
}
};
// Usage
int main() {
// Stack 1: int type, default size 100
Stack<int> intStack;
intStack.push(10);
intStack.push(20);
std::cout << intStack.pop() << std::endl; // 20
// Stack 2: double type, size 50
Stack<double, 50> doubleStack;
doubleStack.push(3.14);
std::cout << doubleStack.pop() << std::endl; // 3.14
return 0;
} -
Note: If a class template’s member function is defined outside the class, the template parameters must be re-declared:
1
2
3
4template <typename T, int Size>
void Stack<T, Size>::push(const T& val) {
// Implementation outside the class...
}
3. Association with STL Container Underpinnings
The core of the STL (Standard Template Library) is “templatized” design. All containers (vector, list, map, etc.) are essentially class templates that support any copyable type:
-
vector<int>: A dynamic array with template parameterT=int. -
map<std::string, int>: An associative container with template parameterskey=std::string,value=int. -
Advantage of templates: STL containers don’t need separate implementations for each data type; they adapt to all types via template parameters, achieving “write once, reuse many times”.
-
Interview Focus:
- Difference between function templates and function overloading (templates are for “generic types,” overloading is for “different parameter lists”).
- How class templates are instantiated (explicitly specifying the type).
- Limitations of templates (the type must support the operators used within the template, e.g.,
addtemplate requiresTto support+).
说些什么吧!