C++八股总结

C++ 核心知识点系统性总结,涵盖内存管理、多态、智能指针、STL 等高频面试考点

一、内存管理

1.1 C++ 内存布局

程序运行时的内存分布(从高地址到低地址):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
┌─────────────────┐  高地址
│     栈(Stack)    │  ← 局部变量、函数参数(编译器自动管理)
├─────────────────┤
│     堆(Heap)     │  ← new/malloc 分配(程序员手动管理)
├─────────────────┤
│  全局/静态存储区   │  ← 全局变量、static 变量
│  .data(已初始化) │
│  .bss(未初始化)  │
├─────────────────┤
│  常量存储区 .rodata│  ← 字符串常量、const 全局变量
├─────────────────┤
│   代码区 .text     │  ← 可执行机器指令
└─────────────────┘  低地址

各区域特点:

区域管理方式大小生命周期
栈编译器自动分配释放小(默认 ~1MB)函数结束时
堆程序员手动管理大(虚拟内存)直到 delete/free
全局/静态区程序启动分配,结束释放取决于变量整个程序运行期
常量区只读,不可修改取决于常量整个程序运行期
代码区只读取决于代码整个程序运行期

堆和栈的区别:

栈堆
生长方向向下(地址减小)向上(地址增加)
分配方式静态分配(编译器)+ 动态分配(alloca)仅动态分配
分配效率高(有专门的寄存器指令)低(库函数搜索空闲内存)
碎片无碎片可能产生内存碎片
典型问题栈溢出(递归过深)内存泄漏、野指针

1.2 内存泄漏

定义:申请的内存空间使用完毕后没有释放,导致该内存无法再被访问和回收。

类型:

  • 堆内存泄漏:new/malloc 后没有 delete/free
  • 系统资源泄漏:socket、handle、Bitmap 等系统资源未释放

检测工具:

  • Valgrind(Linux)
  • Visual Studio 诊断工具(Windows)
  • Dr.Memory

如何避免:

  • 使用智能指针(unique_ptr、shared_ptr)
  • RAII 机制(资源获取即初始化)
  • 规范配对使用 new/delete、malloc/free

1.3 new/delete 和 malloc/free 的区别

new/deletemalloc/free
本质操作符(可重载)库函数
内存计算编译器自动计算需显式指定字节数
返回值类型安全的指针void*
失败处理抛出 std::bad_alloc 异常返回 nullptr
构造函数/析构函数调用不调用
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// malloc/free:仅分配和释放内存
int* p = (int*)malloc(sizeof(int) * 10);
free(p);

// new/delete:分配内存 + 构造/析构对象
int* p = new int(42);           // 分配 + 初始化
delete p;                        // 析构 + 释放

Person* arr = new Person[10];   // 数组
delete[] arr;                    // 注意:数组必须用 delete[]

// 有了 malloc/free 为什么还需要 new/delete?
// 因为对于非内部数据类型,malloc/free 无法调用构造/析构函数
class Foo {
public:
    Foo() { cout << "构造"; }
    ~Foo() { cout << "析构"; }
};

Foo* p1 = (Foo*)malloc(sizeof(Foo));  // 不会调用构造函数
Foo* p2 = new Foo();                   // 会调用构造函数
delete p2;                             // 会调用析构函数

1.4 野指针和空指针

野指针(Wild Pointer):未被初始化的指针。指向不确定的内存地址。

空指针(Dangling Pointer):原指向的内存已被释放,但指针未被置空。

1
2
3
4
5
6
7
int* p;               // 野指针:未初始化
// *p = 10;           // 危险!

int* q = new int(5);
delete q;
// *q = 10;           // 空指针:内存已释放
q = nullptr;           // 安全:释放后置空

如何避免:

  • 定义指针后立即初始化
  • 释放内存后立即将指针置为 nullptr
  • 优先使用智能指针

二、智能指针

2.1 为什么需要智能指针

