C++ 智能指针(共享指针、唯一指针、自动指针)

1. 概述

        当类中有指针成员时,一般有两种方式来管理指针成员:一是采用值型的方式管理,每个类对象都保留一份指针指向的对象的拷贝;另一种更优雅的方式是使用智能指针,从而实现指针指向的对象的共享。

        智能指针是存储指向动态分配对象指针的类。除了能够在适当的时间自动删除指向的对象外,他们的工作机制很像C++的内置指针。从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。智能指针在面对异常的时候格外有用,因为他们能够确保正确的销毁动态分配的对象。他们也可以用于跟踪被多用户共享的动态分配对象。

        智能指针在 C++11 中被引入,分为以下三种:

  •  shared_ptr:共享指针
  • unique_ptr:唯一指针
  • auto_ptr:自动指针 

         使用智能指针时需要加上头文件 #include

        智能指针(smart pointer)的一种通用实现技术是使用引用计数(reference count)。智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象的指针指向同一对象 。实现原理为:每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,析构函数减少引用计数(如果引用计数减至0,则删除基础对象)。

        接下来,我们具体了解以下share_ptr、unique_ptr、auto_ptr的使用与实现。

2. 共享指针(share_ptr)

        shared_ptr 允许多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。

        共享指针不是线程安全的;C++标准库提供了针对共享指针的原子接口;针对共享指针本身的操作是原子的,但并不包含该指针引用的具体值。因此,shared_ptr内部的引用计数是线程安全的,而对象的读取需要加锁。

2.1 初始化

        shared_ptr可以通过 reset 方法初始化shared_ptr。

#include 
#include 
using namespace std;

int main(){
    shared_ptr p(new int(1));
    shared_ptr p1 = p;    // p和p1指向同一块内存 
    shared_ptr p2;
    // 使用reset初始化share_ptr
    p2.reset(new int(1));    // p2和p3指向同一块内存
    shared_ptr p3(p2);
    cout << (int*)&(*p) << endl;
    cout << (int*)&(*p1) << endl;
    cout << (int*)&(*p2) << endl;
    cout << (int*)&(*p3) << endl;
}

运行结果:

         还可以通过std::make_shared辅助函数初始化,尽量使用std::make_shared初始化共享指针。

auto p1 = std::make_shared(10);
auto p2 = std::make_shared(10,"s");

        可以通过重载的bool类型操作符判断智能指针是否为空,即是否未初始化。

if(bool(ptr))
    cout << "the ptr is nullptr"<

 2.1 获取原始指针与指定删除器

        我们可以通过get方法获取原始指针。

// 获取智能指针p的原始指针
int *ptr = p.get();

        删除器可以是普通函数、匿名函数、函数指针等符合签名要求的可调用的对象,只有最后引用对象的共享指针销毁时才会销毁对象。

void deleteIntPtr(int* p)
{
    delete p;
}
std::shared_ptr p5(new int,deleteIntPtr);
// 或者lambda表达式
std::shared_ptr p6(new int,[](int* p){delete p;});

        注意:管理动态数组时,需要指定删除器,shared_ptr默认的删除器(使用的是 delete )不支持数组对象。

std::shared_ptr p6(new int[10],[](int* p){delete[] p;});

        或者使用std::default_delete(使用的是 delete[] )作为删除器

std::shared_ptr p7(new int[10],std::default_delete());

        此外还可以通过封装方法让shared_ptr支持数组。

template
std::shared_ptr make_shared_array(size_t size)
{
    return std::shared_ptr(new T[size],std::default_delete());
}
std::shared_ptr p8 = make_shared_array(10);
std::shared_ptr p9 = make_shared_array(10);

2.3 注意事项

        (1)不能使用原始指针初始化多个shared_ptr,否则会造成二次释放同一内存

int* p11 = new int;
std::shared_ptr p12(p11);
std::shared_ptr p13(p11);

        (2)不要在实参中创建shared_ptr,应该先创建一个智能指针,再使用。

deleteIntPtr(std::shared_ptr(new int));//错误的
std::shared_ptr p14(new int());
deleteIntPtr(p14);    //OK

        (3)返回this指针要通过shared_from_this()

