导语: C++指针的内存管理相信是大部分C++入门程序员的梦魇,受到Boost的启发,C++11标准推出了智能指针,让我们从指针的内存管理中释放出来,几乎消灭所有new和delete。既然智能指针如此强大,今天我们来一窥智能指针的原理以及在多线程操作中需要注意的细节。

智能指针的由来

在远古时代,C++使用了指针这把双刃剑,既可以让程序员精确地控制堆上每一块内存,也让程序更容易发生crash,大大增加了使用指针的技术门槛。因此,从C++98开始便推出了auto_ptr,对裸指针进行封装,让程序员无需手动释放指针指向的内存区域,在auto_ptr生命周期结束时自动释放,然而,由于auto_ptr在转移指针所有权后会产生野指针,导致程序运行时crash,如下面示例代码所示:

auto_ptr<int> p1(new int(10));
auto_ptr<int> p2 = p1; //转移控制权
*p1 += 10; //crash,p1为空指针,可以用p1->get判空做保护

因此在C++11又推出了unique_ptr、shared_ptr、weak_ptr三种智能指针,慢慢取代auto_ptr。

unique_ptr的使用

unique_ptr是auto_ptr的继承者,对于同一块内存只能有一个持有者,而unique_ptr和auto_ptr唯一区别就是unique_ptr不允许赋值操作,也就是不能放在等号的右边(函数的参数和返回值例外),这一定程度避免了一些误操作导致指针所有权转移,然而,unique_str依然有提供所有权转移的方法move,调用move后,原unique_ptr就会失效,再用其访问裸指针也会发生和auto_ptr相似的crash,如下面示例代码,所以,即使使用了unique_ptr,也要慎重使用move方法,防止指针所有权被转移。

unique_ptr<int> up(new int(5));
//auto up2 = up; // 编译错误
auto up2 = move(up);
cout << *up << endl; //crash,up已经失效,无法访问其裸指针

除了上述用法,unique_ptr还支持创建动态数组。在C++中,创建数组有很多方法,如下所示:

// 静态数组,在编译时决定了数组大小
int arr[10];    // 通过指针创建在堆上的数组,可在运行时动态指定数组大小,但需要手动释放内存
int *arr = new int[10];    // 通过std::vector容器创建动态数组,无需手动释放数组内存
vector<int> arr(10);  // 通过unique_ptr创建动态数组,也无需手动释放数组内存,比vector更轻量化
unique_ptr<int[]> arr(new int[10]);

这里需要注意的是,不管vector还是unique_ptr,虽然可以帮我们自动释放数组内存,但如果数组的元素是复杂数据类型时,我们还需要在其析构函数中正确释放内存。

真正的智能指针:shared_ptr

auto_ptr和unique_ptr都有或多或少的缺陷,因此C++11还推出了shared_ptr,这也是目前工程内使用最多最广泛的智能指针,他使用引用计数(感觉有参考Objective-C的嫌疑),实现对同一块内存可以有多个引用,在最后一个引用被释放时,指向的内存才释放,这也是和unique_ptr最大的区别。

另外,使用shared_ptr过程中有几点需要注意:

  • 构造shared_ptr的方法,如下示例代码所示,我们尽量使用shared_ptr构造函数或者make_shared的方式创建shared_ptr,禁止使用裸指针赋值的方式,这样会shared_ptr难于管理指针的生命周期。
// 使用裸指针赋值构造,不推荐,裸指针被释放后,shared_ptr就野了,不能完全控制裸指针的生命周期,失去了智能指针价值
int *p = new int(10);
shared_ptr<int>sp = p;
delete p; // sp将成为野指针,使用sp将crash // 将裸指针作为匿名指针传入构造函数,一般做法,让shared_ptr接管裸指针的生命周期,更安全
shared_ptr<int>sp1(new int(10));  // 使用make_shared,推荐做法,更符合工厂模式,可以连代码中的所有new,更高效;方法的参数是用来初始化模板类
shared_ptr<int>sp2 = make_shared<int>(10);
  • 禁止使用指向shared_ptr的裸指针,也就是智能指针的指针,这听起来就很奇怪,但开发中我们还需要注意,使用shared_ptr的指针指向一个shared_ptr时,引用计数并不会加一,操作shared_ptr的指针很容易就发生野指针异常。
