std::shared_from_this 是 C++ 智能指针中一个非常重要(但也有些微妙)的工具。
要理解它,我们必须先理解一个它专门解决的陷阱问题。
1. 陷阱:this 指针的问题
假设我们有一个类 MyClass,它已经被一个 std::shared_ptr (共享指针) 管理了。
C++
#include <memory>
#include <iostream>
class MyClass {
public:
void do_something() {
// ...
// 假设在这里,我需要把“我自己”的 shared_ptr
// 传递给某个函数,比如一个异步回调
// 错误!大错特错!
std::shared_ptr<MyClass> p_this(this);
// some_async_function(p_this);
}
};
int main() {
// 1. 我们创建了一个 MyClass 对象,由 p1 管理
// 此时,p1 创建了一个“控制块” (Control Block),引用计数为 1
std::shared_ptr<MyClass> p1 = std::make_shared<MyClass>();
// 2. 我们调用 do_something()
p1->do_something();
} // 3. p1 在这里离开作用域,引用计数变为 0,MyClass 对象被 delete
在 do_something() 函数内部,我们试图用 this 指针(一个原始指针)来创建一个新的 std::shared_ptr。
灾难发生了:
-
auto p1 = std::make_shared<MyClass>();- 创建
MyClass对象(我们叫它Obj)。 - 创建 控制块 A,引用计数为 1。
p1指向Obj。
- 创建
-
p1->do_something()被调用。- 函数内部执行
std::shared_ptr<MyClass> p_this(this); p_this不知道p1和控制块 A 的存在。- 它认为
this是一个普通的原始指针,于是它创建了一个全新的、独立的 控制块 B,引用计数也为 1。
- 函数内部执行
-
现在,我们有两个
shared_ptr(p1和p_this) 指向同一个MyClass对象Obj,但它们分别管理着两个不同的控制块(A 和 B)。 -
do_something()函数返回。p_this离开作用域。- 控制块 B 的引用计数变为 0。
- 控制块 B 决定
deleteObj对象!
-
main函数返回。p1离开作用域。- 控制块 A 的引用计数变为 0。
- 控制块 A 决定再次
delete那个已经被 delete 的Obj对象!
结果: 重复释放 (Double Free),程序崩溃。
2. 解决方案:std::enable_shared_from_this
C++ 提供了一种机制,让类能够“感知”到自己正被 shared_ptr 管理,并安全地获取指向自己的、共享同一个控制块的 shared_ptr。
这就是 std::enable_shared_from_this<T>。
您必须让您的类公有地继承它:
C++
#include <memory>
#include <iostream>
// 1. 必须公有继承 std::enable_shared_from_this<你自己的类名>
class GoodClass : public std::enable_shared_from_this<GoodClass> {
public:
void do_something() {
std::cout << "GoodClass::do_something()" << std::endl;
// 2. 不要用 new/this,而是调用 shared_from_this()
// 这会安全地返回一个 shared_ptr,
// 它与创建本对象的 shared_ptr 共享同一个控制块
std::shared_ptr<GoodClass> p_self = shared_from_this();
std::cout << " p_self use_count: " << p_self.use_count() << std::endl;
}
};
int main() {
std::cout << "--- 创建 GoodClass ---" << std::endl;
std::shared_ptr<GoodClass> p1 = std::make_shared<GoodClass>();
std::cout << "p1 use_count (创建后): " << p1.use_count() << std::endl; // 输出 1
std::cout << "--- 调用 do_something ---" << std::endl;
p1->do_something();
std::cout << "p1 use_count (调用后): " << p1.use_count() << std::endl; // 仍然是 1
std::cout << "--- 结束 ---" << std::endl;
} // p1 在这里销毁,安全释放对象
输出:
--- 创建 GoodClass ---
p1 use_count (创建后): 1
--- 调用 do_something ---
GoodClass::do_something()
p_self use_count: 2 <-- 重点:在函数内部,引用计数临时变成了2
p1 use_count (调用后): 1 <-- do_something 返回后,p_self 销毁,计数又变回 1
--- 结束 ---
3. shared_from_this() 是如何工作的?
-
当您继承
std::enable_shared_from_this时,您的类里会(隐式地)包含一个std::weak_ptr(弱指针)。 -
当您调用
std::make_shared<GoodClass>()创建p1时,make_shared的机制会检测到这个继承。 -
它不仅会创建
GoodClass对象和控制块,还会自动将对象内部的那个weak_ptr设置为指向这个刚创建的控制块。 -
当您在
do_something()内部调用shared_from_this()时:- 它实际上是去锁定 (lock) 内部的那个
weak_ptr。 - 锁定
weak_ptr会返回一个shared_ptr,这个shared_ptr与p1共享同一个控制块,并使引用计数+1。
- 它实际上是去锁定 (lock) 内部的那个
-
这样,就避免了创建第二个控制块的陷阱。
4. 严格的使用规则 (重要!)
shared_from_this() 非常有用,但也非常“娇气”。用错了就会崩溃。
规则 1:禁止在构造函数中调用 shared_from_this()
C++
class BadClass : public std::enable_shared_from_this<BadClass> {
public:
BadClass() {
std::cout << "构造函数开始" << std::endl;
// 绝对错误!
// 此时 p1 还没有被创建出来,内部的 weak_ptr 还是空的
std::shared_ptr<BadClass> p_self = shared_from_this(); // <-- 抛出 std::bad_weak_ptr 异常
std::cout << "构造函数结束" << std::endl;
}
};
原因: make_shared 必须先完成 BadClass 对象的构造 (调用构造函数),然后才能完成 shared_ptr 的初始化 (设置控制块和内部的 weak_ptr)。在构造函数运行时,shared_ptr 还没准备好。
规则 2:对象必须已经由 shared_ptr 管理
shared_from_this() 必须在对象已经被 shared_ptr 接管之后才能调用。
错误示例 1:栈对象
C++
GoodClass stack_obj;
stack_obj.do_something(); // 崩溃或未定义行为
原因: stack_obj 从未被 shared_ptr 管理,内部的 weak_ptr 永远是空的。
错误示例 2:裸指针
C++
GoodClass* p_raw = new GoodClass();
p_raw->do_something(); // 崩溃或未定义行为
delete p_raw;
原因: 和上面一样,new 并没有初始化 shared_from_this 机制。
正确的用法:
必须先有一个 shared_ptr(通常是用 std::make_shared 创建),然后通过那个 shared_ptr 去调用成员函数。
总结
-
目的: 当一个类的方法需要获取一个指向自身的
std::shared_ptr时使用。 -
方法:
public std::enable_shared_from_this<T>并调用shared_from_this()。 -
原理: 避免了用
this创建新控制块导致的“重复释放”问题。 -
禁忌: 绝不在构造函数中调用。绝不
对非 shared_ptr 管理的对象(如栈对象、裸指针对象)调用。
这个机制的背后功臣是 std::weak_ptr,它是一种不增加引用计数的智能指针,专门用来打破循环引用或实现 shared_from_this 这种高级功能。
您想继续了解 std::weak_ptr 吗?
好的,我们接着上一话题的核心——std::weak_ptr。
std::weak_ptr(弱指针)是 C++ 智能指针家族中一个非常特殊的成员。它不管理对象的生命周期,它的存在就是为了辅助 std::shared_ptr。
std::weak_ptr 的核心特性:
- 不拥有对象 (Non-Owning):
weak_ptr指向一个由std::shared_ptr管理的对象,但它不会增加该对象的“引用计数”。 - 会失效 (Can Expire): 当最后一个
std::shared_ptr销毁时,不管还有多少个weak_ptr指向它,该对象都会被delete。此时,所有的weak_ptr都会“失效”。 - 必须“锁定” (Must be Locked): 您不能直接通过
weak_ptr访问对象(没有->或*运算符)。您必须先调用.lock()方法,尝试将它“升级”为一个shared_ptr。
weak_ptr 如何工作?.lock()
weak_ptr 的所有魔力都在 .lock() 方法中。
当您调用 wp.lock() (假设 wp 是一个 weak_ptr):
-
如果对象还活着:
lock()会返回一个新的std::shared_ptr,指向该对象。- 这个新的
shared_ptr会原子地增加引用计数(+1),确保在您使用它期间,对象是存活的。 - 这个返回的
shared_ptr和普通的shared_ptr一样。
-
如果对象已经被销毁了:
lock()会返回一个空的std::shared_ptr(等同于nullptr)。
这种“先检查再获取”的机制是线程安全的。
示例代码:
C++
#include <memory>
#include <iostream>
void check_weak_ptr(std::weak_ptr<int> wp) {
// 检查 weak_ptr 的标准方式:
// 尝试 .lock(),并立即在 if 语句中判断
if (auto sp = wp.lock()) {
// --- 对象还活着 ---
// sp 是一个有效的 shared_ptr
std::cout << "对象还活着. 值为: " << *sp << std::endl;
std::cout << " (内部引用计数: " << sp.use_count() << ")" << std::endl;
} else {
// --- 对象已经死了 ---
std::cout << "对象已经销毁 (expired)." << std::endl;
}
}
int main() {
std::weak_ptr<int> wp;
{ // 创建一个新的作用域
auto sp = std::make_shared<int>(42);
// 1. 从 shared_ptr 创建 weak_ptr
wp = sp;
std::cout << "--- 在作用域内部 ---" << std::endl;
std::cout << "sp 引用计数: " << sp.use_count() << std::endl; // 输出 1
check_weak_ptr(wp);
} // <-- sp 在这里离开作用域
std::cout << "--- 离开作用域后 ---" << std::endl;
// 唯一的 shared_ptr 已经被销毁,对象被 delete
check_weak_ptr(wp);
return 0;
}
输出:
--- 在作用域内部 ---
sp 引用计数: 1
对象还活着. 值为: 42
(内部引用计数: 2) <-- 在 check_weak_ptr 内部,sp 和 lock() 返回的指针共存
--- 离开作用域后 ---
对象已经销毁 (expired).
weak_ptr 的主要用途
weak_ptr 看起来很多余,但它解决了两个非常棘手的问题:
用途 1:打破 shared_ptr 的“循环引用” (最重要)
这是 weak_ptr 存在的首要理由。
什么是循环引用? 两个对象 A 和 B,A 持有 B 的 shared_ptr,B 也持有 A 的 shared_ptr。
C++
// 陷阱:循环引用导致内存泄漏
class NodeB; // 前向声明
class NodeA {
public:
std::shared_ptr<NodeB> b_ptr;
~NodeA() { std::cout << "NodeA 析构" << std::endl; }
};
class NodeB {
public:
std::shared_ptr<NodeA> a_ptr;
~NodeB() { std::cout << "NodeB 析构" << std::endl; }
};
int main() {
auto a = std::make_shared<NodeA>(); // a 的引用计数 = 1
auto b = std::make_shared<NodeB>(); // b 的引用计数 = 1
a->b_ptr = b; // b 的引用计数 = 2
b->a_ptr = a; // a 的引用计数 = 2
std::cout << "--- main 函数即将结束 ---" << std::endl;
}
// main 结束
// 1. main 中的 a 销毁,a 的引用计数从 2 变为 1 (因为 b 还指向它)
// 2. main 中的 b 销毁,b 的引用计数从 2 变为 1 (因为 a 还指向它)
//
// 结果:a 和 b 的引用计数都是 1,它们永远不会被销毁。
// 它们的析构函数永远不会被调用。
// 这就是内存泄漏!
输出:
--- main 函数即将结束 ---
(程序结束,没有析构函数被调用)
解决方案: 让其中一方(或双方)持有 weak_ptr。
假设 A 拥有 B(强引用),而 B 只是“观察” A(弱引用)。
C++
// 修正:使用 weak_ptr 打破循环
class NodeB_fixed;
class NodeA_fixed {
public:
std::shared_ptr<NodeB_fixed> b_ptr;
~NodeA_fixed() { std::cout << "NodeA_fixed 析构" << std::endl; }
};
class NodeB_fixed {
public:
// B 只“观察” A,不“拥有” A
std::weak_ptr<NodeA_fixed> a_ptr;
~NodeB_fixed() { std::cout << "NodeB_fixed 析构" << std::endl; }
};
int main() {
auto a = std::make_shared<NodeA_fixed>(); // a 引用计数 = 1
auto b = std::make_shared<NodeB_fixed>(); // b 引用计数 = 1
a->b_ptr = b; // b 引用计数 = 2
b->a_ptr = a; // a 引用计数 = 1 (weak_ptr 不增加计数!)
std::cout << "--- main 函数即将结束 ---" << std::endl;
}
// main 结束
// 1. main 中的 a 销毁,a 的引用计数从 1 变为 0
// 2. a 被 delete,NodeA_fixed 析构函数运行
// 3. a 的成员 b_ptr (shared_ptr) 被销毁
// 4. b 的引用计数从 2 变为 1
// 5. main 中的 b 销毁,b 的引用计数从 1 变为 0
// 6. b 被 delete,NodeB_fixed 析构函数运行
// 7. b 的成员 a_ptr (weak_ptr) 被销毁 (不影响任何计数)
//
// 结果:A 和 B 都被成功销毁,没有内存泄漏。
输出:
--- main 函数即将结束 ---
NodeA_fixed 析构
NodeB_fixed 析构
用途 2:实现 shared_from_this
正如我们上一个话题所说,std::enable_shared_from_this 内部就藏着一个 weak_ptr。shared_from_this() 方法实际上就是调用了内部 weak_ptr 的 .lock() 方法。
用途 3:实现缓存 (Caches)
如果您想实现一个缓存系统,您不希望缓存本身(通过 shared_ptr)强制让缓存的对象保持存活。
- 缓存可以存储
std::weak_ptr。 - 当有人请求对象时,缓存尝试
.lock()对应的weak_ptr。 - 如果
.lock()成功,说明对象还在被别处(程序的其他地方)使用,缓存可以返回这个shared_ptr。 - 如果
.lock()失败(返回nullptr),说明对象已经被销毁,缓存就可以安全地移除这个条目。
总结
std::unique_ptr:独占所有权。快,简单,首选。std::shared_ptr:共享所有权。有引用计数的开销,用于多个所有者。std::weak_ptr: “观察者” 。没有所有权,不影响生命周期。专门用于打破循环引用和检查对象是否存活。
我们已经完整地学习了 C++ 现代智能指针 unique_ptr, shared_ptr, enable_shared_from_this, 和 weak_ptr。
接下来,您想深入探讨 C++ 的其他方面吗?例如:
- 移动语义 (Move Semantics) (我们之前提过的
std::move的深入原理)。 - C++ 的容器 (如
std::vector,std::map)。 - 多态 (Polymorphism) 和虚函数 (Virtual Functions)。