编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

    • README
    • 入门教程

      • README
      • Cpp简史
      • 基础语法
      • 数据类型
      • 运算符
      • 复合类型
      • 流程语句
      • 函数
      • 指针引用
      • 类和对象
      • 继承多态
      • 内存模型
      • 动态内存
        • 12.1 动态内存概念
          • 12.1.1 何为动态内存
          • 12.1.2 动态内存特点
          • 12.1.3 综合案例与思考
        • 12.2 动态内存使用
          • 12.2.1 new分配内存
          • 12.2.2 delete释放内存
          • 12.2.3 动态内存示例
          • 12.2.4 动态内存最佳实践
          • 12.2.5 综合案例与思考
        • 12.3 动态内存问题
          • 12.3.1 内存泄漏
          • 12.3.2 野指针
          • 12.3.3 悬空指针问题
          • 12.3.5 空指针
          • 12.3.6 综合案例与思考
        • 12.4 智能指针
          • 12.4.1 unique_ptr
          • 12.4.2 shared_ptr
          • 12.4.3 weak_ptr
          • 12.4.4 综合案例与思考
        • 12.5 内存管理函数
          • 12.5.1 malloc和free
          • 12.5.2 calloc
          • 12.5.3 realloc
          • 12.5.4 综合案例与思考
        • 12.6 内存池
          • 12.6.1 内存池概念
          • 12.6.2 内存池核心思想
          • 12.6.3 内存池的优点
          • 12.6.4 内存池的实现
          • 12.6.5 内存池优化
          • 12.6.6 综合案例与思考
        • 12.7 高级用法
          • 12.7.1 enablesharedfrom_this
        • 12.8 动态内存底层原理
          • 12.8.1 堆内存分配器原理
          • 12.8.2 new/delete的底层过程
          • 12.8.3 智能指针的底层实现
        • 12.9 动态内存训练题
          • 12.9.1 简易内存池
          • 12.9.2 不同类型对比
        • 12.10 综合思考题
        • 12.11 新手陷阱 Top 5
      • IO和文件
      • 异常处理
      • 线程和锁
      • STL模版
      • 预处理器
      • 特性图谱
    • 综合案例

    • 专栏博客

    • 开发技巧

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Cpp入门到精通
  • 入门教程
杨充
2026-05-07
目录

动态内存

# 第 12 章 C++ 动态内存

# 目录介绍

  • 12.1 动态内存概念
    • 12.1.1 何为动态内存
    • 12.1.2 动态内存特点
    • 12.1.3 综合案例与思考
  • 12.2 动态内存操作
    • 12.2.1 new分配内存
    • 12.2.2 delete释放内存
    • 12.2.3 动态内存示例
    • 12.2.4 动态内存最佳实践
    • 12.2.5 综合案例与思考
  • 12.3 动态内存问题
    • 12.3.1 内存泄漏
    • 12.3.2 野指针
    • 12.3.3 悬空指针问题
    • 12.3.4 重复释放
    • 12.3.5 空指针
    • 12.3.6 综合案例与思考
  • 12.4 智能指针
    • 12.4.1 unique_ptr
    • 12.4.2 shared_ptr
    • 12.4.3 weak_ptr
    • 12.4.4 综合案例与思考
  • 12.5 内存管理函数
    • 12.5.1 malloc和free
    • 12.5.2 calloc
    • 12.5.3 realloc
    • 12.5.4 综合案例与思考
  • 12.6 内存池
    • 12.6.1 内存池概念
    • 12.6.2 内存池核心思想
    • 12.6.3 内存池的优点
    • 12.6.4 内存池的实现
    • 12.6.5 内存池优化
    • 12.6.6 综合案例与思考
  • 12.7 高级用法
    • 12.7.1 enable_shared_from_this
  • 12.8 动态内存底层原理
    • 12.8.1 堆内存分配器原理
    • 12.8.2 new/delete的底层过程
    • 12.8.3 智能指针的底层实现
  • 12.9 动态内存训练题
    • 12.9.1 简易内存池
    • 12.9.2 不同类型对比
  • 12.10 综合思考题

# 12.1 动态内存概念

动态内存是指在程序运行时(而非编译时)分配和释放的内存。

# 12.1.1 何为动态内存

在 C++ 中,动态内存 是指在程序运行时(而不是编译时)从堆(Heap)中分配的内存。与栈上的自动内存管理不同,动态内存的分配和释放由程序员显式控制。

通过 new 和 delete 操作符(或 C 风格的 malloc 和 free)来分配和释放内存。

动态内存的使用场景包括:

  • 需要灵活管理内存大小(如动态数组)。
  • 对象的生命周期超出其作用域。
  • 需要共享内存(如多线程环境)。

# 12.1.2 动态内存特点

  1. 运行时分配: 内存的分配和释放发生在程序运行时,而不是编译时。
  2. 手动管理: 程序员需要显式分配和释放内存,否则会导致内存泄漏。
  3. 堆区存储: 动态内存从堆区分配,堆区的空间通常比栈区大。
  4. 灵活性: 动态内存的大小可以在运行时动态调整,适合处理不确定大小的数据。

# 12.1.3 综合案例与思考

#include <iostream>
using namespace std;

int globalVar = 10;          // 全局区(静态存储期)

int main() {
    int stackVar = 20;       // 栈区(自动存储期)
    static int staticVar = 30; // 静态区(静态存储期)
    int* heapVar = new int(40); // 堆区(动态存储期)
    
    cout << "=== 内存区域对比 ===" << endl;
    cout << "全局变量地址: " << &globalVar << " 值: " << globalVar << endl;
    cout << "栈变量地址:   " << &stackVar  << " 值: " << stackVar << endl;
    cout << "静态变量地址: " << &staticVar  << " 值: " << staticVar << endl;
    cout << "堆变量地址:   " << heapVar     << " 值: " << *heapVar << endl;
    
    // 动态数组 —— 运行时决定大小
    int size;
    cout << "\n请输入数组大小: ";
    cin >> size;
    int* dynamicArr = new int[size];
    for (int i = 0; i < size; i++) dynamicArr[i] = i * 10;
    for (int i = 0; i < size; i++) cout << dynamicArr[i] << " ";
    cout << endl;
    
    // 必须手动释放
    delete heapVar;
    delete[] dynamicArr;
    heapVar = nullptr;
    dynamicArr = nullptr;
    
    cout << "动态内存已释放" << endl;
    return 0;
}
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

案例知识融合:本案例对比了四种存储区域——全局区、栈区、静态区和堆区。动态内存(堆区)的核心特点是运行时分配、程序员手动管理。通过动态数组展示了"编译时不确定大小"的场景,这正是动态内存的典型使用场景。释放后将指针置为nullptr是防止悬空指针的最佳实践。

思考题:

  1. 观察四个变量的地址,栈和堆的地址通常差异很大,这反映了什么内存布局特点?
  2. 如果忘记 delete[] dynamicArr,程序短期内能正常运行吗?长期运行会怎样?
  3. new int(40) 和 new int[40] 有什么区别?对应的释放方式分别是什么?

# 12.2 动态内存使用

# 12.2.1 new分配内存

C++中利用==new==操作符在堆区开辟数据

堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符 ==delete==

语法:new 数据类型

利用new创建的数据,会返回该数据对应的类型的指针。分配单个对象:

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructor called" << std::endl;
    }

    MyClass(int age, string name) {
        this->age = age;
        this->name = name;
        std::cout << "MyClass constructor " << this->age << " , " << this->name << std::endl;
    }

    ~MyClass() {
        std::cout << "MyClass destructor called" << std::endl;
    }

public:
    int age;
    string name;
};

void test1() {
    // 使用new运算符创建MyClass对象
    MyClass* obj = new MyClass();
    // 使用对象指针调用对象的成员函数
    // ...
    // 释放动态分配的内存
    delete obj;
}

void test2() {
    MyClass* obj = new MyClass(30,"yc");
    delete obj;
}

int main() {
//    test1();
    test2();
    return 0;
}
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

手动管理动态数组

