您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > C/C++/C#

C++如何正确使用智能指针?看完这4个点你就明白了

时间:2019-10-08 13:50:13  来源:  作者:

C++11 中推出了三种智能指针,unique_ptr、shared_ptr 和 weak_ptr,同时也将 auto_ptr 置为废弃(deprecated)。

但是在实际的使用过程中,很多人都会有这样的问题:

  1. 不知道三种智能指针的具体使用场景
  2. 无脑只使用 shared_ptr
  3. 认为应该禁用 raw pointer(裸指针,即 Widget*这种形式),全部使用智能指针

本文试图理清楚三种智能指针的具体使用场景,并讲解三种智能指针背后的性能消耗。

C++如何正确使用智能指针?看完这4个点你就明白了

C/C++

对象所有权

首先需要理清楚的概念就是对象所有权的概念。所有权在 rust 语言中非常严格,写 rust 的时候必须要清楚自己创建的每个对象的所有权。

但是 C++比较自由,似乎我们不需要明白对象的所有权,写的代码也能正常运行。但是明白了对象所有权,我们才可以正确管理好对象生命周期和内存问题。

C++引入了智能指针,也是为了更好的描述对象所有权,简化内存管理,从而大大减少我们 C++内存管理方面的犯错机会。

unique_ptr:专属所有权

我们大多数场景下用到的应该都是 unique_ptr。unique_ptr 代表的是专属所有权,即由 unique_ptr 管理的内存,只能被一个对象持有。所以,unique_ptr 不支持复制和赋值,如下:

auto w = std::make_unique<Widget>();
auto w2 = w; // 编译错误

如果想要把 w 复制给 w2, 是不可以的。因为复制从语义上来说,两个对象将共享同一块内存。

因此,unique_ptr 只支持移动, 即如下:

auto w = std::make_unique<Widget>();
auto w2 = std::move(w); // w2获得内存所有权,w此时等于nullptr

unique_ptr 代表的是专属所有权,如果想要把一个 unique_ptr 的内存交给另外一个 unique_ptr 对象管理。只能使用 std::move 转移当前对象的所有权。转移之后,当前对象不再持有此内存,新的对象将获得专属所有权。

如上代码中,将 w 对象的所有权转移给 w2 后,w 此时等于 nullptr,而 w2 获得了专属所有权。

 

性能

因为 C++的 zero cost abstraction 的特点,unique_ptr 在默认情况下和裸指针的大小是一样的。所以内存上没有任何的额外消耗,性能是最优的。

使用场景 1:忘记 delete

unique_ptr 一个最简单的使用场景是用于类属性。代码如下:

class Box{
public:
 Box() : w(new Widget())
 {}
 ~Box()
 {
 // 忘记delete w
 }
private:
 Widget* w;
};

如果因为一些原因,w 必须建立在堆上。如果用裸指针管理 w,那么需要在析构函数中delete w; 这种写法虽然没什么问题,但是容易漏写 delete 语句,造成内存泄漏。

如果按照 unique_ptr 的写法,不用在析构函数手动 delete 属性,当对象析构时,属性w将会自动释放内存。

使用场景 2:异常安全

假如我们在一段代码中,需要创建一个对象,处理一些事情后返回,返回之前将对象销毁,如下所示:

void process()
{
 Widget* w = new Widget();
 w->do_something(); // 可能会发生异常
 delete w;
}

在正常流程下,我们会在函数末尾 delete 创建的对象 w,正常调用析构函数,释放内存。

但是如果 w->do_something()发生了异常,那么delete w将不会被执行。此时就会发生内存泄漏。我们当然可以使用 try...catch 捕捉异常,在 catch 里面执行 delete,但是这样代码上并不美观,也容易漏写。

如果我们用 std::unique_ptr,那么这个问题就迎刃而解了。无论代码怎么抛异常,在 unique_ptr 离开函数作用域的时候,内存就将会自动释放。

shared_ptr:共享所有权

在使用 shared_ptr 之前应该考虑,是否真的需要使用 shared_ptr, 而非 unique_ptr。

shared_ptr 代表的是共享所有权,即多个 shared_ptr 可以共享同一块内存。因此,从语义上来看,shared_ptr 是支持复制的。如下:

auto w = std::make_shared<Widget>();
{
 auto w2 = w;
 cout << w.use_count() << endl; // 2
}
cout << w.use_count() << endl; // 1

shared_ptr 内部是利用引用计数来实现内存的自动管理,每当复制一个 shared_ptr,引用计数会+1。当一个 shared_ptr 离开作用域时,引用计数会-1。当引用计数为 0 的时候,则 delete 内存。

同时,shared_ptr 也支持移动。从语义上来看,移动指的是所有权的传递。如下:

auto w = std::make_shared<Widget>();
auto w2 = std::move(w); // 此时w等于nullptr,w2.use_count()等于1

我们将 w 对象 move 给 w2,意味着 w 放弃了对内存的所有权和管理,此时 w 对象等于 nullptr。而 w2 获得了对象所有权,但因为此时 w 已不再持有对象,因此 w2 的引用计数为 1。

C++如何正确使用智能指针?看完这4个点你就明白了

C/C++

性能

  1. 内存占用高。 shared_ptr 的内存占用是裸指针的两倍。因为除了要管理一个裸指针外,还要维护一个引用计数。因此相比于 unique_ptr, shared_ptr 的内存占用更高
  2. 原子操作性能低。 考虑到线程安全问题,引用计数的增减必须是原子操作。而原子操作一般情况下都比非原子操作慢。
  3. 使用移动优化性能。shared_ptr 在性能上固然是低于 unique_ptr。而通常情况,我们也可以尽量避免 shared_ptr 复制。如果,一个 shared_ptr 需要将所有权共享给另外一个新的 shared_ptr,而我们确定在之后的代码中都不再使用这个 shared_ptr,那么这是一个非常鲜明的移动语义。对于此种场景,我们尽量使用 std::move,将 shared_ptr 转移给新的对象。因为移动不用增加引用计数,因此性能比复制更好。

使用场景

  1. 1. shared_ptr 通常使用在共享权不明的场景。有可能多个对象同时管理同一个内存时。
  2. 2. 对象的延迟销毁。陈硕在《linux 多线程服务器端编程》中提到,当一个对象的析构非常耗时,甚至影响到了关键线程的速度。可以使用BlockingQueue<std::shared_ptr<void>>将对象转移到另外一个线程中释放,从而解放关键线程。

shared_from_this

我们往往会需要在类内部使用自身的 shared_ptr,例如:

class Widget
{
public:
 void do_something(A& a)
 {
 a.widget = 该对象的shared_ptr;
 }
}

我们需要把当前 shared_ptr 对象同时交由对象 a 进行管理。意味着,当前对象的生命周期的结束不能早于对象 a。因为对象 a 在析构之前还是有可能会使用到a.widget。

如果我们直接a.widget = this;, 那肯定不行, 因为这样并没有增加当前 shared_ptr 的引用计数。shared_ptr 还是有可能早于对象 a 释放。

如果我们使用a.widget = std::make_shared<Widget>(this);,肯定也不行,因为这个新创建的 shared_ptr,跟当前对象的 shared_ptr 毫无关系。当前对象的 shared_ptr 生命周期结束后,依然会释放掉当前内存,那么之后a.widget依然是不合法的。

对于这种,需要在对象内部获取该对象自身的 shared_ptr, 那么该类必须继承std::enable_shared_from_this<T>。代码如下:

class Widget : public std::enable_shared_from_this<Widget>
{
public:
 void do_something(A& a)
 {
 a.widget = shared_from_this();
 }
}

这样才是合法的做法。

weak_ptr

weak_ptr 是为了解决 shared_ptr 双向引用的问题。即:

class B;
struct A{
 shared_ptr<B> b;
};
struct B{
 shared_ptr<A> a;
};
auto pa = make_shared<A>();
auto pb = make_shared<B>();
pa->b = pb;
pb->a = pa;

pa 和 pb 存在着循环引用,根据 shared_ptr 引用计数的原理,pa 和 pb 都无法被正常的释放。对于这种情况, 我们可以使用 weak_ptr:

class B;
struct A{
 shared_ptr<B> b;
};
struct B{
 weak_ptr<A> a;
};
auto pa = make_shared<A>();
auto pb = make_shared<B>();
pa->b = pb;
pb->a = pa;

weak_ptr 不会增加引用计数,因此可以打破 shared_ptr 的循环引用。通常做法是 parent 类持有 child 的 shared_ptr, child 持有指向 parent 的 weak_ptr。这样也更符合语义。

如何指针作为函数传参

很多时候,函数的参数是个指针。这个时候就会面临选择困难症,这个参数应该怎么传,应该是 shared_ptr,还是 const shared_ptr&,还是直接 raw pointer 更合适。

1. 只在函数使用指针,但并不保存。假如我们只需要在函数中,用这个对象处理一些事情,但不打算涉及其生命周期的管理,不打算通过函数传参延长 shared_ptr 的生命周期。对于这种情况,可以使用 raw pointer 或者 const shared_ptr&。即:

void func(Widget*);
void func(const shared_ptr<Widget>&)

实际上第一种裸指针的方式可能更好,从语义上更加清楚,函数也不用关心智能指针的类型。

  1. 在函数中保存智能指针。假如我们需要在函数中把这个智能指针保存起来,这个时候建议直接传值。
