引用计数与强弱指针

引用计数是一种很实用的设计,我们可以在很多地方看到它的使用:

  1. 比如GUI中的自动垃圾回收机制,就是这么搞的,比如我们在一个按键的回调中需要创建一个view并显示,那么我们会new一个pageiew的对象,比如var view_instance = new xx::view ,然后present这个view到窗口上显示. 当关闭这个窗口的时候,我们可以令view_instance = null.这样就不再有人引用这个new出来的xx::view, 然后很快,这块内存就会被垃圾回收机制回收掉
  2. 比如linux中的设备驱动中对硬件资源的管理。举个例子,hal对用户提供的接口有open, close, read, write等。在驱动中,不同的open操作对应的是同一个硬件资源,只有第一次才会真正的分配资源,后面每个使用者open一次只不过是引用计数+1, 每个使用者close则对应引用计数-1,只有引用计数减到0的时候才真正的释放对应的硬件资源
  3. C++中或者android中的智能指针,强弱指针等

在我看来当一个库提供的接口中含有reference, 我认为它的语义就是,不需要用户来管理内存的分配和释放,只需要用户在合适的时候reference和dereference即可。C++中更进了一步,搞出来了智能指针的概念,把这块工作进一步的自动化了。那么引用计数法的缺点是什么呢?有两个缺点:一个是增加了开销,需要为每个对象维护一个计数。另一个才是真正致命的问题,无法处理循环引用的问题,也就是下面这个例子

#include <iostream>
#include <memory>

class A;
class B;

class A {
public:
    std::shared_ptr<B> pointer;
    ~A() {
        std::cout << "A 被销毁" << std::endl;
    }
};

class B {
public:
    std::shared_ptr<A> pointer;
    ~B() {
        std::cout << "B 被销毁" << std::endl;
    }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    a->pointer = b;
    b->pointer = a;
    return 0;

}

运行结果是 A, B 都不会被销毁,这是因为 a,b 内部的 pointer 同时又引用了 a,b,这使得 a,b 的引用计数均变为了 2,而离开作用域时,a,b 智能指针被析构,但是只是把内部的引用计数减一,这样就导致了 a,b 对象指向的内存区域引用计数不为零,而外部已经没有办法找到这块区域了,也就造成了内存泄露

为了解决这个问题,C++中引入了弱指针 std::weak_ptr,std::weak_ptr是一种弱引用(相比较而言 std::shared_ptr 就是一种强引用)

#include <iostream>
#include <memory>

class A;
class B;

class A {
public:
    std::weak_ptr<B> pointer;
    ~A() {
        std::cout << "A 被销毁" << std::endl;
    }
};

class B {
public:
    std::shared_ptr<A> pointer;
    ~B() {
        std::cout << "B 被销毁" << std::endl;
    }
};

int main() {
    std::shared_ptr<A> a(new A); //A的引用计数是1
    std::shared_ptr<B> b(new B); //B的引用计数是1
    a->pointer = b; //B的引用计数是1
    b->pointer = a; //A的引用计数是2
    return 0;

}

为了更好的理解为什弱引用可以解决这个问题,我们逐行分析一下源码,看看到底干了什么。首先先看下shared ptr的实现:

template <class T>
class WeakPtr; //为了用weak_ptr的lock(),来生成share_ptr用,需要拷贝构造用

template <class T>
class SharePtr
{
public:
    SharePtr(T *p = 0) : _ptr(p)  //通过普通指针构造一个智能指针
    {
        cnt = new Counter(); //构造的时候创建一个Counter,用于计数
        if (p)
            cnt->s = 1; //初始化为1
        cout << "in construct " << cnt->s << endl;
    }
    ~SharePtr()
    {
        release();
    }

    SharePtr(SharePtr<T> const &s) //拷贝构造
    {
        cout << "in copy con" << endl;
        _ptr = s._ptr;  //指向同一个对象
        (s.cnt)->s++; //引用计数+1
        cout << "copy construct" << (s.cnt)->s << endl;
        cnt = s.cnt; //把引用计数同步到当前shared ptr
    }
    SharePtr(WeakPtr<T> const &w) //为了用weak_ptr的lock(),来生成share_ptr用,需要拷贝构造用
    {
        cout << "in w copy con " << endl;
        _ptr = w._ptr;
        (w.cnt)->s++;
        cout << "copy w  construct" << (w.cnt)->s << endl;
        cnt = w.cnt;
    }
    SharePtr<T> &operator=(SharePtr<T> &s)
    {
        if (this != &s) 
        {
            release(); //解除或者-1当前shared ptr的引用
            (s.cnt)->s++;
            cout << "assign construct " << (s.cnt)->s << endl;
            cnt = s.cnt; 
            _ptr = s._ptr; //重定向到新的ptr
        }
        return *this;
    }
    T &operator*()
    {
        return *_ptr;
    }
    T *operator->()
    {
        return _ptr;
    }
    friend class WeakPtr<T>; //方便weak_ptr与share_ptr设置引用计数和赋值

protected:
    void release()
    {
        cnt->s--;
        cout << "release " << cnt->s << endl;
        if (cnt->s < 1)
        {
            delete _ptr; //无强引用,释放这块内存;即使有弱引用也要释放
            if (cnt->w < 1) //有弱引用,保留cnt指向的这块内存
            {
                delete cnt;
                cnt = NULL;
            }
        }
    }

private:
    T *_ptr;
    Counter *cnt;
};

