智能指针(转载)

https://www.jianshu.com/p/e4919f1c3a28

什么是智能指针?

智能指针是一个RAII(Resource Acquisition is initialization资源获取即初始化)类模型,用于动态分配内存。它提供所有普遍指针提供的接口,却很少发生异常。在构造是,它分配内存,当离开作用域时,它会自动释放已分配的内存。这样,程序员就从手动管理动态内存的繁杂任务重解放出来。

 

C++98提供第一种智能指针:auto_ptr。

auto_ptr:

class Test {
    public:
    Test(int a = 0):m_a(a){}
    ~Test() {
        cout << "calling destructor" << ends;
    }
};

void main() {
    std::auto_ptr<Test> p(new Test(5));
    cout << p->m_a << endl;
}

上述代码会智能地释放与指针绑定的内存。作用的过程是这样的:我们申请了一块内存来放Test对象,并且把它绑定到auto_ptr p上。当p离开作用域时,它所指向的内存块也会被自动释放。

存在问题1:

当把一个auto_ptr赋给另一个auto_ptr时,它的所有权也转移了。当我在函数间传递auto_ptr时,这就是个问题。我在Foo()中有一个auto_ptr,然后在Foo()中把指针传递给了Fun()函数,当Fun()函数执行完毕时,指针的所有权不会再返还给Foo。

class Test {
public:
    Test(int a = 0):m_a(a) {}
    ~Test() {
        cout << "calling destructor" << ends;
    }

public:
    int m_a;
};

void Fun(auto_ptr<Test> p1) {
    cout << p1->m_a << endl;
}

void main() {
    auto_ptr<Test> p(new Test(5));
    Fun(p);
    cout << p->m_a << endl;
}

由于auto_ptr的野指针行为,导致上述代码程序崩溃。过程如下:p拥有一块内存,当Fun调用时,p把关联的内存块的所有权传给auto_ptr p1,p1是p的copy(注:这里从Fun函数的定义式看出,函数参数时值传递,所有把p的值拷进函数中,这个时候p中已经没有内容了),这时p1就拥有了之前p拥有的内存块。在Fun函数执行完了,p1离开了作用域,所以p1关联的内存块也就释放了。此时进行main函数,p中什么都没有,这是程序崩溃的原因。下一行还试图访问p,但是p中没有拥有资源。

存在问题2:

auto_ptr不能指向一组对象,也就是说不能和操作符new[]一起使用。

void main() {
  std::auto_ptr<Test> p(new Test[5]);
}

上面代码将产生一个运行时错误。因为auto_ptr离开作用域时,delete被默认使用释放关联内存空间。如果创建的是一组对象,应该使用delete[]释放,而不是delete。

存在问题3:

auto_ptr不能和标准容器(vector、list、map......)一起使用。

由于auto_ptr容易产生错误,所以它也将被废弃。

 

C++11提供一组新的智能指针:shared_ptr、weak_ptr、unique_ptr。

shared_ptr:

shared_ptr有一个共享所有权(shared ownership)的概念。shared_ptr的目标非常简单:多个指针可以同时指向一个对象,当最后一个shared_ptr离开作用域时,内存才会释放。

创建:

void main() {
    shared_ptr<int> sptr1(new int);
}

使用make_shared宏来加速创建的过程。因为shared_ptr主动分配内存并且保存引用计数(reference count),make_shared以一种更有效率的方法来实现创建工作。

void main() {
    shared_ptr<int> sptr2 = make_shared<int>(100);
}

上面的代码创建了一个shared_ptr,指向一块内存,该内存包含一个整数100,以及引用计数1。如何通过sptr2再创建一个shared_ptr,引用计数就会变成2。该计数被称为强引用(strong reference),除此之外,shared_ptr 还有另外一种引用计数叫做弱引用(weak reference)。

通过调用use_count()可以得到引用计数,据此你能找到shared_ptr的数量。当debug时,可以通过观察shared_ptr中strong_ref的值得到引用计数。

析构:

