C++ 从入门到精通之八

大纲

C++ 智能指针

直接内存管理

直接内存管理的示例代码

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
#include <iostream>
#include <string>
#include <vector>

using namespace std;

class MyClass {
public:
int m_i;
};

// 自动内存管理
void func() {
int a = 0; // 局部变量(临时)
MyClass mc; // 局部变量(临时)
static int b = 30; // 静态局部变量(全局)
}

// 直接内存管理
void func2() {
// 初值未定义
int* pint = new int;
delete pint;

// 初值为0,空括号()表示值初始化
int* pint2 = new int();
delete pint2;

// 初值为10
int* pint3 = new int(10);
delete pint3;

// 初值是空字符串,调用string的默认构造函数
string* pstr = new string;
delete pstr;

// 调用string的默认构造函数,空括号()表示值初始化
string* pstr2 = new string();
delete pstr2;

// 初值为AAAAA
string* pstr3 = new string(5, 'A');

// auto 配合 new 使用,pstr4 推断的类型是 string **
auto pstr4 = new auto(pstr3);
delete pstr3;
delete pstr4;

// 初始值为 1, 2, 3
vector<int>* pvec = new vector<int>{1, 2, 3};
delete pvec;

// 调用MyClass的默认构造函数
MyClass* pmc = new MyClass;
delete pmc;

// 调用MyClass的默认构造函数,空括号()表示值初始化
MyClass* pmc2 = new MyClass();
delete pmc2;

// 动态分配const对象
const int* pconst = new const int(100);
delete pconst;
}

同一块内存不能 delete 多次

  • 不能 delete 多次的根本原因是:
    • 设计假设:内存管理器假设每次 delete 的都是正在使用中的内存
    • 数据结构破坏:第二次 delete 会修改已被修改过的元数据,破坏空闲链表(Free List)
    • 性能取舍:为了性能,不维护复杂的 “是否已释放” 检测机制
    • 不可确定性:第一次 delete 的内存可能已被重新分配,导致第二次 delete 释放的是其他对象的内存
delete 操作结果
delete p; delete p;❌ 未定义行为(通常崩溃)
delete p; p=nullptr; delete p;✅ 安全(delete nullptr 无效果)
两个指针指向同一内存,多次 delete❌ 未定义行为
delete nullptr 多次✅ 安全
  • 错误写法一:同一指针的重复删除
1
2
3
4
int* p = new int(10);
delete p;
p = nullptr; // ✅ 好习惯
delete p; // ❌ 错误!同一指针重复删除
  • 错误写法二:两个指针指向同一地址
1
2
3
4
int* p1 = new int(10);
int* p2 = p1; // p2 指向同一内存
delete p1; // ✅ 释放内存
delete p2; // ❌ 错误!p1 和 p2 指向同一块内存
  • 错误写法三:拷贝指针后重复删除
1
2
3
4
5
6
7
8
9
10
void badFunction(int* ptr) {
delete ptr; // 删除传入的指针
}

int main() {
int* p = new int(42);
badFunction(p);
delete p; // ❌ 错误!p 已经被删除
return 0;
}
  • 特殊情况:delete 空指针是合法的
1
2
3
int* p = nullptr;
delete p; // ✅ 安全!C++ 标准保证 delete nullptr 不做任何事
delete p; // ✅ 仍然安全,对 nullptr 重复 delete 是安全的

delete 操作的最佳实践总结

  • delete 后立即置空
1
2
delete p;
p = nullptr; // 后续 delete nullptr 是安全的
  • 使用智能指针(推荐)
1
2
3
4
unique_ptr<int> p1 = make_unique<int>(42);  // 自动管理
shared_ptr<int> p2 = make_shared<int>(42); // 引用计数,自动处理

// 不需要手动 delete,不会出现指针双重删除问题
  • 明确指针所有权
1
2
3
4
5
6
7
8
9
// 明确谁拥有指针,谁负责删除
class MyClass {
int* data;
public:
MyClass() : data(new int(42)) {}
~MyClass() { delete data; } // 唯一删除点

// 禁止拷贝或使用引用计数
};

最佳实践总结

  • 每个 new 只对应一个 deletedelete 后立即将指针置为 nullptr,更好的是方案使用智能指针自动管理内存。

new、delete 探秘

重载 new、delete 运算符

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
#include <iostream>
#include <memory>

using namespace std;

class MyClass {
public:
// 构造函数
MyClass() {
cout << "MyClass() called" << endl;
}

// 析构函数
~MyClass() {
cout << "~MyClass() called" << endl;
}

// 重载 new 运算符,用于对象分配内存
static void* operator new(size_t size) {
cout << "operator new called, size: " << size << endl;
void* ptr = malloc(size);
if (!ptr) {
throw bad_alloc();
}
return ptr;
}

// 重载 delete 运算符,用于对象释放内存
static void operator delete(void* ptr) {
cout << "operator delete called" << endl;
if (ptr) {
free(ptr);
}
}

// 重载 new[] 运算符,用于数组分配内存
static void* operator new[](size_t size) {
cout << "operator new[] called, size: " << size << endl;
void* ptr = malloc(size);
if (!ptr) {
throw bad_alloc();
}
return ptr;
}

// 重载 delete[] 运算符,用于数组释放内存
static void operator delete[](void* ptr) {
cout << "operator delete[] called" << endl;
if (ptr) {
free(ptr);
}
}

public:
int m_i;
};