智能指针是一个类,当超出作用域时自动调用析构函数释放资源,避免手动 delete 导致的内存泄漏。

核心机制:RAII(Resource Acquisition Is Initialization,资源获取即初始化)。

2.2 四种智能指针

(1)auto_ptr(C++11 已废弃):采用所有权模式,存在潜在的内存崩溃问题。

1
2
3
auto_ptr<string> p1(new string("hello"));
auto_ptr<string> p2;
p2 = p1;  // p1 失去所有权,再次访问 p1 会崩溃

(2)unique_ptr(独占所有权):同一时刻只能有一个 unique_ptr 指向对象,禁止拷贝但支持移动。

1
2
3
4
5
6
unique_ptr<string> p1(new string("hello"));
// unique_ptr<string> p2 = p1;     // 编译错误!禁止拷贝
unique_ptr<string> p2 = move(p1);  // 转移所有权,p1 变为空

// 推荐:使用 make_unique(C++14)
auto p = make_unique<string>("hello");

(3)shared_ptr(共享所有权):多个 shared_ptr 可指向同一对象,通过引用计数管理生命周期。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
shared_ptr<int> p1 = make_shared<int>(42);
{
    shared_ptr<int> p2 = p1;  // 引用计数 +1
    cout << p1.use_count();   // 输出 2
}  // p2 析构,引用计数 -1
// p1 析构时引用计数归零,释放内存

// 引用计数存储在控制块(Control Block)中:
// - 强引用计数:shared_ptr 数量
// - 弱引用计数:weak_ptr 数量
// - 删除器、分配器

shared_ptr 的线程安全:

  • 引用计数的增减是原子操作(线程安全)
  • 对同一 shared_ptr 实例的写操作(如 reset())不是线程安全的
  • 对管理对象的直接访问需要外部同步

(4)weak_ptr(弱引用):不控制对象生命周期,配合 shared_ptr 解决循环引用问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
struct B;
struct A {
    shared_ptr<B> b_ptr;
    ~A() { cout << "A 析构"; }
};
struct B {
    // shared_ptr<A> a_ptr;  // 循环引用,导致内存泄漏
    weak_ptr<A> a_ptr;        // 使用 weak_ptr 打破循环
    ~B() { cout << "B 析构"; }
};

auto a = make_shared<A>();
auto b = make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;  // weak_ptr 不会增加引用计数
// a 和 b 都能正常析构

// weak_ptr 通过 lock() 获取 shared_ptr
if (auto shared = weak.lock()) {
    // 安全访问
}

2.3 智能指针的缺点

缺点说明解决方案
性能开销引用计数需要原子操作优先用 unique_ptr
循环引用shared_ptr 相互引用使用 weak_ptr 打破
线程安全代价原子操作有性能损耗无跨线程共享时用局部 shared_ptr
与裸指针混用可能导致悬垂指针避免暴露裸指针

三、指针和引用

3.1 指针和引用的区别

指针(Pointer)引用(Reference)
本质实体,存储地址值别名,不占用独立存储
初始化可以不初始化(野指针危险)定义时必须初始化
可修改可改变指向绑定后不可改变
空值可为 nullptr不能为空
const有 const 和非 const 区别无此概念
sizeof指针本身的大小(64位=8字节)所引用对象的大小
运算支持算术运算(++、–)不支持
使用需要解引用 *p直接使用
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int a = 10;
int* p = &a;     // 指针:存储 a 的地址
int& r = a;      // 引用:a 的别名

*p = 20;         // 通过指针修改
r = 30;          // 通过引用修改(无需解引用)

// 引用参数传递:无需拷贝,直接操作原对象
void swap(int& a, int& b) {
    int t = a;
    a = b;
    b = t;
}

3.2 函数参数传递方式

