[c++] Smart Pointers

前言


初步认识

原文链接:https://blog.csdn.net/flowing_wind/java/article/details/81301001

参考资料:《C++ Primer中文版 第五版》

我们知道除了静态内存和栈内存外,每个程序还有一个内存池,这部分内存被称为自由空间或者堆。程序用堆来存储动态分配的对象即那些在程序运行时分配的对象,当动态对象不再使用时,我们的代码必须显式的销毁它们。

在C++中,动态内存的管理是用一对运算符完成的:new和delete,new:在动态内存中为对象分配一块空间并返回一个指向该对象的指针,delete:指向一个动态独享的指针,销毁对象,并释放与之关联的内存。

动态内存管理经常会出现两种问题:一种是忘记释放内存,会造成内存泄漏;一种是尚有指针引用内存的情况下就释放了它,就会产生引用非法内存的指针。

为了更加容易(更加安全)的使用动态内存,引入了智能指针的概念。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象

标准库提供的两种智能指针,区别在于管理底层指针的方法不同,

    1. shared_ptr允许多个指针指向同一个对象,
    2. unique_ptr则“独占”所指向的对象。
    3. 标准库还定义了一种名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象,这三种智能指针都定义在memory头文件中。

 

Ref: C++中的智能指针

自C语言以来,指针就是一个重要却又充满了麻烦的特性。使用指针的一个理由是在作用域以外使用引用语义。但是,指针的生命期和所指对象的生命期的确认是十分恼人的一件事情,尤其是多个指针指向同一个对象。例如,为了在多个集合中拥有同一个对象,必须要给每个集合传入指针。在理想的情况下,其中一个指针被销毁了,没有任何问题会发生(即没有空悬指针,也不会重复删除被引用的对象),当指向对象的最后一个指针被销毁时,该对象才会被销毁(即没有资源泄漏)。

为了避免各种问题,一种通用的解决方案是使用智能指针。智能指针之所以智能是因为它们可以支持程序员来避免上述的问题。例如,智能指针可以 智能地知道 它是不是最后一个指向对象的指针,并且据此可以实现由对象的最后一个指针来决定对象的销毁。

但是仅仅只有一种智能指针是不足够的。智能指针针对各种情况是十分智能的,但会导致其他方面的延时,因为要为智能付出代价。即便使用智能指针,也会存在误用和产生错误的情况。

 

 

生命周期

一、变量de底层原理

构建、析构

#include <iostream>
#include <stack>
#include <memory>

using namespace std;



struct X {
    X() { cout << "X() ";}
    ~X() { cout << "~X() ";}
};

struct Y {
    Y() { cout << "Y() ";}
    ~Y() { cout << "~Y() ";}
};

class A {
    X x;
public:
    A() { cout << "A() ";}
    ~A() { cout << "~A() ";}
};

class B: public A {
    Y y;
public:
    B() { cout << "B() ";}
    ~B() { cout << "~B() ";}
};

class S {
public:
    S() { cout << "S() ";}
    ~S() { cout << "~S() ";}
};


/////////////////////////////////////////////////////////////

int main()
{
    cout << "Hello World!" << endl;
#if 1
    B b;
#else
    A a;
    {
        cout << "1" << endl;
        A &s = a;
        cout << "2" << endl;
    }
    cout << "3" << endl;
#endif


    return 0;
}
View Code

Output: 

 

 

二、变量分类

变量类型

生命周期 Lifetime: the period of time in which memory is allocated for an object

Different kinds of objects:

      • 静态变量 Static objects: allocated in a global (static) area
      • 栈对象 Local (or automatic or stack) objects
      • 堆对象 Heap objects

 

是否命名

Named objects: (named by the programmer): their lifetimes determined by their scope

Heap objects (unnamed objects): their lifetimes determined by the programmer

 

大括号作用域

Local Objects - 大括号的重要性

 

常对象作用域

const - 对 lifetime的影响

 

命名空间

三大永生:

    1. static Objects
    2. Global Objects
    3. namespace      <----

 

 

三、新建 Object

堆内存分配

int *pi = new int {1}

 

pi, pj 是 named local objects。

int *f() {
    int *pi = new int{1}    //unnamed heap object
    return pi;
}

int main() {
    int *pj = f();
    delete pj;
}

 

另外一些特殊的例子

特殊的 new (Dynamically Allocate and Initialise Objects)。

  注意:vector<int>内存分配失败的话,可以“自定义处理”。

 

内存耗尽 - 异常

虽然现代计算机通常都配备大容量内存,但是自由空间被耗尽的情况还是有可能发生。一旦一个程序用光了它所有可用的空间,new表达式就会失败。

默认情况下,如果new不能分配所需的内存空间,他会抛出一个 bad_alloc 的异常,我们可以改变使用new的方式来阻止它抛出异常。

//如果分配失败,new返回一个空指针
int *p1 = new int;        // 如果分配失败,new抛出std::bad_alloc
int *p2 = new (nothrow)int;   // 如果分配失败,new返回一个空指针

 