int main() {
// 单个对象
MyClass* mc = new MyClass();
delete mc;

cout << "------------------" << endl;

// 对象数组
MyClass* marry = new MyClass[3];
delete[] marry;

return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
operator new called, size: 4
MyClass() called
~MyClass() called
operator delete called
------------------
operator new[] called, size: 20
MyClass() called
MyClass() called
MyClass() called
~MyClass() called
~MyClass() called
~MyClass() called
operator delete[] called

特别注意

  • 在 C++ 中,newdelete 具备对堆上分配的内存进行初始化和释放的能力,这是 malloc()free() 不具备的。
  • 对象构造时,先使用 new 分配对象内存,然后再调用对象的构造函数;对象析构时,先调用对象的析构函数,再调用 delete 释放对象内存。

如何记录 new 分配的内存大小供 delete 使用

C++ 编译器或运行时库在 new 分配内存时,通常会在用户请求的内存前额外多分配一块空间,用于存储内存块的大小信息,然后返回偏移后的指针;delete 根据指针向前访问这个内存块大小信息,从而知道实际分配的内存大小并释放。new 的底层通常调用 malloc() 来分配内存,而 malloc() 自身在内存块前维护了内存块的大小信息,以便 free() 正确释放内存;这些内存块大小信息由底层内存管理器维护,而不是由 new 本身维护。

  • 理解注意事项:
    • (1) 当调用 malloc(totalSize) 时,底层内存管理器(C 标准库)会记录这块内存的实际大小和管理信息(通常在内存块头部隐藏存储)。
    • (2) 当调用 free(ptr) 时,它会读取自己内部的管理信息来知道这块内存的大小,然后回收,不需要用户手动告诉它大小。
    • (3) 因此在 operator delete 中,用户只需要传给 free() 原始分配的指针,底层内存管理器会自动处理。

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
#include <iostream>

using namespace std;

class MyClass {
public:
// 构造函数
MyClass() {
cout << "MyClass() called" << endl;
}

// 析构函数
~MyClass() {
cout << "~MyClass() called" << endl;
}
};

void test01() {
cout << "---------- test01() ----------" << endl;

// 空类(不包含任何成员变量)占一个字节大小
MyClass mc;
size_t len = sizeof(mc);
cout << "size: " << len << endl; // 输出 1
}

void test02() {
// 分配整型数组
int* iarray = new int[2];

// 释放整型数组,如果这里不执行 delete[],会泄露 16 个字节内存(不同平台有差异)
delete[] iarray;
}

void test03() {
cout << "---------- test03() ----------" << endl;

// 分配对象数组
MyClass* parray = new MyClass[2];

// 释放对象数组,如果这里不执行 delete[],会泄露 18 个字节内存(不同平台有差异)
delete[] parray;
}

为什么在上述的 test03() 函数中,如果不手动通过 delete[] 释放对象数组时,会泄露 10 个字节呢?

  • (1) 对象大小

    • MyClass 类只有构造函数和析构函数,没有成员变量,对象本身大小为 1 字节(空类保证每个对象都有唯一地址)。
    • 在大多数 64 位编译器下,空类大小通常是 1 字节。
    • 如果类里有成员变量,例如 int m_i,类的大小通常是 sizeof(int),按 4 或者 8 字节对齐。
  • (2) 数组长度信息

    • 编译器在用 new[] 分配数组时,通常会额外在内存块前额外多分配一块空间来存储数组长度信息,用于告诉 delete[] 需要调用多少次析构函数。
    • 这个数组长度通常是 size_t(在 64 位系统上占 8 字节)。
  • (3) 内存块大小信息

    • new 底层调用 malloc() 分配内存,malloc 自身会在内存块前额外多分配一块空间,用于存储内存块的大小信息,以便 free() 回收内存。
    • 这内存块大小信息通常占 8 ~ 16 字节(依赖系统和实现)。
  • (4) 内存布局示意(简化版)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    +------------------------+ 
    | 内存块大小 = 8 ~ 16 字节 | <-- malloc 内部隐藏头(管理内存块大小)
    +------------------------+
    | 数组长度 size_t = 8 字节 | <-- 编译器存储数组元素数量
    +------------------------+
    | 对象 MyClass[0] = 1 字节 |
    +------------------------+
    | 对象 MyClass[1] = 1 字节 |
    +------------------------+
  • (5) 实际泄露的内存大小

    • 对象数据本身大小:2 × sizeof(MyClass)
    • 额外存储的数组长度信息:sizeof(size_t)(在 64 位系统上占 8 字节)
    • 额外存储的内存块大小信息:约占 8 ~ 16 字节(依赖系统和实现)
  • (6) 举例说明

    • 假设是 64 位系统,空类大小占 1 字节,数组长度(size_t)占 8 字节,内存块大小信息占 8 字节:
      1
      总的内存泄漏大小 ≈ 8 (内存块大小) + 8 (数组长度) + 2 × 1 (对象大小) = 18 字节
    • 如果类有一个 int 成员变量(4 字节),并按 4 字节对齐:
      1
      2
      sizeof(MyClass) = 4字节
      总的内存泄漏大小 ≈ 8 (内存块大小) + 8 (数组长度) + 2 × 4 (对象大小) = 24 字节

为什么在上述的 test02() 函数中,如果不手动通过 delete[] 释放对象数组时,会泄露 16 个字节呢?

  • (1) 对象大小

    • int 在 64 位系统上通常是占用 4 字节(32 位整数)。
    • 数组有 2 个元素,所以对象数据占用:
      1
      2 × sizeof(int) = 2 × 4 = 8 字节
  • (2) 数组长度信息

    • 编译器在用 new[] 分配数组时,通常会额外在内存块前额外多分配一块空间来存储数组长度信息,用于告诉 delete[] 需要调用多少次析构函数。
    • 对于 int 这种基础类型没有析构函数,数组长度信息通常会被编译器优化掉不存储(也就是说不会占用额外字节)。
    • 因此数组长度占用 0 字节。
  • (3) 内存块大小信息

    • new 底层调用 malloc() 分配内存,malloc 自身会在内存块前额外多分配一块空间,用于存储内存块的大小信息,以便 free() 回收内存。
    • 这内存块大小信息通常占 8 ~ 16 字节(依赖系统和实现)。
  • (4) 内存布局示意(简化版)

    1
    2
    3
    4
    5
    6
    7
    +------------------------+ 
    | 内存块大小 = 8 ~ 16 字节 | <-- malloc 内部隐藏头(管理内存块大小)
    +------------------------+
    | 对象 MyClass[0] = 4 字节 |
    +------------------------+
    | 对象 MyClass[1] = 4 字节 |
    +------------------------+
  • (5) 实际泄露的内存大小

    • 对象数据本身大小:2 × sizeof(int)
    • 额外存储的数组长度信息:0 字节(基础类型)
    • 额外存储的内存块大小信息:约占 8 ~ 16 字节(依赖系统和实现)
      1
      总的内存泄漏大小 ≈ 8 (内存块大小) + 2 x 4 (对象大小) = 16 字节

关键点

  • 数组长度信息:由编译器自己存储,用于告诉 delete[] 需要调用多少次析构函数。
  • 内存块大小信息:由 malloc() / 底层内存管理器存储,用于告诉 free() 实际回收的内存大小。
  • delete[] 需要的额外信息

    • 当用 new MyClass[2] 分配数组内存时,编译器必须知道数组里有多少个对象,以便告诉 delete[] 需要调用多少次析构函数。
    • 所以编译器通常会在数组实际内存的前面额外多分配一块空间,用于存储一个 size_t(或者其它形式)记录数组长度,这不是整个内存块的大小,而是数组长度信息。
    • 这块数组长度信息只为 delete[] 调用析构函数而服务,与底层内存管理器(malloc)内部记录的 “内存块大小信息” 是两回事。
  • operator delete / free 需要的额外信息

    • 底层内存管理器(malloc/free)内部会记录每块内存的大小,但这是给 free() 自己用的,不是编译器额外添加给 delete[] 使用的。
    • 用户不需要知道每块内存的大小,也不需要将内存大小传给 free(),因为 free() 会自动查到。

特别注意

在 C++ 中,使用 new[] 分配数组内存后,必须使用 delete[] 释放内存;若误用 delete 释放数组内存,仅当数组元素类型为基础类型或者不含自定义析构函数的类类型时,行为才不是未定义的(通常不会引发错误),否则会导致内存泄漏或程序崩溃。正确写法应始终成对使用 new[]delete[]

智能指针的使用

智能指针的类型

在 C++ 中,智能指针的类型有以下几种:

  • (1) 带引用计数的智能指针:shared_ptr
  • (2) 不带引用计数的智能指针:auto_ptrscoped_ptrunique_ptr
  • (3) 特殊的智能指针:weak_ptr(不增加引用计数,可以用于避免 shared_ptr 发生循环引用)
智能指针 C++ 标准所有权带引用计数适用场景核心特性
auto_ptrC++ 98 独占(拷贝时转移)⚠ 已废弃,建议改用 unique_ptr独占所有权,在复制或赋值时会转移所有权,导致原指针变为空(nullptr
scoped_ptrBoost 独占生命周期受限于作用域,适用于简单的场景,避免资源泄漏独占所有权,不可复制或赋值,不支持移动语义,即不可以使用 std::move() 函数转移所有权
unique_ptrC++ 11 独占资源独占,生命周期明确独占所有权,不可复制(拷贝构造和赋值),但可以移动(移动构造和移动赋值),即支持使用 std::move() 函数转移所有权
shared_ptrC++ 11 共享资源共享,生命周期不固定共享所有权(允许多个智能指针管理同一个资源)
weak_ptrC++ 11 观察 shared_ptr避免 shared_ptr 发生循环引用不增加引用计数,用于避免 shared_ptr 发生循环引用,可以通过 lock() 函数转换为 shared_ptr

shared_ptr 智能指针

shared_ptr 的使用语法
正确的使用语法
  • shared_ptr 的正确用法
    • 第一种正确用法
      • shared_ptr<int> sp(new int(100));
      • shared_ptr<MyClass> sp(new MyClass());
    • 第二种正确用法
      • shared_ptr<int> sp = make_shared<int>(100);
      • shared_ptr<MyClass> sp = make_shared<MyClass>();
    • 第三种正确用法
      • shared_ptr<int> sp;
      • sp 是指向 int 的智能指针,但目前指向的内存地址为空,即属于空指针
错误的使用语法
  • shared_ptr 的错误用法
    • 第一种错误用法(代码编译失败)
      • shared_ptr<int> sp = new int(100);
      • 智能指针是 explicit,不可以进行隐式类型转换,必须用直接初始化形式
    • 第二种错误用法(代码可以正常运行,但不推荐使用)
      • int *pi = new int(100); shared_ptr<int> spi(pi);
      • 裸指针与智能指针尽量不要混用,否则会影响代码的健壮性
shared_ptr 的引用计数

特别注意

在 C++ 中,shared_ptr 的引用计数操作是线程安全的(使用原子操作),但 shared_ptr 指向的对象本身不是线程安全的,需要额外同步处理。

引用计数增加
  • shared_ptr 引用计数增加的说明
    • 移动操作:移动构造 / 移动赋值不会增加原对象的引用计数,只是转移所有权
      1
      2
      3
      shared_ptr<int> p1 = std::make_shared<int>(10);  // 引用计数为 1
      shared_ptr<int> p2 = std::move(p1); // 移动构造,p1 变为空指针,p1 指向的原对象的引用计数仍为 1
      p1 = std::move(p2); // 移动赋值,同样不增加引用计数

  • shared_ptr 引用计数增加的情况
    • (1) 拷贝构造
      1
      2
      3
      shared_ptr<int> p1 = std::make_shared<int>(42);  // 引用计数为 1
      shared_ptr<int> p2(p1); // 引用计数为 2
      shared_ptr<int> p3 = p1; // 引用计数为 3
    • (2) 拷贝赋值
      1
      2
      3
      4
      shared_ptr<int> p1 = std::make_shared<int>(10);  // 引用计数为 1
      shared_ptr<int> p2 = std::make_shared<int>(20); // 引用计数为 1
      p2 = p1; // p2 改为指向 p1 的对象,p2 指向原对象的引用计数从 1 减少到 0,释放资源
      // p1 指向的对象引用计数从 1 增加到 2
    • (3) 从 weak_ptr 构造 shared_ptr
      1
      2
      3
      4
      5
      6
      7
      shared_ptr<int> sp = std::make_shared<int>(5);
      std::weak_ptr<int> wp = sp; // 不影响引用计数

      shared_ptr<int> sp2 = wp.lock(); // 成功时返回 shared_ptr,引用计数加 1
      if (sp2) {
      // sp2 有效,原对象引用计数变为 2
      }
    • (4) 智能指针作为函数的形参(按值传递)
      1
      2
      3
      4
      5
      6
      7
      // 注意:如果这里的形参是引用(按引用传递),那么智能指针的引用计数不会增加
      void func(shared_ptr<int> sp) {

      }

      shared_ptr<int> sp = make_shared<int>(100); // 引用计数为 1
      func(sp); // 引用计数为 2
    • (5) 智能指针作为函数的返回值
      1
      2
      3
      4
      5
      6
      7
      std::shared_ptr<int> create() {
      shared_ptr<int> sp = std::make_shared<int>(50); // 引用计数为 1
      return sp; // 返回时可能移动或拷贝,但不一定增加引用计数
      }

      shared_ptr<int> sp = create(); // 通常会使用移动语义,引用计数保持 1
      // 但如果编译器没有优化,可能会临时增加引用计数到 2
    • (6) 容器操作(拷贝元素)
      1
      2
      3
      4
      std::vector<std::shared_ptr<int>> vec;
      shared_ptr<int> sp = std::make_shared<int>(10); // 引用计数为 1
      vec.push_back(sp); // 拷贝到容器,引用计数为 2
      vec.insert(vec.begin(), sp); // 再次拷贝到容器,引用计数为 3
引用计数减少
  • shared_ptr 引用计数减少的说明
    • 当引用计数减少到 0 时,shared_ptr 会:
      • (1) 调用删除器释放管理的对象
      • (2) 释放控制块(如果没有 weak_ptr 指向它)
    • 移动操作:移动构造 / 移动赋值不会减少原对象的引用计数,只是转移所有权
      1
      2
      shared_ptr<int> p1 = std::make_shared<int>(42);    // 引用计数为 1
      shared_ptr<int> p2 = std::move(p1); // 移动后 p1 变为空指针,但 p1 原来的引用计数不变(转移而非减少)
    • weak_ptr 不会影响引用计数,但 weak_ptr 可以延长控制块的生存时间
      1
      2
      3
      4
      shared_ptr<int> sp = std::make_shared<int>(10);
      std::weak_ptr<int> wp = sp; // 不影响引用计数
      sp.reset(); // 引用计数从 1 减到 0,对象销毁
      // 但控制块仍存在,直到 wp 也销毁

  • shared_ptr 引用计数减少的情况
    • (1) 智能指针对象被销毁
      1
      2
      3
      4
      void func() {
      shared_ptr<int> p = std::make_shared<int>(42); // 引用计数 = 1
      // ...
      } // 离开作用域,p 被销毁,引用计数减为 0,释放资源
    • (2) 智能指针指向新的对象
      1
      2
      3
      shared_ptr<int> sp = make_shared<int>(100);  // 引用计数为 1
      shared_ptr<int> sp2 = sp; // 引用计数为 2
      sp = make_shared<int>(200); // sp 指向新对象,sp 指向的原对象的引用计数减为 1
    • (3) 智能指针的 reset() 方法被调用
      1
      2
      3
      shared_ptr<int> p = std::make_shared<int>(10);     // 引用计数为 1
      p.reset(); // 引用计数为 0,释放资源
      p.reset(new int(20)); // 先减原引用计数,再管理新资源
    • (4) 容器中移除或替换元素
      1
      2
      3
      4
      5
      std::vector<std::shared_ptr<int>> vec;
      vec.push_back(std::make_shared<int>(1)); // 引用计数为 1
      vec.push_back(std::make_shared<int>(2));
      vec.erase(vec.begin()); // 第一个元素引用计数减 1
      vec[0] = std::make_shared<int>(3); // 第二个元素引用计数减 1
shared_ptr 的常用操作
use_count()
  • use_count():返回有多少个智能指针指向某个对象,主要用于代码调试目的
1
2
3
4
5
6
7
8
9
10
11
12
shared_ptr<int> sp = make_shared<int>(100);
int count1 = sp.use_count();
cout << count1 << endl; // 输出 1

shared_ptr<int> sp2(sp);
int count2 = sp.use_count();
cout << count2 << endl; // 输出 2

shared_ptr<int> sp3;
sp3 = sp;
int count3 = sp.use_count();
cout << count3 << endl; // 输出 3
unique()
  • unique():判断该智能指针是否独占某个指向的对象,也就是如果只有一个智能指针指向某个对象,那么 unique() 会返回 1,否则返回 0
1
2
3
4
5
shared_ptr<int> sp = make_shared<int>(100);
cout << sp.unique() << endl; // 输出 1,表示 true

shared_ptr<int> sp2(sp);
cout << sp.unique() << endl; // 输出 0,表示 false
reset()
  • reset():用于改变或释放 shared_ptr 所管理的对象

    • reset() 不带参数:
      • 唯一指向时:若当前 shared_ptr 是唯一指向所管理对象的智能指针(即引用计数为 1),则释放原对象的内存,然后将当前 shared_ptr 置为空指针
      • 非唯一指向时:若有多个 shared_ptr 共享同一个对象(引用计数 > 1),则不释放原对象的内存,仅将原对象的引用计数减 1,然后将当前 shared_ptr 置为空指针
      • 无论是哪种情况,调用 reset() 后,当前 shared_ptr 都会变为空指针(nullptr
    • reset(newPtr) 带参数:
      • 唯一指向时:若当前 shared_ptr 是唯一指向所管理对象的智能指针(即引用计数为 1),则释放原对象的内存,然后将当前 shared_ptr 指向新对象(newPtr
      • 非唯一指向时:若有多个 shared_ptr 共享同一个对象(引用计数 > 1),则不释放原对象的内存,仅将原对象的引用计数减 1,然后将当前 shared_ptr 指向新对象(newPtr
      • 无论是哪种情况,原对象的引用计数都会减少 1(唯一指向时减到 0 则释放原对象的内存,非唯一指向时仅减 1 不释放原对象内存),然后将当前 shared_ptr 转而管理新对象(newPtr
      • 注意:参数 newPtr 是裸指针(通过 new 得到的原始指针),另外还有两个参数的重载版本 reset(newPtr, deleter),用于替换并指定自定义删除器
    • 如果 reset() 抛出异常(如自定义删除器在释放内存时抛出异常),原对象保持原样(强异常保证)
  • (1) 代码案例一

1
2
3
4
shared_ptr<int> sp = make_shared<int>(100);
shared_ptr<int> sp2 = sp; // 引用计数为 2
sp.reset(); // 非唯一指向时:不释放原对象的内存,引用计数减为 1,sp 置空,此时 sp2 仍有效
sp2.reset(); // 唯一指向时:释放原对象的内存,引用计数减为 0,sp2 置空
  • (2) 代码案例二
1
2
3
4
5
6
7
8
9
10
auto p1 = std::make_shared<int>(100);  // 对象 A
auto p2 = p1; // 对象 A,引用计数为 2

// 情况1:非唯一指向
p1.reset(new int(200)); // 对象 A 的引用计数减为 1(不释放原对象内存),p1 现在指向新对象 B(200),此时 p2 仍指向原对象 A(100) 且有效

auto p3 = std::make_shared<int>(300); // 对象 C,引用计数为 1

// 情况2:唯一指向
p3.reset(new int(400)); // 对象 C 的引用计数减为 0(释放原对象内存),p3 现在指向新对象 D(400)
  • (3) 代码案例三
1
2
shared_ptr<int> sp;       // 空指针
sp.reset(new int(100)); // 空指针也可以通过 reset() 来初始化
* 解引用
  • 通过 * 获取 shared_ptr 所指向的对象
1
2
shared_ptr<int> sp = make_shared<int>(100);
cout << *sp << endl; // 输出 100
get()
  • get():返回 shared_ptr 存储的裸指针(通过 new 得到的原始指针)
    • shared_ptr 返回的裸指针,千万不要手动 delete,否则会导致程序运行崩溃(未定义行为)
    • 注意:如果 shared_ptr 释放了所指向对象的内存,那么 get() 返回的裸指针也会变得无效
1
2
3
4
shared_ptr<int> sp = make_shared<int>(100);
int *p = sp.get();
*p = 150;
cout << *p << endl; // 输出 150
swap()
  • swap():用于交换两个 shared_ptr 所指向的对象
1
2
3
4
5
6
shared_ptr<int> sp = make_shared<int>(100);
shared_ptr<int> sp2 = make_shared<int>(300);
sp.swap(sp2);

cout << *sp << endl; // 输出 300
cout << *sp2 << endl; // 输出 100
= nullptr
  • = nullptr
    • 唯一指向时:若当前 shared_ptr 是唯一指向所管理对象的智能指针(即引用计数为 1),则释放原对象的内存,然后将当前 shared_ptr 置为空指针
    • 非唯一指向时:若有多个 shared_ptr 共享同一个对象(引用计数 > 1),则不释放原对象的内存,仅将原对象的引用计数减 1,然后将当前 shared_ptr 置为空指针
    • 无论是哪种情况,执行 = nullptr 后,当前 shared_ptr 都会变为空指针(nullptr
1
2
3
4
shared_ptr<int> sp = make_shared<int>(100);
shared_ptr<int> sp2 = sp; // 引用计数为 2
sp = nullptr; // 非唯一指向时:不释放原对象的内存,引用计数减为 1,sp 置空,此时 sp2 仍有效
sp2 = nullptr; // 唯一指向时:释放原对象的内存,引用计数减为 0,sp2 置空
shared_ptr 的删除器
自定义删除器
  • shared_ptr 会在一定的时机自动删除所指向的对象,并且底层使用 delete 操作符作为默认的资源析构方式

  • shared_ptr 还支持指定自定义的删除器来取代 C++ 编译器提供的默认删除器

  • (1) 代码案例一

1
2
3
4
5
6
7
8
void myDeleter(int *p) {
delete p;
p = nullptr;
}

// 这里不能使用 make_shared,因为它不支持自定义删除器
shared_ptr<int> sp(new int(100), myDeleter);
sp.reset(); // 唯一指向时:释放原对象的内存(会调用自定义删除器),引用计数减为 0,sp 置空
  • (2) 代码案例二
1
2
3
4
5
6
// 自定义的删除器还可以是 Lambda 表达式
shared_ptr<int> sp(new int(100), [](int *p) {
delete p;
p = nullptr;
cout << "delete int *" << endl;
});

make_shared 不支持自定义删除器的原因

  • 设计目的:make_shared 是为了一次分配内存(控制块和对象内存在一起)和异常安全,而自定义删除器通常用于管理非 new 分配的资源,比如文件句柄、自定义释放逻辑(动态数组)等
  • 类型推导问题:删除器的类型会改变 shared_ptr 的类型签名,而 make_shared 无法推断这种类型信息
  • 语义清晰:需要自定义删除器说明资源不是普通方式分配的,此时使用 new 显式创建对象更符合意图
使用数组的问题

如果 shared_ptr 指向的是数组,那么就需要自定义删除器或者特殊语法,否则 C++ 编译器不会使用 delete [] 正确释放数组的内存,而是默认使用 delete 导致内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 自定义类(带自定义的析构函数)
class MyClass {
public:
MyClass() {
}
~MyClass() {
cout << "~MyClass()" << endl;
}
};

// 解决使用数组的问题
int main() {
// 如果 shared_ptr 指向的是数组,那么就需要自定义删除器,否则不会使用 delete [] 正确释放数组的内存
shared_ptr<MyClass> sp(new MyClass[3], [](MyClass *p) {
delete[] p;
cout << "delete MyClass []" << endl;
});
}

程序运行输出的结果如下:

1
2
3
4
~MyClass()
~MyClass()
~MyClass()
delete MyClass []

除了上面通过函数或者 Lambda 表达式作为自定义删除器来释放数组内存,还可以使用 std::default_delete 来做删除器,std::default_delete 是标准库(STL)里的模板类,同样可以正确释放数组的内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 自定义类(带自定义的析构函数)
class MyClass {
public:
MyClass() {
}
~MyClass() {
cout << "~MyClass()" << endl;
}
};

// 解决使用数组的问题
int main() {
// 如果 shared_ptr 指向的是数组,还可以使用 default_delete 来正确释放数组的内存
shared_ptr<MyClass> sp(new MyClass[3], default_delete<MyClass[]>());
}

程序运行输出的结果如下:

1
2
3
~MyClass()
~MyClass()
~MyClass()

如果实在不想通过自定义删除器或者 std::default_delete 来正确释放数组的内存,还可以使用数组特化版本 shared_ptr<T[]>,同样可以正确释放数组内存(从 C++ 17 开始支持以下写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass {
public:
MyClass() {
}
~MyClass() {
cout << "~MyClass()" << endl;
}
};

// 解决使用数组的问题
int main() {
// 从 C++ 17 开始正式支持以下写法,使用数组特化版本 shared_ptr<T[]>,同样可以正确释放数组内存
shared_ptr<MyClass[]> sp2(new MyClass[3]);
}

程序运行输出的结果如下:

1
2
3
~MyClass()
~MyClass()
~MyClass()

同样的,还可以封装一个函数模板来正确释放数组内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyClass {
public:
MyClass() {
}
~MyClass() {
cout << "~MyClass()" << endl;
}
};

// 函数模板
template <typename T>
shared_ptr<T> make_shared_array(size_t size) {
return shared_ptr<T>(new T[size], default_delete<T[]>());
}

// 解决使用数组的问题
int main() {
// 调用自定义的函数模板
shared_ptr<MyClass> sp = make_shared_array<MyClass>(3);
}

程序运行输出的结果如下:

1
2
3
~MyClass()
~MyClass()
~MyClass()
同一种类型的说明

特别注意

如果两个 shared_ptr 指定了不同的删除器,只要它们所指向的对象类型是相同的,那么这两个 shared_ptr 也属于同一种类型。这就代表只要类型相同,就可以将它们放入到元素类型为该对象类型的容器里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main() {
auto lambda1 = [](int *p) {
delete p;
cout << "delete by lambda1" << endl;
};

auto lambda2 = [](int *p) {
delete p;
cout << "delete by lambda2" << endl;
};

shared_ptr<int> p1(new int(100), lambda1);

shared_ptr<int> p2(new int(200), lambda2);

// 类型相同,就可以放入到元素类型为该对象类型的容器里面
// vector<shared_ptr<int>> vec{p1, p2};

p2 = p1; // p2 先调用 lambda2 释放自己所指向的对象,然后 p2 重新指向 p1 所指向的对象,此时 p1 所指向对象的引用计数为 2
// 当作用域结束后,最终还会调用 lambda1 释放 p1、p2 共同指向的对象
}

程序运行输出的结果如下:

1
2
delete by lambda2
delete by lambda1
shared_ptr 的使用陷阱
裸指针与智能指针混合使用

将一个裸指针绑定到一个 shared_ptr 之后,那内存管理的责任就应该交给 shared_ptr 了,这个时候就不能够再通过裸指针来访问 shared_ptr 所指向的内存。简而言之,在 C++ 中应该尽量避免同时使用裸指针和智能指针(如 shared_ptr

  • 错误使用案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <memory>

using namespace std;

void proc(shared_ptr<int> sp) {
return;
}

int main() {
int *p = new int(100); // 裸指针
proc(shared_ptr<int>(p)); // 使用 shared_ptr 临时对象作为函数参数
*p = 45; // 错误写法(存在未定义行为),因为 proc() 执行结束后,p 指向的内存已经被 shared_ptr 临时对象释放掉
return 0;
}
  • 正确使用案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <memory>

using namespace std;

void proc(shared_ptr<int> sp) {
return;
}

int main() {
shared_ptr<int> sp = make_shared<int>(100); // 定义 shared_ptr 变量,避免使用裸指针
proc(sp); // 直接传递 shared_ptr 变量,而不是临时对象
*sp = 45; // 可以更改 shared_ptr 所指向的对象,不会出现未定义行为
return 0;
}
使用裸指针初始化多个智能指针

C++ 禁止使用同一个裸指针初始化多个独立的 shared_ptr,核心原因是避免重复释放内存。每个 shared_ptr 都有自己的控制块,如果从同一个裸指针分别创建,它们会互不知情,导致析构时对同一块内存释放两次,造成未定义行为。正确的做法是先创建一个 shared_ptr,然后用它拷贝构造其他 shared_ptr,这样它们才能共享同一个控制块和引用计数。

  • 错误使用案例
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <memory>

using namespace std;

int main() {
int * p = new int(100);
shared_ptr<int> sp1(p);
shared_ptr<int> sp2(p);
return 0;
// 离开作用域后,p 指向的内存会被 delete 两次(因为 sp1 与 sp2 的引用计数是互相独立的,没有任何关联关系),导致程序运行崩溃
}
  • 正确使用案例
1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <memory>

using namespace std;

int main() {
shared_ptr<int> sp1 = make_shared<int>(100); // 避免使用裸指针
shared_ptr<int> sp2(sp1); // 使用拷贝构造
return 0;
}
谨慎使用 get () 返回的裸指针

在 C++ 中,需要谨慎使用 shared_ptrget() 返回的裸指针,是因为这个裸指针不参与引用计数管理。如果你 delete 这个裸指针,会导致原始内存被释放,但 shared_ptr 仍认为它拥有这块内存,最终在析构时会再次尝试释放,造成重复释放内存的未定义行为。此外,如果用这个裸指针去初始化另一个 shared_ptr,也会造成独立的控制块,同样导致重复释放内存get() 返回的裸指针仅适合用于那些不转移所有权、不延长生命周期的只读访问场景。

  • 错误使用案例
1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <memory>

using namespace std;

int main() {
shared_ptr<int> sp = make_shared<int>(100);
int *p = sp.get();
shared_ptr<int> sp2(p); // 错误写法,不能将 get() 返回的裸指针绑定到其他 shared_ptr,否则会重复释放内存,导致程序运行崩溃
delete p; // 错误写法,不能将 get() 返回的裸指针 delete 掉,否则会重复释放内存,导致程序运行崩溃
}
  • 正确使用案例
1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <memory>

using namespace std;

int main() {
shared_ptr<int> sp = make_shared<int>(100);
int *p = sp.get();
// 作用域结束后,p 指向的内存会被 shared_ptr 自动释放掉
}
不要将 this 作为 shared_ptr 返回

在 C++ 中,将 this(类对象指针)作为 shared_ptr 返回是危险的,因为 this 本质上是一个原始指针,用它直接构造 shared_ptr 会创建一个独立于现有引用计数体系的新控制块。这会导致同一个对象被多个相互不知晓的 shared_ptr 管理,从而在析构时引发重复释放内存(Double Free)的未定义行为。正确的做法是让类继承自 enable_shared_from_this,然后通过 shared_from_this() 返回安全的智能指针。

  • 错误使用案例
1
2
3
4
5
6
7
8
9
class MyClass {
public:
shared_ptr<MyClass> getSelf() {
return shared_ptr<MyClass>(this); // 错误写法(危险操作)
}
};

shared_ptr<MyClass> p1 = make_shared<MyClass>();
shared_ptr<MyClass> p2 = p1->getSelf(); // p1 和 p2 独立管理同一对象(不共享引用计数),会导致重复释放内存
  • 正确使用案例
1
2
3
4
5
6
7
8
9
class MyClass : public enable_shared_from_this<MyClass> {
public:
shared_ptr<MyClass> getSelf() {
return shared_from_this(); // 安全
}
};

shared_ptr<MyClass> p1 = make_shared<MyClass>();
shared_ptr<MyClass> p2 = p1->getSelf(); // p1, p2 共享引用计数,不会导致重复释放内存
应避免 shared_ptr 发生循环引用

在 C++ 中,循环引用是指两个或多个 shared_ptr 互相持有对方的引用,导致每个对象的引用计数始终无法降为零,从而永远不会被自动析构,造成内存泄漏。这是因为 shared_ptr 基于引用计数机制工作,只有当计数变为零时才会释放对象,而循环引用使得每个对象至少还被一个其他对象强引用着。解决方法是使用 weak_ptr 来打破循环:将其中一个方向的引用改为弱引用,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
22
23
24
25
class CB;

class CA {
public:
shared_ptr<CB> m_cb;
~CA() {
cout << "~CA()" << endl;
}
};

class CB {
public:
shared_ptr<CA> m_ca;
~CB() {
cout << "~CB()" << endl;
}
};

int main() {
shared_ptr<CA> sp_ca = make_shared<CA>();
shared_ptr<CB> sp_cb = make_shared<CB>();
sp_ca->m_cb = sp_cb;
sp_cb->m_ca = sp_ca;
// 离开作用域后,由于存在循环引用,CA 和 CB 的对象都不会自动析构(释放内存),导致内存泄漏
}
  • 正确使用案例
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
class CB;

class CA {
public:
shared_ptr<CB> m_cb;
~CA() {
cout << "~CA()" << endl;
}
};

class CB {
public:
weak_ptr<CA> m_ca; // 使用 weak_ptr 打破循环引用
~CB() {
cout << "~CB()" << endl;
}
};

int main() {
shared_ptr<CA> sp_ca = make_shared<CA>();
shared_ptr<CB> sp_cb = make_shared<CB>();
sp_ca->m_cb = sp_cb;
sp_cb->m_ca = sp_ca;
// 离开作用域后,由于不存在循环引用,CA 和 CB 的对象都会自动析构(释放内存),不会导致内存泄漏
}

shared_ptr 的性能分析

shared_ptr 的指针大小

在 C++ 中,weak_ptrshared_ptr 的大小是一样的,是裸指针(通过 new 得到的指针)大小的两倍;其中包括两个指针(如下图所示),第一个是指向 T 类型对象的指针,第二个是指向控制块的指针。

1
2
3
4
int *p;
shared_ptr<int> sp;
int length1 = sizeof(p); // 8 个字节(64 位系统)
int length2 = sizeof(sp); // 16 个字节(64 位系统)

提示

控制块是由第一个指向某个对象的 shared_ptr 创建的。

shared_ptr 的移动语义
1
2
3
4
5
6
7
shared_ptr<int> sp = make_shared<int>(100);
shared_ptr<int> sp2 = (move(sp)); // 移动构造一个新的 shared_ptr 对象 sp2
cout << sp2.use_count() << endl; // sp 不再指向原对象(置为空),但原对象的引用计数仍然是 1

shared_ptr<int> sp3;
sp3 = move(sp2); // 移动赋值
cout << sp3.use_count() << endl; // sp2 不再指向原对象(置为空),但原对象的引用计数仍然是 1

note

对于 shared_ptr,移动构造函数的效率高过拷贝构造函数,移动赋值运算符的效率高过拷贝赋值运算符,因为拷贝需要增加引用计数,而移动语义不需要增加引用计数。

线程安全问题的解决

使用智能指针(如 shared_ptr)后,可以解决在多个线程同时访问同一个对象(共享对象)时产生的线程安全问题,详细介绍可以看 这里