方式特点对实参的影响
值传递拷贝实参不影响
指针传递传地址值,解引用可修改可通过解引用修改
引用传递传别名,直接操作直接修改
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void byValue(int x) { x = 10; }        // 不影响实参
void byPointer(int* p) { *p = 10; }    // 修改实参
void byReference(int& r) { r = 10; }   // 修改实参

int a = 5;
byValue(a);      // a = 5
byPointer(&a);   // a = 10
byReference(a);  // a = 10

// 指针传参时,改变指针本身不影响外部
void changePtr(int* p) {
    p = new int(20);  // 只改了形参的指向,外部无影响
}
// 需要用指针的引用才能修改指针本身
void changePtrRef(int*& p) {
    p = new int(20);  // 外部指针也会改变
}

四、面向对象三大特性

4.1 封装

将数据和操作数据的方法绑定在一起,通过访问修饰符控制外部访问。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class BankAccount {
private:
    double balance;  // 私有,外部不可直接访问

public:
    void deposit(double amount) {
        if (amount > 0) balance += amount;
    }
    double getBalance() const { return balance; }
};

4.2 继承

派生类继承基类的属性和方法,实现代码复用。

继承方式:

继承方式基类 public基类 protected基类 private
public 继承publicprotected不可见
protected 继承protectedprotected不可见
private 继承privateprivate不可见

构造/析构顺序:

1
2
构造函数顺序:基类 → 成员对象 → 派生类自身
析构函数顺序:派生类自身 → 成员对象 → 基类

4.3 多态

三要素:继承、方法重写(virtual)、父类引用指向子类对象。

静态多态(编译时):函数重载、模板。编译时期决定调用的函数。

动态多态(运行时):通过虚函数实现。运行时期根据对象实际类型决定调用的函数。

4.4 虚函数原理(虚函数表)

虚函数表(vtable):当类中包含虚函数时,编译器为每个类生成一个虚函数表,保存该类中所有虚函数的地址。

虚函数指针(vptr):每个包含虚函数的类的对象有一个隐藏的虚函数指针,指向该类的虚函数表。vptr 在构造函数中初始化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Base {
public:
    virtual void func1() { cout << "Base::func1"; }
    virtual void func2() { cout << "Base::func2"; }
};

class Derived : public Base {
public:
    virtual void func1() override { cout << "Derived::func1"; }
    virtual void func3() { cout << "Derived::func3"; }
};

内存布局:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Base 对象:
┌─────────────┐
│ vptr → Base │  ← 指向 Base 的虚函数表
│ vtable      │
│   func1 ────┼──→ Base::func1
│   func2 ────┼──→ Base::func2
└─────────────┘

Derived 对象:
┌─────────────┐
│ vptr →Derived│  ← 指向 Derived 的虚函数表
│ vtable       │
│   func1 ─────┼──→ Derived::func1(重写,覆盖基类地址)
│   func2 ─────┼──→ Base::func2(未重写,保留基类地址)
│   func3 ─────┼──→ Derived::func3(新增)
└─────────────┘

编译器处理虚函数表的步骤:

  1. 拷贝基类的虚函数表(多继承则拷贝多个)
  2. 如果派生类重写基类虚函数,替换为已重写的函数地址
  3. 如果派生类有自身虚函数,追加到自身的虚函数表中

4.5 析构函数为什么一般定义为虚函数

1
2
Base* p = new Derived();
delete p;  // 如果析构函数不是虚函数,只会调用 Base 的析构,Derived 的资源泄漏

原因:基类指针指向派生类对象时,如果析构函数不是虚函数,编译器根据指针类型(Base)调用析构函数,派生类的资源无法释放,造成内存泄漏。定义为虚函数后,根据实际对象类型调用,先执行派生类析构,再执行基类析构。

4.6 构造函数为什么一般不定义为虚函数

  1. 虚函数调用需要知道对象的具体类型,但构造函数调用时对象还未创建
  2. 虚函数通过 vptr 调用,但 vptr 是在构造函数中才初始化的
  3. 从设计角度,构造函数是为了创建对象,不应该被重写

