RAII与智能指针

1.RAII

1.1RAII理解

RAII(Resource Acquisition ls lnitialization) ,资源获取即初始化,是由 c++之父 Biarne Stroustrup 提出的。
使用局部对象管理资源的技术称为资源获取即初始化,这里的资源主要是指操作系统中有限的东西如内存、网络套接字,互斥量,文件句柄等等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入。

1.2RAII的原理

资源的使用一般经历三个步骤:

  1. 获取资源(创建对象),
  2. 使用资源,
  3. 销毁资源 (析构对象)。

但是资源的销毁往往是程序员经常忘记的一个环节,所以程序界就想如何在程序员中让资源自动销毁呢?解决问题的方案是: RAIl,它充分的利用了 C++语言局部对象自动销毁的特性来控制资源的生命周期。

1.2.1简单的例子说明局部对象的自动销毁的特性

#include <iostream>
#include <string>

using namespace std;

class Student
{
private:
    const string s_name;
    int s_age;
public:
    Student(const string name = "",int age = 0):
        s_name(name),s_age(age){
            cout<<"Construct a Student"<<endl;
        }
    ~Student(){
        cout<<"Destory a Student"<<endl;
    }
};

int main(){
    Student stu1; #局部对象

    return 0;
}

在这里插入图片描述
从 Student类可以看出,当我们在 main 函数中声明一个局部对象的时候,会自动调用构造函数进行对象的初始化,当整个 main 函数执行完成后,自动调用析构函数来销毁对象,整个过程无需人工介入,由操作系统自动完成。
于是,基于上述实现方式,可以想到,当我们在使用资源的时候,在构造函数中进行初始化,在析构函数中进行销毁

1.2.2 RAII 过程

  1. 设计一个类封装资源
  2. 在构造函数中初始化
  3. 在析构函数中执行销毁操作
  4. 使用时声明一个该类的对象

2.智能指针

智能指针其实是一个类,是对普通指针进行了封装,将其作为参数传入智能指针的构造函数实现绑定。只不过通过运算符重载让它“假装”是一个指针,也可以进行解引用等操作。既然智能指针是一个类,对象都存在于栈上,那么创建出来的对象在出作用域的时候(函数或者程序结束)会自己消亡,所以在这个类中的析构函数中写上delete就可以完成智能的内存回收,避免忘记释放指针指向的内存地址造成内存泄漏。。

C11 里面的四个智能指针:

  1. auto ptr,
  2. unique ptr,
  3. shared ptr,
  4. weak ptr
    下面对这四个智能指针进行一一介绍:

2.1 auto_ptr

auto_ptr是较早版本的智能指针,已经被 C11 弃用,C98 中 auto_ptr 所做的事情,就是动态分配对象以及当对象不再需要时自动执行清理。
使用时,需要包含头文件:memory

2.1.1auto_ptr的使用

构造函数与析构函数

auto_ptr 在构造时获取对某个对象的所有权(ownership),在析构时释放该对象,提高代码安全性,因为我们不必关系应该如何释放auto ptr,也不用担心发生异常时会有内存泄漏。

int *p = new int(10);

auto_ptr<int> ap(p);

注意点

  1. auto ptr 析构的时候会删除他所拥有的那个对象,所以我们要注意两个auto_ptr 不能同时拥有同一个对象。
int *p = new int(10);

auto_ptr<int> ap1(p);
auto_ptr<int> ap2(p);

因为 ap1 与 ap2 都认为指针 p 是归它管的,在析构时都试图删除 p,两次删除同一个对象的行为在C++标准中是未定义的。所以我们必须防止这样使用 auto ptr

  1. 不应该用 auto ptr 来管理一个数组指针
string * sar = new string[10];

auto_ptr<string> ap3(sar);
  1. 构造函数的 explicit 关键词有效阻止从一个“裸指针隐式转换成 auto ptr 类型
