c++11添加了新的智能指针,unique_ptr、shared_ptr和weak_ptr,同时也将auto_ptr置为废弃(deprecated)。

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

  1. 不知道三种智能指针的具体使用场景

  2. 无脑只使用shared_ptr

  3. 认为应该禁用raw pointer(裸指针,即Widget*这种形式),全部使用智能指针

初始化方法


class A
{
public:
A(int size){ this->size = size; }
A(){}
void Show()
{
std::cout << "A::" << size << __FUNCTION__ << std::endl;
}
private:
int size = 5;
};
...
//[1]
auto p1 = std::make_shared<int>();
auto p2 = std::make_shared<A>();
//[2]
std::shared_ptr<int> p3(new int(5));
std::shared_ptr<A> p4(new A());
//[3]
std::shared_ptr<int> p5;
p5.reset(new int(5));
std::shared_ptr<A> p6;
p6.reset(new A());

推荐使用第一种方法~

使用场景

  1. unique_ptr

    1. 忘记delete

      class Box{
      public:
      Box() : w(new Widget())
      {}
      ~Box()
      {
      // 忘记delete w
      }
      private:
      Widget* w;
      };
    2. 异常安全

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

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

性能

  1. unique_ptr

    因为C++的zero cost abstraction的特点,unique_ptr在默认情况下和裸指针的大小是一样的。

    所以内存上没有任何的额外消耗,性能是最优的。

  2. shared_ptr

    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转移给新的对象。因为移动不用增加引用计数,因此性能比复制更好。

对象所有权

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

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

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

  1. 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获得了专属所有权。

  2. 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。

指针作为函数传参

  1. 只在函数使用指针,但并不保存

    假如我们只需要在函数中,用这个对象处理一些事情,但不打算涉及其生命周期的管理,不打算通过函数传参延长shared_ptr的生命周期。

    对于这种情况,可以使用raw pointer或者const shared_ptr&。

    即:

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

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

  1. 在函数中保存智能指针

    假如我们需要在函数中把这个智能指针保存起来,这个时候建议直接传值。void func(std::shared_ptr ptr);这样的话,外部传过来值的时候,可以选择move或者赋值。函数内部直接把这个对象通过move的方式保存起来。 这样性能更好,而且外部调用也有多种选择。

为什么要用shared_from_this

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

class A
{
public:
private:
std::shared_ptr<widget> widget;
} class Widget
{
public:
void do_something(A& a)
{
a.widget = std::make_shared<Widget>(this);
}
}

我们需要把当前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();
}
}
  • 这样才是合法的做法。

总结

  1. 重点理解三种智能指针的使用场景,
  • unique_ptr性能高,没有特殊要求的话可以直接用来取代raw pointer(原始指针)。
  • shared_ptr开销大,在前者不能满足的场景例如需要多个智能指针同时拥有同一个控件的所有权的时候使用。
  • weak_ptr不单独使用,通常用来配合shared_ptr使用,避免循环引用的问题。
  1. 优点:不用手动管理内存,尤其是根本不知道释放时机的时候
  2. 缺点:shared_ptr的内存占用高(多了一个引用计数),对多线程不友好(对引用计数的操作要原子性)