4.7 纯虚函数和抽象类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 纯虚函数:在基类中声明但不实现,派生类必须重写
class Shape {
public:
    virtual double area() const = 0;  // 纯虚函数
    virtual void draw() const = 0;
};

// 含有纯虚函数的类称为抽象类,不能实例化
// Shape s;  // 编译错误!

class Circle : public Shape {
public:
    double area() const override { return 3.14 * r * r; }
    void draw() const override { cout << "画圆形"; }
private:
    double r;
};

纯虚函数的目的:让派生类只继承函数的接口(interface),派生类必须提供实现。


五、重载、重写、隐藏

5.1 重载(Overload)

同一作用域内,同名函数但参数列表不同(个数/类型/顺序),不关心返回值。

1
2
3
4
5
6
7
class Print {
public:
    void show(int i) { cout << i; }
    void show(double d) { cout << d; }
    void show(string s) { cout << s; }
};
// 编译时根据参数确定调用哪个版本

5.2 重写(Override)

派生类重新定义基类的虚函数,函数名、参数列表、返回值必须完全相同。

1
2
3
4
5
6
7
8
class Base {
public:
    virtual void func() { cout << "Base"; }
};
class Derived : public Base {
public:
    void func() override { cout << "Derived"; }  // 重写
};

override(C++11):显式标记重写,编译器会检查是否真的重写了基类虚函数,避免因签名不一致导致的隐藏。

final(C++11):禁止类被继承,或禁止虚函数被重写。

1
2
3
4
5
6
class Base final { };  // 禁止继承
// class Derived : Base { };  // 编译错误

class Base2 {
    virtual void func() final;  // 禁止重写
};

5.3 隐藏(Hide)

派生类的函数屏蔽了基类的同名函数,只要函数名相同就会隐藏(不管参数是否相同)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Base {
public:
    void func(int x) { }
};
class Derived : public Base {
public:
    void func(string s) { }  // 隐藏了基类的 func(int)
};
// Derived d;
// d.func(10);      // 编译错误!因为 func(int) 被隐藏了
// d.Base::func(10); // 需通过基类作用域调用

六、const 和 static

6.1 const 关键字

修饰基本数据类型:变量成为常量,不可修改。

1
2
const int MAX = 100;      // 常量
int const MIN = 0;        // 等价

修饰指针:

1
2
3
4
const int* p = &a;  // 指向常量的指针(指针指向的内容不可变)
int* const p = &a;  // 常量指针(指针本身不可变,不能改指向)
const int* const p = &a;  // 指向常量的常量指针
// 左定值(const 在 * 左),右定向(const 在 * 右)

修饰函数参数:保护原对象,常用于指针或引用参数。

1
void print(const string& s);  // 不修改 s

修饰成员函数:承诺不修改成员变量,常量对象只能调用 const 成员函数。

1
2
3
4
5
6
7
8
class Person {
    int age;
public:
    int getAge() const { return age; }  // const 成员函数
    // 若需要修改某个成员,用 mutable
    mutable int cache;
    int getCache() const { return cache++; }  // mutable 可在 const 函数中修改
};

6.2 static 关键字

修饰局部变量:生命周期延长至程序结束,但作用域不变(仍限语句块内)。

1
2
3
4
5
void func() {
    static int count = 0;  // 只初始化一次
    count++;
    cout << count;  // 多次调用时,count 会累加
}

修饰全局变量/函数:作用域从整个工程缩小到本文件。

1
2
3
// file1.cpp
static int x = 10;      // 只在 file1.cpp 中可见
static void helper() { } // 只在 file1.cpp 中可调用

修饰类成员:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class MyClass {
public:
    static int value;            // 静态成员变量(所有对象共享一份)
    static void func() { }       // 静态成员函数(无 this 指针)
};
int MyClass::value = 0;          // 必须在类外初始化