//堆区开辟数组
void test() {
    int* arr = new int[10];
    for (int i = 0; i < 10; i++) {
        arr[i] = i+ 100;
    }
    for (int i = 0; i < 10; i++) {
        cout << arr[i] << endl;
    }
    delete[] arr;
}

int main() {
    test();
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

动态数组,C++ 提供了 std::vector 作为动态数组的替代方案,避免手动管理内存。

#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3}; // 动态数组
    vec.push_back(4); // 添加元素
    for (int i : vec) {
        std::cout << i << " "; // 输出: 1 2 3 4
    }
    return 0;
}
1
2
3
4
5
6
7
8
9
10

# 12.2.2 delete释放内存

释放单个对象:

delete ptr; // 释放单个对象
ptr = nullptr; // 将指针置为 nullptr,避免野指针
1
2

释放数组:

delete[] arr; // 释放数组
arr = nullptr; // 将指针置为 nullptr
1
2

# 12.2.3 动态内存示例

动态数组

#include <iostream>

int main() {
    int size = 5;
    int* arr = new int[size]; // 动态分配数组

    for (int i = 0; i < size; i++) {
        arr[i] = i * 2; // 初始化数组
    }

    for (int i = 0; i < size; i++) {
        std::cout << arr[i] << " "; // 输出数组
    }

    delete[] arr; // 释放数组内存
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

动态对象

#include <iostream>

class MyClass {
public:
    MyClass() { std::cout << "Constructor called!\n"; }
    ~MyClass() { std::cout << "Destructor called!\n"; }
    void print() { std::cout << "Hello, World!\n"; }
};

int main() {
    MyClass* obj = new MyClass; // 动态分配对象
    obj->print();
    delete obj; // 释放对象内存
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 12.2.4 动态内存最佳实践

  1. 优先使用智能指针:避免手动管理内存,减少内存泄漏和悬空指针的风险。
  2. 避免裸指针:尽量使用 std::unique_ptr 或 std::shared_ptr。
  3. 使用容器类:如 std::vector、std::list 等,避免手动管理动态数组。
  4. 检查内存分配是否成功:在分配大量内存时,检查指针是否为 nullptr。
  5. 避免内存泄漏:确保每次 new 都有对应的 delete,或使用智能指针。

# 12.2.5 综合案例与思考

#include <iostream>
#include <string>
#include <vector>
using namespace std;

class Student {
    string name;
    int* scores;
    int count;
public:
    Student(const string& n, int cnt) : name(n), count(cnt) {
        scores = new int[count]();  // 动态分配 + 零初始化
        cout << "创建学生: " << name << " (分配" << count << "门课)" << endl;
    }
    
    ~Student() {
        cout << "销毁学生: " << name << endl;
        delete[] scores;
    }
    
    void setScore(int idx, int val) {
        if (idx >= 0 && idx < count) scores[idx] = val;
    }
    
    void display() const {
        cout << name << " 成绩: ";
        for (int i = 0; i < count; i++) cout << scores[i] << " ";
        cout << endl;
    }
};

int main() {
    // 1. 单个对象 new/delete
    cout << "=== 单个对象 ===" << endl;
    Student* s1 = new Student("张三", 3);
    s1->setScore(0, 90);
    s1->setScore(1, 85);
    s1->setScore(2, 92);
    s1->display();
    delete s1;  // 调用析构函数释放内部 scores
    s1 = nullptr;
    
    // 2. 对象数组 new[]/delete[]
    cout << "\n=== 对象数组 ===" << endl;
    Student* arr = new Student[2]{{"李四", 2}, {"王五", 2}};
    arr[0].setScore(0, 78);
    arr[1].setScore(0, 88);
    arr[0].display();
    arr[1].display();
    delete[] arr;
    
    // 3. 推荐:用 vector 替代手动管理
    cout << "\n=== vector 自动管理 ===" << endl;
    vector<int> vec = {1, 2, 3};
    vec.push_back(4);
    for (int v : vec) cout << v << " ";
    cout << endl;
    // 无需手动释放
    
    return 0;
}
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

案例知识融合:本案例综合了 new/delete 的各种用法:单个对象的分配与释放、对象数组的分配(new[]必须用delete[]释放)、类内部的动态内存管理(构造函数分配、析构函数释放)。最后展示了 vector 作为手动动态数组的替代方案,体现了"优先使用容器类"的最佳实践。

思考题:

  1. 如果对 new Student[2] 使用 delete 而非 delete[],会发生什么?析构函数会被正确调用吗?
  2. new int[count]() 末尾的 () 是什么意思?去掉后数组元素的初始值是什么?
  3. 在什么情况下必须用 new 而不能用 vector?

# 12.3 动态内存问题

# 12.3.1 内存泄漏

内存泄漏是指程序在动态分配内存后,未能正确释放该内存,导致这部分内存无法被再次使用。内存泄漏会逐渐消耗系统的内存资源,最终可能导致程序或系统崩溃。

以下是一个典型的内存泄漏案例及其分析:

#include <iostream>

void createMemoryLeak() {
    int* ptr = new int(42);  // 动态分配内存
    std::cout << "Value: " << *ptr << std::endl;
    // 忘记释放内存
}

int main() {
    for (int i = 0; i < 1000000; i++) {
        createMemoryLeak();  // 多次调用,导致内存泄漏
    }
    std::cout << "Program finished." << std::endl;
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在 main 函数中,createMemoryLeak 被调用了 100 万次,每次都会泄漏一块内存。随着程序运行,泄漏的内存会逐渐增加,最终可能导致系统内存耗尽,程序崩溃。

修复后的代码:解决方法是确保每次 new 都有对应的 delete。

void noMemoryLeak() {
    int* ptr = new int(42);  // 动态分配内存
    std::cout << "Value: " << *ptr << std::endl;
    delete ptr;  // 释放内存
    ptr = nullptr;  // 避免野指针
}
1
2
3
4
5
6

# 12.3.2 野指针

野指针(Dangling Pointer) 是指指向已经释放或无效内存的指针。访问野指针会导致未定义行为,可能导致程序崩溃或数据损坏。

以下是一个典型的野指针案例:代码示例

#include <iostream>

int* createInt() {
    int value = 42;
    return &value;  // 返回局部变量的地址
}

int main() {
    int* ptr = createInt();  // ptr 成为野指针
    std::cout << "Value: " << *ptr << std::endl;  // 未定义行为
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12

代码分析如下所示:

  1. 局部变量的生命周期: 在 createInt 函数中,value 是一个局部变量,其生命周期仅限于函数作用域。 当 createInt 函数返回时,value 的内存被释放。
  2. 返回局部变量的地址:createInt 函数返回了局部变量 value 的地址。由于 value 的内存已经被释放,返回的指针 ptr 成为野指针。
  3. 访问野指针:在 main 函数中,尝试通过野指针 ptr 访问内存,导致未定义行为。程序可能会崩溃、输出错误值,或者表现出其他异常行为。

修复野指针的方法是确保指针始终指向有效内存。

#include <iostream>

int* createInt() {
    int* value = new int(42);  // 动态分配内存
    return value;  // 返回动态分配的内存地址
}

int main() {
    int* ptr = createInt();  // ptr 指向有效内存
    std::cout << "Value: " << *ptr << std::endl;  // 安全访问
    delete ptr;  // 释放内存
    ptr = nullptr;  // 避免野指针
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. 动态分配内存:在 createInt 函数中,使用 new 动态分配内存,确保内存的生命周期不受函数作用域限制。
  2. 返回有效指针:createInt 函数返回动态分配的内存地址,指针 ptr 指向有效内存。
  3. 释放内存: 在 main 函数中,使用 delete 释放动态分配的内存。将指针 ptr 设置为 nullptr,避免野指针。

# 12.3.3 悬空指针问题

悬空指针(Dangling Pointer) 是指指向已经释放或无效内存的指针。悬空指针和野指针是同一个概念,通常可以互换使用。访问悬空指针会导致未定义行为,可能导致程序崩溃或数据损坏。

以下是一个典型的悬空指针案例及其分析:代码示例

#include <iostream>

int main() {
    int* ptr = new int(42);  // 动态分配内存
    std::cout << "Value: " << *ptr << std::endl;  // 输出 42
    delete ptr;  // 释放内存
    // 此时 ptr 成为悬空指针
    std::cout << "Value after delete: " << *ptr << std::endl;  // 未定义行为
    return 0;
}
1
2
3
4
5
6
7
8
9
10

代码分析

  1. 动态分配内存:使用 new 动态分配了一块内存,并将其地址赋值给指针 ptr。通过 *ptr 可以访问该内存,输出值为 42。
  2. 释放内存:使用 delete 释放了 ptr 指向的内存。此时,ptr 仍然指向原来的内存地址,但该内存已经被释放,ptr 成为悬空指针。
  3. 访问悬空指针: 在释放内存后,尝试通过 *ptr 访问内存,导致未定义行为。程序可能会崩溃、输出错误值,或者表现出其他异常行为。

修复悬空指针的方法是确保指针在释放内存后不再被访问。

#include <iostream>

int main() {
    int* ptr = new int(42);  // 动态分配内存
    std::cout << "Value: " << *ptr << std::endl;  // 输出 42

    delete ptr;  // 释放内存
    ptr = nullptr;  // 将指针置为 nullptr

    if (ptr != nullptr) {
        std::cout << "Value after delete: " << *ptr << std::endl;  // 不会执行
    } else {
        std::cout << "Pointer is null, memory is freed." << std::endl;  // 安全处理
    }
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  1. 释放内存:使用 delete 释放了 ptr 指向的内存。
  2. 置空指针:将 ptr 设置为 nullptr,避免悬空指针。
  3. 安全访问:在访问指针之前,检查指针是否为 nullptr,确保不会访问无效内存。

# 12.3.5 空指针

空指针:空指针是指不指向任何有效内存地址的指针。空指针通常用来表示指针没有被初始化或者指向了无效的内存地址。在 C++ 中,空指针的值通常是 0 或者使用 nullptr 关键字表示。

用途:初始化指针变量

注意:空指针指向的内存是不可以访问的

int main() {
	//指针变量p指向内存地址编号为0的空间
	int * p = NULL;
	//访问空指针报错 
	//内存编号0 ~255为系统占用内存,不允许用户访问
	cout << *p << endl;
	return 0;
}
1
2
3
4
5
6
7
8

# 12.3.6 综合案例与思考

#include <iostream>
using namespace std;

// 错误示范集合 + 修复
void demo_memory_problems() {
    // 问题1: 内存泄漏
    cout << "=== 1. 内存泄漏 ===" << endl;
    {
        int* leak = new int(42);
        cout << "分配了内存但忘记释放: " << *leak << endl;
        // 修复: delete leak;
        delete leak;  // 修复后
    }
    
    // 问题2: 悬空指针
    cout << "\n=== 2. 悬空指针 ===" << endl;
    {
        int* p = new int(100);
        cout << "释放前: " << *p << endl;
        delete p;
        // cout << *p;  // 危险!悬空指针
        p = nullptr;    // 修复:置空
        if (p) cout << *p;
        else cout << "指针已置空,安全" << endl;
    }
    
    // 问题3: 重复释放
    cout << "\n=== 3. 重复释放 ===" << endl;
    {
        int* p = new int(200);
        delete p;
        p = nullptr;    // 修复:置空后 delete nullptr 是安全的
        delete p;       // delete nullptr 不会崩溃
        cout << "delete nullptr 是安全的" << endl;
    }
    
    // 问题4: 数组与非数组混淆
    cout << "\n=== 4. new[]/delete[] 匹配 ===" << endl;
    {
        int* arr = new int[5]{10, 20, 30, 40, 50};
        // delete arr;   // 错误!应该用 delete[]
        delete[] arr;    // 正确
        cout << "new[] 必须配 delete[]" << endl;
    }
}

int main() {
    demo_memory_problems();
    cout << "\n结论: 优先使用智能指针,避免手动管理内存!" << endl;
    return 0;
}
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
46
47
48
49
50
51

案例知识融合:本案例将动态内存的四大常见问题——内存泄漏、悬空指针、重复释放、new[]/delete[]不匹配——集中演示并给出修复方案。核心修复原则:释放后置nullptr、new/delete成对出现、new[]/delete[]匹配。这些都是C++手动内存管理中最容易踩的坑。

思考题:

  1. 如何检测程序中的内存泄漏?常用的工具有哪些(如Valgrind、AddressSanitizer)?
  2. 为什么 delete nullptr 是安全的?C++标准是如何规定的?
  3. 如果一个函数可能在中间 throw 异常,如何确保已分配的内存不会泄漏?(提示:RAII)

# 12.4 智能指针

C++11 引入了智能指针,用于自动管理动态内存,避免内存泄漏和悬空指针。

  • std::unique_ptr:独占所有权的智能指针。
  • std::shared_ptr:共享所有权的智能指针。
  • std::weak_ptr:弱引用,不增加引用计数。

# 12.4.1 unique_ptr

  • 独占所有权的智能指针。
  • 不能复制,只能移动。
  • 适用于单一所有者场景。

std::unique_ptr 是 C++11 引入的智能指针,用于管理动态分配的内存。它通过 独占所有权 的机制确保内存资源在不再需要时自动释放,从而避免内存泄漏和悬空指针问题。

以下是一个使用 std::unique_ptr 的案例及其详细说明。

#include <iostream>
#include <memory>  // 包含 std::unique_ptr

class MyClass {
public:
    MyClass(int value) : value_(value) {
        std::cout << "MyClass constructed with value: " << value_ << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass destroyed with value: " << value_ << std::endl;
    }
    void printValue() const {
        std::cout << "Value: " << value_ << std::endl;
    }
private:
    int value_;
};

int main() {
    // 创建一个 std::unique_ptr,管理 MyClass 对象
    std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>(42);
    // 使用指针访问对象成员函数
    ptr->printValue();
    // 转移所有权到另一个 std::unique_ptr
    std::unique_ptr<MyClass> ptr2 = std::move(ptr);
    if (!ptr) {
        std::cout << "ptr is now nullptr, ownership transferred to ptr2." << std::endl;
    }
    // 使用新的指针访问对象成员函数
    ptr2->printValue();
    // ptr2 超出作用域,自动释放内存
    return 0;
}
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
  1. 创建 std::unique_ptr: 使用 std::make_unique<MyClass>(42) 创建一个 std::unique_ptr,它管理一个动态分配的 MyClass 对象。std::make_unique 是 C++14 引入的便捷函数,用于创建 std::unique_ptr。
  2. 访问对象成员函数: 通过 ptr->printValue() 访问 MyClass 对象的成员函数。
  3. 转移所有权:使用 std::move(ptr) 将 ptr 的所有权转移给 ptr2。转移后,ptr 变为 nullptr,不再指向任何对象。
  4. 自动释放内存:当 ptr2 超出作用域时,std::unique_ptr 会自动调用 delete 释放其管理的对象。

输出结果

MyClass constructed with value: 42
Value: 42
ptr is now nullptr, ownership transferred to ptr2.
Value: 42
MyClass destroyed with value: 42
1
2
3
4
5

std::unique_ptr 的特点

  1. 独占所有权:一个 std::unique_ptr 独占其管理的对象,不能复制,只能移动。这确保了同一时间只有一个 std::unique_ptr 指向该对象。
  2. 自动释放内存:当 std::unique_ptr 超出作用域时,会自动释放其管理的对象,无需手动调用 delete。
  3. 避免内存泄漏和悬空指针:由于 std::unique_ptr 自动管理内存,可以避免内存泄漏和悬空指针问题。
  4. 支持自定义删除器:可以通过自定义删除器来管理非动态分配的资源(如文件句柄、网络连接等)。

# 12.4.2 shared_ptr

共享所有权的智能指针。std::shared_ptr 是 C++11 引入的智能指针,用于管理动态分配的内存。

与 std::unique_ptr 不同,std::shared_ptr 支持 共享所有权,即多个 std::shared_ptr 可以共享同一个对象,当最后一个 std::shared_ptr 超出作用域时,对象才会被释放。

以下是一个使用 std::shared_ptr 的案例及其详细说明。代码示例

#include <iostream>
#include <memory>  // 包含 std::shared_ptr

class MyClass {
public:
    MyClass(int value) : value_(value) {
        std::cout << "MyClass constructed with value: " << value_ << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass destroyed with value: " << value_ << std::endl;
    }
    void printValue() const {
        std::cout << "Value: " << value_ << std::endl;
    }
private:
    int value_;
};

int main() {
    // 创建一个 std::shared_ptr,管理 MyClass 对象
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(42);

    // 使用指针访问对象成员函数
    ptr1->printValue();

    // 创建另一个 std::shared_ptr,共享所有权
    std::shared_ptr<MyClass> ptr2 = ptr1;

    // 输出引用计数
    std::cout << "Reference count after ptr2 creation: " << ptr1.use_count() << std::endl;

    // 使用新的指针访问对象成员函数
    ptr2->printValue();

    // ptr1 超出作用域,引用计数减 1
    {
        std::shared_ptr<MyClass> ptr3 = ptr1;
        std::cout << "Reference count after ptr3 creation: " << ptr1.use_count() << std::endl;
    }

    // ptr3 超出作用域,引用计数减 1
    std::cout << "Reference count after ptr3 destruction: " << ptr1.use_count() << std::endl;

    // ptr2 超出作用域,引用计数减 1,对象被释放
    return 0;
}
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
46
  1. 创建 std::shared_ptr: 使用 std::make_shared<MyClass>(42) 创建一个 std::shared_ptr,它管理一个动态分配的 MyClass 对象。std::make_shared 是 C++11 引入的便捷函数,用于创建 std::shared_ptr。
  2. 共享所有权: 通过赋值操作 ptr2 = ptr1,ptr2 与 ptr1 共享对同一个对象的所有权。引用计数增加,表示当前有两个 std::shared_ptr 指向该对象。
  3. 访问对象成员函数:通过 ptr1->printValue() 和 ptr2->printValue() 访问 MyClass 对象的成员函数。
  4. 引用计数:使用 use_count() 方法获取当前 std::shared_ptr 的引用计数。当新的 std::shared_ptr 共享所有权时,引用计数增加;当 std::shared_ptr 超出作用域时,引用计数减少。
  5. 自动释放内存:当最后一个 std::shared_ptr 超出作用域时,引用计数变为 0,对象被自动释放。

输出结果

MyClass constructed with value: 42
Value: 42
Reference count after ptr2 creation: 2
Value: 42
Reference count after ptr3 creation: 3
Reference count after ptr3 destruction: 2
MyClass destroyed with value: 42
1
2
3
4
5
6
7

std::shared_ptr 的特点

  1. 共享所有权:多个 std::shared_ptr 可以共享同一个对象的所有权。引用计数机制用于跟踪当前有多少个 std::shared_ptr 指向该对象。
  2. 自动释放内存:当最后一个 std::shared_ptr 超出作用域时,对象会被自动释放。
  3. 避免内存泄漏和悬空指针:由于 std::shared_ptr 自动管理内存,可以避免内存泄漏和悬空指针问题。
  4. 支持自定义删除器:可以通过自定义删除器来管理非动态分配的资源(如文件句柄、网络连接等)。

# 12.4.3 weak_ptr

std::weak_ptr 是 C++11 引入的智能指针,用于解决 std::shared_ptr 的循环引用问题。

它是对 std::shared_ptr 管理的对象的 弱引用,不会增加引用计数,也不会影响对象的生命周期。

以下是一个使用 std::weak_ptr 的案例及其详细说明。

#include <iostream>
#include <memory>  // 包含 std::shared_ptr 和 std::weak_ptr

class MyClass {
public:
    MyClass(int value) : value_(value) {
        std::cout << "MyClass constructed with value: " << value_ << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass destroyed with value: " << value_ << std::endl;
    }
    void setSharedPtr(std::shared_ptr<MyClass> ptr) {
        sharedPtr_ = ptr;
    }
    void printValue() const {
        std::cout << "Value: " << value_ << std::endl;
    }

private:
    int value_;
    std::shared_ptr<MyClass> sharedPtr_;  // 用于演示循环引用
};

int main() {
    // 创建两个 std::shared_ptr,管理 MyClass 对象
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(42);
    std::shared_ptr<MyClass> ptr2 = std::make_shared<MyClass>(100);

    // 使用 std::weak_ptr 观察 ptr1
    std::weak_ptr<MyClass> weakPtr = ptr1;

    // 检查 weakPtr 是否有效
    if (auto sharedPtr = weakPtr.lock()) {
        std::cout << "weakPtr is valid. Value: ";
        sharedPtr->printValue();
    } else {
        std::cout << "weakPtr is expired." << std::endl;
    }

    // 释放 ptr1
    ptr1.reset();

    // 再次检查 weakPtr 是否有效
    if (auto sharedPtr = weakPtr.lock()) {
        std::cout << "weakPtr is valid. Value: ";
        sharedPtr->printValue();
    } else {
        std::cout << "weakPtr is expired." << std::endl;
    }

    // 演示循环引用问题
    {
        std::shared_ptr<MyClass> ptrA = std::make_shared<MyClass>(10);
        std::shared_ptr<MyClass> ptrB = std::make_shared<MyClass>(20);

        ptrA->setSharedPtr(ptrB);  // ptrA 持有 ptrB
        ptrB->setSharedPtr(ptrA);  // ptrB 持有 ptrA

        // 此时 ptrA 和 ptrB 互相引用,导致引用计数无法归零,内存泄漏
    }

    // 使用 std::weak_ptr 解决循环引用问题
    {
        std::shared_ptr<MyClass> ptrA = std::make_shared<MyClass>(10);
        std::shared_ptr<MyClass> ptrB = std::make_shared<MyClass>(20);

        std::weak_ptr<MyClass> weakPtrA = ptrA;
        std::weak_ptr<MyClass> weakPtrB = ptrB;

        ptrA->setSharedPtr(ptrB);  // ptrA 持有 ptrB
        ptrB->setSharedPtr(weakPtrA.lock());  // ptrB 持有 ptrA 的弱引用

        // 此时 ptrA 和 ptrB 的引用计数可以正常归零,避免内存泄漏
    }

    return 0;
}
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
  1. 创建 std::weak_ptr:使用 std::weak_ptr<MyClass> weakPtr = ptr1 创建一个 std::weak_ptr,它观察 ptr1 管理的对象。std::weak_ptr 不会增加引用计数。
  2. 检查 std::weak_ptr 是否有效: 使用 weakPtr.lock() 获取一个 std::shared_ptr,如果对象仍然存在,则返回有效的 std::shared_ptr;否则返回 nullptr。
  3. 释放 std::shared_ptr: 使用 ptr1.reset() 释放 ptr1 管理的对象。此时 weakPtr 观察的对象已被释放,weakPtr.lock() 返回 nullptr。
  4. 循环引用问题: 当两个 std::shared_ptr 互相引用时,会导致引用计数无法归零,从而引发内存泄漏。使用 std::weak_ptr 可以打破循环引用,确保对象能够正确释放。

输出结果

MyClass constructed with value: 42
MyClass constructed with value: 100
weakPtr is valid. Value: Value: 42
MyClass destroyed with value: 42
weakPtr is expired.
MyClass constructed with value: 10
MyClass constructed with value: 20
MyClass constructed with value: 10
MyClass constructed with value: 20
MyClass destroyed with value: 20
MyClass destroyed with value: 10
1
2
3
4
5
6
7
8
9
10
11

std::weak_ptr 的特点

  1. 弱引用:std::weak_ptr 是对 std::shared_ptr 管理的对象的弱引用,不会增加引用计数。
  2. 解决循环引用:通过使用 std::weak_ptr,可以打破 std::shared_ptr 之间的循环引用,避免内存泄漏。
  3. 安全访问:使用 lock() 方法可以安全地获取 std::shared_ptr,避免访问已释放的对象。
  4. 生命周期管理:std::weak_ptr 不会影响对象的生命周期,对象是否释放由 std::shared_ptr 的引用计数决定。

# 12.4.4 综合案例与思考

#include <iostream>
#include <memory>
using namespace std;

class Resource {
    string name;
public:
    Resource(const string& n) : name(n) { cout << "创建: " << name << endl; }
    ~Resource() { cout << "销毁: " << name << endl; }
    void use() const { cout << "使用: " << name << endl; }
};

int main() {
    // 1. unique_ptr:独占所有权
    cout << "=== unique_ptr ===" << endl;
    {
        auto p1 = make_unique<Resource>("独占资源A");
        p1->use();
        // auto p2 = p1;  // 编译错误!不能复制
        auto p2 = move(p1);  // 转移所有权
        if (!p1) cout << "p1 已为空" << endl;
        p2->use();
    }  // p2 离开作用域自动释放
    
    // 2. shared_ptr:共享所有权
    cout << "\n=== shared_ptr ===" << endl;
    {
        auto sp1 = make_shared<Resource>("共享资源B");
        cout << "引用计数: " << sp1.use_count() << endl;
        {
            auto sp2 = sp1;  // 共享
            cout << "引用计数: " << sp1.use_count() << endl;
            sp2->use();
        }  // sp2 离开,引用计数减1
        cout << "引用计数: " << sp1.use_count() << endl;
    }  // sp1 离开,引用计数归0,释放
    
    // 3. weak_ptr:打破循环引用
    cout << "\n=== weak_ptr ===" << endl;
    weak_ptr<Resource> wp;
    {
        auto sp = make_shared<Resource>("弱引用资源C");
        wp = sp;  // 弱引用不增加计数
        cout << "引用计数: " << sp.use_count() << endl;
        if (auto locked = wp.lock()) {
            locked->use();
        }
    }  // sp 销毁
    if (wp.expired()) {
        cout << "资源已过期,weak_ptr 检测到" << endl;
    }
    
    // 4. 智能指针选择指南
    cout << "\n=== 选择指南 ===" << endl;
    cout << "独占所有权 -> unique_ptr (最常用,零开销)" << endl;
    cout << "共享所有权 -> shared_ptr (有引用计数开销)" << endl;
    cout << "观察不拥有 -> weak_ptr  (解决循环引用)" << endl;
    
    return 0;
}
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

案例知识融合:本案例完整展示了三种智能指针的使用场景:unique_ptr独占所有权(只能move不能copy)、shared_ptr共享所有权(引用计数机制)、weak_ptr弱引用(不增加引用计数,可检测资源是否过期)。通过RAII机制,智能指针确保资源在离开作用域时自动释放,彻底避免内存泄漏。

思考题:

  1. make_unique 和 make_shared 相比直接 new 有什么优势?(提示:异常安全、性能)
  2. shared_ptr 的引用计数本身是线程安全的吗?shared_ptr 指向的对象是线程安全的吗?
  3. 什么场景下必须用 shared_ptr 而不能用 unique_ptr?

# 12.5 内存管理函数

C++ 提供了底层内存管理函数,如 malloc、free、calloc 和 realloc,但通常不推荐使用。

# 12.5.1 malloc和free

void* malloc(size_t size): 分配指定大小的内存块,返回指向该内存块的指针。 分配的内存未初始化。

void free(void* ptr):释放由 malloc、calloc 或 realloc 分配的内存。

#include <cstdlib>

int main() {
    int *p = (int *)malloc(sizeof(int)); // 分配内存
    *p = 10;
    std::cout << *p << std::endl; // 输出: 10
    free(p); // 释放内存
    return 0;
}
1
2
3
4
5
6
7
8
9

# 12.5.2 calloc

void* calloc(size_t num, size_t size):分配 num 个大小为 size 的内存块,并将内存初始化为 0。

#include <cstdlib>

int main() {
    int* arr = (int*)calloc(10, sizeof(int));  // 分配并初始化 10 个 int 的内存
    if (arr) {
        // 使用内存
        free(arr);  // 释放内存
    }
    return 0;
}
1
2
3
4
5
6
7
8
9
10

# 12.5.3 realloc

void* realloc(void* ptr, size_t size):调整已分配内存块的大小。如果 ptr 为 nullptr,则等同于 malloc。

#include <cstdlib>

int main() {
    int* arr = (int*)malloc(10 * sizeof(int));  // 分配 10 个 int 的内存
    if (arr) {
        arr = (int*)realloc(arr, 20 * sizeof(int));  // 调整大小为 20 个 int
        // 使用内存
        free(arr);  // 释放内存
    }
    return 0;
}
1
2
3
4
5
6
7
8
9
10
11

# 12.5.4 综合案例与思考

#include <iostream>
#include <cstdlib>
#include <cstring>
using namespace std;

int main() {
    // 1. malloc: 分配未初始化内存
    cout << "=== malloc ===" << endl;
    int* p1 = (int*)malloc(5 * sizeof(int));
    if (p1) {
        for (int i = 0; i < 5; i++) p1[i] = i * 10;
        for (int i = 0; i < 5; i++) cout << p1[i] << " ";
        cout << endl;
    }
    
    // 2. calloc: 分配并零初始化
    cout << "\n=== calloc ===" << endl;
    int* p2 = (int*)calloc(5, sizeof(int));
    if (p2) {
        cout << "calloc 初始值: ";
        for (int i = 0; i < 5; i++) cout << p2[i] << " ";  // 全为0
        cout << endl;
    }
    
    // 3. realloc: 调整大小
    cout << "\n=== realloc ===" << endl;
    p1 = (int*)realloc(p1, 10 * sizeof(int));
    if (p1) {
        for (int i = 5; i < 10; i++) p1[i] = i * 10;
        cout << "扩展后: ";
        for (int i = 0; i < 10; i++) cout << p1[i] << " ";
        cout << endl;
    }
    
    // 4. 释放
    free(p1);
    free(p2);
    
    // 对比:C++ 的 new vs C 的 malloc
    cout << "\n=== new vs malloc 对比 ===" << endl;
    cout << "new  : 调用构造函数, 类型安全, 可抛异常" << endl;
    cout << "malloc: 不调用构造函数, 返回void*, 返回NULL" << endl;
    cout << "C++ 中优先使用 new/delete 或智能指针!" << endl;
    
    return 0;
}
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
46

案例知识融合:本案例对比了 C 风格的三个内存管理函数:malloc(分配未初始化内存)、calloc(分配并零初始化)、realloc(调整已分配内存大小)。它们都返回 void* 需要强制转换,且不会调用构造/析构函数。在C++中通常不推荐使用,但在与C代码交互或底层编程时仍然需要了解。

思考题:

  1. malloc(0) 会返回什么?new int[0] 呢?行为是否一致?
  2. realloc 如果扩展失败会返回 NULL,但原指针仍然有效。如果直接写 p = realloc(p, newsize) 而 realloc 失败了,会发生什么?
  3. 为什么不能用 free 释放 new 分配的对象?delete 比 free 多做了什么?

# 12.6 内存池

# 12.6.1 内存池概念

内存池(Memory Pool) 是一种高效的内存管理技术,通过预先分配一大块内存,然后在需要时从中分配小块内存,减少频繁调用 new 和 delete 的开销。 内存池特别适用于需要频繁分配和释放小块内存的场景。

# 12.6.2 内存池核心思想

  1. 预先分配内存: 在程序启动时,预先分配一大块连续的内存。
  2. 管理内存块: 将大块内存划分为多个固定大小的小块,并维护一个空闲块列表。
  3. 分配内存: 当需要内存时,从空闲块列表中分配一块内存。
  4. 释放内存: 当内存不再使用时,将其归还到空闲块列表中。

# 12.6.3 内存池的优点

  1. 减少内存碎片: 通过固定大小的内存块分配,减少内存碎片。
  2. 提高性能: 减少频繁调用 new 和 delete 的开销。
  3. 简化内存管理: 集中管理内存分配和释放,避免内存泄漏和悬空指针。

# 12.6.4 内存池的实现

以下是一个简单的内存池实现示例:

#include <iostream>
#include <vector>
#include <stdexcept>

class MemoryPool {
public:
    // 构造函数:初始化内存池
    MemoryPool(size_t blockSize, size_t blockCount)
        : blockSize_(blockSize), blockCount_(blockCount) {
        // 分配一大块内存
        pool_.resize(blockSize_ * blockCount_);
        // 初始化空闲块列表
        freeBlocks_.reserve(blockCount_);
        for (size_t i = 0; i < blockCount_; ++i) {
            freeBlocks_.push_back(&pool_[i * blockSize_]);
        }
    }

    // 分配一块内存
    void* allocate() {
        if (freeBlocks_.empty()) {
            throw std::bad_alloc();  // 如果没有空闲块,抛出异常
        }
        void* block = freeBlocks_.back();  // 从空闲块列表中获取一块内存
        freeBlocks_.pop_back();            // 从列表中移除
        return block;
    }

    // 释放一块内存
    void deallocate(void* block) {
        freeBlocks_.push_back(static_cast<char*>(block));  // 将内存块归还到空闲块列表
    }

    // 获取空闲块数量
    size_t getFreeBlockCount() const {
        return freeBlocks_.size();
    }

private:
    size_t blockSize_;           // 每个内存块的大小
    size_t blockCount_;          // 内存块的总数
    std::vector<char> pool_;     // 内存池
    std::vector<void*> freeBlocks_;  // 空闲块列表
};

int main() {
    // 创建一个内存池,每个块大小为 sizeof(int),共 10 个块
    MemoryPool pool(sizeof(int), 10);

    // 分配一块内存
    int* p1 = static_cast<int*>(pool.allocate());
    *p1 = 42;
    std::cout << "Allocated value: " << *p1 << std::endl;

    // 分配另一块内存
    int* p2 = static_cast<int*>(pool.allocate());
    *p2 = 100;
    std::cout << "Allocated value: " << *p2 << std::endl;

    // 释放内存
    pool.deallocate(p1);
    pool.deallocate(p2);

    // 打印空闲块数量
    std::cout << "Free blocks after deallocation: " << pool.getFreeBlockCount() << std::endl;

    return 0;
}
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
  1. 构造函数: 初始化内存池,分配一大块内存,并将其划分为多个固定大小的块。 将每个块的起始地址添加到空闲块列表中。
  2. allocate 方法: 从空闲块列表中分配一块内存。 如果空闲块列表为空,抛出 std::bad_alloc 异常。
  3. deallocate 方法: 将释放的内存块归还到空闲块列表中。
  4. getFreeBlockCount 方法: 返回当前空闲块的数量。

输出结果

Allocated value: 42
Allocated value: 100
Free blocks after deallocation: 10
1
2
3

# 12.6.5 内存池优化

  1. 支持不同大小的内存块: 可以为不同大小的内存块创建多个内存池。
  2. 线程安全: 在多线程环境中,可以使用互斥锁(std::mutex)保护内存池的分配和释放操作。
  3. 内存对齐: 确保分配的内存块满足对齐要求,提高访问效率。

# 12.6.6 综合案例与思考

#include <iostream>
#include <vector>
#include <chrono>
using namespace std;

// 简化版对象池
template<typename T>
class ObjectPool {
    vector<T*> pool;
    vector<T*> available;
public:
    ObjectPool(size_t initialSize) {
        for (size_t i = 0; i < initialSize; i++) {
            T* obj = new T();
            pool.push_back(obj);
            available.push_back(obj);
        }
        cout << "对象池创建,预分配 " << initialSize << " 个对象" << endl;
    }
    
    T* acquire() {
        if (available.empty()) {
            T* obj = new T();  // 池耗尽则新建
            pool.push_back(obj);
            return obj;
        }
        T* obj = available.back();
        available.pop_back();
        return obj;
    }
    
    void release(T* obj) {
        available.push_back(obj);  // 归还到池中
    }
    
    ~ObjectPool() {
        for (T* obj : pool) delete obj;
        cout << "对象池销毁,共管理 " << pool.size() << " 个对象" << endl;
    }
};

struct Bullet {
    float x = 0, y = 0;
    bool active = false;
};

int main() {
    const int COUNT = 100000;
    
    // 对比:直接 new/delete vs 对象池
    cout << "=== new/delete 性能 ===" << endl;
    auto t1 = chrono::high_resolution_clock::now();
    for (int i = 0; i < COUNT; i++) {
        Bullet* b = new Bullet();
        b->active = true;
        delete b;
    }
    auto t2 = chrono::high_resolution_clock::now();
    cout << "耗时: " << chrono::duration_cast<chrono::microseconds>(t2 - t1).count() << "us" << endl;
    
    cout << "\n=== 对象池性能 ===" << endl;
    ObjectPool<Bullet> pool(100);
    t1 = chrono::high_resolution_clock::now();
    for (int i = 0; i < COUNT; i++) {
        Bullet* b = pool.acquire();
        b->active = true;
        pool.release(b);
    }
    t2 = chrono::high_resolution_clock::now();
    cout << "耗时: " << chrono::duration_cast<chrono::microseconds>(t2 - t1).count() << "us" << endl;
    
    return 0;
}
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

案例知识融合:本案例通过游戏子弹对象池的实现,展示了内存池的核心思想——预分配 + 复用。对比了直接 new/delete 与对象池的性能差异。内存池减少了系统调用和内存碎片,在频繁创建销毁小对象的场景(如游戏、网络服务器)中效果显著。

思考题:

  1. 上面的对象池在多线程环境中安全吗?如何添加线程安全支持?
  2. 对象池中 release 的对象并没有调用析构函数,这在什么情况下可能有问题?
  3. 标准库中的 std::allocator 和本案例的对象池有什么异同?

# 12.7 高级用法

# 12.7.1 enable_shared_from_this

有一个 AsyncTask 类,它需要在内部将自己传递给一个异步任务,并在任务完成后自行销毁。如果直接通过 this 创建 std::shared_ptr,会导致多个独立的 std::shared_ptr 管理同一个对象,从而引发未定义行为(通常是双重释放)。

#include <iostream>
#include <memory>
#include <thread>
#include <chrono>

class AsyncTask {
public:
    static std::shared_ptr<AsyncTask> Create(std::string name) {
        return std::shared_ptr<AsyncTask>(new AsyncTask(std::move(name)));
    }
    void Start() {
        // 错误:直接通过 this 创建 shared_ptr
        auto self = std::shared_ptr<AsyncTask>(this);
        std::cout << "[" << self->name_ << "] start...\n";
        // 模拟异步操作:在新线程中执行耗时任务
        std::thread([self] {
            std::cout << "[" << self->name_ << "] working...\n";
            std::this_thread::sleep_for(std::chrono::seconds(2));
            std::cout << "[" << self->name_ << "] done\n";
        }).detach();
    }
    ~AsyncTask() {
        std::cout << "[" << name_ << "] destroyed\n";
    }
private:
    explicit AsyncTask(std::string name) : name_(std::move(name)){}
    std::string name_;
};

int main() {
    {
        auto task = AsyncTask::Create("MyTask");
        task->Start();
    }
    // 出了作用域,外部的 shared_ptr 已销毁
    std::cout << "outer shared_ptr gone\n";
    std::this_thread::sleep_for(std::chrono::seconds(3));
    return 0;
}
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

运行结果:

[MyTask] start...
[MyTask] destroyed
outer shared_ptr gone
[] working...
[] done
[] destroyed      // 双重释放,导致程序崩溃
a.out(70675,0x16d7d7000) malloc: *** error for object 0x89d000940: pointer being freed was not allocated
a.out(70675,0x16d7d7000) malloc: *** set a breakpoint in malloc_error_break to debug
Abort trap: 6
1
2
3
4
5
6
7
8
9

双重管理问题分析:

  • 在 Start 方法中,直接通过 this 创建 std::shared_ptr,会导致多个独立的 std::shared_ptr 管理同一个对象。
  • 当外部的 std::shared_ptr 离开作用域时,对象的引用计数减少到 0,对象被销毁。
  • 然而,异步任务中仍然持有一个 std::shared_ptr,这会导致双重释放问题。

解决方案:std::enable_shared_from_this 提供了一种机制,让对象可以在内部安全地持有自己的 std::shared_ptr,从而延长生命周期,直到任务完成。

#include <iostream>
#include <memory>
#include <thread>
#include <chrono>

// 场景:一个异步任务,启动后需要保活自己,完成后再自行销毁。
class AsyncTask : public std::enable_shared_from_this<AsyncTask> {
public:
    static std::shared_ptr<AsyncTask> Create(std::string name) {
        // 必须通过工厂方法创建,确保 shared_ptr 管理对象
        return std::shared_ptr<AsyncTask>(new AsyncTask(std::move(name)));
    }

    void Start() {
        // 必须先有一个 shared_ptr 管理该对象,才能调用 shared_from_this()
        // 持有自己,防止外部释放后被析构
        self_ = shared_from_this();
        // 模拟异步操作:在新线程中执行耗时任务
        std::thread([this]() {
          std::cout << "[" << name_ << "] working...\n";
          std::this_thread::sleep_for(std::chrono::seconds(2));
          std::cout << "[" << name_ << "] done, releasing self\n";
          self_.reset();  // 任务完成,释放自引用,允许析构
        }).detach();
    }

    ~AsyncTask() {
        std::cout << "[" << name_ << "] destroyed\n";
    }

private:
    explicit AsyncTask(std::string name) : name_(std::move(name)) {}
    std::string name_;
    std::shared_ptr<AsyncTask> self_;  // 自引用保活
};

int main() {
  {
    auto task = AsyncTask::Create("MyTask");
    task->Start();
  }
  // 出了作用域,外部的 shared_ptr 已销毁,但对象还活着(self_ 保活)
  std::cout << "outer shared_ptr gone\n";
  std::this_thread::sleep_for(std::chrono::seconds(3));
  // 异步任务完成,self_.reset() 后对象才被销毁
  return 0;
}
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
46
47

运行结果

outer shared_ptr gone
[MyTask] working...
[MyTask] done, releasing self
[MyTask] destroyed
1
2
3
4

解决的问题:

  • 对象生命周期管理:在异步任务中,任务可能需要在后台运行一段时间,而外部的 std::shared_ptr 可能已经释放。如果没有保活机制,对象可能会被提前销毁,导致未定义行为。
  • 安全地持有自身引用:直接通过 this 创建 std::shared_ptr 会导致多个独立的 std::shared_ptr 管理同一个对象,从而引发双重释放问题。std::enable_shared_from_this 解决了这个问题。

核心原理:std::enable_shared_from_this 的核心原理是通过在对象内部嵌入一个 std::weak_ptr,从而安全地生成 std::shared_ptr:

  1. 内部嵌入 std::weak_ptr:std::enable_shared_from_this 在对象内部维护一个 std::weak_ptr,用于跟踪对象的生命周期。
  2. shared_from_this 方法: 通过 shared_from_this() 方法,可以从对象内部安全地生成一个 std::shared_ptr,而不会创建额外的控制块。
  3. 避免双重管理:所有通过 shared_from_this() 生成的 std::shared_ptr 共享同一个控制块,从而避免双重释放。
特性 直接使用 this 创建 shared_ptr 使用 std::enable_shared_from_this
安全性 不安全,可能导致双重释放 安全,避免双重释放
控制块管理 多个独立的控制块 共享同一个控制块
适用场景 不推荐使用 需要在对象内部获取 shared_ptr 时使用

# 12.8 动态内存底层原理

# 12.8.1 堆内存分配器原理

进程的内存空间划分:

高地址 ┌──────────────┐
       │   内核空间     │ 用户不可访问
       ├──────────────┤
       │     栈(Stack)  │ 局部变量,向低地址增长
       │      ↓        │
       │   (空闲区域)  │
       │      ↑        │
       │     堆(Heap)   │ 动态分配,向高地址增长
       ├──────────────┤
       │   BSS段       │ 未初始化的全局/静态变量
       ├──────────────┤
       │   数据段       │ 已初始化的全局/静态变量
       ├──────────────┤
低地址 │   代码段(Text) │ 可执行指令
       └──────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

glibc的malloc实现(ptmalloc2):

  1. 小块内存(< 128KB):使用brk()系统调用扩展堆顶。malloc维护多个空闲链表(free list),按大小分类(16, 24, 32, 40...字节),分配时从对应链表取一块,释放时放回链表
  2. 大块内存(>= 128KB):使用mmap()系统调用直接向内核申请独立的内存映射区域,释放时用munmap()归还
  3. 内存块头部(chunk header):每个分配的内存块前面有8-16字节的元数据,记录块大小和使用状态。这就是为什么malloc(100)实际消耗约112字节
malloc(100) 返回的内存块实际布局:
┌──────────┬──────────────────────┐
│ 头部16字节 │ 用户数据100字节          │
│ (size+flag)│ (返回的指针指向这里)     │
└──────────┴──────────────────────┘
1
2
3
4
5

# 12.8.2 new/delete的底层过程

new表达式的三步操作:

MyClass* p = new MyClass(42);
1

编译器将其转换为:

// 伪代码
void* raw = operator new(sizeof(MyClass));  // 1.分配内存
try {
    new(raw) MyClass(42);                    // 2.placement new,调用构造函数
} catch (...) {
    operator delete(raw);                    // 异常时释放内存
    throw;
}
MyClass* p = static_cast<MyClass*>(raw);     // 3.类型转换
1
2
3
4
5
6
7
8
9

operator new的默认实现就是调用malloc,operator delete调用free。但它们可以被全局重载或类内重载:

class Pool {
public:
    // 自定义内存分配
    void* operator new(size_t size) {
        cout << "自定义new: " << size << "字节" << endl;
        return malloc(size);
    }
    void operator delete(void* p) {
        cout << "自定义delete" << endl;
        free(p);
    }
};
1
2
3
4
5
6
7
8
9
10
11
12

new[]和delete[]的区别:

int* arr = new int[10];
// 底层:operator new[](10 * sizeof(int) + 额外字节)
// 额外字节用于记录数组元素个数,delete[]需要知道调用多少次析构函数

delete[] arr;
// 底层:根据隐藏的计数器调用10次析构函数,然后operator delete[]()
1
2
3
4
5
6

# 12.8.3 智能指针的底层实现

unique_ptr——零开销抽象:

template<typename T>
class unique_ptr {
    T* ptr_;           // 只有一个裸指针成员
public:
    ~unique_ptr() { delete ptr_; }
    // 禁止拷贝
    unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;
    // 允许移动
    unique_ptr(unique_ptr&& other) : ptr_(other.ptr_) { other.ptr_ = nullptr; }
};
// sizeof(unique_ptr<T>) == sizeof(T*) == 8字节
1
2
3
4
5
6
7
8
9
10
11
12

unique_ptr的大小和裸指针完全相同(8字节),编译器会将其所有操作内联,生成的代码与手写的new/delete完全一致。这就是零开销抽象。

shared_ptr——引用计数:

// shared_ptr的简化模型
template<typename T>
class shared_ptr {
    T* ptr_;                    // 指向管理的对象(8字节)
    ControlBlock* control_;     // 指向控制块(8字节)
};

struct ControlBlock {
    atomic<int> strong_count;   // 强引用计数(原子操作保证线程安全)
    atomic<int> weak_count;     // 弱引用计数
    // deleter, allocator...
};
// sizeof(shared_ptr<T>) == 16字节
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 拷贝时:strong_count++(原子递增)
  • 析构时:strong_count--,如果变为0则delete对象
  • weak_count为0且strong_count为0时释放控制块

make_shared的优化:make_shared<T>(args)在一次内存分配中同时创建对象和控制块,而shared_ptr<T>(new T(args))需要两次分配。

# 12.9 动态内存训练题

# 12.9.1 简易内存池

训练题1:实现简易内存池

#include <iostream>
#include <vector>
using namespace std;

class SimplePool {
    struct Block {
        Block* next;
    };
    Block* freeList_;
    vector<void*> chunks_;
    size_t blockSize_;
    size_t chunkSize_;

public:
    SimplePool(size_t blockSize, size_t blocksPerChunk = 32)
        : freeList_(nullptr), blockSize_(max(blockSize, sizeof(Block))),
          chunkSize_(blocksPerChunk) {}

    ~SimplePool() {
        for (void* chunk : chunks_) ::operator delete(chunk);
    }

    void* allocate() {
        if (!freeList_) expandPool();
        Block* block = freeList_;
        freeList_ = freeList_->next;
        return block;
    }

    void deallocate(void* p) {
        Block* block = static_cast<Block*>(p);
        block->next = freeList_;
        freeList_ = block;
    }

private:
    void expandPool() {
        char* chunk = static_cast<char*>(::operator new(blockSize_ * chunkSize_));
        chunks_.push_back(chunk);
        for (size_t i = 0; i < chunkSize_; ++i) {
            Block* block = reinterpret_cast<Block*>(chunk + i * blockSize_);
            block->next = freeList_;
            freeList_ = block;
        }
    }
};

struct Point {
    double x, y, z;
};

int main() {
    SimplePool pool(sizeof(Point));
    
    // 分配
    vector<Point*> points;
    for (int i = 0; i < 100; ++i) {
        Point* p = static_cast<Point*>(pool.allocate());
        p->x = i; p->y = i * 2; p->z = i * 3;
        points.push_back(p);
    }
    
    cout << "分配了" << points.size() << "个Point对象" << endl;
    cout << "Point[50] = (" << points[50]->x << ", " << points[50]->y << ")" << endl;
    
    // 释放
    for (Point* p : points) pool.deallocate(p);
    cout << "全部释放完毕" << endl;

    return 0;
}
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

练习重点:空闲链表的分配/回收机制、批量预分配减少系统调用、内存池避免碎片。


训练题2:实现unique_ptr

#include <iostream>
using namespace std;

template<typename T>
class MyUniquePtr {
    T* ptr_;
public:
    explicit MyUniquePtr(T* p = nullptr) : ptr_(p) {}
    ~MyUniquePtr() { delete ptr_; }
    
    // 禁止拷贝
    MyUniquePtr(const MyUniquePtr&) = delete;
    MyUniquePtr& operator=(const MyUniquePtr&) = delete;
    
    // 允许移动
    MyUniquePtr(MyUniquePtr&& other) noexcept : ptr_(other.ptr_) {
        other.ptr_ = nullptr;
    }
    MyUniquePtr& operator=(MyUniquePtr&& other) noexcept {
        if (this != &other) {
            delete ptr_;
            ptr_ = other.ptr_;
            other.ptr_ = nullptr;
        }
        return *this;
    }
    
    T& operator*() const { return *ptr_; }
    T* operator->() const { return ptr_; }
    T* get() const { return ptr_; }
    explicit operator bool() const { return ptr_ != nullptr; }
    
    T* release() {
        T* tmp = ptr_;
        ptr_ = nullptr;
        return tmp;
    }
    
    void reset(T* p = nullptr) {
        delete ptr_;
        ptr_ = p;
    }
};

int main() {
    MyUniquePtr<int> p1(new int(42));
    cout << "*p1 = " << *p1 << endl;
    
    // MyUniquePtr<int> p2 = p1;  // 编译错误!禁止拷贝
    MyUniquePtr<int> p2 = std::move(p1);  // 移动语义
    cout << "p1 is null? " << (!p1 ? "yes" : "no") << endl;
    cout << "*p2 = " << *p2 << endl;
    
    p2.reset(new int(100));
    cout << "*p2 = " << *p2 << endl;

    return 0;
}
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
46
47
48
49
50
51
52
53
54
55
56
57
58

练习重点:移动语义实现所有权转移、禁止拷贝的语法、RAII资源管理。

# 12.9.2 不同类型对比

#include <iostream>

int main() {
    int i1 = 10;          // 整型变量,初始化为 10
    int* i2 = &i1;        // 整型指针,指向 i1
    int* i3 = new int(10); // 动态分配的整型指针,初始化为 10
    int& i4 = i1;         // i4 是 i1 的引用

    std::cout << "i1: " << i1 << std::endl; // 输出 10
    std::cout << "*i2: " << *i2 << std::endl; // 输出 10
    std::cout << "*i3: " << *i3 << std::endl; // 输出 10
    std::cout << "i4: " << i4 << std::endl; // 输出 10

    i1 = 20;              // 修改 i1 的值
    std::cout << "After modifying i1:" << std::endl;
    std::cout << "i1: " << i1 << std::endl; // 输出 20
    std::cout << "*i2: " << *i2 << std::endl; // 输出 20
    std::cout << "i4: " << i4 << std::endl; // 输出 20

    *i3 = 30;             // 修改 i3 指向的值
    std::cout << "After modifying *i3:" << std::endl;
    std::cout << "*i3: " << *i3 << std::endl; // 输出 30

    delete i3;            // 释放堆内存
    return 0;
}
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

这四行代码分别声明了不同类型的变量,涉及普通变量、指针、动态内存分配和引用。以下是它们的详细区别:

int i1 = 10;
1

类型: int(整型变量)。作用: 声明一个名为 i1 的整型变量,并初始化为 10。

内存分配: 在栈上分配内存,存储一个整数值。生命周期: 当 i1 所在的作用域结束时,i1 的内存会自动释放。

int* i2 = &i1;
1

类型: int*(指向整型的指针)。作用: 声明一个名为 i2 的指针变量,并将其初始化为 i1 的地址。

内存分配: 在栈上分配内存,存储一个指针(地址)。生命周期: 当 i2 所在的作用域结束时,i2 的内存会自动释放,但它指向的内存(i1)不会被自动释放。

int* i3 = new int(10);
1

类型: int*(指向整型的指针)。作用: 声明一个名为 i3 的指针变量,并动态分配一个整型内存,初始化为 10。

内存分配:i3 本身在栈上分配内存,存储一个指针(地址)。new int(10) 在堆上分配内存,存储一个整数值,并初始化为 10。

生命周期:i3 的生命周期与其所在作用域相同。 堆上分配的内存不会自动释放,必须手动使用 delete 释放:

int& i4 = i1;
1

类型: int&(整型引用)。作用: 声明一个名为 i4 的引用变量,它是 i1 的别名。

内存分配: 引用本身不占用额外内存,它只是 i1 的别名。生命周期: i4 的生命周期与其绑定的变量 i1 相同。

区别总结

特性 int i1 = 10; int* i2 = &i1; int* i3 = new int(10); int& i4 = i1;
类型 整型变量 整型指针 整型指针 整型引用
内存分配 栈上分配 栈上分配 栈上分配指针,堆上分配值 无额外内存分配
初始值 初始化为 10 初始化为 i1 的地址 初始化为 10 必须初始化
生命周期 作用域结束时释放 作用域结束时释放 指针释放,堆内存需手动释放 与绑定变量相同
是否可更改 可以更改 可以更改 可以更改 不可更改
是否可为空 不能为空 不能为空 可以为空 不能为空

# 12.10 综合思考题

  1. 内存碎片问题:频繁的小块malloc/free会导致外部碎片(空闲内存总量足够但不连续)。内存分配器如何缓解碎片?jemalloc和tcmalloc相比glibc的ptmalloc2有什么优化?

  2. placement new的应用:new(buffer) T(args)在指定地址构造对象,不分配内存。它在内存池、STL容器实现、共享内存中有什么应用?使用placement new时为什么必须手动调用析构函数?

  3. 栈 vs 堆的性能差异:栈分配只需一条指令(sub rsp, N),堆分配需要调用复杂的分配器甚至系统调用。在实际编程中,什么时候应该优先使用栈?alloca()为什么存在但不推荐使用?

  4. RAII与异常安全:RAII(Resource Acquisition Is Initialization)是C++最重要的编程范式之一。请解释RAII如何保证资源在异常情况下也能正确释放?为什么"裸new+手动delete"在异常存在时是不安全的?

# 12.11 新手陷阱 Top 5

# 陷阱 说明
1 new[] 配 delete(无方括号) UB,必须 delete[] 配 new[];用 vector 更安全
2 shared_ptr 循环引用 A 持 B、B 持 A,引用计数永不归零;用 weak_ptr
3 用 new 后 shared_ptr<T>(p) 异常安全弱;优先 make_shared
4 把 unique_ptr 当函数参数按值传 触发所有权转移,调用方变空;明确语义再用
5 把同一裸指针包给两个 shared_ptr 双重释放;只通过工厂函数获取智能指针
上次更新: 2026/06/10, 11:13:41
内存模型
IO和文件

← 内存模型 IO和文件→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式