拷贝构造函数与赋值

auto_ptr 要求其对“裸”指针的完全占有性。也就是说一个“裸”指针不能同时被两个以上的 auto_ptr 所拥有。那么,在拷贝构造或赋值操作时,就必须作特殊的处理来保证这个特性。
auto_ptr的做法是“所有权转移reset(Myptr.release())”,即拷贝或赋值的源对象将失去对“裸”指针的所有权,所以,与一般拷贝构造函数,赋值承数不同,auto_ptr 的拷贝构造函数,赋值函数的参数为引用而不是常引用(const reference).
当然,一个 auto ptr 也不能同时拥有两个以上的“裸”指针,所以,拷贝或赋值的目标指针将先释放其所拥有的对象,然后管理旧指针的资源,因此旧指针即指向nullptr,在访问旧指针时会出现悬空现象
注意点:

  1. auto ptr 被拷贝或被赋值后,其已经失去对原对象的所有权,这个时候,对这个 auto ptr 的提领(dereference)操作是不安全的。
int *p = new int(10);
auto_ptr<int> ap1(p);
auto_ptr<int> ap2 = ap1;

cout<<*ap1<<endl; //error,此时ap1已经失去对p指针的拥有权
  1. 将 auto_ptr 作为函数参数按值传递时,函数调用过程中在函数的作用域中会产生一个局部对象来接收传入的 auto ptr(拷贝构造),这样,传入的实参 auto ptr 就失去了其对原对象的所有权,而该对象会在函数退出时被局部 auto ptr 删除。
void fun(auto_ptr<int> ap)
{
    cout<< *ap<< endl;
}

int main(){

    
    auto_ptr<int> ap1(new int(10));

    fun(ap1);

    cout<<*ap1<<endl; //error,经过fun(ap1)函数调用,ap1已经不再拥有任何对象了

    return 0;
}

这种情况容易出错,所以 auto ptr 作为函数参数按值传递是一定要避免的。或许用auto ptr 的指针或引用作为函数参数或许可以,但是我们并不知道在函数中对传入的 auto ptr 做了什么,如果当中某些操作使其失去了对对象的所有权,那么这还是可能会导致致命的执行期错误。也许用 const reference 的形式来传递 auto ptr 会是一个不错的选择。

  1. auto_ptr在基类和子类隐式转换
class Object{ };

class Base: public Object{ };

auto_ptr<Object> apobj = auto_ptr<Base>(new Base);
提领操作

提领操作有两个操作:

  1. 返回其所拥有的对象的引用,
  2. 实现了通过 auto ptr 调用其所拥有的对象的成员(首先要确保这个智能指针确实拥有某个对象,否则,这个操作的行为即对空指针的提领是未定义)。
auto_ptr其它函数
  1. get() 用来显式的返回 auto_ptr 所拥有的对象指针(返回值为0则为nullptr)。我们可以发现,标准库提供的 auto_ptr 既不提供从裸”指针到auto _ptr 的隐式转换(构造函数为 explicit),也不提供 auto_ptr 到指针的隐式转换,从使用上来讲可能不那么的灵活,考虑到其所带来的安全性还是值得的。
  2. release()用来释放所有权。
  3. reset()用来接收所有权,如果接收所有权的 auto ptr 如果已经拥有某对象,必须先释放该对象.

2.1.2auto_ptr使用的注意事项

  1. auto ptr 不能指向数组
  2. auto_ptr 不能共享所有权
  3. auto_ptr 不能通过复制操作来初始化
  4. auto_ptr 不能放入容器中使用
  5. auto ptr 不能作为容器的成员

2.2 unique_ptr

C11 中使用 unique_ptr 替代auto_ptr
unique 是独特的、唯一的意思,故名思议,unique ptr 可以“独占”地拥有它所指向的对象,是一种定义在<memory中的智能指针(smart pointer),保证一个对象同一时间只有一个智能指针。
unique_ptr 对象中保存指向某个对象的指针,当它本身被删除或者离开其作用域时会自动释放其指向对象所占用的资源。