void func(std::shared_ptr<Widget> ptr);

这样的话,外部传过来值的时候,可以选择 move 或者赋值。函数内部直接把这个对象通过 move 的方式保存起来。这样性能更好,而且外部调用也有多种选择。

总结

对于智能指针的使用,实际上是对所有权和生命周期的思考,一旦想明白了这两点,那对智能指针的使用也就得心应手了。同时理解了每种智能指针背后的性能消耗、使用场景,那智能指针也不再是黑盒子和洪水猛兽。

C++如何正确使用智能指针?看完这4个点你就明白了

C/C++

加C/C++学习交.流群,免费获取C语言、C++学习资料,学习路线指导和梳理,更有进阶干货的直播免费学习权限 ,都是大牛带飞 让你少走很多的弯路的 群...号是 546912356



Tags:C++ 智能指针   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
C++11 中推出了三种智能指针,unique_ptr、shared_ptr 和 weak_ptr,同时也将 auto_ptr 置为废弃(deprecated)。但是在实际的使用过程中,很多人都会有这样的问题: 不知道三种智能...【详细内容】
2019-10-08  Tags: C++ 智能指针  点击:(165)  评论:(0)  加入收藏
▌简易百科推荐
一、简介很多时候我们都需要用到一些验证的方法,有时候需要用正则表达式校验数据时,往往需要到网上找很久,结果找到的还不是很符合自己想要的。所以我把自己整理的校验帮助类分...【详细内容】
2021-12-27  中年农码工    Tags:C#   点击:(2)  评论:(0)  加入收藏
引言在学习C语言或者其他编程语言的时候,我们编写的一个程序代码,基本都是在屏幕上打印出 hello world ,开始步入编程世(深)界(坑)的。C 语言版本的 hello world 代码:#include <std...【详细内容】
2021-12-21  一起学嵌入式    Tags:C 语言   点击:(11)  评论:(0)  加入收藏
读取SQLite数据库,就是读取一个路径\\192.168.100.**\position\db.sqlite下的文件<startup useLegacyV2RuntimeActivationPolicy="true"> <supportedRuntime version="v4.0"/...【详细内容】
2021-12-16  今朝我的奋斗    Tags:c#   点击:(21)  评论:(0)  加入收藏
什么是shellshell是c语言编写的程序,它在用户和操作系统之间架起了一座桥梁,用户可以通过这个桥梁访问操作系统内核服务。 它既是一种命令语言,同时也是一种程序设计语言,你可以...【详细内容】
2021-12-16  梦回故里归来    Tags:shell脚本   点击:(18)  评论:(0)  加入收藏
一、编程语言1.根据熟悉的语言,谈谈两种语言的区别?主要浅谈下C/C++和PHP语言的区别:1)PHP弱类型语言,一种脚本语言,对数据的类型不要求过多,较多的应用于Web应用开发,现在好多互...【详细内容】
2021-12-15  linux上的码农    Tags:c/c++   点击:(17)  评论:(0)  加入收藏
1.字符串数组+初始化char s1[]="array"; //字符数组char s2[6]="array"; //数组长度=字符串长度+1,因为字符串末尾会自动添&lsquo;\0&lsquo;printf("%s,%c\n",s1,s2[2]);...【详细内容】
2021-12-08  灯-灯灯    Tags:C语言   点击:(47)  评论:(0)  加入收藏
函数调用约定(Calling Convention),是一个重要的基础概念,用来规定调用者和被调用者是如何传递参数的,既调用者如何将参数按照什么样的规范传递给被调用者。在参数传递中,有两个很...【详细内容】
2021-11-30  小智雅汇    Tags:函数   点击:(19)  评论:(0)  加入收藏
一、问题提出问题:把m个苹果放入n个盘子中,允许有的盘子为空,共有多少种方法?注:5,1,1和1 5 1属同一种方法m,n均小于10二、算法分析设f(m,n) 为m个苹果,n个盘子的放法数目,则先对...【详细内容】
2021-11-17  C语言编程    Tags:C语言   点击:(49)  评论:(0)  加入收藏
一、为什么需要使用内存池在C/C++中我们通常使用malloc,free或new,delete来动态分配内存。一方面,因为这些函数涉及到了系统调用,所以频繁的调用必然会导致程序性能的损耗;另一...【详细内容】
2021-11-17  深度Linux    Tags:C++   点击:(38)  评论:(0)  加入收藏
OpenCV(Open Source Computer Vision Library)是一个(开源免费)发行的跨平台计算机视觉库,可以运行在Linux、Windows、Android、ios等操作系统上,它轻量级而且高效---由一系列...【详细内容】
2021-11-11  zls315    Tags:C#   点击:(50)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条