shared_ptr默认调用delete释放关联资源。如果用户采用一个不一样的析构策略时,可以指定构造这个shared_ptr的策略。下面的例子是由于采用默认析构策略导致的问题。

class Test {
public:
    Test(int a=0) : m_a(0){}
    ~Test() {
        cout << "calling destructor" << ends;
    }
public:
    int m_a;
}

void main() {
    shared_ptr<Test> sptr1(new Test[5]);
}

在此场景下,shared_ptr指向一组对象,但是当离开作用域时,默认的析构函数调用delete释放资源。实际上,我们应该调用delete[]来销毁这个数组。用户可以通过调用一个函数,例如一个lamda表达式,来指定一个通用的释放步骤。

void mian() {
    shared_ptr<Test> sptr1(new Test[5], [](Test* p){ delete[] p;});
}

接口:

就像一个普通指针一样,shared_ptr也提供解引用操作符*,->。除此之外,它还提供一些更重要的接口。

get():获取shared_ptr绑定的资源。

reset():释放关联内存块的所有权,如果是最后一个指向该资源的shared_ptr,就释放这块内存。

unique():判断是否是唯一指向当前内存的shared_ptr。

operator bool:判断当前的shared_ptr是否指向一个内存块。可以用if表达式判断。

存在问题1:

void main() {
    int* p = new int;
    shared_ptr<int> sptr1(p);
    shared_ptr<int> sptr2(p);
}

上述代码会产生一个错误,因为两个来自不同组的shared_ptr指向同一个资源。在析构的时候会出现p指针 double free。

避免这个问题,尽量不要从一个裸指针(naked pointer)创建shared_ptr。但是可以使用new的方式进行创建。

存在问题2:  循环引用问题

class B;
class A {
public:
    A() : m_sptrB(nullptr){}
    ~A(){
        cout << "this is A destroyed" << ends;
    }
    shared_ptr<B> m_sptrB;
};

class B {
    B(): m_sptrA(nullptr) {}
    ~B {
        cout << "this is B destroyed" << endl;
    }
    shared_ptr<A> m_sptrA;
};

void main() {
    shared_ptr<B> sptrB(new B);
    shared_ptr<A> sptrA(new A);
    
    sptrB->m_sptrA = sptrA;
    sptrA->m_sptrB = sptrB;
}

A和B有一个shared_ptr,B对A也有一个shared_ptr,与sptrA和sptrB关联的资源没有被释放,参考下表:

 

当sptrA和sptrB离开作用域时,它们的引用计数都只减少到1,所以它们指向的资源并没有释放。

1、如果几个shared_ptr指向的内存块属于不同组,将产生错误。

2、如果从一个普通指针创建一个shared_ptr还会引发另外一个问题。在上面代码中,考虑到只有一个shared_ptr是由p创建的,代码可以正常工作。如果程序在智能指针作用域结束之前删除普通指针p,会出现崩溃。

3、循环引用:如果共享智能指针卷入循环引用,资源不会正常释放。

为了解决循环引用,C++提供了weak_ptr。

 

weak_ptr:

weak_ptr拥有共享语义,和不包含语义。说明weak_ptr可以共享shared_ptr持有的资源。所以可以从一个包含资源的shared_ptr创建weak_ptr。

weak_ptr不支持普通指针包含的*,->操作。它并不包含资源也不允许程序员操作资源。既然如此,如何使用weak_ptr?

答案是从weak_ptr中创建shared_ptr,然后再使用它。通过增加强引用计数,当使用时可以确保资源不会被销毁。当引用计数增加时,可以肯定的是从weak_ptr中创建的shared_ptr引用计数至少为1.否则,当使用weak_ptr就可能发生如下问题:当shared_ptr离开作用域时,其拥有的资源会释放,从而导致混乱。

创建:

以shared_ptr作为参数构造weak_ptr。从shared_ptr创建weak_ptr增加了共享指针的弱引用计数(weak reference),意味着shared_ptr与其他指针共享着它所拥有的资源。但是当shared_ptr离开作用域时,这个计数不作为是否释放资源的依据。换句话说,就是除非强引用计数变为0,才会释放指针指向的资源,在这里弱引用计数不起作用。

void main() {
    shared_ptr<Test> sptr(new Test);
    weak_ptr<Test> wptr(sptr);
    weak_ptr<Test> wptr1 = ptr;
}

shared_ptr和weak_ptr的引用计数如下:

 

将weak_ptr赋给另一个weak_ptr会增加弱引用计数。

当shared_ptr离开作用域时,其内的资源释放了,这时指向该shared_ptr的weak_ptr会变成过期(expired)。

如何判断weak_ptr是否指向有效资源,有两种方法:

1、调用use_count()获取引用计数,该方法只返回强引用计数,不返回弱引用计数。

2、调用expired()方法。比调用use_count()方法更快。

从weak_ptr调用lock()可以得到shared_ptr,或者直接将weak_ptr转型为shared_ptr。

void main() {
    shared_ptr<Test> sptr(new Test);
    weak_ptr<Test> wptr(ptr);
    shared_ptr<Test> sptr2 = wptr.lock();
}

如之前所述,从weak_ptr中获取shared_ptr增加强引用计数。

weak_ptr解决循环引用问题。

class B;
class A {
public:
    A():m_a(5) {};
    ~A() {
    }
weak_ptr<B> m_wptrB;
int m_a;
}

class  B {
     B():m_b(10){}
    ~B(){}
 weak_ptr<A> m_wptrA;
int m_b;
}

void main() {
shared_ptr<A> sptrA(new A);
shared_ptr<B> sptrB(new B);
sptrA->m_sptrB = sptrB;
sptrB->m_sptrA = sptrA;
}

 

 

unique_ptr:

unique_ptr是对auto_ptr的替换。unique_ptr遵循独占语义。在任何时间点,资源只能唯一的一个unique_ptr占有。当unique_ptr离开作用域,所包含资源被释放。如果资源被其他资源重写了,之前拥有的资源将被释放。所以保证关联的资源总是能被释放。

创建:

unique_ptr的创建方法和shared_ptr一样,除非创建指向数组类型的unique_ptr。

unique_ptr<int> uptr(new int);

 

unique_ptr提供了创建数组对象的特殊方法,当指针离开作用域时,调用delete[]代替delete。当创建unique_ptr时,这一组对象被视作模板参数的部分。这样,程序员就不需要再提供一个指定的析构方法。如下:

unique_ptr<int[]> uptr(new int[5]);

 

当把unique_ptr赋给另一个对象资源所有权就会被转移。另外,unique_ptr不提供复制语义(拷贝赋值和拷贝构造都不可以),只支持移动语义。

unique_ptr提供的接口和传统指针差不多,但是不支持指针运算。

unique_ptr提供一个release()方法,释放所有权。release和reset的区别在于,release仅仅释放所有权但是不释放资源,reset释放资源和所有权。

 

如何选择?

共享资源使用shared_ptr,独占资源使用unique_ptr。

除此之外,shared_ptr比unique_ptr需要更多空间,因为需要做其他事情,比如存储强引用计数、弱引用计数。而unique_ptr不需要这些,只独占保存资源对象。

 

shared_ptr的实现:

实现的函数有:

构造函数:参数为new T

构造函数:参数为shared_ptr

重载赋值操作符:shared_ptr<T> &operator = (const shared_ptr<T> &s)。注意需要先释放原shared_ptr的资源。

重载*操作符:T& operator* () {return *(this->ptr)};

重载->操作符:T* operator->() {return this->ptr};

析构函数:注意需要释放资源。

https://blog.csdn.net/xiaodu655/article/details/85780240

 

auto_ptr和unique_ptr的区别:

https://blog.csdn.net/crazy_father/article/details/46524709

 

posted @ 2020-11-15 00:11  小海哥哥de  阅读(185)  评论(0编辑  收藏  举报