shared_ptr<int>sp = make_shared<int>(10);
cout << sp.use_count() << endl; //输出1
shared_ptr<int> *sp1 = &sp;
cout << (*sp1).use_count() << endl; //输出依然是1
(*sp1).reset(); //sp成为野指针
cout << *sp << endl; //crash
  • 使用shared_ptr创建动态数组,在介绍unique_ptr时我们就讲过创建动态数组,而shared_ptr同样可以做到,不过稍微复杂一点,如下代码所示,除了要显示指定析构方法外(因为默认是T的析构函数,不是T[]),另外对外的数据类型依然是shared_ptr<T>,非常有迷惑性,看不出来是数组,最后不能直接使用下标读写数组,要先get()获取裸指针才可以使用下标。所以,不推荐使用shared_ptr来创建动态数组,尽量使用unique_ptr,这可是unique_ptr为数不多的优势了。
template &lt;typename T&gt;
shared_ptr&lt;T&gt; make_shared_array(size_t size) {    return shared_ptr&lt;T&gt;(new T[size], default_delete&lt;T[]&gt;());
}   shared_ptr&lt;int&gt;sp = make_shared_array(10); //看上去是shared&lt;int&gt;类型,实际上是数组
sp.get()[0] = 100; //不能直接使用下标读写数组元素,需要通过get()方法获取裸指针后再操作
  • 用shared_ptr实现多态,在我们使用裸指针时,实现多态就免不了定义虚函数,那么用shared_ptr时也不例外,不过有一处是可以省下的,就是析构函数我们不需要定义为虚函数了,如下面代码所示:
class A {
public: ~A() {  cout &lt;&lt; "dealloc A" &lt;&lt; endl;  }
};
class B : public A {
public: ~B() {  cout &lt;&lt; "dealloc B" &lt;&lt; endl;  }
};
int main(int argc, const char * argv[]) {   A *a = new B();    delete a; //只打印dealloc A    shared_ptr&lt;A&gt;spa = make_shared&lt;B&gt;(); //析构spa是会先打印dealloc B,再打印dealloc A return 0;
}
  • 循环引用,笔者最先接触引用计数的语言就是Objective-C,而OC中最常出现的内存问题就是循环引用,如下面代码所示,A中引用B,B中引用A,spa和spb的强引用计数永远大于等于1,所以直到程序退出前都不会被退出,这种情况有时候在正常的业务逻辑中是不可避免的,而解决循环引用的方法最有效就是改用weak_ptr,具体可见下一章。
class A {
public: shared_ptr&lt;B&gt; b;
};
class B {
public: shared_ptr&lt;A&gt; a;
};
int main(int argc, const char * argv[]) {   shared_ptr&lt;A&gt; spa = make_shared&lt;A&gt;();  shared_ptr&lt;B&gt; spb = make_shared&lt;B&gt;();  spa-&gt;b = spb;   spb-&gt;a = spa;   return 0;
} //main函数退出后,spa和spb强引用计数依然为1,无法释放

刚柔并济:weak_ptr

正如上一章提到,使用shared_ptr过程中有可能会出现循环引用,关键原因是使用shared_ptr引用一个指针时会导致强引用计数+1,从此该指针的生命周期就会取决于该shared_ptr的生命周期,然而,有些情况我们一个类A里面只是想引用一下另外一个类B的对象,类B对象的创建不在类A,因此类A也无需管理类B对象的释放,这个时候weak_ptr就应运而生了,使用shared_ptr赋值给一个weak_ptr不会增加强引用计数(strong_count),取而代之的是增加一个弱引用计数(weak_count),而弱引用计数不会影响到指针的生命周期,这就解开了循环引用,上一章最后的代码使用weak_ptr可改造为如下代码。

