再探智能指针
又是许久未见的一期非人机博客,哈哈哈,感觉事好多(才不是寒假偷懒)
写完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 | //通过一个裸指针构造 |
相比先创建对象,然后再创建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 | weak_ptr<int> wp1(sp1);//弱引用不增加引用计数 |
所以我们选择原子操作:
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 | std::unique_ptr<test>up1(new test(1)); |
reset的参数是一个裸指针(或者为空),将原unique_ptr指向的对象销毁后指向参数的指针指向的对象
release的返回值是裸指针
1 | vector<unique_ptr<test>>v1; |
智能指针在多线程中的强大作用
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
所指向的对象”这两种不同的操作。
在下一节中,我们将详细介绍写时复制