struct A
{
    std::shared_ptr getSelf()
    {
        return std::shared_ptr(this); //错误,
    }
};
int main()
{
    std::shared_ptr sp1(new A);
    std::shared_ptr sp2 = sp1->getSelf(); //会导致重复析构
    return 0;
}

        应采用如下方式,使用enable_shared_from_this类的shared_from_this()方法返回this指针:

class A:public std::enable_shared_from_this
{
public:
    std::shared_ptr getSelf()
    {
        return shared_from_this();
    }
};

        (4)避免循环引用, 循环引用会导致堆内存无法正确释放,导致内存泄漏。

struct A;
struct B;
struct A
{    
    std::shared_ptr bptr;    
    ~A(){cout << "A is deleted!"< aptr;
    ~B() {cout << "B is deleted!"< ap(new A);        
        std::shared_ptr bp(new B);
        ap->bptr = bp;
        bp->aptr = ap;
    }
}

        上述代码中,ap 和 bp 都无法正确释放内存,造成内存泄漏。

        解决方法:可以将A或B的任何一个shared_ptr 成员改为 weak_ptr。

        (5) 同类型的共享指针才能使用共享指针

shared_ptr p1(new int(1));
shared_ptr p2(new int(2));
shared_ptr p3(new double(3));
bool c1 = p1 > p2;      // 同类型的可以进行比较操作, <、>、== 等
//bool c2 = p1 < p3;    // 不允许

        共享指针强制转换运算符允许将其中包含的指针强制转换为其他类型指针;

        只能使用智能指针特定的强制转换运算符:

  • static_pointer_cast
  • dynamic_pointer_cast
  • const_pointer_cast
share_ptr point(new int(1)); //共享指针内部保存void型指针
share_ptr point(static_cast(point.get())); //compile error,undefined pointer
static_pointer_cast(point);    // OK

2.4 weak_ptr

        weak_ptr(弱指针)是共享指针辅助类,其允许共享但不拥有对象,因此不会增加关联对象的引用次数。它指向一个由share_ptr管理的对象而不影响所指对象的生命周期。不论是否有weak_ptr指向,一旦最后一个指向对象的share_ptr被销毁,对象就会被释放,任何弱指针都会自动变为空。

        不能使用运算符*和->直接访问弱指针的引用对象,而是使用lock函数生成关联对象的共享指针(可能为空)。

        weak_ptr 是用来解决shared_ptr相互引用时的死锁问题,和 shared_ptr 之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr

weak_ptr 的基本用法:

1. 通过 use_count() 获得当前观测资源的引用计数

2. 通过 expired() 判断观测的资源是否已经被释放(等同于 use_count == 0,但更快)

3. 通过 lock() 方法获取所检测的 shared_ptr

#include 
#include 
using namespace std;

int main()
{
    shared_ptr sptr(new int(10));
    shared_ptr p1 = sptr;
    cout << sptr.use_count() << endl;   // 引用计数为 2
    cout << p1.use_count() << endl;     // 引用计数为 2
    // weak_ptr 并不会影响引用计数
    weak_ptr p2 = p1;
    cout << p1.use_count() << endl;     // 引用计数为 2
    cout << *p2.lock() << endl;         // 打印结果为 10
    return 0;
}

3. 唯一指针(unique_ptr)

        unique_ptr “唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象。相比与原始指针 unique_ptr 用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr 指针本身的生命周期:从 unique_ptr 指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。

        unique_ptr 继承了自动指针auto_ptr,更不易出错;抛出异常时可最大限度避免资源泄漏。

3.1 unique_ptr 的使用

        唯一指针不可使用赋值语法进行初始化,应使用普通指针初始化。

#include 
#include 
 
int main() {
    {
        std::unique_ptr uptr(new int(10));  //绑定动态对象
        //std::unique_ptr uptr2 = uptr;  //不能賦值
        //std::unique_ptr uptr2(uptr);  //不能拷貝
        std::unique_ptr uptr2 = std::move(uptr); //转换所有权
        uptr2.release(); //释放所有权
    }
    //超過uptr的作用域,內存釋放
    return 0;
}

        唯一指针定义了*、->运算符,没有定义类似++的指针算法。

//创建唯一指针
unique_ptr uptr(new string("hello"));
(*uptr)[0] = 'H'; //替换第一个字符
uptr->append(" World."); //追加字符串
cout << *uptr << endl; //运行结果: Hello World.

