带有引用计数器的c++结构导致内存泄漏

C++ struct with reference counter causes memory leak

本文关键字:内存 泄漏 结构 c++ 引用 计数器      更新时间:2023-10-16

我正试图实现我自己的结构与引用计数器,是在Visual Studio 2008的c++/Windows编译器下编译的。我想到了这个:

struct STOP_FLAG
{
    STOP_FLAG() 
        : mRefCount(0)
        , pbStopFlag(NULL)
    {
        pbStopFlag = new (std::nothrow) volatile BOOL;
        ASSERT(pbStopFlag);
        this->grab();
    }
    ~STOP_FLAG()
    {
        this->release();
    }
    STOP_FLAG(const STOP_FLAG& s) 
        : pbStopFlag(s.pbStopFlag)
        , mRefCount(s.mRefCount)
    {
        //Copy constructor
        this->grab();
    }
    STOP_FLAG& operator = (const STOP_FLAG& s)
    {
        //Assignment operator
        if(pbStopFlag != s.pbStopFlag)
        {
            this->release();
            s.grab();
            pbStopFlag = s.pbStopFlag;
            mRefCount = s.mRefCount;
        }
        return *this;
    }
    //Helper methods
    volatile BOOL* operator->() const {return pbStopFlag;}      //x->member
    volatile BOOL& operator*() const {return *pbStopFlag;}      //*x, (*x).member
    operator volatile BOOL*() const {return pbStopFlag;}        //T* y = x;
    operator bool() const {return pbStopFlag != NULL;}          //if(x)
private:
    void grab() const 
    {
        //++mRefCount;
        ::InterlockedIncrement(&mRefCount);
    }
    void release() const
    {
        ASSERT(mRefCount > 0);
        //--mRefCount;
        LONG mCnt = ::InterlockedDecrement(&mRefCount);
        if(mCnt == 0)
        {
            ASSERT(pbStopFlag);
            if(pbStopFlag)
            {
                delete pbStopFlag;
                pbStopFlag = NULL;
            }
        }
    }
private:
    mutable volatile BOOL* pbStopFlag;
    mutable LONG mRefCount;
};

但是当我用调试器测试以下内容(在单个线程中运行)时:

{
    STOP_FLAG sf;
    {
        STOP_FLAG s2(sf);
        s2 = sf;
        STOP_FLAG s3;
        s3 = s2;
        STOP_FLAG s4[3];
        s4[0] = s3;
        s4[1] = s3;
        s4[2] = s3;
        STOP_FLAG s5;
        s3 = s5;
    }
}

碰巧我的new volatile BOOL算子被调用了6次而delete只被调用了5次。

那么我的内存泄漏是从哪里来的?

EDIT:这是根据下面的建议更新的版本。它仍然产生相同的结果:

struct _S_FLAG{
    volatile BOOL* pbStopFlag;
    LONG mRefCount;
    _S_FLAG(volatile BOOL* pb, LONG cntr)
    {
        pbStopFlag = pb;
        mRefCount = cntr;
    }
};
struct STOP_FLAG
{
    STOP_FLAG() 
        : _sf(NULL, 0)
    {
        _sf.pbStopFlag = new (std::nothrow) volatile BOOL;
        TRACE("newn");
        ASSERT(_sf.pbStopFlag);
        this->grab();
    }
    ~STOP_FLAG()
    {
        this->release();
    }
    STOP_FLAG(const STOP_FLAG& s) 
        : _sf(s._sf.pbStopFlag, s._sf.mRefCount)
    {
        //Copy constructor
        this->grab();
    }
    STOP_FLAG& operator = (const STOP_FLAG& s)
    {
        //Assignment operator
        if(_sf.pbStopFlag != s._sf.pbStopFlag)
        {
            this->release();
            s.grab();
            _sf.pbStopFlag = s._sf.pbStopFlag;
            _sf.mRefCount = s._sf.mRefCount;
        }
        return *this;
    }
    //Helper methods
    volatile BOOL* operator->() const {return _sf.pbStopFlag;}      //x->member
    volatile BOOL& operator*() const {return *_sf.pbStopFlag;}      //*x, (*x).member
    operator volatile BOOL*() const {return _sf.pbStopFlag;}        //T* y = x;
    operator bool() const {return _sf.pbStopFlag != NULL;}          //if(x)
private:
    void grab() const 
    {
        //++mRefCount;
        ::InterlockedIncrement(&_sf.mRefCount);
    }
    void release() const
    {
        ASSERT(_sf.mRefCount > 0);
        //--mRefCount;
        LONG mCnt = ::InterlockedDecrement(&_sf.mRefCount);
        if(mCnt == 0)
        {
            ASSERT(_sf.pbStopFlag);
            if(_sf.pbStopFlag)
            {
                delete _sf.pbStopFlag;
                TRACE("deleten");
                _sf.pbStopFlag = NULL;
            }
        }
    }
private:
    mutable _S_FLAG _sf;
};

引用计数需要在对象的逻辑引用之间共享。

相反,你正在复制它。

这并不能完全解释你观察到的效果,但它是更基本的:你担心引擎发出一些不好的声音,我是指出这辆车有方形车轮,所以不要担心引擎。

例如:

STOP_FLAG s4[3];
s4[0] = s3;
s4[1] = s3;
s4[2] = s3;

这些都正确地破坏了s4[]的每个元素中的先前共享块(实际上没有与任何东西共享,因为每个元素都是单个实例,并且永远不会在任何地方分配/复制)。但是想想这段代码的作用:

//Assignment operator
if(pbStopFlag != s.pbStopFlag)
{
    this->release();
    s.grab();
    pbStopFlag = s.pbStopFlag;
    mRefCount = s.mRefCount; // <<== makes a one-time copy of the ref count
}

