C++ 从入门到精通之九

大纲

C++ 智能指针

weak_ptr 智能指针

weak_ptr 是 C++ 中的一个类模板,属于智能指针的一种。它指向一个由 shared_ptr 管理的对象,但与 shared_ptr 不同的是,weak_ptr 不控制所指向对象的生命周期。换句话说,将一个 weak_ptr 绑定到 shared_ptr 上,不会增加引用计数。因此,weak_ptr 常用于解决 shared_ptr 可能导致的循环引用问题,同时也可以用来监视 shared_ptr 所管理对象的生命周期。如果需要访问 weak_ptr 所指向的对象,可以调用其 lock() 成员函数,它会返回一个有效的 shared_ptr(如果原对象还存在);若对象已被释放,则返回一个空的 shared_ptr值得注意的是,weak_ptr 的构造和析构不会增加或减少所指向对象的引用计数。因此,当最后一个管理该对象的 shared_ptr 被销毁时,对象会照常释放,而不会因为仍有 weak_ptr 指向它而延迟或阻止释放。在 C++ 的术语中,通常将 shared_ptr 称为强智能指针(强引用),而将 weak_ptr 称为弱智能指针(弱引用、弱共享)。这种 “弱” 正体现在它不参与对象的生命周期管理上。

特别注意

weak_ptr 不是一种独立的智能指针,不能用于操作所指向的对象,所以它看起来像是一个 shared_ptr 的助手(旁观者),能够监视到所指向的对象是否存在。

weak_ptr 的使用语法

  • weak_ptr 的创建方式
1
2
3
// 直接使用 shared_ptr 来初始化 weak_ptr
shared_ptr<int> sp = make_shared<int>(100);
weak_ptr<int> wp(sp);
1
2
3
4
// 或者将 shared_ptr 赋值给 weak_ptr
shared_ptr<int> sp = make_shared<int>(100);
weak_ptr<int> wp;
wp = sp;
1
2
3
4
5
// 或者将 weak_ptr 赋值给 weak_ptr
shared_ptr<int> sp = make_shared<int>(100);
weak_ptr<int> wp(sp);
weak_ptr<int> wp2;
wp2 = wp;

weak_ptr 的常用操作

lock()
  • lock():作用是检查 weak_ptr 所指向的对象是否存在
    • 如果存在,则 lock() 会返回一个指向该对象的 shared_ptr(即所指向对象的强引用计数会加一)
    • 如果不存在,则 lock() 会返回一个空的 shared_ptr(空指针)
1
2
3
4
5
6
7
8
9
shared_ptr<int> sp = make_shared<int>(100);
weak_ptr<int> wp(sp);

shared_ptr<int> sp2 = wp.lock();
if (sp2 != nullptr) {
cout << "object value is " << *sp2 << endl;
} else {
cout << "object not exist" << endl;
}

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

1
object value is 100

1
2
3
4
5
6
7
8
9
10
11
12
shared_ptr<int> sp = make_shared<int>(100);
weak_ptr<int> wp(sp);

// 释放 shared_ptr 所管理的对象
sp.reset();

shared_ptr<int> sp2 = wp.lock();
if (sp2 != nullptr) {
cout << "object value is " << *sp2 << endl;
} else {
cout << "object not exist" << endl;
}

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

1
object not exist
use_count()
  • use_count():返回有多少个 shared_ptr 指向某个对象(即获取与该 weak_ptr 一起共享对象的其他 shared_ptr 的数量),主要用于代码调试目的
1
2
3
4
shared_ptr<int> sp = make_shared<int>(100);
weak_ptr<int> wp(sp);
int count = wp.use_count();
cout << count << endl; // 输出 1
1
2
3
4
5
6
7
8
shared_ptr<int> sp = make_shared<int>(100);
weak_ptr<int> wp(sp);

// 释放 shared_ptr 所管理的对象
sp.reset();

int count = wp.use_count();
cout << count << endl; // 输出 0
expired()
  • expired():监视的对象是否已过期
    • 已过期是指 weak_ptruse_count() 返回 0(表示该弱智能指针所指向的对象已经不存在)
1
2
3
4
shared_ptr<int> sp = make_shared<int>(100);
weak_ptr<int> wp(sp);
bool expired = wp.expired();
cout << expired << endl; // 输出 0,表示 false(未过期)
1
2
3
4
5
6
7
8
shared_ptr<int> sp = make_shared<int>(100);
weak_ptr<int> wp(sp);

// 释放 shared_ptr 所管理的对象
sp.reset();

bool expired = wp.expired();
cout << expired << endl; // 输出 1,表示 true(已过期)
reset()
  • reset():将 weak_ptr 设置为空指针,不影响所指向对象的强引用计数(即 shared_ptr 所指向对象的引用计数),但指向该对象的弱引用计数会减少一