// 静态成员函数的特点:
// 1. 无 this 指针
// 2. 只能访问静态成员变量
// 3. 不能被 virtual 修饰
// 4. 通过 类名:: 调用

七、拷贝控制

7.1 深拷贝和浅拷贝

浅拷贝:默认拷贝构造函数,逐字节复制成员。如果成员有指针,两个对象指向同一块内存,析构时会重复释放。

深拷贝:显式定义拷贝构造函数,为指针成员在堆上分配新内存。

 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
class String {
private:
    char* data;
public:
    // 浅拷贝(默认):两个 String 的 data 指向同一块内存
    // String(const String& other) : data(other.data) { }

    // 深拷贝:分配新内存
    String(const String& other) {
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
    }

    ~String() {
        delete[] data;
    }

    // 赋值运算符也需要深拷贝
    String& operator=(const String& other) {
        if (this == &other) return *this;  // 自赋值检查
        delete[] data;
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
        return *this;
    }
};

何时必须用深拷贝:类中包含指针成员、动态分配的资源、文件句柄等。

7.2 拷贝构造函数的调用时机

1
2
3
4
5
6
7
void funcByValue(String s) { }     // 值传递:拷贝构造
String funcReturn() {
    String s("hello");
    return s;                       // 返回值:拷贝构造
}
String s1 = s2;                     // 初始化:拷贝构造
String s3(s2);                      // 直接构造:拷贝构造

7.3 为什么拷贝构造函数必须是引用传递

1
2
3
4
// String(String other)  // 值传递 → 又需要拷贝构造来创建 other
// → 无限递归调用!

String(const String& other)  // 引用传递,避免递归

如果拷贝构造函数是值传递,为了传递实参又要调用拷贝构造函数,导致无限递归。


八、C++11 新特性

8.1 Lambda 表达式

Lambda 表达式提供匿名函数,可以在需要函数对象的地方直接定义。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 基本语法
auto lambda = [] { cout << "Hello"; };
lambda();

// 带参数
auto add = [](int a, int b) -> int { return a + b; };

// 捕获外部变量
int x = 10;
auto byValue = [x] { return x; };         // 值捕获
auto byRef = [&x] { return ++x; };        // 引用捕获
auto allValue = [=] { /* 值捕获所有 */ };
auto allRef = [&] { /* 引用捕获所有 */ };
auto mix = [=, &x] { /* 其余值捕获,x 引用捕获 */ };

// STL 算法中的 Lambda
vector<int> v = {1, 8, 5, 3, 6, 10};
int val = 3;
int count = count_if(v.begin(), v.end(), [val](int x) { return x > val; });

// mutable:允值捕获的变量在函数体内修改
auto mutableLambda = [x]() mutable { x++; return x; };

Lambda 的原理:编译器生成一个匿名类(闭包类型),重载 operator(),捕获的变量成为类的成员。

8.2 右值引用和移动语义

右值引用(&&):绑定到即将销毁的临时对象,允许"偷取"资源而不必深拷贝。

 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
int&& rref = 42;  // 右值引用绑定到临时值

class Person {
    char* data;
public:
    // 移动构造函数:从右值中"偷"资源
    Person(Person&& other) noexcept {
        data = other.data;         // 偷取指针
        other.data = nullptr;      // 原对象置空,防止双重释放
    }

    // 移动赋值运算符
    Person& operator=(Person&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            other.data = nullptr;
        }
        return *this;
    }
};

// std::move:将左值转为右值引用,触发移动语义
vector<int> v1 = {1, 2, 3};
vector<int> v2 = move(v1);  // v1 的资源被移动到 v2,v1 变为空

8.3 auto 和 decltype 类型推导

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
auto a = 10;                 // a 是 int
auto b = 3.14;               // b 是 double
auto c = "hello";            // c 是 const char*

vector<int> v = {1, 2, 3};
for (auto it = v.begin(); it != v.end(); ++it) { }  // 自动推导迭代器类型

