shared_ptr循环依赖因相互强引用导致引用计数无法归零,造成内存泄漏;解决方法是使用weak_ptr打破循环,weak_ptr不增加引用计数,通过lock()安全访问对象,确保在无强引用时对象可被释放。
C++中
导致的循环依赖,本质上是对象间相互持有强引用,导致引用计数永远无法归零,从而造成内存泄漏。解决这个问题的核心方案是引入
,它提供了一种非拥有性的引用,能够打破循环。
当我们谈论C++的智能指针,尤其是
时,它无疑是管理动态内存的一把利器。它通过引用计数机制,确保对象在不再被任何
引用时自动释放。然而,这套机制并非万无一失,它有一个著名的陷阱——循环依赖(或称循环引用)。说实话,我个人第一次遇到这个问题时,着实困惑了一阵子,代码逻辑看起来都没错,但内存就是不释放。
解决方案
循环依赖的发生,通常是因为两个或多个对象通过
相互持有对方的引用。想象一下A对象有一个
指向B,同时B对象也有一个
指向A。当外部对A和B的
都失效后,A的引用计数因为B的存在而不会降到0,B的引用计数也因为A的存在而不会降到0。它们就像两个互相抱紧溺水的人,谁也无法放手,最终一同沉没,导致内存泄漏。
立即学习
“
C++免费学习笔记(深入)
”;
解决之道,就是引入
。
是一种“弱引用”智能指针,它不增加对象的引用计数。你可以把它理解为一种观察者,它能“看”到对象,但不会“拥有”对象。当所有
都释放后,即便还有
指向该对象,对象也会被正确销毁。
的强大之处在于,它提供了一个
方法,可以尝试获取一个
。如果对象仍然存在(即至少有一个
还在引用它),
会返回一个有效的
;否则,它会返回一个空的
。
以下是一个经典的循环依赖示例及其
解决方案:
运行上述代码,你会看到A和B的析构函数被正确调用,表明内存得到了释放。关键在于,当
函数结束,
和
这两个
离开
作用域
时,它们所持有的对象的引用计数会减一。对于
对象,它的引用计数降为0(因为
持有的是
,而
持有的是
,
不增加引用计数),
被销毁。
销毁后,其内部的
也会被销毁,导致
的引用计数降为0,
也被销毁。这样,循环就被完美打破了。
C++
循环引用究竟是如何发生的?
要真正理解
的巧妙,我们得先深挖一下
循环引用的根源。这并不是
设计上的缺陷,而是它“共享所有权”语义的自然结果。每个
内部都维护着一个控制块(control block),这个控制块存储着两个计数器:一个是强引用计数(use_count),记录有多少个
指向该对象;另一个是弱引用计数(weak_count),记录有多少个
指向该对象。
当一个
被创建或复制时,强引用计数增加。当
被销毁或重新赋值时,强引用计数减少。只有当强引用计数降到零时,被管理的对象才会被销毁。
C函数速查手册(CHM版)
C函数速查手册(CHM版)
下载
循环引用就发生在两个或多个对象彼此“强拥有”对方的时候。
举个例子:
当
函数执行完毕,局部变量
和
被销毁。
被销毁,
对象的强引用计数从2降到1(因为
还在持有)。
被销毁,
对象的强引用计数从2降到1(因为
还在持有)。
此时,
和
对象的强引用计数都为1,谁都无法降到0。这意味着它们所指向的内存永远不会被释放,即使它们已经无法从程序中被访问到,形成了内存泄漏。这就是
循环引用的发生机制。它不是错误,而是
强所有权语义在特定场景下的一个副作用。
是如何解决循环依赖的,以及它有哪些使用上的注意事项?
解决循环依赖的核心机制,在于它不参与对象的强引用计数。它仅仅是“观察”对象是否存在,而不会影响对象的生命周期。当一个
被创建时,它只会增加对象的弱引用计数(weak_count),这个计数只用于判断控制块是否可以被销毁,而不是对象本身。只要对象的强引用计数不为零,它就不会被销毁。
使用
时,最关键的一点是,你不能直接通过
访问它所指向的对象。你需要先通过
方法,尝试获取一个
。
如果对象仍然存在(即至少有一个
在引用它),
会返回一个有效的
。你可以像使用普通
一样安全地访问对象。
如果对象已经被销毁(所有
都已释放),
会返回一个空的
(即
)。这时,你必须检查返回的
是否为空,以避免访问已销毁的内存,这是一种非常重要的安全机制。
使用注意事项:
务必检查的返回值:
这是
使用的黄金法则。
所指向的对象随时可能被销毁,因此在使用前必须通过
这样的结构来确保对象仍然有效。
选择正确的“弱”边:
在设计对象关系时,需要仔细考虑哪一方应该持有弱引用。通常,拥有者持有
,被拥有者或者观察者持有
。
父子关系:
如果父对象拥有子对象,子对象需要访问父对象但不能影响父对象的生命周期,那么子对象应该持有父对象的
。例如,一个
持有其
的
,而
则持有
的
。
观察者模式:
在观察者模式中,被观察者通常持有观察者的
。这样,当观察者自身生命周期结束时,它就可以被安全销毁,而不会因为被观察者持有强引用而造成泄漏。
缓存:
缓存系统有时会使用
来引用缓存项。如果一个缓存项没有其他强引用,它就可以被垃圾回收,即使缓存本身还“记得”它。
的开销:
的创建、复制和销毁都会操作控制块,
方法也需要一定的开销。但这些开销通常很小,在大多数应用中可以忽略不计。过度担心性能而避免使用
,可能导致更严重的内存泄漏问题。
不能直接解引用:
记住,
本身不提供
或
。它只是一个句柄,必须先提升为
才能使用。
除了
,还有其他避免
循环引用的策略吗?
虽然
是解决
循环依赖最标准、最推荐的方案,但在某些情况下,我们也可以从设计层面去规避这个问题。这往往需要我们重新审视对象间的关系和所有权语义。
重新设计所有权关系:
这是最根本的策略。很多时候,循环依赖的出现,可能暗示着对象模型本身存在一些不清晰或不合理之处。
单向所有权:
问问自己,两个对象真的都需要“拥有”对方吗?是否可以将其中的一个关系改为单向引用?例如,A拥有B,B知道A的存在但并不拥有A(即B内部持有A的裸指针或
)。
明确的层次结构:
在树形或图状结构中,尽量建立明确的父子关系,让父节点拥有子节点,子节点通过
或裸指针(在生命周期明确受控的情况下)引用父节点。
引入中间管理者:
有时,可以将相互引用的两个对象A和B的共同管理职责抽离到一个第三者C。由C持有A和B的
,而A和B之间则只通过裸指针或
进行通信。这样,C负责它们的生命周期,A和B则避免了直接的强引用循环。
使用裸指针(极度谨慎):
在某些非常特殊且生命周期严格受控的场景下,可以考虑使用裸指针来打破循环。但这种做法风险极高,因为它完全放弃了智能指针提供的安全性。你必须100%确定:
被裸指针指向的对象在其生命周期内不会被提前销毁。
裸指针绝不会被用于删除对象。
裸指针的使用范围和时间都非常有限。
这种方法通常只适用于内部实现细节,且有明确的注释和文档说明。对于初学者或大多数应用场景,强烈不建议使用。
事件/回调机制:
当对象之间需要相互通信但又不想建立直接的强引用时,可以考虑事件或回调机制。
例如,A需要知道B的状态变化,而不是直接持有B的
。B可以提供一个注册回调的接口,A通过这个接口注册一个lambda函数或成员函数。当B状态变化时,它调用这些回调。这里的关键是,B在存储这些回调时,如果回调涉及到A的成员函数,B应该存储一个
,并且这个
内部捕获的
指针应该是
的
结果,或者干脆只存储一个不捕获A的
的普通函数指针。
总的来说,
是C++
标准库
为解决
循环引用提供的优雅且安全的方案。而其他策略更多的是从设计思想上进行规避,它们在某些特定场景下可能更合适,但通常也伴随着更高的设计复杂性或潜在的风险。在实际开发中,优先考虑
,如果发现
导致代码结构复杂或不自然,再回头审视对象间的关系是否可以简化。
shared_ptrweak_ptrshared_ptrshared_ptrshared_ptrshared_ptrshared_ptrshared_ptrshared_ptrweak_ptrweak_ptrshared_ptrweak_ptrweak_ptrlock()shared_ptrshared_ptrlock()shared_ptrshared_ptrweak_ptr#include
#include
#include
class B; // 前向声明
class A {
public:
std::shared_ptr b_ptr;
std::string name;
A(const std::string& n) : name(n) {
std::cout << "A " << name << " constructed." << std::endl;
}
~A() {
std::cout << "A " << name << " destructed." << std::endl;
}
void set_b(std::shared_ptr b) {
b_ptr = b;
}
};
class B {
public:
// 循环依赖问题:这里如果也是 shared_ptr a_ptr; 就会形成循环
// 解决方案:使用 weak_ptr
std::weak_ptr a_ptr;
std::string name;
B(const std::string& n) : name(n) {
std::cout << "B " << name << " constructed." << std::endl;
}
~B() {
std::cout << "B " << name << " destructed." << std::endl;
}
void set_a(std::shared_ptr a) {
a_ptr = a;
}
void use_a() {
if (auto sharedA = a_ptr.lock()) { // 尝试获取 shared_ptr
std::cout << "B " << name << " is using A " << sharedA->name << std::endl;
} else {
std::cout << "B " << name << ": A is no longer available." << std::endl;
}
}
};
void create_circular_dependency() {
std::shared_ptr a = std::make_shared("Alpha");
std::shared_ptr b = std::make_shared("Beta");
// 建立相互引用
a->set_b(b);
b->set_a(a); // 这里B持有A的weak_ptr
std::cout << "A's ref count: " << a.use_count() << std::endl; // 此时为1 (因为b_ptr持有)
std::cout << "B's ref count: " << b.use_count() << std::endl; // 此时为1 (因为a_ptr持有)
b->use_a(); // B可以安全地使用A
} // a 和 b 在这里离开作用域,shared_ptr 被销毁
int main() {
create_circular_dependency();
std::cout << "End of main function." << std::endl;
// 如果没有使用 weak_ptr,A和B的析构函数将不会被调用,造成内存泄漏。
// 使用 weak_ptr 后,A和B会正确析构。
return 0;
} create_circular_dependencyabshared_ptrab_ptrshared_ptrbweak_ptraab_ptrbbshared_ptrweak_ptrshared_ptrshared_ptrshared_ptrshared_ptrweak_ptrshared_ptrshared_ptrclass Parent;
class Child;
class Parent {
public:
std::shared_ptr child;
Parent() { std::cout << "Parent constructed." << std::endl; }
~Parent() { std::cout << "Parent destructed." << std::endl; }
};
class Child {
public:
std::shared_ptr parent; // 问题所在:这里是 shared_ptr
Child() { std::cout << "Child constructed." << std::endl; }
~Child() { std::cout << "Child destructed." << std::endl; }
};
void create_problem() {
std::shared_ptr p = std::make_shared();
std::shared_ptr c = std::make_shared();
p->child = c; // Parent持有Child,Child的强引用计数变为2 (p->child 和 c)
c->parent = p; // Child持有Parent,Parent的强引用计数变为2 (c->parent 和 p)
std::cout << "Parent ref count: " << p.use_count() << std::endl; // 输出 2
std::cout << "Child ref count: " << c.use_count() << std::endl; // 输出 2
} // p 和 c 离开作用域 create_problempcpParentc->parentcChildp->childParentChildshared_ptrshared_ptrweak_ptrweak_ptrweak_ptrweak_ptrweak_ptrweak_ptr::lock()shared_ptrshared_ptrlock()shared_ptrshared_ptrshared_ptrlock()shared_ptrnullptrshared_ptrlock()weak_ptrweak_ptrif (auto shared_obj = weak_ptr_instance.lock()) { ... }shared_ptrweak_ptrweak_ptrNodechildrenshared_ptrchildrenParentweak_ptrweak_ptrweak_ptrweak_ptrweak_ptrlock()weak_ptrweak_ptrweak_ptroperator*operator->shared_ptrweak_ptrshared_ptrweak_ptrshared_ptrweak_ptrweak_ptrshared_ptrweak_ptrshared_ptrstd::functionstd::functionthislock()thisweak_ptrshared_ptrweak_ptrweak_ptr