动态内存
# 第 12 章 C++ 动态内存
# 目录介绍
- 12.1 动态内存概念
- 12.2 动态内存操作
- 12.3 动态内存问题
- 12.4 智能指针
- 12.5 内存管理函数
- 12.6 内存池
- 12.7 高级用法
- 12.8 动态内存底层原理
- 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 动态内存特点
- 运行时分配: 内存的分配和释放发生在程序运行时,而不是编译时。
- 手动管理: 程序员需要显式分配和释放内存,否则会导致内存泄漏。
- 堆区存储: 动态内存从堆区分配,堆区的空间通常比栈区大。
- 灵活性: 动态内存的大小可以在运行时动态调整,适合处理不确定大小的数据。
# 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;
}
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是防止悬空指针的最佳实践。
思考题:
- 观察四个变量的地址,栈和堆的地址通常差异很大,这反映了什么内存布局特点?
- 如果忘记
delete[] dynamicArr,程序短期内能正常运行吗?长期运行会怎样? 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;
}
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;
}
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;
}
2
3
4
5
6
7
8
9
10
# 12.2.2 delete释放内存
释放单个对象:
delete ptr; // 释放单个对象
ptr = nullptr; // 将指针置为 nullptr,避免野指针
2
释放数组:
delete[] arr; // 释放数组
arr = nullptr; // 将指针置为 nullptr
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;
}
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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 12.2.4 动态内存最佳实践
- 优先使用智能指针:避免手动管理内存,减少内存泄漏和悬空指针的风险。
- 避免裸指针:尽量使用
std::unique_ptr或std::shared_ptr。 - 使用容器类:如
std::vector、std::list等,避免手动管理动态数组。 - 检查内存分配是否成功:在分配大量内存时,检查指针是否为
nullptr。 - 避免内存泄漏:确保每次
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;
}
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 作为手动动态数组的替代方案,体现了"优先使用容器类"的最佳实践。
思考题:
- 如果对
new Student[2]使用delete而非delete[],会发生什么?析构函数会被正确调用吗? new int[count]()末尾的()是什么意思?去掉后数组元素的初始值是什么?- 在什么情况下必须用
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;
}
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; // 避免野指针
}
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;
}
2
3
4
5
6
7
8
9
10
11
12
代码分析如下所示:
- 局部变量的生命周期: 在
createInt函数中,value是一个局部变量,其生命周期仅限于函数作用域。 当createInt函数返回时,value的内存被释放。 - 返回局部变量的地址:
createInt函数返回了局部变量value的地址。由于value的内存已经被释放,返回的指针ptr成为野指针。 - 访问野指针:在
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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
- 动态分配内存:在
createInt函数中,使用new动态分配内存,确保内存的生命周期不受函数作用域限制。 - 返回有效指针:
createInt函数返回动态分配的内存地址,指针ptr指向有效内存。 - 释放内存: 在
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;
}
2
3
4
5
6
7
8
9
10
代码分析
- 动态分配内存:使用
new动态分配了一块内存,并将其地址赋值给指针ptr。通过*ptr可以访问该内存,输出值为42。 - 释放内存:使用
delete释放了ptr指向的内存。此时,ptr仍然指向原来的内存地址,但该内存已经被释放,ptr成为悬空指针。 - 访问悬空指针: 在释放内存后,尝试通过
*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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 释放内存:使用
delete释放了ptr指向的内存。 - 置空指针:将
ptr设置为nullptr,避免悬空指针。 - 安全访问:在访问指针之前,检查指针是否为
nullptr,确保不会访问无效内存。
# 12.3.5 空指针
空指针:空指针是指不指向任何有效内存地址的指针。空指针通常用来表示指针没有被初始化或者指向了无效的内存地址。在 C++ 中,空指针的值通常是 0 或者使用 nullptr 关键字表示。
用途:初始化指针变量
注意:空指针指向的内存是不可以访问的
int main() {
//指针变量p指向内存地址编号为0的空间
int * p = NULL;
//访问空指针报错
//内存编号0 ~255为系统占用内存,不允许用户访问
cout << *p << endl;
return 0;
}
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;
}
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++手动内存管理中最容易踩的坑。
思考题:
- 如何检测程序中的内存泄漏?常用的工具有哪些(如Valgrind、AddressSanitizer)?
- 为什么
delete nullptr是安全的?C++标准是如何规定的? - 如果一个函数可能在中间 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;
}
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
- 创建
std::unique_ptr: 使用std::make_unique<MyClass>(42)创建一个std::unique_ptr,它管理一个动态分配的MyClass对象。std::make_unique是 C++14 引入的便捷函数,用于创建std::unique_ptr。 - 访问对象成员函数: 通过
ptr->printValue()访问MyClass对象的成员函数。 - 转移所有权:使用
std::move(ptr)将ptr的所有权转移给ptr2。转移后,ptr变为nullptr,不再指向任何对象。 - 自动释放内存:当
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
2
3
4
5
std::unique_ptr 的特点
- 独占所有权:一个
std::unique_ptr独占其管理的对象,不能复制,只能移动。这确保了同一时间只有一个std::unique_ptr指向该对象。 - 自动释放内存:当
std::unique_ptr超出作用域时,会自动释放其管理的对象,无需手动调用delete。 - 避免内存泄漏和悬空指针:由于
std::unique_ptr自动管理内存,可以避免内存泄漏和悬空指针问题。 - 支持自定义删除器:可以通过自定义删除器来管理非动态分配的资源(如文件句柄、网络连接等)。
# 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;
}
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
- 创建
std::shared_ptr: 使用std::make_shared<MyClass>(42)创建一个std::shared_ptr,它管理一个动态分配的MyClass对象。std::make_shared是 C++11 引入的便捷函数,用于创建std::shared_ptr。 - 共享所有权: 通过赋值操作
ptr2 = ptr1,ptr2与ptr1共享对同一个对象的所有权。引用计数增加,表示当前有两个std::shared_ptr指向该对象。 - 访问对象成员函数:通过
ptr1->printValue()和ptr2->printValue()访问MyClass对象的成员函数。 - 引用计数:使用
use_count()方法获取当前std::shared_ptr的引用计数。当新的std::shared_ptr共享所有权时,引用计数增加;当std::shared_ptr超出作用域时,引用计数减少。 - 自动释放内存:当最后一个
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
2
3
4
5
6
7
std::shared_ptr 的特点
- 共享所有权:多个
std::shared_ptr可以共享同一个对象的所有权。引用计数机制用于跟踪当前有多少个std::shared_ptr指向该对象。 - 自动释放内存:当最后一个
std::shared_ptr超出作用域时,对象会被自动释放。 - 避免内存泄漏和悬空指针:由于
std::shared_ptr自动管理内存,可以避免内存泄漏和悬空指针问题。 - 支持自定义删除器:可以通过自定义删除器来管理非动态分配的资源(如文件句柄、网络连接等)。
# 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;
}
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
- 创建
std::weak_ptr:使用std::weak_ptr<MyClass> weakPtr = ptr1创建一个std::weak_ptr,它观察ptr1管理的对象。std::weak_ptr不会增加引用计数。 - 检查
std::weak_ptr是否有效: 使用weakPtr.lock()获取一个std::shared_ptr,如果对象仍然存在,则返回有效的std::shared_ptr;否则返回nullptr。 - 释放
std::shared_ptr: 使用ptr1.reset()释放ptr1管理的对象。此时weakPtr观察的对象已被释放,weakPtr.lock()返回nullptr。 - 循环引用问题: 当两个
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
2
3
4
5
6
7
8
9
10
11
std::weak_ptr 的特点
- 弱引用:
std::weak_ptr是对std::shared_ptr管理的对象的弱引用,不会增加引用计数。 - 解决循环引用:通过使用
std::weak_ptr,可以打破std::shared_ptr之间的循环引用,避免内存泄漏。 - 安全访问:使用
lock()方法可以安全地获取std::shared_ptr,避免访问已释放的对象。 - 生命周期管理:
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;
}
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机制,智能指针确保资源在离开作用域时自动释放,彻底避免内存泄漏。
思考题:
make_unique和make_shared相比直接new有什么优势?(提示:异常安全、性能)shared_ptr的引用计数本身是线程安全的吗?shared_ptr指向的对象是线程安全的吗?- 什么场景下必须用
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;
}
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;
}
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;
}
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;
}
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代码交互或底层编程时仍然需要了解。
思考题:
malloc(0)会返回什么?new int[0]呢?行为是否一致?realloc如果扩展失败会返回 NULL,但原指针仍然有效。如果直接写p = realloc(p, newsize)而 realloc 失败了,会发生什么?- 为什么不能用
free释放new分配的对象?delete比free多做了什么?
# 12.6 内存池
# 12.6.1 内存池概念
内存池(Memory Pool) 是一种高效的内存管理技术,通过预先分配一大块内存,然后在需要时从中分配小块内存,减少频繁调用 new 和 delete 的开销。 内存池特别适用于需要频繁分配和释放小块内存的场景。
# 12.6.2 内存池核心思想
- 预先分配内存: 在程序启动时,预先分配一大块连续的内存。
- 管理内存块: 将大块内存划分为多个固定大小的小块,并维护一个空闲块列表。
- 分配内存: 当需要内存时,从空闲块列表中分配一块内存。
- 释放内存: 当内存不再使用时,将其归还到空闲块列表中。
# 12.6.3 内存池的优点
- 减少内存碎片: 通过固定大小的内存块分配,减少内存碎片。
- 提高性能: 减少频繁调用
new和delete的开销。 - 简化内存管理: 集中管理内存分配和释放,避免内存泄漏和悬空指针。
# 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;
}
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
- 构造函数: 初始化内存池,分配一大块内存,并将其划分为多个固定大小的块。 将每个块的起始地址添加到空闲块列表中。
allocate方法: 从空闲块列表中分配一块内存。 如果空闲块列表为空,抛出std::bad_alloc异常。deallocate方法: 将释放的内存块归还到空闲块列表中。getFreeBlockCount方法: 返回当前空闲块的数量。
输出结果
Allocated value: 42
Allocated value: 100
Free blocks after deallocation: 10
2
3
# 12.6.5 内存池优化
- 支持不同大小的内存块: 可以为不同大小的内存块创建多个内存池。
- 线程安全: 在多线程环境中,可以使用互斥锁(
std::mutex)保护内存池的分配和释放操作。 - 内存对齐: 确保分配的内存块满足对齐要求,提高访问效率。
# 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;
}
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 与对象池的性能差异。内存池减少了系统调用和内存碎片,在频繁创建销毁小对象的场景(如游戏、网络服务器)中效果显著。
思考题:
- 上面的对象池在多线程环境中安全吗?如何添加线程安全支持?
- 对象池中 release 的对象并没有调用析构函数,这在什么情况下可能有问题?
- 标准库中的
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;
}
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
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;
}
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
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:
- 内部嵌入
std::weak_ptr:std::enable_shared_from_this在对象内部维护一个std::weak_ptr,用于跟踪对象的生命周期。 shared_from_this方法: 通过shared_from_this()方法,可以从对象内部安全地生成一个std::shared_ptr,而不会创建额外的控制块。- 避免双重管理:所有通过
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) │ 可执行指令
└──────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
glibc的malloc实现(ptmalloc2):
- 小块内存(< 128KB):使用
brk()系统调用扩展堆顶。malloc维护多个空闲链表(free list),按大小分类(16, 24, 32, 40...字节),分配时从对应链表取一块,释放时放回链表 - 大块内存(>= 128KB):使用
mmap()系统调用直接向内核申请独立的内存映射区域,释放时用munmap()归还 - 内存块头部(chunk header):每个分配的内存块前面有8-16字节的元数据,记录块大小和使用状态。这就是为什么
malloc(100)实际消耗约112字节
malloc(100) 返回的内存块实际布局:
┌──────────┬──────────────────────┐
│ 头部16字节 │ 用户数据100字节 │
│ (size+flag)│ (返回的指针指向这里) │
└──────────┴──────────────────────┘
2
3
4
5
# 12.8.2 new/delete的底层过程
new表达式的三步操作:
MyClass* p = new MyClass(42);
编译器将其转换为:
// 伪代码
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.类型转换
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);
}
};
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[]()
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字节
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字节
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;
}
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;
}
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;
}
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;
类型: int(整型变量)。作用: 声明一个名为 i1 的整型变量,并初始化为 10。
内存分配: 在栈上分配内存,存储一个整数值。生命周期: 当 i1 所在的作用域结束时,i1 的内存会自动释放。
int* i2 = &i1;
类型: int*(指向整型的指针)。作用: 声明一个名为 i2 的指针变量,并将其初始化为 i1 的地址。
内存分配: 在栈上分配内存,存储一个指针(地址)。生命周期: 当 i2 所在的作用域结束时,i2 的内存会自动释放,但它指向的内存(i1)不会被自动释放。
int* i3 = new int(10);
类型: int*(指向整型的指针)。作用: 声明一个名为 i3 的指针变量,并动态分配一个整型内存,初始化为 10。
内存分配:i3 本身在栈上分配内存,存储一个指针(地址)。new int(10) 在堆上分配内存,存储一个整数值,并初始化为 10。
生命周期:i3 的生命周期与其所在作用域相同。 堆上分配的内存不会自动释放,必须手动使用 delete 释放:
int& i4 = i1;
类型: 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 综合思考题
内存碎片问题:频繁的小块
malloc/free会导致外部碎片(空闲内存总量足够但不连续)。内存分配器如何缓解碎片?jemalloc和tcmalloc相比glibc的ptmalloc2有什么优化?placement new的应用:
new(buffer) T(args)在指定地址构造对象,不分配内存。它在内存池、STL容器实现、共享内存中有什么应用?使用placement new时为什么必须手动调用析构函数?栈 vs 堆的性能差异:栈分配只需一条指令(
sub rsp, N),堆分配需要调用复杂的分配器甚至系统调用。在实际编程中,什么时候应该优先使用栈?alloca()为什么存在但不推荐使用?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 | 双重释放;只通过工厂函数获取智能指针 |