关于std::shared_ptr与std::enable_shared_from_this循环引用导致的问题

本文探讨了C++中std::shared_ptr自我引用的问题,包括其在栈对象和堆对象中的表现,分析了引用计数导致的对象无法释放,并提供了解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

自从C++11有了std::shared_ptr这样的智能指针,作为C++程序只要将一个堆上的类对象用std::shared_ptr包裹一下就可以做到内存自动释放了。看一个例子:

#include "stdafx.h"
#include <memory>

class A
{
public:
    A()
    {
        m_i = 9;
    }

    ~A()
    {
        m_i = 0;
    }

public:
    int     m_i;

};

int _tmain(int argc, _TCHAR* argv[])
{
    {
        std::shared_ptr<A> spa(new A());
    }
    

    return 0;
}


如下图,上面的代码new出来一个堆对象A,但是出了作用域后,由于std::shared_ptr对象spa的引用计数减为0,会自动调用A的析构函数来释放这块堆内存:




但是假如,我们有一些开发需求中(也可能是前同事遗留下的代码),我们需要在一个类中引用自身,即一个类的一个成员变量是一个std::shared_ptr对象,它引用了类对象自身,这里分为两种情况,第一种情况是类对象是栈对象,第二种情况是类对象是堆对象。

我们先看类对象是栈对象的情形,示例代码如下:

#include "stdafx.h"
#include <memory>

class A : public std::enable_shared_from_this<A>
{
public:
    A()
    {
        m_i = 9;
        //注意:
        //比较好的做法是在构造函数里面调用shared_from_this()给m_SelfPtr赋值
        //但是很遗憾不能这么做,如果写在构造函数里面程序会直接崩溃
    }
    
    ~A()
    {
        m_i = 0;
    }

    void func()
    {
        m_SelfPtr = shared_from_this();
    }

public:
    int                 m_i;
    std::shared_ptr<A>  m_SelfPtr;

};

int _tmain(int argc, _TCHAR* argv[])
{
    {
        A a;
        a.func();
    }
    

    return 0;
}

上面的代码,在调用a.func()时程序会直接崩溃,崩溃的原因是调用shared_from_this()函数里面,看一下崩溃的调用堆栈:


我们来看下崩溃的原因,看下shared_from_this()函数的调用细节:







也就是说shared_from_this()函数内部会先调用shared_ptr的构造函数去构造一个shared_ptr对象,参数是自己的成员变量_Wptr,这是一个std::weak_ptr:

private:
	template<class _Ty1,
		class _Ty2>
		friend void _Do_enable(
			_Ty1 *,
			enable_shared_from_this<_Ty2>*,
			_Ref_count_base *);

	mutable weak_ptr<_Ty> _Wptr;


而shared_ptr的构造函数里面又会调用reset()先释放之前的对象引用,如果这个之前的对象就是_Wptr这个指针去引用,但是现在_Wptr是空的,就抛出一个异常。_Wptr之所以为空,是这个指针引用的对象并没有被任何智能指针所包裹(A的对象a是栈变量)。这就是崩溃的原因。

我们来接着看下A对象是堆对象的情形:

class A : public std::enable_shared_from_this<A>
{
public:
    A()
    {
        m_i = 9;
        //注意:
        //比较好的做法是在构造函数里面调用shared_from_this()给m_SelfPtr赋值
        //但是很遗憾不能这么做,如果写在构造函数里面程序会直接崩溃
    }
    
    ~A()
    {
        m_i = 0;
    }

    void func()
    {
        m_SelfPtr = shared_from_this();
    }

public:
    int                 m_i;
    std::shared_ptr<A>  m_SelfPtr;

};

int _tmain(int argc, _TCHAR* argv[])
{
    {
        std::shared_ptr<A> spa(new A());
        spa->func();
    }
    

    return 0;
}

这次不会崩溃了,但是遗憾的是,这个new出来的A对象的堆内存再也不会释放了(当然程序退出靠操作系统回收不算)。为啥不会释放呢?我们来分析下原因:

要想堆上的A被释放,那么至少需要所有指向A的std::shared_ptr对象都不再引用A,但是A的成员变量只有在A自己被释放的时候才会不再引用A。反过来说,A的成员变量m_SelfPtr等着A对象本身释放,而A作为堆对象释放的条件是所有引用它的的std::shared_ptr释放。这就相互矛盾了。这种情形导致,这样的A对象永远不会被自动释放。我们使用std::weak_ptr来看看最终这个A的引用计数是多少:

#include "stdafx.h"
#include <memory>

class A : public std::enable_shared_from_this<A>
{
public:
    A()
    {
        m_i = 9;
        //注意:
        //比较好的做法是在构造函数里面调用shared_from_this()给m_SelfPtr赋值
        //但是很遗憾不能这么做,如果写在构造函数里面程序会直接崩溃
    }
    
    ~A()
    {
        m_i = 0;
    }

    void func()
    {
        m_SelfPtr = shared_from_this();
    }

public:
    int                 m_i;
    std::shared_ptr<A>  m_SelfPtr;

};

int _tmain(int argc, _TCHAR* argv[])
{
    std::weak_ptr<A> spwa;
    {
        std::shared_ptr<A> spa(new A());
        spa->func();
        spwa = spa;
    }

    printf("spwa usecount: %d\n", spwa.use_count());
    

    return 0;
}




确实和我们分析的一样,这个堆上的A引用计数永远是A了,所以不会被释放了。


那有什么解决方案呢?

我们可以在增加一个成员函数,在不需要A时,主动释放这个智能指针的成员变量引用的对象:

#include "stdafx.h"
#include <memory>

class A : public std::enable_shared_from_this<A>
{
public:
    A()
    {
        m_i = 9;
        //注意:
        //比较好的做法是在构造函数里面调用shared_from_this()给m_SelfPtr赋值
        //但是很遗憾不能这么做,如果写在构造函数里面程序会直接崩溃
    }
    
    ~A()
    {
        m_i = 0;
    }

    void func()
    {
        m_SelfPtr = shared_from_this();
    }

    void release()
    {
        m_SelfPtr.reset();
    }

public:
    int                 m_i;
    std::shared_ptr<A>  m_SelfPtr;

};

int _tmain(int argc, _TCHAR* argv[])
{
    std::weak_ptr<A> spwa;
    {
        std::shared_ptr<A> spa(new A());
        spa->func();
        spa->release();
        spwa = spa;
    }

    printf("spwa usecount: %d\n", spwa.use_count());
    

    return 0;
}


这样,程序就会自动调用A的析构函数来释放自己呢。但是!!!这样人为地增加一个release()函数相当于手工调用了delete,使用智能指针还有什么意义,我们还得人工管理内存释放。


综合下来,这种在对象内部引用自己的智能指针是一种非常不好的设计,个人觉得还是要杜绝这种错误的用法。