class A {
public: shared_ptr&lt;B&gt; b;
};
class B {
public: weak_ptr&lt;A&gt; a;
};
int main(int argc, const char * argv[]) {   shared_ptr&lt;A&gt; spa = make_shared&lt;A&gt;();  shared_ptr&lt;B&gt; spb = make_shared&lt;B&gt;();  spa-&gt;b = spb; //spb强引用计数为2,弱引用计数为1   spb-&gt;a = spa; //spa强引用计数为1,弱引用计数为2   return 0;
} //main函数退出后,spa先释放,spb再释放,循环解开了

使用weak_ptr也有需要注意的点,因为既然weak_ptr不负责裸指针的生命周期,那么weak_ptr也无法直接操作裸指针,我们需要先转化为shared_ptr,这就和OC的Strong-Weak Dance有点像了,具体操作如下:

shared_ptr&lt;int&gt; spa = make_shared&lt;int&gt;(10);
weak_ptr&lt;int&gt; spb = spa; //weak_ptr无法直接使用裸指针创建
if (!spb.expired()) { //weak_ptr最好判断是否过期,使用expired或use_count方法,前者更快   *spb.lock() += 10; //调用weak_ptr转化为shared_ptr后再操作裸指针
}
cout &lt;&lt; *spa &lt;&lt; endl; //20

智能指针原理

看到这里,智能指针的用法基本介绍完了,后面笔者来粗浅地分析一下为什么智能指针可以有效帮我们管理裸指针的生命周期。

使用栈对象管理堆对象

在C++中,内存会分为三部分,堆、栈和静态存储区,静态存储区会存放全局变量和静态变量,在程序加载时就初始化,而堆是由程序员自行分配,自行释放的,例如我们使用裸指针分配的内存;而最后栈是系统帮我们分配的,所以也会帮我们自动回收。因此,智能指针就是利用这一性质,通过一个栈上的对象(shared_ptr或unique_ptr)来管理一个堆上的对象(裸指针),在shared_ptr或unique_ptr的析构函数中判断当前裸指针的引用计数情况来决定是否释放裸指针。

shared_ptr引用计数的原理

一开始笔者以为引用计数是放在shared_ptr这个模板类中,但是细想了一下,如果这样将shared_ptr赋值给另一个shared_ptr时,是怎么做到两个shared_ptr的引用计数同时加1呢,让等号两边的shared_ptr中的引用计数同时加1?不对,如果还有第二个shared_ptr再赋值给第三个shared_ptr那怎么办呢?或许通过下面的类图便清楚个中奥秘。

[ boost中shared_ptr与weak_ptr类图 ]

我们重点关注shared_ptr<T>的类图,它就是我们可以直接操作的类,这里面包含裸指针T*,还有一个shared_count的对象,而shared_count对象还不是最终的引用计数,它只是包含了一个指向sp_counted_base的指针,这应该就是真正存放引用计数的地方,包括强应用计数和弱引用计数,而且shared_count中包含的是sp_counted_base的指针,不是对象,这也就意味着假如shared_ptr<T> a = b,那么a和b底层pi_指针指向的是同一个sp_counted_base对象,这就很容易做到多个shared_ptr的引用计数永远保持一致了。

多线程安全

本章所说的线程安全有两种情况:

多个线程操作多个不同的shared_ptr对象

C++11中声明了shared_ptr的计数操作具有原子性,不管是赋值导致计数增加还是释放导致计数减少,都是原子性的,这个可以参考sp_counted_base的源码,因此,基于这个特性,假如有多个shared_ptr共同管理一个裸指针,那么多个线程分别通过不同的shared_ptr进行操作是线程安全的。

多个线程操作同一个shared_ptr对象

同样的道理,既然C++11只负责sp_counted_base的原子性,那么shared_ptr本身就没有保证线程安全了,加入两个线程同时访问同一个shared_ptr对象,一个进行释放(reset),另一个读取裸指针的值,那么最后的结果就不确定了,很有可能发生野指针访问crash。