构建、拷贝、移动

Ref: Error: double free or corruption

#include <queue>
using namespace std;

class Test{
    int *myArray;

public:
    Test(){
        myArray = new int[10];
    }

    ~Test(){
        delete[] myArray;
    }

};


int main(){
    queue<Test> q
    Test t;
    q.push(t);
}

 

注意点: delete 和 delete []的真正区别

delete 只会调用一次析构函数,而 delete[] 会调用每一个成员的析构函数。

在 More Effective C++ 中有更为详细的解释:“当delete操作符用于数组时,它为每个数组元素调用析构函数,然后调用operator delete来释放内存”。

delete与new配套,delete []与new []配套。

 

If your object has a RAW pointer then you need to remember the rule of 3 (now the rule of 5 in C++11).

      • Constructor
      • Destructor
      • Copy Constructor
      • Assignment Operator
      • Move Constructor (C++11)
      • Move Assignment (C++11)

 

 

 

 

智能指针


Smart Memory Management

一、丑陋的方案

类似于python中的 with ... as ...,出现异常时要做什么才能完美解决所有问题。

void f() {
  X
* x = new X{};
  
try {     ...   }   catch (...) {     delete x;     throw;     // rethrow the exception   }   delete x; }

直接throw; 而后面不跟任何参数,是将所 catch 到的 exception 直接抛出,这样可以避免复制exception对象。 

 

weak_ptr 是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作。

 

 

二、叁种智能指针

unique_ptr

不能赋值、拷贝。可以转移权限,但就是不能同时服侍二主!


    • reset 方法

调用 unique_ptr 的 reset() 方法将删除与之绑定的 raw 指针,并且将 unique_ptr 对象置空

    • release 方法

直接将对绑定 raw 指针的所有权释放,该函数会将绑定的 raw 指针返回

 

函数返回指针是可以的,因为函数本身消亡,最后还是只有一个指针指向资源。

 

move 转移法,替代了 release + reset 这个组合方法。

    • std::move 方法

跟左值右值有关,具体详见:[c++] Copy Control

 

 

shared_ptr

简而言之,share_ptr的目标是自动释放对象关联的资源,当对象不再被需要的时候。

会记录有多少个shared_ptrs共同指向一个对象。这便是所谓的引用计数(reference counting)。一旦最后一个这样的指针被销毁,也就是一旦某个对象的引用计数变为0,这个对象会被自动删除。

这在非环形数据结构中防止资源泄露很有帮助。使得指针可以共享对象,并且不用考虑内存泄漏问题

1) 为了避免隐式转换,智能指针不能使用赋值的方式初始化,当然使用括号初始化或者列表初始化是没有问题的。

2) 另一种初始化的方法是使用make_shared<>,它是一种更好且更安全的方法:因为使用new时会创建一个对象,计算它的引用计数时又会创建一个对象,而make_shared只会创建一个对象,并且不会出现控制模块失效的情况。

3) 另一种可选方案是先定义一个智能指针再进行赋值。但是不能使用赋值运算符(=),必须使用reset函数。

 

链接:老板不让用shared_ptr,会是什么原因?

我能想到的原因是由这些指针管理的情形同时有如下特征:

1. 对象本身比较小,可能与shared_ptr引用控制块的大小在一个数量级。

2. 指针基本上是独占对象的,没有共享。(你可以用std::unique_ptr啊!)

3. 小内存环境,对内存占用非常敏感。

4. 对象数量异常多。

5. 不可避免的循环引用。

但是话又说回来,如果真出现了上面前4点这些情况。说明内存上需要自己额外下点功夫。使用自定义的分配器管理和使用内存,合理优化分配策略以减少碎片的产生,这些事情往往又不是简单的原生new / delete能做好的。

总之,如果shared_ptr都出问题了,那么使用原生指针出问题的日子也差不了几天了。

 

【可以参考原链接的例子:https://zhuanlan.zhihu.com/p/71649913

auto p = make_shared<int>(10);
shared_ptr<int> q = p;

cout << *p << endl;
p.reset();
cout << *q << endl;
p.reset();

不用delete,而是reset()。

 

// shared_ptr::get example
#include <iostream>
#include <memory>

int main () {
  int* p = new int (10);
  std::shared_ptr<int> a(p);

  if (a.get()==p)
    std::cout << "a and p point to the same location\n";

  // Three ways of accessing the same address:
  std::cout << *a.get() << "\n";
  std::cout << *a << "\n";
  std::cout << *p << "\n";

  return 0;
}
    • get 方法

获得指针的“地址”,

int main(void)
{
    int *p = new int (10);
    std::shared_ptr<int> a(p);
    
    cout << a << endl;
    cout << *a << endl;
    cout << a.get() << endl;
    cout << *a.get() << endl;
    cout << p << endl;
    cout << *p << endl;
 
    return 0;
}
  0x55aa093dce70
  10
  0x55aa093dce70
  10
  0x55aa093dce70
  10 
