又是许久未见的一期非人机博客,哈哈哈,感觉事好多(才不是寒假偷懒)

写完cc_muduo网络库之后,深感c++基础有待加深学习,于是重新看了一遍智能指针

众所周知,智能指针包括三种shared_ptr,weak_ptr,unique_ptr,我们一个个来

shared_ptr

智能指针,作为c++独有的方式,相比malloc,free,以及new delete这两对需要手动释放的情侣,智能指针总算学会了自动释放内存

但是很不幸的是,他学艺不精,依然容易存在双重释放等问题,但依然是巨大的进步

智能指针的自动释放依赖于RALL机制,即在栈上,依赖析构函数实现的自动释放方法

毕竟栈上的内存,在离开作用域之后,会自动释放,便借用这个机制,在析构函数中加入delete,从而实现

shared_ptr,强调的是共享所有权,可以通过裸指针构造,也可以通过另一个智能指针构造

每个shared_ptr的引用计数都会指向该对象的数量,当引用计数为0(最后一个shared_ptr析构时,会自动删除对象和引用计数)

这里强调两点构造时要注意的:

(1)不要用同一个裸指针创建多个shared_ptr

(2)避免用this指针创建shared_ptr

原因都类似,在用裸指针创建shared_ptr时,会创建一个计数对象,如果用同一个裸指针创建两个shared_ptr,那么一个计数对象是2,新的那个却是1.而用一个shared_ptr初始化(赋值)另一个shared_ptr就没问题,只会在已有的计数对象上++

在必须用this指针创建shared_ptr时,我们一般使用shared_from_this()函数,可以安全的获得指针,但是要求this指针指向的该类已被shared_ptr指向

shared_ptr实际上包括两个指针,一个是指向管理对象的指针,另一个是指向控制块的指针

控制块中有引用计数,弱计数,和其他一些东西

而引用计数的内存是动态分配的,递增和递减是原子操作

一般来说,我们创建一个shared_ptr有两种方法

1
2
3
4
5
//通过一个裸指针构造
test* p = new test(1);
std::shared_ptr<test> sp(p);
//使用make_shared实现
std::shared_ptr<test> sp = std::make_shared<test>(1);

相比先创建对象,然后再创建shared_ptr,make_shared不但更安全,而且只会产生一次内存分配,将对象和计数对象共用一块区域,效率更高

weak_ptr

weak_ptr表示临时所有权,是弱引用,不会增加引用计数,需要配合shared_ptr使用,追踪判断shared_ptr的对象是否有效,当需要临时所有权时,也可以将weak_ptr转换成shared_ptr,使引用计数++

weak_ptr的构造可以用一个weak_ptr也可以用shared_ptr

在多线程操作中,管理共享对象是一个令人头疼的事情,最大的困难就是保证共享对象的有效性和共享对象的有效性检测

在这里我们一般使用weak_ptr和shared_ptr的结合使用来完成

正常情况下,在将弱引用转换为强引用的过程中我们很容易出现以下状况:

1
2
3
4
5
6
7
8
9
10
11
weak_ptr<int> wp1(sp1);//弱引用不增加引用计数  

cout << sp1.use_count() << endl;

//在多线程环境下,可能在初始化sp2和wp1.expired()之间,sp1的引用计数减为0,从而导致未定义操作(并非原子操作)
if (!wp1.expired())//检测shared_ptr对象是否已经释放
{
//sp1,wp1指向的被释放
shared_ptr<int>sp2(wp1);
cout << sp2.use_count() << endl;
}

所以我们选择原子操作:

1
shared_ptr<int> sp3 = wp1.lock();//通过weak_ptr对象获取shared_ptr对象(原子操作,将弱引用转换为强引用)

这样的话,如果wp1指向的对象仍存在,就一定能成功初始化sp3,如果不存在,就返回0,不会出现未定义操作

如此,我们便可以根据sp3的状况,判断是否对象有效

这里插播一条提醒:不要出现循环引用

比如,分别创建类A,B的shared_ptr,然后再用该指针,在类内创建shared_ptr分别指向另一个,这样的话,就表示两者互有所有权。这种是不正确的,应该在其中一方使用weak_ptr

unique_ptr

unique_ptr代表的是独占所有权,没有拷贝语义,只能通过移动操作来转移所有权,有三个核心接口:release(),reset(),swap()

1
2
3
4
5
std::unique_ptr<test>up1(new test(1));
std::unique_ptr<test>up2(new test(2));
up1=std::move(up2);//test(2)会销毁,up1指向test(2),up2为空

up1.reset(up2.release());//up2不再指向test(2),up1指向test(2),同时将test(1)销毁,up2变为空

reset的参数是一个裸指针(或者为空),将原unique_ptr指向的对象销毁后指向参数的指针指向的对象

release的返回值是裸指针

1
2
vector<unique_ptr<test>>v1;
v1.emplace_back(move(up1));//在容器中要使用move转移所有权

智能指针在多线程中的强大作用

1. 一个 shared_ptr 对象可以被多个线程同时读

✓ 正确
多个线程可以同时读取同一个 shared_ptr 对象而不会产生问题。std::shared_ptr 的读操作是线程安全的,包括:

  • 获取原始指针 (get())
  • 检查引用计数 (use_count())
  • 检查指针是否为空 (operator bool)

2. 两个 shared_ptr 对象实体可以被两个线程同时读写,即使他们管理的是同一个对象

✓ 正确,但有重要注意事项
shared_ptr 本身的角度看,两个不同的 shared_ptr 实例可以分别被不同线程安全地修改(比如赋值、重置等),即使它们指向同一个底层对象。这是因为你在修改的是两个不同的智能指针实例。

然而,重要注意事项:虽然修改两个不同的 shared_ptr 实例是安全的,但如果通过这两个智能指针同时修改它们指向的同一个对象,则会产生数据竞争,这是不安全的,需要额外的同步机制。

3. 多个线程读写同一个 shared_ptr 对象实体,需要加锁

✓ 正确
如果多个线程需要修改同一个 shared_ptr 实例(例如对同一个 shared_ptr 变量进行赋值或重置操作),则需要同步机制(如互斥锁)来保护这些操作。shared_ptr 的引用计数机制是线程安全的,但智能指针实例本身的修改操作不是线程安全的。

总结与补充说明

  • shared_ptr 的引用计数:内部引用计数的增减是原子的、线程安全的
  • shared_ptr 实例的修改:对同一个 shared_ptr 变量的修改需要同步
  • shared_ptr 指向的对象:对指向对象的并发访问需要用户自行提供同步机制

第二点需要明确区分”修改 shared_ptr 实例本身”和”修改 shared_ptr 所指向的对象”这两种不同的操作。

在下一节中,我们将详细介绍写时复制