【问题标题】:Why does example code access deleted memory in IUnknown?为什么示例代码访问 IUnknown 中已删除的内存?
【发布时间】:2020-07-13 23:21:28
【问题描述】:

使用IUnknown等接口的例子很多,在这个例子中是IDocHostUIHandler,但这并不重要 - 使用类似这样的代码:

 class TDocHostUIHandlerImpl : public IDocHostUIHandler
 {
 private:

    ULONG RefCount;

    public:      

    TDocHostUIHandlerImpl():RefCount(0){ }

    // IUnknown Method
    HRESULT __stdcall QueryInterface(REFIID riid, void **ppv) {
        if (IsEqualIID(riid,IID_IUnknown))
            {
            *ppv = static_cast<IUnknown*>(this);
            return S_OK;
            }
        else if (IsEqualIID(riid, IID_IDocHostUIHandler)) {
            *ppv = static_cast<IDocHostUIHandler*>(this);
            return S_OK;
            }
        else {
            *ppv = NULL;
            return E_NOINTERFACE;
            }
        }

    ULONG   __stdcall AddRef() {
        InterlockedIncrement((long*)&RefCount);
        return RefCount;
        }

    ULONG   __stdcall Release() {
        if (InterlockedDecrement((long*)&RefCount) == 0) delete this;
        return RefCount;
        }

我的问题是Release() 方法使用delete this 删除接口实现,但之后return RefCount 不再引用内存中的有效对象(它访问已删除的内存)。

我认为应该是这样的

    ULONG   __stdcall Release() {
        if (InterlockedDecrement((long*)&RefCount) == 0) { delete this; return 0; }
        return RefCount;
        }

这也不会触发我使用的资源泄漏工具(C++ Builder 中的 Codeguard)。那么为什么这么多例子都使用第一个版本,我在这里遗漏了什么?

还是只是在Release方法结束后在Visual Studio等另一个编译器中调用了“删除这个”?

几个例子:

https://www.codeproject.com/Articles/4758/How-to-customize-the-context-menus-of-a-WebBrowser

addref and release in IUnknown, what do they actually do?

https://bbs.csdn.net/topics/20135139

【问题讨论】:

    标签: c++ interface c++builder


    【解决方案1】:

    是的,你说得对,这样的例子写得不好。它们需要写得更像你描述的那样:

    ULONG __stdcall Release()
    {
        if (InterlockedDecrement((long*)&RefCount) == 0) {
            delete this;
            return 0;
        }
        return RefCount;
    }
    

    但是,他们最好只返回 InterlockedDecrement 返回的任何结果。正如@RaymondChen 在cmets 中指出的那样,这也解决了RefCount 在到达return 之前被另一个线程递减,可能会破坏this 的问题,例如:

    ULONG __stdcall Release()
    {
        ULONG res = (ULONG) InterlockedDecrement((long*)&RefCount);
        if (res == 0) {
            delete this;
        }
        return res;
    }
    

    AddRef() 相同,就此而言:

    ULONG __stdcall AddRef()
    {
        return (ULONG) InterlockedIncrement((long*)&RefCount);
    }
    

    附带说明,您显示的QueryInterface() 示例也写错了,因为它在返回S_OK 时没有增加RefCount,例如:

    HRESULT __stdcall QueryInterface(REFIID riid, void **ppv)
    {
        if (IsEqualIID(riid,IID_IUnknown)) {
            *ppv = static_cast<IUnknown*>(this);
            AddRef(); // <-- add this!
            return S_OK;
        }
        else if (IsEqualIID(riid, IID_IDocHostUIHandler)) {
            *ppv = static_cast<IDocHostUIHandler*>(this);
            AddRef(); // <-- add this!
            return S_OK;
        }
        else {
            *ppv = NULL;
            return E_NOINTERFACE;
        }
    }
    

    通常可以这样写更容易:

    HRESULT __stdcall QueryInterface(REFIID riid, void **ppv)
    {
        if (!ppv) {
            return E_POINTER;
        }
    
        if (IsEqualIID(riid, IID_IUnknown)) {
            *ppv = static_cast<IUnknown*>(this);
        }
        else if (IsEqualIID(riid, IID_IDocHostUIHandler)) {
            *ppv = static_cast<IDocHostUIHandler*>(this);
        }
        else {
            *ppv = NULL;
            return E_NOINTERFACE;
        }
    
        AddRef();
        return S_OK;
    }
    

    我也见过这样写的,当QueryInterface()NULL 指针调用时,这说明了不好的情况:

    HRESULT __stdcall QueryInterface(REFIID riid, void **ppv)
    {
        if (!ppv) {
            return E_POINTER;
        }
    
        if (IsEqualIID(riid, IID_IUnknown)) {
            *ppv = static_cast<IUnknown*>(this);
        }
        else if (IsEqualIID(riid, IID_IDocHostUIHandler)) {
            *ppv = static_cast<IDocHostUIHandler*>(this);
        }
        else {
            *ppv = NULL;
        }
    
        if (*ppv) {
            AddRef();
            return S_OK;
        }
    
        return E_NOINTERFACE;
    }
    

    【讨论】:

    • 第一个版本 (return RefCount) 也是错误的,因为在当前线程可以读取RefCount 之前,对象可能已经在另一个线程上被释放和破坏。你必须用第二种方法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-12-28
    • 1970-01-01
    相关资源
    最近更新 更多