Output

 

#include<iostream>
#include<memory>

int main()
{
  int* test = new int(10);

  std::shared_ptr<int> t1_ptr(nullptr);
  std::shared_ptr<int> t2_ptr(test);

  auto q1 = t1_ptr;
  auto q2 = t2_ptr;
  auto p1 = t1_ptr;
  auto p2 = t2_ptr;

  std::cout << "the t1_ptr's ref count is " << t1_ptr.use_count() << std::endl;
  std::cout << "the t2_ptr's ref count is " << t2_ptr.use_count() << std::endl;

  t1_ptr = t2_ptr;
  std::cout<<"after t1_ptr = t2_ptr,the t1_ptr's ref count is " << t1_ptr.use_count() << std::endl;

  return 0;
}
    • use_count 方法

内存的指针引用计数。

 

 

weak_ptr

weak_ptr 是 为配合shared_ptr而引入 的一种智能指针来协助shared_ptr工作,

它可以从一个shared_ptr或另一个weak_ptr对象构造,它的构造和析构 不会引起 引用记数的增加或减少。

没有重载*和->但可以使用lock获得一个可用的shared_ptr对象。

 

  • "闭环" 相互引用

Ref: http://www.cnblogs.com/TianFang/archive/2008/09/20/1294590.html

#include <string>
#include <iostream>
#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>

class parent;
class children;

typedef boost::shared_ptr<parent>   parent_ptr;
typedef boost::shared_ptr<children> children_ptr;

class parent
{
public:
    ~parent() { std::cout <<"destroying parent\n"; }

public:
    children_ptr children;   // 不要用shared_ptr,改为weak_ptr即可解决问题
};

class children
{
public:
    ~children() { std::cout <<"destroying children\n"; }

public:
    parent_ptr parent;
};


void test()
{
    parent_ptr   father(new parent());
    children_ptr son(new children);
  
// 互相引用对方 father
->children = son; son->parent = father; } void main() { std::cout<<"begin test...\n"; test(); std::cout<<"end test.\n"; }

运行该程序可以看到,即使退出了test函数后,由于parent和children对象互相引用,它们的引用计数都是1,不能自动释放,并且此时这两个对象再无法访问到。这就引起了c++中那臭名昭著的内存泄漏。

一般来讲,解除这种循环引用有下面有三种可行的方法:

    1. 当只剩下最后一个引用的时候需要手动打破循环引用释放对象。
    2. 当parent的生存期超过children的生存期的时候,children改为使用一个普通指针指向parent。
    3. 使用弱引用的智能指针打破这种循环引用。 <-- 推荐

虽然这三种方法都可行,但方法1和方法2都需要程序员手动控制,麻烦且容易出错。这里主要介绍一下第三种方法和boost中的弱引用的智能指针boost::weak_ptr。

 

  • 强引用 & 弱引用

一个强引用,当被引用的对象活着的话,这个引用也存在(就是说,当至少有一个强引用,那么这个对象就不能被释放)。boost::share_ptr就是强引用

相对而言,弱引用当引用的对象活着的时候不一定存在。仅仅是当它存在的时候的一个引用。弱引用并不修改该对象的引用计数,这意味着弱引用它并不对对象的内存进行管理,

在功能上类似于普通指针,然而一个比较大的区别是:弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。

 

boost::weak_ptr 必须从一个boost::share_ptr或另一个boost::weak_ptr转换而来,这也说明,进行该对象的内存管理的是那个强引用的boost::share_ptr。boost::weak_ptr只是提供了对管理对象的一个访问手段

boost::weak_ptr 除了对所管理对象的基本访问功能(通过get()函数)外,还有两个常用的功能函数:

1) expired() 用于检测所管理的对象是否已经释放;

2) lock() 用于获取所管理的对象的强引用指针。

 

  • weak_ptr 打破循环引用

由于弱引用不更改引用计数,类似普通指针,只要把循环引用的一方使用弱引用,即可解除循环引用。对于上面的那个例子来说,只要把children的定义改为如下方式,即可解除循环引用:

class children
{
public:
    ~children() { std::cout <<"destroying children\n"; }

public:
    boost::weak_ptr<parent> parent;
};

最后值得一提的是,虽然通过弱引用指针可以有效的解除循环引用,但这种方式必须在程序员能预见会出现循环引用的情况下才能使用,也可以是说这个仅仅是一种编译期的解决方案,如果程序在运行过程中出现了循环引用,还是会造成内存泄漏的。因此,不要认为只要使用了智能指针便能杜绝内存泄漏。毕竟,对于C++来说,由于没有垃圾回收机制,内存泄漏对每一个程序员来说都是一个非常头痛的问题。

 

  • 不适合数组

Smart Pointers for Arrays, shared_ptr does not provide support for arrays.

 

 End.

posted @ 2016-12-21 21:20  郝壹贰叁  阅读(437)  评论(0编辑  收藏  举报