        唯一指针可以为空。

unique_ptr uptr;
// 第一种
uptr.reset();
// 第二种
uptr = nullptr;

        release()可以让唯一指针返回其拥有的对象,并失去指向该对象的唯一性。

unique_ptr uptr(new int(10));
int *p = uptr.release();
cout << *p << endl;    // 运行结果: 10

        检查唯一指针是否拥有对象的三种方法:

// 方法一:调用重载的操作符bool()
if (uq) //如果uq不为空

// 方法二:与nullptr进行比较
if (uq != nullptr) //如果uq不为空
// 方法三:check unique_ptr中的原始指针是否为空
if (uq.get() != nullptr) //如果uq不为空

        唯一指针可以指向数组。

        针对数组的接口不提供运算符 * 和 ->,而提供运算符 []。

unique_ptr p5(new int[10]);
p5[9] = sizeof(p5);

        unique_ptr指定删除器(相较于share_ptr的写法,模板参数有所不同)

  • share_ptr p(q, d)
  • unique_ptr u(q)
class ClassA {};

class ClassADeleter
{
public:
    void operator () (ClassA* obj) {
        cout << "call ClassA object's Deleter" << endl;
        delete obj;
    }
};

int main()
{
    unique_ptr up(new ClassA());
    return 0;
}

        定义删除器的方法是必须将删除器的类型指定为第二个模板参数。

        删除器类型可以是函数、函数指针或函数对象。

unique_ptr 
uq(new int[666],
	[](int* pointer) {
	...
	delete[] pointer;
	});

unique_ptr> 
uq(new int[666],
	[](int* pointer) {
	...
	delete[] pointer;
	});

auto T = [](int* pointer) {
    ...
    delete[] p;
};
unique_ptr> uq(new int[666], T);

        销毁其它类型资源时,需要指定函数或lambda表达式时,必须将删除程序的类型声明为void(*)(T *)或 function 或使用decltype。

4. 自动指针(auto_ptr)

        C++98中存在,于C++11中使用唯一指针替换其它。

4.1 auto_ptr 的简易实现

template
class autoptr{
private:
    T* p_t;   //此处p_t指向堆对象,当autoptr离开作用域时,触发析构函数,析构函数中执行delete,于是堆对象就能自动释放了
    T* release(){
        T* p = p_t;
        p_t = NULL;
        return p;
    }
    void reset(T* p){
        if(p!=p_t){
            delete p_t;
            p_t = p;
        }    
    }
public:
    autoptr(T* p=NULL):p_t(p){}
    ~autoptr(){
        if(p_t)  delete p_t;
    }
    //拷贝构造
    autoptr(autoptr& that):p_t(that.release()){}
    //重载运算符
    //拷贝构造与赋值运算的差异(我觉得内存的处理差不多,赋值运算内存空间是之前就存在的,而拷贝构造需要新创建内存空间)
    autoptr& operator=(autoptr& that){  
        if(this!=&that)
            reset(that.release());
        return *this;
    }
    T& operator*(void)const{
        return *p_t;
    }
    T* operator->(void)const{
        return &**this;
    }
};

4.2 针对数组类型的特化

        如果p_t指向数组,那么显然autoptr就不起作用了,因为数组的释放需要的是delete[]。

        因为函数模板不能做局部特化,所以在此只能选择类模板的局部特化。(函数可以重载,所以不能做局部特化)

        下面是针对数组类型的特化:

        

//代码微调即可
template
class autoptr{
private:
    T* p_t;   
    T* release(){
        T* p = p_t;
        p_t = NULL;
        return p;
    }
    void reset(T* p){
        if(p!=p_t){
            delete[] p_t;
            p_t = p;
        }    
    }
public:
    autoptr(T* p=NULL):p_t(p){}
    ~autoptr(){
        if(p_t)  delete[] p_t;
    }
    autoptr(autoptr& that):p_t(that-release()){}
    autoptr& operator=(autoptr& that){  
        if(this!=&that)
            reset(that.release());
        return *this;
    }
    T& operator*(void)const{
        return *p_t;
    }
    T* operator->(void)const{
        return &**this;
    }
};

你可能感兴趣的:(c++,开发语言)