1
2
3
shared_ptr<int> sp = make_shared<int>(100);
weak_ptr<int> wp(sp);
wp.reset();

weak_ptr 的指针大小

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

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

unique_ptr 智能指针

C++ 中的 unique_ptr 是一种独占所有权的智能指针,同一时刻只能有一个 unique_ptr 指向某个对象;当该 unique_ptr 被销毁时,它所指的对象也会随之自动销毁。

unique_ptr 的使用语法

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

unique_ptr 的错误操作

在 C++ 中,unique_ptr 是独占式的,不支持拷贝操作(包括拷贝构造函数和拷贝赋值运算符)。

1
2
3
4
5
6
unique_ptr<int> up1(new int(100));
unique_ptr<int> up2(up1); // 错误写法(编译失败),unique_ptr 是独占式的,不支持拷贝构造函数
unique_ptr<int> up2 = up1; // 错误写法(编译失败),unique_ptr 是独占式的,不支持拷贝构造函数

unique_ptr<int> up3;
up3 = up1; // 错误写法(编译失败),unique_ptr 是独占式的,不支持拷贝赋值运算符

unique_ptr 的常用操作

get()
  • get():返回 unique_ptr 存储的裸指针(通过 new 得到的原始指针)
    • unique_ptr 返回的裸指针,千万不要手动 delete,否则会导致程序运行崩溃(未定义行为)
    • 注意:如果 unique_ptr 释放了所指向对象的内存,那么 get() 返回的裸指针也会变得无效
1
2
3
4
unique_ptr<int> up(new int(100));
int* p = up.get();
*p = 50;
cout << *p << endl; // 输出 50
swap()
  • swap():交换两个智能指针所指向的对象
1
2
3
unique_ptr<int> up1(new int(100));
unique_ptr<int> up2(new int(200));
up1.swap(up2); // 交换两个智能指针指向的对象
release()

unique_ptrrelease() 函数用于放弃智能指针对其所指向对象的控制权,切断两者之间的联系。该函数调用后会返回原始的裸指针,同时会将 unique_ptr 自身置为空指针;返回的裸指针需要由程序员手动管理,可以对其执行 delete 释放内存,也可以将其用来初始化另一个智能指针,或者给另一个智能指针赋值。特别注意的是,如果调用 release() 后不对返回的裸指针进行任何处理(既不 delete,也不交给另一个智能指针管理),则原本由 unique_ptr 管理的对象将再也无法被自动释放内存,从而导致内存泄漏。

