「萝卜荟萃」浅谈 C++11 新特性之智能指针、右值引用
本文最后更新于:2022-10-29 23:41:08 UTC+08:00
邦邦让我分享一下华为实习,我觉得好像没啥可以讲的,该讲的前面的同学都讲了。于是想到了参加过一次分享会,员工们分享了这两个东西,于是决定给大家科普一下。
智能指针
我们来看一下下面的代码:
1 |
|
当 something_wrong()
为 true
时会抛出异常,这时 delete p;
将不会被执行,导致内存泄漏。
在实际的 C++ 开发中,我们经常会遇到诸如程序运行中突然崩溃、程序运行所用内存越来越多最终不得不重启等问题,这些问题往往都是内存资源管理不当造成的。比如:
- 有些内存资源已经被释放,但指向它的指针并没有改变指向(成为了野指针),并且后续还在使用;
- 有些内存资源已经被释放,后期又试图再释放一次(重复释放同一块内存会导致程序运行崩溃);
- 没有及时释放不再使用的内存资源,造成内存泄漏,程序占用的内存资源越来越多。
针对以上问题,C++ 提供了智能指针来解决垃圾回收的问题。
使用智能指针需要包含头文件:
1 |
|
auto_ptr
C++98/03 标准中支持使用 auto_ptr
智能指针来实现堆内存自动回收。我们可以把上面的例子改一改:
1 |
|
当程序退出时,p
被销毁,auto_ptr
会自动把原来 p
所指的堆内存释放掉,不会导致内存泄漏。
但是 auto_ptr
存在一些问题。
1 |
|
上面的程序会导致程序退出时释放同一块内存两次,一次是 ps
被销毁时,一次是 vocation
被销毁时,此时仍然没有解决重复 delete
的问题。
1 |
|
编译运行发现程序崩溃了。上面的程序把 films[2]
赋值给了 pwin
,此时 film[2]
的所有权已经转移给了 pwin
,films[2]
此时已经是一个空指针,访问空指针会出错。
C++11 新标准在废弃 auto_ptr
的同时,增添了 unique_ptr
、shared_ptr
以及 weak_ptr
这 3 个智能指针来实现堆内存的自动回收。
unique_ptr
unique_ptr
由 C++11 引入,旨在替代不安全的 auto_ptr
。unique_ptr
和 auto_ptr
类似,持有对对象的独有权——两个 unique_ptr
不能指向一个对象,即 unique_ptr
不共享它所管理的对象。它无法复制到其他 unique_ptr
,无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (STL)算法。只能移动 unique_ptr
,即对资源管理权限可以实现转移。这意味着,内存资源所有权可以转移到另一个 unique_ptr
,并且原始 unique_ptr
不再拥有此资源。
1 |
|
至于为什么 C++11 没有提供
make_unique
,C++ 标准委员会主席 Herb Sutter 在他的博客中提到原因是因为『被他们忘记了』。
shared_ptr
C++ 智能指针
shared_ptr
底层是采用引用计数的方式实现的。简单的理解,智能指针在申请堆内存空间的同时,会为其配备一个整形值(初始值为 1),每当有新对象使用此堆内存时,该整形值 + 1;反之,每当使用此堆内存的对象被释放时,该整形值减 1。当堆空间对应的整形值为 0 时,即表明不再有对象使用它,该堆空间就会被释放掉。
shared_ptr
允许多个指针指向同一个对象,解决了 auto_ptr
在对象所有权上的局限性(auto_ptr
独占对象所有权)。
1 |
|
另外,不能自动将普通指针转换为智能指针对象,必须显式调用构造函数。一个普通指针也不能同时为多个 shared_ptr
赋值。
1 |
|
但是,shared_ptr
仍然存在资源无法释放的问题。
1 |
|
运行结果是 A, B
都不会被销毁,这是因为 a, b
内部的 pointer 同时又引用了 a, b
,这使得 a, b
的引用计数均变为了 2,而离开作用域时,a, b
智能指针被析构,却只能造成这块区域的引用计数减一,这样就导致了 a, b
对象指向的内存区域引用计数不为零,而外部已经没有办法找到这块区域了,也就造成了内存泄露。
weak_ptr
weak_ptr
是为了配合 shared_ptr
而引入的一种智能指针,将一个 weak_ptr
绑定到一个 shared_ptr
不会改变 shared_ptr
的引用计数。weak_ptr
是一种弱引用(相比较而言 shared_ptr
就是一种强引用)。不论是否有 weak_ptr
指向,一旦最后一个指向对象的 shared_ptr
被销毁,对象就会被释放。从这个角度看, weak_ptr
更像是 shared_ptr
的一个助手而不是智能指针。
刚才的例子中,把 struct B
中的 pointer
换成 weak_ptr
,可以解决内存泄露的问题。
如上图,a, b
陆续被销毁后,已经没有强引用对象指向 A
,则 A
被释放,此时 B
也没有被强引用对象指向,所以 A, B
均可以被顺利释放。
1 |
|
weak_ptr
并没有重载 operator->
和 operator *
操作符,因此不可直接通过 weak_ptr
使用对象,典型的用法是调用其 lock()
函数来获得 shared_ptr
,进而访问原始对象。
右值引用
在 C++ 或者 C 语言中,一个表达式(可以是字面量、变量、对象、函数的返回值等)根据其使用场景不同,分为左值表达式和右值表达式。确切的说 C++ 中左值和右值的概念是从 C 语言继承过来的。
值得一提的是,左值的英文简写为 "lvalue",右值的英文简写为 "rvalue"。很多人认为它们分别是 "left value"、"right value" 的缩写,其实不然。lvalue 是 "loactor value" 的缩写,可意为存储在内存中、有明确存储地址(可寻址)的数据,而 rvalue 译为 "read value",指的是那些可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)。
简单理解,
- 左值是可以位于赋值运算符
=
左侧的表达式(当然,左值也可以位于=
的右侧),而 - 右值是不可以位于赋值运算符
=
左侧的表达式。
1 |
|
在 C++ 中,有两种对对象的引用:左值引用和右值引用。
左值引用是常见的引用,所以一般在提到「对象的引用」的时候,指得就是左值引用。如果我们将一个对象的内存空间绑定到另一个变量上,那么这个变量就是左值引用。在建立引用的时候,我们是「将内存空间绑定」,因此我们使用的是一个对象在内存中的位置,这是一个左值。因此,我们不能将一个右值绑定到左值引用上。另一方面,由于常量左值引用保证了我们不能通过引用改变对应内存空间的值,因此我们可以将右值绑定在常量引用上。
1 |
|
右值引用也是引用,和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化。
1 |
|
和常量左值引用不同的是,右值引用还可以对右值进行修改。例如:
1 |
|
由于右值引用只能绑定在右值上,而右值要么是字面常量,要么是临时对象,所以:
- 右值引用的对象,是临时的,即将被销毁;并且
- 右值引用的对象,不会在其它地方使用。
这两个特性意味着:接受和使用右值引用的代码,可以自由地接管所引用的对象的资源,而无需担心对其他代码逻辑造成数据破坏。
延伸阅读:https://liam.page/2016/12/11/rvalue-reference-in-Cpp/ 。
参考
部分文字、图片及代码完全引用自以下链接,侵删。
http://c.biancheng.net/view/7898.html
https://www.cnblogs.com/lanxuezaipiao/p/4132096.html
https://blog.csdn.net/K346K346/article/details/81478223
https://changkun.de/modern-cpp/zh-cn/05-pointers
https://blog.csdn.net/Xiejingfa/article/details/50772571