窥见C++11智能指针相关推荐

  1. 详解C++11智能指针

    详解C++11智能指针 前言 C++里面的四个智能指针: auto_ptr, unique_ptr,shared_ptr, weak_ptr 其中后三个是C++11支持,并且第一个已经被C++11弃用 ...

  2. 【C++11新特性】 C++11智能指针之weak_ptr

    http://blog.csdn.net/xiejingfa/article/details/50772571 原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/ar ...

  3. c++11新特性_【C++11新特性】 C++11智能指针之weak_ptr

    如题,我们今天要讲的是 C++11 引入的三种智能指针中的:weak_ptr. 在学习 weak_ptr 之前最好对 shared_ptr 有所了解.如果你还不知道 shared_ptr 是何物,可以 ...

  4. C++11智能指针(unique_ptr、shared_ptr、weak_ptr)boost::scoped_ptr

    C++11智能指针(unique_ptr.shared_ptr.weak_ptr)_-码农小非-的专栏-CSDN博客_c++ shared_ptr weak_ptr 原创)智能指针拾遗 (原创)智能指 ...

  5. C++11 智能指针时要避开的 10 大错误

    我很喜欢C++11全新的智能指针,对于那些苦恼于自己管理内存的开发者来说,这是天大的好事.但是,在广泛使用C++11智能指针的两年里,因为使用方法不当而导致程序的效率下降甚至直接crash掉的事情发生 ...

  6. 使用 C++11 智能指针时要避开的 10 大错误

    http://blog.jobbole.com/104666/ 我很喜欢新的C++11的智能指针.在很多时候,对很多讨厌自己管理内存的人来说是天赐的礼物.在我看来,C++11的智能指针能使得C++新手 ...

  7. 十大愚蠢的错误,以避免与C ++ 11智能指针

    Top 10 dumb mistakes to avoid with C++ 11 smart pointers I love the new C++ 11 smart pointers. In ma ...

  8. c++11 智能指针 unique_ptr、shared_ptr与weak_ptr

    C++11中有unique_ptr.shared_ptr与weak_ptr等智能指针(smart pointer),定义在<memory>中. 可以对动态资源进行管理,保证任何情况下,已构 ...

  9. 【C++11新特性】 C++11智能指针之shared_ptr

    http://blog.csdn.net/Xiejingfa/article/details/50750037 原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/ar ...

最新文章

  1. 微信python天天学_刚学Python一礼拜!我就能模拟登录微信公众号!我是天才吧!...
  2. 连续 4 年成为“开发者最喜欢的语言”,这门编程语言你了解过吗?
  3. ubuntu突然连不上-调试方式
  4. NetBeans Java EE技巧#1 –数据库中的实体类
  5. 神经网络中的激活函数
  6. 【数字信号处理】基于matlab GUI数字信号处理系统【含Matlab源码 1088期】
  7. NanoHTTPD服务
  8. 牛客网——判断上三角矩阵
  9. C++: STL: atomic
  10. 成功的发行模式(标题 我自己改的 转至 敏思博客)
  11. html打折代码,HTML打折计算价格原理与脚本代码
  12. 计算机手动配置信息,手动修改并设置电脑开机画面入您所愿
  13. 怎么快速做动态图?gif动图在线怎么制作?
  14. Apache2.4 HTTP服务器配置反向代理和负载均衡
  15. uni-app如何使用vant-ui
  16. 拼多多退款怎么设置?万顿思教育
  17. 疯狂java讲义(李刚)6.2
  18. faiss search(检索)截断
  19. 支付宝小程序云开发(Serverless)
  20. html5中的meter标签改变颜色规则

热门文章

  1. Apache JMeter 3.2版新特性详述
  2. Java Streams,第 4 部分: 从并发到并行
  3. linux中redis的主从
  4. Mac下django简单安装配置步骤
  5. 今天在海淀黄庄地铁站真实经历 写下来希望我的好友可以看一下!
  6. aspnet前后台条件下根目录的读取
  7. ASP.NET 3.5中的一个超简单的Ajax实验
  8. HDU - 4248 A Famous Stone Collector(dp+组合数学)
  9. nexus 代理阿里云_Azure容器镜像代理服务失效
  10. Docker教程-简介