2.2.1unique_ptr的使用

unique_ptr的创建

要想创建一个 unique ptr,需要将一个 new 操作符返回的指针传递给 unique ptr 的构造函数.

int main()
{    
    unique_ptr<int> pt(new int(10));

    cout<< *pt<< endl;  //10

    return 0;
}
unique_ptr不能进行拷贝构造和赋值操作
unique_ptr& operator=( const unique_ptr& ) = delete;

在这里插入图片描述

unique_ptr可以进行移动构造和移动赋值操作

unique ptr 虽然没有支持普通的拷贝和赋值操作,但却提供了一种移动机制来将指针的所有权从一个 unique ptr转移给另一个 unique ptr。如果需要转移所有权,可以使用 std:.move()函数

int main(){
    unique_ptr<int> pt1(new int(10));

    cout<< *pt1<< endl;  //10

    unique_ptr<int> pt2(move(pt1)); 
    cout<<"--------------"<<endl;
    //cout<< *pt1<< endl;  //出错,为空
    cout<< *pt2<< endl;  //10

    unique_ptr<int> pt3 = move(pt2); 
    cout<<"~~~~~~~~~~~~~~~~"<<endl;
    //cout<< *pt1<< endl;  //出错,为空
    //cout<< *pt2<< endl;  //出错,为空
    cout<< *pt3<< endl;  //10

    return 0;
}

在这里插入图片描述

unique_ptr虽然没有拷贝操作,但是可以从函数中返回unique_ptr
unique_ptr<int> clone(int a)
{
    unique_ptr<int> ptr(new int(a));

    return ptr;//返回unique_ptr
}

int main()
{
    int val = 11;

    unique_ptr<int> pt1 = clone(val);

    cout<< *pt1<< endl;  //11

    return 0;
}

2.2.2unique_ptr使用场景

  1. 为动态申请的资源提供异常安全保证
void fun_(int a)
{
    int *p = new int(5);

    //.....(抛出异常)
}

void fun(int a)
{
    unique_ptr<int> ptr(new int(a));

    //.....(抛出异常)
    
}

fun_是传统的写法:在动态申请内存后,有可能接下来的代码由于抛出异常或者提前退出 (if 语句)而没有执行 delete 操作。
解决的方法是使用 unique ptr 来管理动态内存,只要 unique ptr 指针创建成功,其析构函数都会被调用。确保动态资源被释放。
2. 返回函数内动态申请资源的所有权

unique_ptr<int> fun(int a)
{
    unique_ptr<int> ptr(new int(a));

    return ptr;//返回unique_ptr
}

int main()
{
    int a = 12;
    unique_ptr<int> ret = fun(a);
    
    cout<< *ret<< endl; //12
    
    //函数结束后,自动释放资源

    return 0;
}
  1. 在容器中保存指针
int main()
{
    vector<unique_ptr<int>> vec;
    unique_ptr<int> pt(new int(5));
    
    vec.push_back(move(pt));

    for(int i = 0;i < vec.size();i++){
        cout<< *vec[i]<< endl;  // 5
    }
    

    return 0;
}
  1. 管理动态数组
int main()
{   

    unique_ptr<vector<int>[]> pt(new vector<int> {1,2,3,4,5});
        
    
    for(int i = 0;i < pt.get()->size();i++){
        cout<<pt.get()->at(i)<<endl;    //1,2,3,4,5
    }

    return 0;
}

2.3 shared_ptr(共享指针)

shared ptr 是一个引用计数的智能指针,用于共享对象的所有权。也就是说它允许多个指针指向同一个对象,并且维护了一个共享的引用计数器,当这个对象所有的智能指针被销毁时(引用计数器==0)就会自动进行回收(共享指针的析构函数就把指向的内存区域释放掉)。