decltype(a) d = 20;          // d 是 int
const int& ref = a;
decltype(ref) e = a;         // e 是 const int&

8.4 nullptr

1
2
3
4
5
6
7
// C++03:NULL 是宏,可能被定义为 0 或 (void*)0
void func(int);       // #1
void func(char*);     // #2
func(NULL);           // 调用 #1(意外!)

// C++11:nullptr 是真正的空指针类型,安全
func(nullptr);        // 调用 #2(符合预期)

九、STL 容器

9.1 vector 扩容机制

触发条件:插入元素时 size 超过 capacity。

扩容策略:

  • GCC:新容量 = 旧容量 × 2
  • MSVC:新容量 = 旧容量 × 1.5

扩容步骤:

  1. 分配更大的连续内存块
  2. 将旧元素移动/拷贝到新内存
  3. 释放旧内存
  4. 更新内部指针

时间复杂度:分摊 O(1)(每次插入的平均成本)。

迭代器失效:扩容后所有迭代器、指针、引用均失效。

1
2
3
4
5
6
vector<int> v;
v.reserve(100);  // 预分配容量,避免多次扩容

// 扩容过程示意图:
// capacity=1 → push_back → capacity=2(分配新内存,移动元素)
// → push_back → capacity=4 → push_back → capacity=8 ...

9.2 list 和 vector 的区别

vectorlist
内存连续不连续(双向链表)
随机访问O(1)O(n)
插入/删除O(n)(需移动元素)O(1)(已知迭代器时)
迭代器失效扩容时全部失效仅被删除的元素失效
适用场景频繁随机访问频繁插入删除

9.3 红黑树(RB-Tree)

定义:一种自平衡的二叉查找树,确保没有一条路径比其他路径长出两倍。

性质:

  1. 每个节点是红色或黑色
  2. 根节点是黑色
  3. 叶节点(NIL)是黑色
  4. 红色节点的子节点必须是黑色
  5. 任一节点到每个叶子的路径包含相同数量的黑节点

应用:C++ STL 中的 std::map、std::set 底层基于红黑树。

和 AVL 树的对比:红黑树是弱平衡,旋转次数少,适合插入/删除频繁的场景。


十、四种类型转换(cast)

10.1 static_cast

编译时检测的相关类型转换。用于整型和浮点数之间、相关类之间的指针/引用转换。

1
2
3
4
int i = 10;
float f = static_cast<float>(i);                // 数值类型转换
Base* b = new Derived();
Derived* d = static_cast<Derived*>(b);          // 向下转换(不安全,不检查)

10.2 dynamic_cast

运行时检测的向下转换,需要 RTTI(运行时类型信息)。安全但有一定性能代价。

1
2
3
4
5
6
Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b);  // 成功,返回有效指针
if (d) { /* 安全操作 */ }

Base* base = new Base();
Derived* d2 = dynamic_cast<Derived*>(base);  // 失败,返回 nullptr

10.3 const_cast

移除或添加 const / volatile 属性。

1
2
3
const int ci = 10;
int& modifiable = const_cast<int&>(ci);  // 移除 const 属性
modifiable = 20;  // 如果原对象本质是 const,这是未定义行为

10.4 reinterpret_cast

最危险的 cast,在几乎任意两种类型之间转换(如整型和指针)。

1
2
int* p = new int(65);
char* ch = reinterpret_cast<char*>(p);  // 指针类型强制转换

使用建议:

  • 优先使用 static_cast
  • 需要多态时用 dynamic_cast
  • 仅处理 const 时用 const_cast
  • 尽量避免 reinterpret_cast

十一、内存池

定义:预先分配一块大内存,将其划分为多个固定大小的块,程序从池中分配和释放,减少频繁 new/delete 的开销和内存碎片。

优点:

  • 减少系统调用
  • 避免内存碎片
  • 分配速度快(O(1))
 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
