1. 什么是智能指针

简而言之,为了更安全的使用指针。
实现方式简单来说,就是用一个模板类把一般的指针包装起来。用这个类来维护内部指针的释放操作。
std中有四种智能指针。

  • auto_ptr(已弃用)
  • unique_ptr
  • shared_ptr
  • weak_ptr

2. auto_ptr

比较简单的智能指针,实现逻辑如下代码。
实现的原则是:内存空间只能由一个指针所拥有。
存在的问题:拷贝和赋值后,原对象内部的指针会为空,有风险。

template<class T>
class autoPtr{

public:
    autoPtr(T *a):m_ptr(a){

    }


    ~autoPtr(){
        if(m_ptr){
            delete m_ptr;
            m_ptr = nullptr;
        }

    }

    // 拷贝构造 权限转移
    autoPtr(autoPtr<T> &a):m_ptr(a.m_ptr) {
        a.m_ptr = nullptr;  
    }

    // 赋值 权限转移
    autoPtr& operator= (autoPtr<T> &a){
        if(this != &a){
            if(m_ptr) delete m_ptr;
            m_ptr = a.m_ptr;
            a.m_ptr = nullptr;
            
        }

        return *this;
    }
    
    T* operator->(){
        return m_ptr;
    }

    T& operator* (){
        return *m_ptr;
    }
    

private:
    T *m_ptr;

};

3. unique_ptr

是对auto_ptr的改进,实现逻辑如下代码。
实现的原则是:内存空间只能由一个指针所拥有,但是禁止了拷贝和赋值。
在C++中unique_ptr的进制拷贝有一个特例,如果一个unique_ptr是将亡值的话,是允许的,比如unique_ptr作为函数的返回值时,是允许的。

// uniquePtr  对于autoPtr来说 禁止了拷贝和赋值
template<class T>
class uniquePtr{
public:
    
    uniquePtr(const T*a):m_ptr(a) { };

    ~uniquePtr(){ if(m_ptr) delete m_ptr; }

    T& operator *(){
        return *m_ptr;
    }

    T* operator->(){
        return m_ptr;
    }

    T* get(){ return m_ptr; }

private:
    T *m_ptr;

    // 禁止拷贝和赋值
    uniquePtr(const uniquePtr<T> &a) { };

    uniquePtr<T> & operator = (const uniquePtr<T> &a) { };

};

unique_ptr禁止了拷贝,那么如何把unique_ptr作为参数传递呢?

  • 法1:使用指针或者引用
void func(unique_ptr<int> &a){

}

*法2: 暂时转移内存的所有权,函数返回时,再把所有权交回来。

unique_ptr<int> func2(unique_ptr<int> a){
	cout << *a << endl;
	return a;
}

int main()
{
	unique_ptr<int> a(new int(6));
	// 暂时释放所有权
	a = func2(unique_ptr<int>(a.release()));
	return 0;
}
  • 法3:move语义
void func1(unique_ptr<int> a)
{
	cout << *a << endl;
}

int main()
{
	unique_ptr<int> a(new int(6));
	func1(std::move(a));
	return 0;
}

4. shared_ptr

实现逻辑如下代码。
实现的原则是:内存空间可以由多个指针所拥有,但是要控制内存释放的时机,这个时机由“引用计数”来实现。
存在为问题:会存在循环引用问题,导致内存无法正常释放。

template<class T>
class sharedPtr{

public:

    sharedPtr(T *a):m_ptr(a), m_count(new int(1)) { };

    ~sharedPtr(){
        
        if(--(*m_count) == 0){
            delete m_ptr;
            delete m_count;
            m_ptr = nullptr;
            m_count = nullptr;

        }

    }


    T& operator *(){
        return *m_ptr;
    }

    T* operator->(){
        return m_ptr;
    }

    // 拷贝构造
    sharedPtr(const sharedPtr<T> &a):m_ptr(a.m_ptr),m_count(a.m_count) {
        ++(*m_count);
    }

    // 赋值函数
    sharedPtr<T> & operator=(const sharedPtr<T> &a){
        
        if(m_ptr != a.m_ptr){
            // this 原来的那个区域处理
            if(--(*m_count) == 0){   // 这里不仅仅是检查,也会引用计数减1
                delete m_ptr;
                delete m_count;
                m_ptr = nullptr;
                m_count = nullptr;
            }

            m_ptr = a.m_ptr;
            m_count = a.m_count;

            ++(*m_count);
        }

        return *this;
        
    }

    int use_count(){
        return *m_count;
    }

    int * use_countPtr(){
        return m_count;
    }

    T* get(){
        return m_ptr;
    }
    


private:
    T *m_ptr;
    int *m_count;

};

shared_ptr会产生循环引用问题,如下代码

template<class T>
class sharedPtr{

public:

    sharedPtr(T *a):m_ptr(a), m_count(new int(1)) { };

    ~sharedPtr(){
        
        if(--(*m_count) == 0){
            delete m_ptr;
            delete m_count;
            m_ptr = nullptr;
            m_count = nullptr;

        }

    }


    T& operator *(){
        return *m_ptr;
    }

    T* operator->(){
        return m_ptr;
    }

    // 拷贝构造
    sharedPtr(const sharedPtr<T> &a):m_ptr(a.m_ptr),m_count(a.m_count) {
        ++(*m_count);
    }