然后再看一下弱引用的源码:

template <class T>
class WeakPtr
{
public: //给出默认构造和拷贝构造,其中拷贝构造不能有从原始指针进行构造
    WeakPtr()
    {
        _ptr = 0;
        cnt = 0;
    }
    WeakPtr(SharePtr<T> &s) : _ptr(s._ptr), cnt(s.cnt)//从shared ptr进行构造
    {
        cout << "w con s" << endl;
        cnt->w++; //对s的对象的弱引用计数+1
    }
    WeakPtr(WeakPtr<T> &w) : _ptr(w._ptr), cnt(w.cnt)
    {
        cnt->w++;
    }
    ~WeakPtr()
    {
        release();
    }
    WeakPtr<T> &operator=(WeakPtr<T> &w)
    {
        if (this != &w)
        {
            release();
            cnt = w.cnt;
            cnt->w++;
            _ptr = w._ptr;
        }
        return *this;
    }
    WeakPtr<T> &operator=(SharePtr<T> &s)
    {
        cout << "w = s" << endl;
        release();
        cnt = s.cnt;
        cnt->w++;
        _ptr = s._ptr;
        return *this;
    }
    SharePtr<T> lock()
    {
        return SharePtr<T>(*this);
    }
    bool expired()
    {
        if (cnt) //结合上面的shared ptr看,如果对象被析构,这个有可能是null
        {
            if (cnt->s > 0)//若还有强引用,则说明没有expired
            {
                cout << "empty" << cnt->s << endl;
                return false;
            }
        }
        return true; //是null的话,铁定内存已经被释放了
    }
    friend class SharePtr<T>; //方便weak_ptr与share_ptr设置引用计数和赋值
    
protected:
    void release()
    {
        if (cnt)
        {
            cnt->w--;
            cout << "weakptr release" << cnt->w << endl;
            if (cnt->w < 1 && cnt->s < 1)
            {
                cnt = NULL; //可以看到弱引用计数不会影响对象的释放
            }
        }
    }

private:
    T *_ptr;
    Counter *cnt;
};

注意,delete一个对象的时候,除了会执行这个对象的析构函数,还会自动执行这个对象内部的其它对象的析构,举个例子:

class B {
public:
    std::weak_ptr<A> pointer;
    ~B() {
        std::cout << "B 被销毁" << std::endl;
    }
};

class C {
public:
    ~C() { std::cout << "c的析构" << std::endl; }
    B mB;
};

int main() {
    C *c = new C;
    delete c;
    return 0;
}

执行结果为:

引用计数与强弱指针

 

 

那么为什么引入弱引用可以解决循环引用的问题也就迎刃而解了。a被析构的时候,执行release(), 对A的引用计数从2减到1(b还持有一个),然后b析构的时候,执行release, B的引用计数是1减到0(a对B是弱引用),这样就会delete B, B的析构函数会执行,然后B中的对象会被执行析构,也就是一个指向A的shared ptr对象会被析构,于是这个时候A的引用计数由1减到0,A也最终被释放了。

 


 

后记:

  • 虽然通过弱引用指针可以有效的解除循环引用,但这种方式必须在程序员能预见会出现循环引用的情况下才能使用,也可以是说弱引用仅仅是一种编译期的解决方案,如果程序在运行过程中出现了循环引用,还是会造成内存泄漏的。因此,不要认为只要使用了智能指针便能杜绝内存泄漏
  • 弱引用:它仅仅是对象存在时候的引用,当对象不存在时弱引用能够检测到,从而避免非法访问,弱引用也不会修改对象的引用计数。这意味这弱引用它并不对对象的内存进行管理,在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存
  • 智能指针不是线程安全的,多线程使用时记得加锁

 

上一篇:C++字符串拼接


下一篇:C++ 探索类成员变量的初始化