1
2
3
unique_ptr<int> up1(new int(100));
int* p = up1.release(); // 释放完成后,up1 置为空指针
unique_ptr<int> up2(p); // up2 指向 up1 原来所指向的对象,不会发生内存泄漏
reset()
  • reset():用于改变或释放 unique_ptr 所管理的对象
    • reset() 不带参数:
      • 释放当前 unique_ptr 所指向对象的内存,然后将当前 unique_ptr 置为空指针
    • reset(newPtr) 带参数:
      • 释放当前 unique_ptr 所指向对象的内存,然后将当前 unique_ptr 指向新对象(newPtr
      • 注意:参数 newPtr 是裸指针(通过 new 得到的原始指针),另外还有两个参数的重载版本 reset(newPtr, deleter),用于替换并指定自定义删除器
    • 如果 reset() 抛出异常(如自定义删除器在释放内存时抛出异常),原对象保持原样(强异常保证)
1
2
3
4
5
6
7
8
// 情况一(不带参数)
unique_ptr<int> up1(new int(100));
up1.reset(); // up1 所指向对象的内存会被释放,up1 置为空指针

// 情况二(带参数)
unique_ptr<int> up2(new int(100));
int* p = new int(200);
up2.reset(p); // up2 原来所指向对象的内存会被释放,up2 指向最新的对象 p
= nullptr
  • = nullptr 会释放 unique_ptr 所指向对象的内存,并将 unique_ptr 置为空指针。
1
2
3
4
void test04() {
unique_ptr<int> up(new int(100));
up = nullptr; // up 所指向对象的内存会被释放,up 置为空指针
}
* 解引用
  • 通过 * 获取 unique_ptr 所指向的对象
1
2
unique_ptr<int> sp(new int(100));
cout << *sp << endl; // 输出 100
移动语义

unique_ptr 支持移动语义,可以通过移动构造函数或移动赋值运算符将所有权从一个 unique_ptr 转移到另一个 unique_ptr。当 unique_ptr 移动后,原来的 unique_ptr 会变为空指针,不再指向任何任何对象。

1
2
unique_ptr<int> up1(new int(100));
unique_ptr<int> up2 = move(up1); // 移动完成后,up1 置为空指针,up2 指向 up1 原来所指向的对象
作为判断条件
  • 智能指针(如 unique_ptr)重载了 operator bool,因此它可以被直接用作条件判断:当智能指针指向一个有效对象时,条件为 true;当它为空(即未指向任何对象)时,条件为 false
1
2
3
4
5
6
7
8
9
10
11
unique_ptr<int> up(new int(100));
if (up != nullptr) {
cout << "not nullptr" << endl; // 执行输出
}

up.reset();
if (up != nullptr) {
cout << "not nullptr" << endl;
} else {
cout << "nullptr" << endl; // 执行输出
}
返回 unique_ptr 类型
  • 虽然 unique_ptr 是独占式的,不支持拷贝操作(包括拷贝构造函数和拷贝赋值运算符);但是,当 unique_ptr 即将被销毁的时候,它是可以拷贝的。最常见的用法就是从函数返回一个 unique_ptr
1
2
3
4
5
6
7
8
9
10
11
unique_ptr<int> func() {
unique_ptr<int> up(new int(100));
return up; // 返回一个局部对象,编译器会生成一个临时对象,并调用 unique_ptr 的移动构造函数
}

int main() {
unique_ptr<int> up = func(); // 接收函数返回的 unique_ptr,调用 unique_ptr 的拷贝构造函数

unique_ptr<int> up2; // 接收函数返回的 unique_ptr
up2 = func(); // 调用 unique_ptr 的赋值运算符
}
转换为 shared_ptr 类型
  • 如果 unique_ptr 是右值,可以将其转换为 shared_ptr 类型。shared_ptr 提供了一个显式构造函数来接收右值 unique_ptr,并接管原来归 unique_ptr 管理的对象。

  • (1) 代码案例一

1
2
3
4
5
6
7
8
unique_ptr<int> func() {
return unique_ptr<int>(new int(100)); // 返回一个右值(临时对象都是右值)
}

int main() {
shared_ptr<int> sp = func(); // 转换为 shared_ptr 类型,这里会额外创建一个控制块(用于存储引用计数、删除器等)
return 0;
}
  • (2) 代码案例二
1
2
3
4
5
int main() {
unique_ptr<int> up(new int(100)); // 左值
shared_ptr<int> sp = move(up); // 将左值转换为右值
// 移动完成后,up 置为空指针,sp 指向 up 原来所指向的对象
}

unique_ptr 的删除器

自定义删除器
  • unique_ptr 会在一定的时机自动删除所指向的对象,并且底层使用 delete 操作符作为默认的资源析构方式。
  • unique_ptr 还支持指定自定义的删除器来取代 C++ 编译器提供的默认删除器,语法格式为:unique_ptr<对象类型, 删除器类型> 智能指针变量名
  • unique_ptr 的删除器可以是任何可调用对象,例如普通函数、重载了 operator() 的类,或者 Lambda 表达式。

第一种写法(使用普通函数自定义删除器)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义普通函数
void myDeleter(int *p) {
delete p;
p = nullptr;
cout << "delete int *" << endl;
}

int main() {
// 使用 typedef 定义一个函数指针类型,类型名称是 func
typedef void (*func)(int *);
unique_ptr<int, func> up(new int(100), myDeleter);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义普通函数
void myDeleter(int *p) {
delete p;
p = nullptr;
cout << "delete int *" << endl;
}

int main() {
// 或者使用 using 定义一个函数指针类型,类型名称是 func
using func = void (*)(int *);
unique_ptr<int, func> up(new int(100), myDeleter);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
// 定义普通函数
void myDeleter(int *p) {
delete p;
p = nullptr;
cout << "delete int *" << endl;
}

int main() {
// 或者直接省去函数指针类型的定义
unique_ptr<int, void (*)(int *)> up(new int(100), myDeleter);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义普通函数
void myDeleter(int *p) {
delete p;
p = nullptr;
cout << "delete int *" << endl;
}

int main() {
// 或者配合使用 typedef 和 decltype
typedef decltype(myDeleter) *func;
unique_ptr<int, func> up(new int(100), myDeleter);

// 或者配合使用 using 和 decltype
using func2 = decltype(myDeleter) *;
unique_ptr<int, func2> up2(new int(100), myDeleter);

// 或者直接使用 decltype 获取具体的类型
unique_ptr<int, decltype(myDeleter) *> up3(new int(100), myDeleter);
return 0;
}

第二种写法(使用 Lambda 表达式自定义删除器)

1
2
3
4
5
6
7
8
9
10
11
12
// 定义 Lambda 表达式
auto myDelLambda = [](int *p) {
delete p;
p = nullptr;
cout << "delete int *" << endl;
};

int main() {
// 配合使用 decltype 和 Lambda 表达式
unique_ptr<int, decltype(myDelLambda)> up(new int(100), myDelLambda);
return 0;
}
1
2
3
4
5
6
7
8
9
int main() {
// 或者直接使用 Lambda 表达式
unique_ptr<int, void (*)(int *)> up(new int(100), [](int *p) {
delete p;
p = nullptr;
cout << "delete int *" << endl;
});
return 0;
}
使用数组的问题

如果 unique_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() {
// 如果 unique_ptr 指向的是数组,那么就需要自定义删除器,否则不会使用 delete [] 正确释放数组的内存
unique_ptr<MyClass[], void (*)(MyClass *)> sp2(new MyClass[3], [](MyClass *p) {
delete[] p;
cout << "delete MyClass []" << endl;
});
}

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

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

还可以使用数组特化版本 unique_ptr<T[]>,同样可以正确释放数组内存(从 C++ 17 开始支持以下写法

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() {
// 从 C++ 17 开始正式支持以下写法,使用数组特化版本 unique_ptr<T[]>,可以正确释放数组内存
unique_ptr<MyClass[]> up(new MyClass[3]);
}

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

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

还可以使用 std::default_delete 来做删除器,std::default_delete 是标准库(STL)里的模板类,同样可以正确释放数组的内存(从 C++ 17 开始支持以下写法

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() {
// 如果 unique_ptr 指向的是数组,还可以使用 default_delete 来正确释放数组的内存
unique_ptr<MyClass[], default_delete<MyClass[]>> up(new MyClass[3]);
}

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

1
2
3
~MyClass()
~MyClass()
~MyClass()
不是同一种类型的说明
  • 如果两个 shared_ptr 指定了不同的删除器,只要它们所指向的对象类型是相同的,那么这两个 shared_ptr 也属于同一种类型。这就代表只要类型相同,就可以将它们放入到元素类型为该对象类型的容器里面。
  • 但是,对于 unique_ptr 来说,指定 unique_ptr 的删除器后,会影响 unique_ptr 的类型。
  • 简而言之,如果两个 unique_ptr 的删除器类型不同,那么即使它们指向的对象类型相同,这两个 unique_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;
};

unique_ptr<int, decltype(lambda1)> up1(new int(100), lambda1);

unique_ptr<int, decltype(lambda2)> up2(new int(200), lambda2);

vector<unique_ptr<int, decltype(lambda1)>> vec;
vec.emplace_back(move(up1));
// vec.emplace_back(move(up2)); // up1 与 up2 的删除器类型不相同,两者不属于同一种类型,不可以放入同一个容器中

return 0;
}

unique_ptr 的性能分析

unique_ptr 的大小

在 C++ 中,unique_ptr 的大小通常与裸指针(通过 new 得到的指针)相同。但是,如果 unique_ptr 指定了自定义删除器(尤其是带有状态的删除器,如函数对象或 Lambda 表达式捕获了变量),则其大小可能超过裸指针的大小。

  • 默认删除器(std::default_delete)无状态,通常不增加额外大小。
  • 无捕获的 Lambda 表达式作为删除器时,由于继承空基类优化(EBO),也通常不增加大小。
  • 使用函数指针作为删除器时,会额外占用一个指针的大小。
  • 有捕获的 Lambda 表达式或自定义类删除器带有成员变量时,会增加相应大小。
1
2
3
4
5
6
int main() {
int *p;
unique_ptr<int> up;
int length1 = sizeof(p); // 8 个字节(64 位系统)
int length2 = sizeof(up); // 8 个字节(64 位系统)
}
1
2
3
4
5
6
7
8
9
10
11
void myDeleter(int *p) {
delete p;
p = nullptr;
cout << "delete int *" << endl;
}

int main() {
unique_ptr<int, void (*)(int *)> up3(new int(100), myDeleter);
int length = sizeof(up3);
cout << length << endl; // 16 个字节(64位系统)
}
unique_ptr 的选择建议
  • 如果程序需要多个指针共享同一个对象,应选择 shared_ptr
  • 如果程序不需要多个指针共享同一个对象,应优先使用 unique_ptr,性能更高。

auto_ptr 智能指针

auto_ptr 为什么会被废弃

auto_ptr 是 C++ 98 时代的智能指针,其被废弃的主要原因是:它使用拷贝语义实现移动所有权,导致拷贝后原指针变为空,违反了常规拷贝的直觉,极易引发运行时错误。C++ 11 引入移动语义后,由 unique_ptr 安全替代 auto_ptr

1
2
auto_ptr<int> ap1(new int(100));
auto_ptr<int> ap2 = ap1; // ap1 置为空指针,ap2 指向 ap1 原来所指向的对象