    // 赋值函数
    sharedPtr<T> & operator=(const sharedPtr<T> &a){
        
        if(m_ptr != a.m_ptr){
            // this 原来的那个区域处理
            if(--(*m_count) == 0){   // 这里不仅仅是检查,也会引用计数减1
                delete m_ptr;
                delete m_count;
                m_ptr = nullptr;
                m_count = nullptr;
            }

            m_ptr = a.m_ptr;
            m_count = a.m_count;

            ++(*m_count);
        }

        return *this;
        
    }

    int use_count(){
        return *m_count;
    }

    int * use_countPtr(){
        return m_count;
    }

    T* get(){
        return m_ptr;
    }
    


private:
    T *m_ptr;
    int *m_count;

};

int main() {

    struct Node{
        Node():pre(nullptr), next(nullptr) { }
        int data;
        sharedPtr<Node> pre;
        sharedPtr<Node> next;
    };


    int *c1 = nullptr;
    int *c2 = nullptr;

    { // 这里一个花括号,限制了作用域
        sharedPtr<Node> a(new Node);
        sharedPtr<Node> b(new Node);

        a->next = b;
        b->pre = a;

        // 把各自的m_count取出来
        c1 = a.use_countPtr();
        c2 = b.use_countPtr();

        cout << *c1 <<endl;
        cout << *c2 <<endl;

    }

    // 因为上边a 和 b在析构时,没有释放m_ptr和m_count,所以,下边两行可以访问,并且为1
    cout << *c1 <<endl;
    cout << *c2 <<endl;

    return 0;
}

对象a和b本身为sharedPtr类型,内部各包含一个Node*并且开辟了内存。为了方便后续描述,a中申请的这块内存称之为A_Node*,b中的这块内存称之为B_Node*

a和b初始化时,内部的引用计数均为1,因为这个时候只有自己使用自己内部的这块内存空间。
当执行了

a->next = b;
b->pre = a;

这两行代码以后,a和b内部的引用技术均为2。此时对于a来说,不仅仅自己使用A_Node*,同时b中的一个sharedPtr<Node>类型(与a同类型)的pre,也使用A_Node*,所以a中的引用计数为2,b中pre内部的引用计数也为2。

对于b来说,同理,不仅仅自己使用B_Node*,同时a中的一个sharedPtr<Node>类型(与b同类型)的next,也使用B_Node*,所以b中的引用计数为2,a中next内部的引用计数也为2。

重点来了,当程序执行到花括号外边的时候,a和b两个对象的生命周期到了,要执行各自的析构函数。但是!执行a的析构函数时,由于引用计数为2,减去1之后不为0,所以,不会释放a内部的m_ptr和m_count,只不过把m_count变为了1,由于引用计数机制的存在,此时b中的pre,这个智能指针内部的引用计数也会变为1。总结一下,就是a这个对象被析构了,但是 A_Node* 没释放。

同理,执行b的析构函数时,由于引用计数为2,减去1之后不为0,所以,不会释放b内部的m_ptr和m_count,只不过把m_count变为了1,由于引用计数机制的存在,此时a中的next,这个智能指针内部的引用计数也会变为1。总结一下,就是b这个对象被析构了,但是 B_Node* 没释放。

经过了a和b两个对象的析构函数,现在的情况是怎么样的?

A_Node*中的next指向B_Node*
B_Node*中的pre指向A_Node*

此时呢,形成了环。

在没有人为干预的情况下,想释放A_Node*,就必须先释放 B_Node*

因为一旦B_Node*释放,会触发B_Node*内部pre指针的析构函数,因此这时pre内部的引用计数为1,减1之后变为0,所以,可以释放掉pre所指向的内存空间,也就是会释放掉A_Node*,然后接着会触发A_Node*内部next指针的析构函数,同理,next指针会把自己指向的B_Node*给释放掉,这样,A_Node*和B_Node*这两块被a和b遗留下来的内存空间就可以正常被释放。

但是!!!问题来了,想释放B_Node*,就必须先释放 A_Node*

二者互相等待,都等待对方先释放,这个就是循环引用

解决办法
把Node中的两个shared_ptr换成weak_ptr即可,这样,a和b的引用计数均为1,可以正常是释放。

5. weak_ptr

是对shared_ptr的修改,实现逻辑如下代码。
实现的原则是:允许一块内存由多个指针拥有,与shared_ptr不同的是,不使用引用计数。
存在的问题:适用性不如shared_ptr广,因为一个weak_ptr释放的时候,就会把自己指向的区域释放掉,可能会重复释放。

// weak ptr ,与sharedPtr的不同在于,没有引用计数,总是,weakPtr,共享,但不计数,直接释放
template<class T>
class weakPtr{

public:
    weakPtr(T *a):m_ptr(a){ }

    ~weakPtr(){ 
        if(m_ptr) {
            delete m_ptr; 
            m_ptr = nullptr;
        }
        
    }

    // 赋值
    weakPtr<T> & operator= (const weakPtr<T> &a){
        if(m_ptr) delete m_ptr;
        m_ptr = a.m_ptr;
        return *this;
    }

    weakPtr<T> & operator= (T *a){
        m_ptr = a;
        return *this;
    }


    T& operator *(){
        return *m_ptr;
    }

    T* operator->(){
        return m_ptr;
    }


private:
    T *m_ptr;
};