好的,以下是关于直接将`this`指针作为`std::shared_ptr`返回的问题及解决方案的精简总结: --- 直接将`this`指针作为`std::shared_ptr`返回的问题 1.引用计数问题 `std::shared_ptr`使用引用计数来管理对象的生命周期。当一个对象被多个`std::shared_ptr`实例共享时,它们通过共享同一个控制块来维护引用计数。如果直接将`this`指针传递给`std::shared_ptr`,会创建一个新的控制块,导致多个独立的`std::shared_ptr`实例指向同一个对象,但它们的引用计数是独立的。这会导致以下问题: • 重复析构:当其中一个`std::shared_ptr`的生命周期结束时,它会减少引用计数并释放对象。如果其他`std::shared_ptr`仍然存在,它们可能会尝试访问或释放已经被释放的对象,从而导致未定义行为。 • 生命周期管理混乱:多个独立的`std::shared_ptr`无法正确同步对象的生命周期,可能导致对象过早释放或内存泄漏。 2.内存管理问题 直接返回`this`指针作为`std::shared_ptr`绕过了`std::shared_ptr`的内存管理机制,可能会导致以下问题: • 内存泄漏:如果对象在返回`std::shared_ptr`之前已经被释放,那么后续使用该`std::shared_ptr`时可能会导致错误。 • 未定义行为:访问已释放的内存可能导致程序崩溃或其他不可预测的行为。 3.线程安全问题 在多线程环境中,直接返回`this`指针作为`std::shared_ptr`可能导致线程安全问题: • 数据竞争:多个线程可能同时访问和修改同一个对象,而没有适当的同步机制,这可能会导致数据竞争和其他并发问题。 • 引用计数混乱:如果多个线程同时创建独立的`std::shared_ptr`,它们的引用计数可能会出现不一致的情况。 --- 解决方案:`std::enable_shared_from_this` 为了安全地返回`this`指针作为`std::shared_ptr`,应该让目标类继承`std::enable_shared_from_this`,并使用其成员函数`shared_from_this()`来返回`this`的`std::shared_ptr`。`std::enable_shared_from_this`的内部实现机制如下: • 内部维护一个`std::weak_ptr`:`std::enable_shared_from_this`类内部维护一个`std::weak_ptr`,用于观察当前对象的生命周期。 • `shared_from_this()`的实现:当调用`shared_from_this()`时,它会通过内部的`std::weak_ptr`调用`lock()`方法来获取一个共享当前对象生命周期的`std::shared_ptr`。这样可以确保所有`std::shared_ptr`实例共享同一个控制块,从而避免引用计数问题。 示例代码 ```cpp #include <memory> #include <iostream> class MyClass : public std::enable_shared_from_this<MyClass> { public: std::shared_ptr<MyClass> getShared() { return shared_from_this(); // 安全地返回 thisstd::shared_ptr } void doSomething() { std::cout << "Doing something..." << std::endl; } }; int main() { std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>(); std::shared_ptr<MyClass> anotherPtr = ptr->getShared(); // 获取共享的 std::shared_ptr anotherPtr->doSomething(); return 0; } ``` 注意事项 1. `shared_from_this()`的使用时机: • `shared_from_this()`只能在对象已经被`std::shared_ptr`管理后使用。如果在对象尚未被`std::shared_ptr`管理时调用`shared_from_this()`,会抛出异常。 • 例如,不能在构造函数中调用`shared_from_this()`,因为此时对象尚未被`std::shared_ptr`管理。 2. 线程安全: • `std::shared_ptr`的引用计数操作是线程安全的,但`std::enable_shared_from_this`的内部`std::weak_ptr`也需要正确同步。在多线程环境中,确保对象的生命周期管理是线程安全的。 --- 通过使用`std::enable_shared_from_this`,可以安全地返回`this`指针作为`std::shared_ptr`,同时避免引用计数问题、内存管理问题和线程安全问题
05-06
### 关于 `std::shared_ptr` 和 `this` 指针的问题 直接将 `this` 指针传递给 `std::shared_ptr<T>(this)` 存在一个潜在的风险,即它会创建一个新的独立的 `std::shared_ptr` 实例[^1]。这个新实例不会已经存在的任何其他 `std::shared_ptr` 共享引用计数器,因此可能会导致对象被多次释放,进而引发未定义行为。 为了避免这种问题的发生,C++ 提供了一个专门设计的工具——`std::enable_shared_from_this` 类模板[^2]。通过继承自 `std::enable_shared_from_this<T>`,类可以获得一种安全的方式来获取当前对象的一个新的 `std::shared_ptr` 实例,而无需担心重复删除或破坏现有的引用计数机制。具体来说,当调用成员函数 `shared_from_this()` 时,其内部逻辑实际上是基于已有的弱引用 (`std::weak_ptr`) 来构建一个新的强引用(`std::shared_ptr`)[^4]。 下面是一个典型的实现方式: ```cpp #include <memory> #include <iostream> class Test : public std::enable_shared_from_this<Test> { public: void func() { // 使用 shared_from_this 安全地获得一个指向自身的 shared_ptr std::shared_ptr<Test> pTest = shared_from_this(); std::cout << "Reference count: " << pTest.use_count() << "\n"; } }; int main() { auto pTest = std::make_shared<Test>(); pTest->func(); return 0; } ``` 在这个例子中,我们展示了如何利用 `std::enable_shared_from_this` 避免手动构造 `std::shared_ptr` 带来的风险。注意,在实际应用过程中应当始终优先考虑使用 `std::make_shared` 而不是裸 new 运算符来初始化对象,这样可以确保从一开始就正确设置好所有的智能指针关系[^3]。 此外需要注意的是,虽然这里讨论的重点在于解决由错误使用 `std::shared_ptr` 导致的对象生命周期管理难题,但在某些情况下如果只需要单一所有权模型,则应选用更轻量级且语义清晰的 `std::unique_ptr`[^5]。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值