class Object
{
private:
    int val;
public:
    Object(int x = 0):val(0){
        cout<< "Construct Object"<< endl;
    }
    ~Object(){
        cout<< "Destory Object"<< endl;
    }

};

int main()
{
        
    shared_ptr<Object> pobj(new Object(13));
    //指针引用对象的个数
    cout<< " pobj:"<< pobj.use_count()<<endl; //1

    shared_ptr<Object> pobj1 = pobj;
    
    cout<< " pobj:"<< pobj.use_count()<<endl; //2
    cout<< " pobj1:"<< pobj1.use_count()<<endl; //2


    return 0;
}

在这里插入图片描述
一方面,跟 STL 中大多数容器类型一样,shared ptr 也是模板类,因此在创建 shared ptr 时需要指定其指向的类型。另一方面,shared ptr 指针允许让多个该类型的指针共享同一堆分配对象。同时 shared ptr 使用经典的“引用计数”方法来管理对象资源,每个 shared ptr 对象关联一个共享的引用计数。

2.3.1 shared_ptr的使用

shared_ptr的创建
  1. 调用 make shared 库函数,该函数会在堆中分配一个对象并初始化,最后返回指向此对象的share ptr 实例(安全、高效
  2. 先 new 出一个对象,然后把其原始指针传递给 share ptr 的构造函数
shared_ptr<int> ptm = make_shared<int>(16);

cout<< *ptm<< endl; //16

shared_ptr<int> ptn(new int(15));
    
cout<< *ptn<< endl; //15

shared_ptr的访问
  1. 解引用操作符*获得原始对象进而访问其各个成员,
  2. 指针访问符->来访问原始对象的各个成员。
shared_ptr的拷贝和赋值操作

对于 shared ptr 在拷贝和赋值时的行为是,每个 shared ptr 都有一个关联的计数值,通常称为引用计数。无论何时我们拷贝一个 shared ptr,计数器都会加1。当我们将一个指针的对象交给另一个指针管理后,其关联的引用计数就会减1。
例如,当用一个 shared ptr 初始化另一个 shred ptr时,或将它当做参数传递给一个函数以及作为函数的返回值时它所关联的计数器就会加1。
当我们给 shared ptr 赋予一个新值或是 shared ptr 被销(例如一个局部的 shared ptr 离开其作用域)时,计数器就会递减。
shared ptr 对象的计数器变为 0,它就会自动释放自己所管理的对象。

class Object
{
private:
    int val;
public:
    Object(int x = 0):val(0){
        cout<< "Construct Object"<< endl;
    }
    ~Object(){
        cout<< "Destory Object"<< endl;
    }

};

int main()
{
        
    shared_ptr<Object> pobj = make_shared<Object>(13);
    //指针引用对象的个数
    cout<< " pobj:"<< pobj.use_count()<<endl; //1

    shared_ptr<Object> pobj1 = pobj;
    
    cout<< " pobj:"<< pobj.use_count()<<endl; //2
    cout<< " pobj1:"<< pobj1.use_count()<<endl; //2


    return 0;
}

对比我们上面的代码可以看到: 接下来,我们用 pob初始化 pobit1,两者关联的引用计数值增加为 2。随后,函数结束,pObi和 PObi2 相继离开函数作用域,相应的引用计数值分别自减 1 最后变为 0,于是 Obiect 对象被自动释放(调用其析构函数)。

shared_ptr的引用计数

shared _ptr 提供了两个函数来检查其共享的引用计数值,分别是

  1. unique()函数用来测试该 shared ptr 是否是原始指针唯一拥有者,也就是 use count()的返回值为 1时unique()返回 true,否则返回 false。
  2. use count()函数,该函数返回当前指针的引用计数值。值得注意的是 use count()函数能效率很低,应该只把它用于测试或调试。

2.3.2 shared_ptr的线程安全

  1. (shared_ptr) 的引用计数本身是线程安全(引用计数是原子操作)
  2. 多个线程同时读同一个 shared ptr 对象是线程安全的。
  3. 如果是多个线程对同一个 shared ptr 对象进行读和写,则需要加锁
  4. 多线程读写 shared_ptr 所指向的同一个对象,不管是相同的 shared ptr 对象,还是不同的 shared ptr 对象,也需要加锁保护。

2.3.3 shared_ptr与unordered map使用

如果把 shared_ptr 放到 unordered set 中,或者用于 unrdered map 的 key, 那么要小心 hash table 退化为链表。但是其 hash value 是 shared_ptr 隐式转换为 bool 的结果。也就是说,如果不自定义 hash 函数,那么unordered {set/map] 会退化为链表。
为什么要尽量使用 make shared()? 申请被管理对象以及引用计数的内存:调用适当的构造函数初始化对象,返回shared_ptr。
为了节省一次内存分配,原来 shared_ptr x(new Obiect (10) ; 需要为 Obiect对象 和 RefCnt 各分配次内存,现在用 make shared() 的话,可以一次分配一块足够大的内存,供 Obiect 和 RefCnt 对象容身。不过Obiect 的构造函数所需的参数要传给 make shared(),后者再传给 Obiect; : Obiect),这只有在 C++11 里通过perfect forwarding(完美转发) 才能完美解决。

2.4 weak_ptr

2.4.1 weak_ptr理解

weak_ptr 是为了配合 shared ptr 而引入的一种智能指针,它指向一个由 shared ptr 管理的对象而不影响所指对象的生命周期,也就是将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ ptr 的引用计数
即weak_ptr是为了协助shared_ptr而出现的。它不能访问对象,只能观测shared_ptr的引用计数,防止出现死锁。
不论是否有 weak ptr 指向,一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放。从这个角度看weak_ptr 更像是 shared_ptr 的一个助手而不是智能指针。

2.4.2 为什么会有weak_ptr

在出现了循环引用(或环形引用)的情况下,shared_ptr。

class Child;

class Parent
{
public:
    shared_ptr<Child> child;
    ~Parent(){
        cout<< "Destory Parent"<< endl;
    }
    void Priant() const{
        cout<< "Parent-----------"<< endl;
    }
};

class Child
{
public:
    shared_ptr<Parent> parent;
    ~Child(){
        cout<< "Destory Child"<< endl;
    }
};

int main()
{
        
    shared_ptr<Parent> parent = make_shared<Parent>();
    shared_ptr<Child> child = make_shared<Child>();

    parent->child = child;
    child->parent = parent;

    child->parent->Priant();

    return 0;
}

在这里插入图片描述
上面代码的运行结果,只打印出”Parent-----------”,而并没有打印出"Destory Parent”或”Destory Child",说明 Parent 和 Child 的析构函数并没有调用到。这是因为Parent 和 Child 对象内部,具有各自指向对方的 shared_ptr,加上 parent和 child 这两个shared_ptr,说明每个对象的引用计数都是 2。当程序退出时,即使 parent 和 child 被销毁,也仅仅是导致引用计数变为了1,因此并未销毁 Parent 和 Child 对象。

2.3.3 weak_ptr使用

创建weak_ptr实例

创建一个weak_ptr时,需要用一个 shared_ptr 实例来初始化weak_ptr,由于是弱共享,weak_ptr的创建并不会影响 shared_ptr 的引用计数值

int main()
{
        
    shared_ptr<int> sp = make_shared<int>(5);

    cout<< sp.use_count()<< endl; //1
    
    weak_ptr<int> wp(sp);
    cout<< sp.use_count()<< endl; //1
    cout<< wp.use_count()<< endl; //1
    

    return 0;
}
判断weak_ptr指向对象是否存在

weak_ptr 并不改变其所共享的 shared_ptr 实例的用计数,那就可能存在 weak ptr 指向的对象被释放掉这种情况。这时,我们就不能使用 weak ptr 直接访问对象。那么我们如何判断 weak ptr 指向对象是否存在呢?C++中提供了 lock 函数来实现该功能。
如果对象存在,lock(函数返回一个指向共享对象的 shared ptr,否则返回一个空 shared ptr。

shared_ptr<int> sp = make_shared<int>(5);

//sp = nullptr;
    
weak_ptr<int> wp(sp);
    
if(shared_ptr<int> pa = wp.lock()){
    cout<< *pa <<endl;
}
else{
    cout<< wp.expired()<< endl; weak_ptr 还提供了 expired0函数来判断所指对象是否已经被销毁
    cout<< "*wp is nullptr"<< endl;
}
weak_ptr使用

weak_ptr 并没有重载 operator->和 operator 操作符,因此不可直接通过 weak_ptr 使用对象,典型的用法是调用其lock 函数来获得 shared_ptr 示例,进而访问原始对象。
最后,我们来看看如何使用 weak_ptr 来改造最前面的代码,打破循环引用问题。

class Child;

class Parent
{
public:
    weak_ptr<Child> child;
    
    ~Parent(){
        cout<< "Destory Parent"<< endl;
    }
    void Priant() {
        cout<< "Parent-----------"<< endl;
    }
};

class Child
{
public:
    weak_ptr<Parent> parent;
    ~Child(){
        cout<< "Destory Child"<< endl;
    }
};

int main()
{
        
    shared_ptr<Parent> parent1 = make_shared<Parent>();
    shared_ptr<Child> child1 = make_shared<Child>();

    weak_ptr<Parent> parent(parent1);
    weak_ptr<Child> child(child1);

    parent1->child = child1;
    child1->parent = parent1;

    //child1->parent.lock()->Priant();
    
    if(!child.expired()){
        child1->parent.lock()->Priant();
    }
    

    return 0;
}

2.5 几种智能指针的比较

unique_ptr和 shared ptr 类型指针有很大的不同: shared ptr 允许多个指针指同一对象,而 nique_ptr 在某时刻只能有一个指针指向该对象(两个 unique ptr 不能指向同一个对象)。

  1. 使用场景:
    如果程序要使用多个指向同一个对象的指针,应该选择 shared_ptr;
    如果程序要使用一个指向一个对象的指针,则可以使用 unique_ptr;
    如果使用 new [] 分配内存,应该选择 unique_ptr;
    如果函数使用 new 分配内存,并返回指向该内存的指针,将其返回类型声明为 unique_ptr 是不错的选择。

  2. 智能指针实现原理:建立所有权(ownership)概念。
    auto_ptr 和 unique_ptr 的策略:对于特定的对象,只能有一个智能指针可拥有它,这样只有拥有对象的智能指针的析构函数会删除该对象。然后,让赋值操作转让所有权。但 unique_ptr 的策略更严格,unique_ptr 能够在编译期识别错误。
    shared_ptr 采用的策略。:跟踪引用特定对象的智能指针计数,这称为引用计数(reference counting)。例如,赋值时,计数将加 1,而指针过期时,计数将减 1. 仅当最后一个指针过期时,才调用 delete。

  3. 线程安全:
    shared_ptr:引用计数在手段上使用了 atomic 原子操作,只要 shared_ptr 在拷贝或赋值时增加引用,析构时减少引用就可以了。首先原子是线程安全的,所有 shared_ptr 智能指针在多线程下引用计数也是安全的,也就是说 shared_ptr 智能指针在多线程下传递使用时引用计数是不会有线程安全问题的。 但是指向对象的指针不是线程安全的,使用 shared_ptr 智能指针访问资源不是线程安全的,需要手动加锁解锁。智能指针的拷贝也不是线程安全的。

2.6 几种智能指针的仿写

见下一篇,C++四种智能指针的仿写

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值