注意添加的注释。因为引用计数与对象绑定,而不是与共享数据绑定,所以每个对象跟踪的mRefCount完全没有用。在此之后,s[0]s[2]都指向相同的共享数据,但是每个都有不同的引用计数。这样,s中引用计数的变化不会反映在先前赋值或构造中连接到相同共享数据块的每个对象上。

这些都是std::shared_ptr<>和类似的东西。此外,并非所有的"共享"指针都是相似的。回顾std::make_shared<>std::shared_ptr<>的实际构造之间的差异是值得研究的,但足以说明其中一个做的事情与您在这里尝试的类似,另一个通过通常称为压缩对来管理单个分配。

代码的修改版本,显然不是线程安全的,但详细说明了一种方法,您可以完成下面解释的答案。它绝不是什么灵丹妙药,显然需要为线程安全的交换等工作,但它记录了如何在单个共享块中共享引用计数数据来修复上面发现的问题:


Live Code Here

#include <iostream>
#include <algorithm>
#include <utility>
typedef long LONG;
typedef int BOOL;
struct STOP_FLAG
{
    STOP_FLAG()
        : shared(new Shared())
    {
    }
    ~STOP_FLAG()
    {
        shared->release();
    }
    // though not mandatory, note the use of the comma-operator to establish
    // the source object's shared data grab before constructing the new object's
    // shared member pointer. By the time the constructor body is entered the
    // grab is complete and the shared member is properly setup.
    STOP_FLAG(const STOP_FLAG& s)
        : shared((s.shared->grab(), s.shared))
    {
    }
    // assignment operator by copy/swap idiom
    STOP_FLAG& operator = (STOP_FLAG s)
    {
        std::swap(shared, s.shared);
        return *this;
    }
private:
    struct Shared
    {
        BOOL value;
        LONG refCount;
        Shared() : value(), refCount()
        {
            grab();
        }
        void grab()
        {
            std::cout << 't' << __PRETTY_FUNCTION__ << ':' << refCount+1 << 'n';
            ++refCount;
        }
        void release()
        {
            std::cout << 't' << __PRETTY_FUNCTION__ << ':' << refCount-1 << 'n';
            if (!--refCount)
                delete this;
        }
    } *shared;
};
int main()
{
    std::cout << "STOP_FLAG sf;" << 'n';
    STOP_FLAG sf; // one block
    {
        std::cout << "STOP_FLAG s2(sf);" << 'n';
        STOP_FLAG s2(sf);  // two objets, one shared block, refcount=2
        // still two objects, after settling, one shared block, refcount=2
        std::cout << "s2 = sf;" << 'n';
        s2 = sf; 
        // three objects, two shared (refcount=2), one stand-alone (refcount=1)
        std::cout << "STOP_FLAG s3;" << 'n';
        STOP_FLAG s3; 
        // three objects, one shared block, (refcount=3)
        std::cout << "s3 = s2;" << 'n';
        s3 = s2; 
        // three more objects, all singular, 
        std::cout << "STOP_FLAG s4[3];" << 'n';
        STOP_FLAG s4[3];
        // each assignment below attaches to sf, detaches prior content.
        std::cout << "s4[0] = s3;" << 'n';
        s4[0] = s3;
        std::cout << "s4[1] = s3;" << 'n';
        s4[1] = s3;
        std::cout << "s4[2] = s3;" << 'n';
        s4[2] = s3;
        // by this point every object so far all shared the same
        //  shared data, refcount=6.
        // another stand-alone object
        std::cout << "STOP_FLAG s5;" << 'n';
        STOP_FLAG s5;
        // s3 attaches to s5, detaches from prior shared data
        //  meaning we now have two shared data blocks with
        // reference counts of 2 and 5 respectively.
        std::cout << "s3 = s5;" << 'n';
        s3 = s5;
        std::cout << "scope exitingn";
        // in leaving scope, *all* objects are detached except ONE
        //  (the one outside of this; sf), which leaves a single 
        // shared block with refcount of 1 only.
    }
    std::cout << "scope exitedn";
    // the last object, sf, is released here.
}

STOP_FLAG sf;
    void STOP_FLAG::Shared::grab():1
STOP_FLAG s2(sf);
    void STOP_FLAG::Shared::grab():2
s2 = sf;
    void STOP_FLAG::Shared::grab():3
    void STOP_FLAG::Shared::release():2
STOP_FLAG s3;
    void STOP_FLAG::Shared::grab():1
s3 = s2;
    void STOP_FLAG::Shared::grab():3
    void STOP_FLAG::Shared::release():0
STOP_FLAG s4[3];
    void STOP_FLAG::Shared::grab():1
    void STOP_FLAG::Shared::grab():1
    void STOP_FLAG::Shared::grab():1
s4[0] = s3;
    void STOP_FLAG::Shared::grab():4
    void STOP_FLAG::Shared::release():0
s4[1] = s3;
    void STOP_FLAG::Shared::grab():5
    void STOP_FLAG::Shared::release():0
s4[2] = s3;
    void STOP_FLAG::Shared::grab():6
    void STOP_FLAG::Shared::release():0
STOP_FLAG s5;
    void STOP_FLAG::Shared::grab():1
s3 = s5;
    void STOP_FLAG::Shared::grab():2
    void STOP_FLAG::Shared::release():5
scope exiting
    void STOP_FLAG::Shared::release():1
    void STOP_FLAG::Shared::release():4
    void STOP_FLAG::Shared::release():3
    void STOP_FLAG::Shared::release():2
    void STOP_FLAG::Shared::release():0
    void STOP_FLAG::Shared::release():1
scope exited
    void STOP_FLAG::Shared::release():0

你可能会发现boost::intrusive_ptr很有用。