class MemoryPool {
private:
    struct Block {
        Block* next;
    };
    Block* freeList = nullptr;

public:
    MemoryPool(size_t blockSize, size_t count) {
        for (size_t i = 0; i < count; i++) {
            Block* block = reinterpret_cast<Block*>(new char[blockSize]);
            block->next = freeList;
            freeList = block;
        }
    }

    void* allocate() {
        if (!freeList) throw bad_alloc();
        Block* block = freeList;
        freeList = freeList->next;
        return block;
    }

    void deallocate(void* ptr) {
        Block* block = reinterpret_cast<Block*>(ptr);
        block->next = freeList;
        freeList = block;
    }
};

十二、链接与编译

12.1 从源码到可执行文件的四个阶段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
.cpp 文件
    ↓ 预处理(Preprocessing)
展开 #define、处理 #include、删除注释
    ↓
.i 文件
    ↓ 编译(Compilation)
词法分析 → 语法分析 → 语义分析 → 生成汇编代码
    ↓
.s 文件
    ↓ 汇编(Assembly)
汇编代码 → 机器指令
    ↓
.o 文件(目标文件)
    ↓ 链接(Linking)
符号解析 + 重定向 → 合并目标文件和库
    ↓
可执行文件(.exe / a.out)

12.2 静态链接 vs 动态链接

静态链接(.a / .lib)动态链接(.so / .dll)
代码位置嵌入可执行文件运行时加载
可执行文件大小大小
依赖独立运行依赖外部 DLL
更新需重新编译替换库文件即可
内存占用每个进程一份多个进程共享一份
加载速度快稍慢(需加载库)

十三、this 指针

定义:this 是隐含于每一个非静态成员函数中的特殊指针,指向正在被该成员函数操作的对象。

本质:

  • this 的类型是 ClassName* const(不能修改指向)
  • 在 const 成员函数中,类型为 const ClassName* const(指向的内容也不能修改)
  • this 是一个右值,不能取地址(不能 &this)

使用场景:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class MyClass {
    int value;
public:
    MyClass& setValue(int value) {
        this->value = value;    // 区分成员变量和参数
        return *this;            // 链式调用
    }

    bool operator==(const MyClass& other) {
        return this == &other;  // 检查是否同一对象
    }
};
// 链式调用:obj.setValue(10).setValue(20);

十四、面试高频问答

14.1 程序是怎样跑起来的

  1. 编译阶段:预处理 → 编译 → 汇编 → 链接,生成可执行文件
  2. 进程创建:操作系统分配虚拟地址空间,加载代码段和数据段
  3. 初始化:设置堆栈,初始化全局变量,调用全局对象的构造函数
  4. 入口点:跳转到 _start,最终调用 main()
  5. 执行:CPU 逐条执行指令,通过 MMU 将虚拟地址映射为物理地址
  6. 系统调用:需要操作系统服务时(如 IO),进入内核态执行
  7. 结束:程序执行完,操作系统回收资源

14.2 构造函数的类型

类型说明示例
默认构造函数无参数或所有参数有默认值Person() { }
拷贝构造函数接受同类型常量引用Person(const Person&)
转换构造函数单参数构造函数explicit Damage(int)
委托构造函数(C++11)一个构造调用另一个构造Person() : Person(0) { }
继承构造函数(C++11)using 继承基类构造using Base::Base;

14.3 智能指针的核心组成

  1. 原始指针封装:内部持有 T*
  2. 所有权模型:unique_ptr 独占、shared_ptr 共享、weak_ptr 弱引用
  3. 引用计数控制块:强引用计数 + 弱引用计数 + 删除器
  4. 删除器:支持自定义释放逻辑
  5. 运算符重载:operator*、operator->
  6. RAII:析构时自动释放

本文涵盖 C++ 从基础到进阶的核心知识点,适合面试复习和系统性查阅。

不嘻嘻
使用 Hugo 构建
主题 Stack 由 Jimmy 设计