智能指针
[TOC]
1 什么是智能指针
C++ 没有垃圾回收机制,需要程序员自己释放和分配内存,否则就会照成内存泄漏。
智能指针是指向动态对象的指针,当其应该被释放时,智能指针可以确保自动释放内存,不需要手动释放,避免内存泄漏问题,更加容易也更加安全地使用动态内存。
智能指针的本质是类模版,当智能指针所指向的对象使用完后,对象会自动调用析构函数去释放指针所指向的空间。
以下是智能指针基本框架,所有智能指针类模版都包含一个对象指针、构造函数、析构函数:1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;
template <typename T>
class SmartPtr {
public:
SmartPtr(T* _ptr) :ptr(_ptr) {} //构造函数
~SmartPtr()
{ //析构函数
if (ptr != nullptr)
{
cout << "smartprt: delete" << endl;
delete ptr;
ptr = nullptr;
}
}
private:
T* ptr; //指针对象
};
int main(int argc, char* argv[])
{
SmartPtr<int> prt_int(new int(1)); //指向int类型的智能指针
SmartPtr<string> prt_string(new string("abc")); //指向string类型的智能指针
return 0;
}
2 智能指针的类型
现行可用的智能指针包含三种:unique_ptr、shared_ptr、weak_ptr,以下依次介绍了这三种智能指针的大概原理和基本用法。
还有一种 auto_ptr,他是 C++98 的智能指针,C++11 已抛弃,故本文中不做介绍。
我在 VS2022 中新建了一个控制台程序,可以直接调用上述指针。但是若提示报错的话,就需要 #include <memory> 。
2.1 unique_ptr
注意 unique_ptr 是独占对象的所有权的,它不允许其他的智能指针共享其内部的指针。就是在某个时候一定只有一个 unique_ptr 指向一个特定的对象,当 unique_prt 被销毁的时候它所指向的对象也会被销毁2。
unique_ptr 直接禁用了拷贝构造函数和赋值构造函数1。可以通过它的构造函数初始化一个独占智能指针对象,但是不允许通过赋值将一个 unique_ptr 赋值给另一个 unique_ptr3。
能使用 unique_ptr 时就不要使用 share_ptr 指针(后者需要保证线程安全,所以在赋值或销毁时 overhead 开销更高)4。
(1)构造方式
1
2
3
4
5
6
7
std::unique_ptr<Entity> e1 = new Entity(); //不合法,指针是不可转让的
std::unique_ptr<Entity> e1(new Entity()); //OK
std::unique_ptr<Entity> e1 = std::make_unique<Entity>(); //首选
auto e1 = std::make_unique<Entity>(); //首选
std::unique_ptr<Entity> e2 = e1; //不合法,指针是不能复制的
std::unique_ptr<Entity> e2 = std::move(e1); //可移动,所有权转移
func(std::move(e1)); //这样函数传参:所有权转移
(2)其他成员函数
1
2
3
4
5
6
7
8
9
10
11
12
13
//通过函数返回的方式初始化
unique_ptr<int> SmartPtr = func();
//解除对原始内存的管理
SmartPtr.reset();
//重新指定智能指针管理的原始内存
SmartPtr.reset(new int(2));
//get()方法可以获取智能智能指针管理的原始地址
cout << "ptr addr = " << SmartPtr.get() << endl;
cout << "ptr content = " << *SmartPtr.get() << endl;
//放弃对指针的控制权返回指针,并将自身置为空。
//共享指针释放,内存不释放:
SmartPtr.release();
SmartPtr = nullptr;
(3)使用方法
可通过智能指针直接调用类内的成员函数。也可以通过 get() 函数取得原始资源的指针,再通过该指针进行调用。
在参考文章5中有用法示例的代码。
2.2 shared_ptr
shared_ptr 实现了共享拥有的概念,利用 “引用计数” 来控制堆上对象的生命周期。允许多个 shared_ptr 指向同一块资源,并且保证共享资源只会被释放一次,所以程序不会崩溃1。
原理:在初始化的时候引用计数设为 1,每当被拷贝或者赋值的时候引用计数 +1,析构的时候引用计数 -1,直到引用计数被减到 0,那么就可以 delete 掉对象的指针了。
每个 shared_ptr 都有两个指针,一个原始指针,一个计数区域的指针(SharedPtrControlBlock)5。
(1)提供的函数
构造 shared_ptr 的方法:
1
2
3
4
5
6
7
8
9
std::shared_ptr<Entity> e1(new Entity()); //OK
std::shared_ptr<Entity> e1 = std::make_shared<Entity>(); //首选
auto e1 = std::make_shared<Entity>(); //首选
std::shared_ptr<Entity> e2 = e1; //可复制,计数+1
std::shared_ptr<Entity> e2 = std::move(e1); //可移动,计数不变
e2.reset(); //释放托管对象的所有权(若有),计数-1
e2.reset(new Entity()); //用新的指针代替原先托管的对象
func(std::move(e1)); //这样函数传参:计数不变
func(e1); //这样函数传参:计数+1
创建新的 shared_ptr 对象的最佳方法是使用 std :: make_shared,因为 std::make_shared 一次性为 int 对象和用于引用计数的数据都分配了内存,最为高效,而 new 操作符只是为 int 分配了内存6。
详细解释下为什么使用 make_shared 的创建方式会更加的高效了,在我们是使用 new 的方法去初始化一个 shared_ptr 的时候,我们需要先在堆上申请分配一块内存,用来存储对象,然后用这个变量调用 share_ptr 的构造函数,再次分配一次内存,而使用 make_shared 就只需要分配一次内存就可以了2。
关于智能指针调用 reset 的初始化方式,这个函数在智能指针没有值的时候调用是用来初始化的,当这个智能指针有值的时候,调用 reset 函数就会引起原有对象智能指针的引用计数 -12。
shared_ptr 的其他成员函数:
1
2
3
4
5
6
use_count() //返回引用计数的个数
unique() //返回是否是独占所有权(use_count是否为1)
swap() //交换两个shared_ptr对象(即交换所拥有的对象,引用计数也随之交换)
reset() //放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
get() //返回存储的指针。
//存储的指针指向shared_ptr对象解引用的对象,一般与其拥有的指针相同
(2)使用方法
1
2
3
4
5
6
std::shared_ptr<entity> e = std::make_shared<entity>();
//使用方法一,取得原始资源进行使用:
entity * t = e.get();
t->func(); //func()是entity类内的成员函数
//使用方法二,可通过智能指针直接调用类内的成员函数
e->func(); //func()是entity类内的成员函数
一定要谨慎的使用 get() 函数,当我们用一个裸指针上面的 (entity*) 来保存地址的时候,我们没有办法掌握ptr会在什么时候释放这块内存地址,这样我们在使用的过程中就会产生不可预知的错误。如果我们获取了这个裸指针,一不小心调用了 delete,就会导致同一块内存地址析构了两次。如果我们用一个 shared_ptr 来保存这个地址,那么我们就相当于又有了一个从 1 开始计数的 shared_ptr 与 ptr 都指向这块内存,最终的结果就是调用两次析构2。
在参考文章5中有用法示例的代码。
(3)其他
使用 make_shared 的优势和劣势,见参考文章5。
2.3 weak_ptr
weak_ptr 不共享指针,不能操作资源,它的构造不会增加引用计数,析构也不会减少引用计数,它的主要作用是监测 shared_ptr 所管理的资源是否存在1。
C++11 标准虽然将 weak_ptr 定位为智能指针的一种,但该类型指针通常不单独使用(没有实际用处),只能和 shared_ptr 类型指针搭配使用。甚至可以说,weak_ptr 是为了辅助 shared_ptr 的存在5,它不管理 shared_ptr 内部的指针,借助 weak_ptr 类型指针,我们可以获取 shared_ptr 指针的一些状态信息,比如有多少指向相同的 shared_ptr 指针、shared_ptr 指针指向的堆内存是否已经被释放等等7。
需要注意的是,当 weak_ptr 类型指针的指向和某一 shared_ptr 指针相同时,weak_ptr 指针并不会使所指堆内存的引用计数加 1;同样,当 weak_ptr 指针被释放时,之前所指堆内存的引用计数也不会因此而减 1。也就是说,weak_ptr 类型指针并不会影响所指堆内存空间的引用计数7。
除此之外,weak_ptr<T> 模板类中没有重载 * 和 -> 运算符,因为他不共享指针,不能操作资源,只能访问所指的堆内存,而无法修改它7,所以它的构造不会增加引用计数,析构也不会减少引用计数,它的主要作用就是作为一个旁观者监视 shared_ptr 中管理的资源是否存在3。
利用 weak_ptr 可以解决 shared_ptr 的一些问题:
(1)函数
构造方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//创建一个空weak_ptr指针:
std::weak_ptr<int> wp1;
//凭借已有weak_ptr指针,创建一个新的weak_ptr指针:
std::weak_ptr<int> wp2(wp1);
//若 wp1 为空指针,则 wp2 也为空指针;
//反之,如果 wp1 指向某一 shared_ptr 指针拥有的堆内存,
//则 wp2 也指向该块存储空间(可以访问,但无所有权)。
//weak_ptr 指针更常用于指向某一 shared_ptr 指针拥有的堆内存,
//因为在构建 weak_ptr 指针对象时,
//可以利用已有的 shared_ptr 指针为其初始化。例如:
std::shared_ptr<int> sp(new int);
std::weak_ptr<int> wp3(sp);
//由此,wp3 指针和 sp 指针有相同的指针。
//再次强调,weak_ptr 类型指针不会导致堆内存空间的引用计数增加或减少。
其他常用的成员方法及各自功能见下面的表格7。
| 成员方法 | 功能 |
|---|---|
| operator=() | 重载 = 赋值运算符, weak_ptr 指针可以直接被 weak_ptr 或者 shared_ptr 类型指针赋值。 |
| swap(x) | 其中 x 表示一个同类型的 weak_ptr 类型指针,该函数可以互换 2 个同类型 weak_ptr 指针的内容。 |
| reset() | 将当前weak_ptr指针置为空指针(清空对象,使其不监测任何资源)。 |
| use_count() | 查看指向和当前 weak_ptr 指针相同的 shared_ptr 指针的数量。 |
| expired() | 判断当前 weak_ptr 指针为否过期,判断观测的资源是否已经被释放。 |
| lock() | 获取管理所监测资源的 shared_ptr 对象。如果当前 weak_ptr 已经过期,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针。 |
| operator=() | 重载 = 赋值运算符,是的 weak_ptr 指针可以直接被 weak_ptr 或者 shared_ptr 类型指针赋值。 |
(2)使用方法
在参考文章5中有用法示例的代码。
2.4 auto_ptr
auto_ptr 是管理权转移的思想,即原对象拷贝给新对象,原对象就会被设置成 nullptr。此时就只有新对象指向资源空间。如果此时再去调用原对象,那整个程序会崩溃,所以现在很多公司禁用 auto_ptr1。不仅如此,在 C++11 中,该智能指针已被弃用。
注意:不要使用 std::auto_ptr5。
2.5 小结
| 类型 | 描述 |
|---|---|
| unique_ptr | 独占所指向的对象。同一时间只有一个智能指针能指向该对象,禁止指针的拷贝。 |
| shared_ptr | 共享指针,强引用。允许多个指针指向同一个对象,使用计数机制记录被共享指针数,对象与资源在最后一个引用被销毁时释放。 |
| weak_ptr | 弱引用,不共享指针、不能操作资源。监视shared_ptr所管理的对象,进行对象内存管理的是 shared_ptr。 |
| auto_ptr | 【已弃用】采用所有权模式,可以剥夺所有权,即当对象拷贝或者赋值后,前面的对象就悬空了。 |
(1)从指针与对象的对应关系进行分类4:
一种是可以使用多个智能指针管理同一块内存区域,每增加一个智能指针,就会增加 1 次引用计数。shared_ptr 属于这种。
另一类是不能使用多个智能指针管理同一块内存区域,通俗来说,当智能指针 2 来管理这一块内存时,原先管理这一块内存的智能指针 1 只能释放对这一块指针的所有权。auto_ptr、unique_ptr、weak_ptr 属于这种。
(2)从是否引用计数的角度进行分类8:
-
不带引用计数的智能指针:
auto_ptr:不推荐使用,且 C++11 标准中被抛弃。scoped_ptr:不支持拷贝构造,和赋值重载,实现到私有权限,无法访问,后期 g++ 10 没有该对象了。unique_ptr:推荐使用 -
带引用计数:
多个智能指针可以管理同一个资源,每个对象资源匹配一个引用计数,给一个资源做赋值或者拷贝构造的时候,引用计数加 1。一个资源出了它的作用域之后,引用计数减 1,如果引用计数 count == 0,资源就释放了。
shared_ptr:强智能指针,可以改变资源的引用计数。weak_ptr:弱智能指针,不会改变资源的引用计数。定义对象的时候,用强智能指针;引用对象的地方使用弱指针,注意:弱智能指针是无法调用对象的成员的,它只是一个观察的作用,可以在使用的时候升级为强指针8:
std::shared_ptr<A> ps = _ptra.lock();
3 删除器
智能指针初始化的时候可以指定删除动作,这个删除操作对应的函数是删除器。删除器本质是一个回调函数,我们只需进行实现,其调用是由智能指针完成。
在 c++11 中,当智能指针指向数组时,需要指定删除器,因为默认删除器不支持数组对象。自定义删除器可以是函数指针、仿函数、lambda、包装器1。
当我们用 shared_ptr 管理数组的时候,一定要指定删除器2。
unique_ptr 指定删除器与 shared_ptr 的不同,unique_ptr 指定删除器的时候,需要确定删除器的类型2。
4 std::enable_shared_from_this
std::enable_shared_from_this 是一个模板类,它在标准库(STL)中提供,用于 让类对象能安全地生成自身的 std::shared_ptr 实例。9
当你希望 类的实例能够从内部成员函数中获取指向自身的 std::shared_ptr 时,这个特性非常有用。为了这样做,该类必须继承自 std::enable_shared_from_this。
下面是一个简单的例子,展示了如何用 std::enable_shared_from_this:
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
#include <memory>
#include <iostream>
class MyClass : public std::enable_shared_from_this<MyClass>
{
public:
std::shared_ptr<MyClass> get_shared() {
return shared_from_this();
}
void do_something() {
// 在这里,你可以安全地使用 shared_from_this
std::shared_ptr<MyClass> sharedPtr = get_shared();
// ...
}
~MyClass() {
std::cout << "MyClass destroyed" << std::endl;
}
};
int main() {
std::shared_ptr<MyClass> myInstance = std::make_shared<MyClass>();
myInstance->do_something();
return 0;
}
在这个例子中,MyClass 继承自 std::enable_shared_from_this<MyClass>。这允许 MyClass 的实例在成员函数do_something 中安全地调用 get_shared 方法, get_shared 会返回一个新的 std::shared_ptr<MyClass>,指向调用方法的对象实例。这是安全的,因为 myInstance 已经是通过 std::make_shared 创建的 std::shared_ptr。
请注意,为了能够安全地使用 shared_from_this,对象必须已经被 std::shared_ptr 管理。如果对象不是由 std::shared_ptr 创建的,调用 shared_from_this 将会抛出一个 std::bad_weak_ptr 异常。
5 小结
当你需要一个独占资源所有权(访问权+生命控制权)的指针,且不允许任何外界访问,使用 std::unique_ptr;
当需要一个共享资源所有权(访问权+生命控制权)的指针,使用 std::shared_ptr;
当需要一个能访问资源,但不控制其生命周期的指针,请使用 std::weak_ptr
推荐用法5:一个 shared_ptr 和 n 个 weak_ptr 搭配使用 而不是 n 个 shared_ptr,因为一般模型中,最好总是被一个指针控制生命周期,然后可以被 n 个指针控制访问。这样从逻辑上看,大部分模型的生命在直观上总是受某一样东西直接控制而不是多样东西共同控制。从程序上看,也能够完全避免生命周期互相控制引发的循环引用问题。
6 一些智能指针相关的问题
6.1 智能指针怎么解决交叉引用造成的内存泄漏
- 循环引用的本质:多个对象通过
shared_ptr互相引用,形成引用环,导致引用计数无法降到 0,析构函数不执行,引发内存泄漏; - 核心解决方法:将循环引用链中至少一方的
shared_ptr替换为weak_ptr(弱引用不增加计数,打破引用环); - weak_ptr 使用要点:需通过
lock()获取shared_ptr才能访问对象,且lock()会检查对象是否存活,避免野指针。
简单来说:shared_ptr的循环引用是 “计数降不到 0”,weak_ptr是 “不参与计数,打破循环”。
什么叫交叉引用
当 两个或多个对象 通过shared_ptr互相持有对方的引用,形成一个 “引用环” 时,就会发生循环引用。此时每个对象的引用计数都至少为 1(因为互相引用),导致它们的引用计数永远无法降到 0,析构函数不会被调用,最终造成内存泄漏。
代码示例:下面的代码模拟了两个Node对象互相持有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
26
27
28
29
30
31
32
33
#include <iostream>
#include <memory>
class Node {
public:
std::shared_ptr<Node> peer; // 用shared_ptr引用另一个Node
Node() {
std::cout << "Node 构造函数执行" << std::endl;
}
~Node() {
std::cout << "Node 析构函数执行" << std::endl;
}
};
int main() {
// 创建两个Node对象,互相引用
std::shared_ptr<Node> node1 = std::make_shared<Node>();
std::shared_ptr<Node> node2 = std::make_shared<Node>();
// 循环引用:node1持有node2的引用,node2持有node1的引用
node1->peer = node2;
node2->peer = node1;
// 此时:
// node1的引用计数 = 2(main中的node1 + node2->peer)
// node2的引用计数 = 2(main中的node2 + node1->peer)
// main函数结束时,node1和node2被销毁,引用计数各减1 → 变为1
// 但因为互相引用,引用计数无法降到0,析构函数不会执行!
return 0;
}
如何避免
核心解决方案是:将循环引用链中的一方(或多方)的shared_ptr替换为weak_ptr。
weak_ptr 的关键特性:
weak_ptr是 “弱引用”,指向shared_ptr管理的对象,但不会增加引用计数;weak_ptr不能直接访问对象,需要通过lock()方法获取shared_ptr(若对象已销毁,lock()返回空的shared_ptr);- 正因为不增加引用计数,能打破循环引用的 “环”,让引用计数正常降到 0。
修复后的代码示例:只需将其中一个shared_ptr改为weak_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
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <memory>
class Node {
public:
std::weak_ptr<Node> peer; // 把一方改为weak_ptr,不增加引用计数
Node() {
std::cout << "Node 构造函数执行" << std::endl;
}
~Node() {
std::cout << "Node 析构函数执行" << std::endl;
}
};
int main() {
std::shared_ptr<Node> node1 = std::make_shared<Node>();
std::shared_ptr<Node> node2 = std::make_shared<Node>();
// 循环引用被打破:
// node1->peer是weak_ptr,指向node2但不增加其引用计数;
// node2->peer(如果还是shared_ptr)指向node1,增加其引用计数,但这里我们改了一方就够
node1->peer = node2; // weak_ptr接收shared_ptr,引用计数不变
node2->peer = node1; // 同理
// 此时:
// node1的引用计数 = 1(仅main中的node1)
// node2的引用计数 = 1(仅main中的node2)
// main函数结束时,node1和node2被销毁,引用计数各减1 → 变为0
// 析构函数正常执行,内存释放!
return 0;
}
运行结果:
1
2
3
4
Node 构造函数执行
Node 构造函数执行
Node 析构函数执行
Node 析构函数执行
可以看到,析构函数正常执行,内存泄漏问题解决。
访问 weak_ptr 指向的对象
如果需要通过weak_ptr访问对象,必须先调用lock()获取shared_ptr(确保对象未被销毁):
1
2
3
4
5
6
7
8
9
10
// 补充:在Node类中添加访问peer的方法
void access_peer() {
// lock()返回shared_ptr:若对象存在,引用计数+1;若已销毁,返回空
std::shared_ptr<Node> p = peer.lock();
if (p) {
std::cout << "成功访问peer对象" << std::endl;
} else {
std::cout << "peer对象已销毁" << std::endl;
}
}
其他辅助避免方式
- 设计层面:尽量避免对象之间的双向引用,比如用单向引用代替双向引用;
- 手动打破循环:在合适的时机(比如对象不再需要互相引用时),将其中一方的
shared_ptr置为nullptr,主动打破循环(但不如weak_ptr优雅)。
6.2 不要混合使用智能指针和裸指针
在同一个项目中坚持只使用智能指针,不使用裸指针,否则会出现忘记 delete 裸指针的情况。另外需要注意的事项还会很多,很复杂10。
6.3 不使用 get() 函数初始化或 reset 另外的智能指针4。
6.4 不要使用同一个原始指针构造 shared_ptr6
创建多个 shared_ptr 的正常方法是使用一个已存在的 shared_ptr 进行创建,而不是使用同一个原始指针进行创建。
假如使用原始指针 num 创建了 p1,又同样方法创建了 p3,当 p1 超出作用域时会调用 delete 释放 num 内存,此时 num 成了悬空指针,当 p3 超出作用域再次 delete 的时候就可能会出错6。
6.5 不要用栈中的指针构造 shared_ptr 对象6
shared_ptr 默认的构造函数中使用的是 delete 来删除关联的指针,所以构造的时候也必须使用 new 出来的堆空间的指针。
当 shared_ptr 对象超出作用域调用析构函数 delete 栈上对象的指针时会出错。
6.6 shared_ptr 和 unique_ptr 哪个好
std::shared_ptr 和 std::unique_ptr 都有它们自己的使用场景,不能简单地说哪一个更好。它们提供了不同类型的内存管理策略,适用于不同的情况。9
std::unique_ptr:
- 是 C++11 引入的智能指针,代表了对某个资源的独占所有权。
- 当
std::unique_ptr被销毁时,它所拥有的资源也会被自动释放。 std::unique_ptr尺寸小,通常与原生指针一样,没有额外的内存开销。- 不支持拷贝操作,但可以通过
std::move进行所有权的转移。 - 适合用于确保资源在任何时候只有一个拥有者的情况。
std::shared_ptr:
- 是 C++11 引入的另一种智能指针,它允许多个指针实例共享对同一个对象的所有权。
std::shared_ptr使用引用计数机制来跟踪有多少个std::shared_ptr实例拥有共同的对象。- 当最后一个拥有对象的
std::shared_ptr被销毁时,关联的资源会被自动释放。 std::shared_ptr有额外的内存和性能开销,因为它需要维护引用计数。- 适合用于多个所有者需要管理同一个资源的情况。
选择哪一个取决于你的具体需求:
- 如果你需要独占所有权模型,或者是为了避免额外的性能开销,
std::unique_ptr是更好的选择。 - 如果你需要多个所有者共享同一个资源,并且不介意额外的内存和性能开销,那么
std::shared_ptr是更合适的。
在不需要共享所有权的情况下,推荐的做法是默认使用 std::unique_ptr,因为它更轻量级,并且通过明确单一所有权有助于减少错误。只有在确实需要多个引用共享所有权时,才选用 std::shared_ptr。
6.7 智能指针是线程安全的吗?
智能指针并非完全线程安全,也非完全不安全,其线程安全性取决于具体类型和操作场景。
在讨论线程安全前,先明确两个关键对象的区别:
- 智能指针对象本身:比如
shared_ptr<int> sp这个变量; - 智能指针指向的底层对象:比如
*sp对应的内存中的 int 值。
C++ 标准对智能指针的线程安全仅保障 部分场景,且不同智能指针的行为差异很大:
1. unique_ptr 的线程安全
unique_ptr是独占式智能指针(不允许拷贝,仅支持移动),其线程安全规则:
- ✅ 若多个线程拥有各自独立的
unique_ptr(指向同一对象或不同对象),且仅操作自己的unique_ptr,则安全; - ❌ 若多个线程同时操作同一个
unique_ptr对象(比如同时调用std::move、reset、赋值),则不安全(无任何线程安全保护); - ❌ 无论如何,
unique_ptr不会保护其指向的底层对象——多个线程通过unique_ptr修改*ptr,必须手动加锁。
2. shared_ptr 的线程安全(重点)
shared_ptr是共享式智能指针(通过引用计数管理生命周期),C++11及以上标准明确了其线程安全规则:
- ✅ 引用计数的操作是线程安全的:多个线程同时拷贝/析构同一个
shared_ptr对象(导致引用计数增减),是原子操作,无需额外加锁; - ❌ 智能指针对象本身的读写/修改非线程安全:多个线程同时对同一个
shared_ptr变量执行赋值、reset、swap、移动等操作(比如sp = nullptr、sp = make_shared<int>(10)),会导致数据竞争,必须加锁; - ❌ 指向的底层对象非线程安全:多个线程通过
shared_ptr读写*sp,必须自己加锁保护(智能指针不负责)。
代码示例: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
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
#include <iostream>
#include <memory>
#include <thread>
#include <mutex>
std::shared_ptr<int> sp = std::make_shared<int>(0);
std::mutex mtx;
// 场景1:多个线程拷贝同一个sp(引用计数操作,线程安全)
void copy_sp() {
for (int i = 0; i < 1000000; ++i) {
std::shared_ptr<int> local_sp = sp; // 仅拷贝,引用计数原子增减,安全
}
}
// 场景2:多个线程修改同一个sp对象(非线程安全,需加锁)
void modify_sp() {
for (int i = 0; i < 1000000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 必须加锁!
sp = std::make_shared<int>(i); // 修改sp本身,不加锁会数据竞争
}
}
// 场景3:多个线程修改底层对象(非线程安全,需加锁)
void modify_underlying_obj() {
for (int i = 0; i < 1000000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 必须加锁!
*sp += 1; // 修改底层int值,智能指针不保护
}
}
int main() {
// 测试场景1:3个线程拷贝sp
std::thread t1(copy_sp);
std::thread t2(copy_sp);
std::thread t3(copy_sp);
t1.join(); t2.join(); t3.join();
// 测试场景2:2个线程修改sp
std::thread t4(modify_sp);
std::thread t5(modify_sp);
t4.join(); t5.join();
// 测试场景3:2个线程修改底层对象
std::thread t6(modify_underlying_obj);
std::thread t7(modify_underlying_obj);
t6.join(); t7.join();
std::cout << *sp << std::endl;
return 0;
}
3. weak_ptr 的线程安全
weak_ptr依附于shared_ptr存在,其线程安全规则与shared_ptr一致:
- ✅ 对
weak_ptr的lock()、析构等操作(仅影响引用计数)是线程安全的; - ❌ 对
weak_ptr对象本身的赋值、reset等修改操作非线程安全; - ❌ 不会保护指向的底层对象。
总结
- 仅引用计数操作安全:
shared_ptr/weak_ptr的引用计数增减是原子的,无需加锁; - 智能指针对象本身不安全:任何对同一个智能指针变量的修改(赋值、reset、移动等),多线程下必须加锁;
- 底层对象永远不安全:无论哪种智能指针,多线程读写其指向的对象时,都需要手动加锁(如
std::mutex)。
简单记:智能指针只解决“对象生命周期管理”的线程安全(引用计数),不解决“对象数据访问”的线程安全。
参考文章
【占位】11121314151617181920212223
-
CSDN. c++11智能指针[DB/OL]. (2022-07-17).https://blog.csdn.net/weixin_43858819/article/details/125529689 ↩︎ ↩︎2 ↩︎3 ↩︎4 ↩︎5 ↩︎6 ↩︎7 ↩︎8
-
CSDN. C++11_智能指针 [DB/OL]. (2022-04-23). https://blog.csdn.net/weixin_44387482/article/details/124356614 ↩︎ ↩︎2 ↩︎3 ↩︎4 ↩︎5 ↩︎6
-
CSDN. c++11之智能指针 [DB/OL]. (2022-09-14). https://blog.csdn.net/qq_56673429/article/details/124837626 ↩︎ ↩︎2
-
ELEMENT-UI. 【C++11】Smart Pointer智能指针[DB/OL]. (2023-01-08). https://www.ngui.cc/el/2678278.html?action=onClick ↩︎ ↩︎2 ↩︎3
-
博客园. 【C++11】4种智能指针[DB/OL]. (2022-10-04). https://www.cnblogs.com/bandaoyu/p/16752819.html ↩︎ ↩︎2 ↩︎3 ↩︎4 ↩︎5 ↩︎6 ↩︎7 ↩︎8
-
CSDN. C++ 智能指针 shared_ptr 详解与示例 [DB/OL]. (2018-12-24). https://blog.csdn.net/shaosunrise/article/details/85228823 ↩︎ ↩︎2 ↩︎3 ↩︎4
-
C语言中文网. C++11 weak_ptr智能指针 [DB/OL]. (2019-01-05). http://c.biancheng.net/view/7918.html ↩︎ ↩︎2 ↩︎3 ↩︎4
-
CSDN. 深入理解掌握智能指针 [DB/OL]. (2022-02-12). https://blog.csdn.net/weixin_40533189/article/details/122857572 ↩︎ ↩︎2
-
CSDN. 智能指针和普通指针混用注意之一[DB/OL]. (2019-01-05). https://blog.csdn.net/ziliwangmoe/article/details/85840770 ↩︎
-
CSDN. 智能指针shared_ptr的reset使用 [DB/OL]. (2023-02-07). https://blog.csdn.net/tianyexing2008/article/details/128919341 ↩︎
-
博客园. C++11智能指针——shared_ptr类成员函数详解[DB/OL]. (2021-07-19). https://www.cnblogs.com/JCpeng/p/15031742.html ↩︎
-
CSDN. c++ unique_ptr共享指针release函数 [DB/OL]. (2023-02-09). https://blog.csdn.net/weixin_43061687/article/details/128862268 ↩︎
-
CSDN. 深入掌握C++智能指针 [DB/OL]. (2019-03-20).https://blog.csdn.net/QIANGWEIYUAN/article/details/88562935?spm=1001.2014.3001.5502 ↩︎
-
CSDN. C++动态内存与智能指针[DB/OL]. (2021-12-16). https://blog.csdn.net/weixin_43848885/article/details/121983343 ↩︎