c++11新特性实战(二):智能指针的更多相关文章

  1. C++11新特性总结 (二)

    1. 范围for语句 C++11 引入了一种更为简单的for语句,这种for语句可以很方便的遍历容器或其他序列的所有元素 vector<int> vec = {1,2,3,4,5,6}; ...

  2. 【校招面试 之 C/C++】第31题 C++ 11新特性(二)之nullptr关键字

    1. 引入nullptr的原因 引入nullptr的原因,这个要从NULL说起.对于C和C++程序员来说,一定不会对NULL感到陌生.但是C和C++中的NULL却不等价.NULL表示指针不指向任何对象 ...

  3. c++11 新特性实战 (一):多线程操作

    c++11多线程操作 线程 thread int main() { thread t1(Test1); t1.join(); thread t2(Test2); t2.join(); thread t ...

  4. C++11新特性之二——std::bind std::function 高级用法

    /* * File: main.cpp * Author: Vicky.H * Email: eclipser@163.com */ #include <iostream> #includ ...

  5. C++11 新特性之智能指针(shared_ptr, unique_ptr, weak_ptr)

    这是C++11新特性介绍的第五部分,涉及到智能指针的相关内容(shared_ptr, unique_ptr, weak_ptr). shared_ptr shared_ptr 基本用法 shared_ ...

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

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

  7. [转载] C++11新特性

    C++11标准发布已有一段时间了, 维基百科上有对C++11新标准的变化和C++11新特性介绍的文章. 我是一名C++程序员,非常想了解一下C++11. 英文版的维基百科看起来非常费劲,而中文版维基百 ...

  8. 在C++98基础上学习C++11新特性

    自己一直用的是C++98规范来编程,对于C++11只闻其名却没用过其特性.近期因为工作的需要,需要掌握C++11的一些特性,所以查阅了一些C++11资料.因为自己有C++98的基础,所以从C++98过 ...

  9. C++11新特性总结 (一)

    1. 概述 最近在看C++ Primer5 刚好看到一半,总结一下C++11里面确实加了很多新东西,如果没有任何了解,别说自己写了,看别人写的代码估计都会有些吃力.C++ Primer5是学习C++1 ...

随机推荐

  1. Windows系统设置多用户同时远程登录

    一.在键盘上按Win+R键(也可以在开始菜单右键然后选择运行),在运行的输入框里面输入"gpedit.msc"命令.然后点击确定 二.在"计算机组策略"中依次展 ...

  2. Mac使用HomeBrew

    前言 考虑许久终于决定入手mac耍耍,还是因为要找工作了,手上的win本大学入的,现在使用卡顿太多,另外就是mac作为程序员之友仰慕已久.决定在PDD入了.到手后发现mac真的跟win有很大差别.还是 ...

  3. 【剑指 Offer】03.1.不修改数组找出重复的数字

    找出数组中重复的数字. 在一个长度为 n + 1 的数组 nums 里的所有数字都在 1-n 的范围内.所以数组中至少有一个是重复的.请找出数组中任意一个重复的数字. 示例 1: 输入: [2, 3, ...

  4. Openstack dashboard 仪表盘服务 (八)

    Openstack dashboard 仪表盘服务 (八) # 说明: 这个部分将描述如何在控制节点上安装和配置仪表板.dashboard仅在核心服务中要求认证服务.你可以将dashboard与其他服 ...

  5. Rabbitmq可靠消息投递,消息确认机制

    前言 我们知道,消息从发送到签收的整个过程是 Producer-->Broker/Exchange-->Broker/Queue-->Consumer,因此如果只是要保证消息的可靠投 ...

  6. ABAP-ALV-如何去掉OO方法中的ALV的标准按钮

    SAP在做报表开发中,不同公司对报表的风格往往各异,为此经常在使用OO方法做ALV报表中需要去掉自带的工具栏而自行添加一些工具按钮,下面将简单介绍一些其实现过程与原理: 步骤一: DATA : gt_ ...

  7. 编年史:OI算法总结

    目录(按字典序) A --A* D --DFS找环 J --基环树 S --数位动规 --树形动规 T --Tarjan(e-DCC) --Tarjan(LCA) --Tarjan(SCC) --Ta ...

  8. Py变量,递归,作用域,匿名函数

    局部变量与全局变量 全局变量:全局生效的变量,在顶头的,无缩进的定义的变量. 局部变量:函数内生效的变量,在函数内定义的变量. name='1fh' def changename(): name='s ...

  9. Linux更换软件源

    1. Ubuntu16.04 sudo cp /etc/apt/sources.list /etc/apt/sources_origin.list # 备份 sudo gedit /etc/apt/s ...

  10. 从零搭建一个IdentityServer——集成Asp.net core Identity

    前面的文章使用Asp.net core 5.0以及IdentityServer4搭建了一个基础的验证服务器,并实现了基于客户端证书的Oauth2.0授权流程,以及通过access token访问被保护 ...