std::shared_from_this()和weak_ptr()

57 阅读10分钟

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

灾难发生了:

  1. auto p1 = std::make_shared<MyClass>();

    • 创建 MyClass 对象(我们叫它 Obj)。
    • 创建 控制块 A,引用计数为 1。p1 指向 Obj
  2. p1->do_something() 被调用。

    • 函数内部执行 std::shared_ptr<MyClass> p_this(this);
    • p_this 不知道 p1 和控制块 A 的存在
    • 它认为 this 是一个普通的原始指针,于是它创建了一个全新的独立的 控制块 B,引用计数也为 1。
  3. 现在,我们有两个 shared_ptr (p1p_this) 指向同一个 MyClass 对象 Obj,但它们分别管理着两个不同的控制块(A 和 B)。

  4. do_something() 函数返回。

    • p_this 离开作用域。
    • 控制块 B 的引用计数变为 0。
    • 控制块 B 决定 delete Obj 对象!
  5. 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() 是如何工作的?

  1. 当您继承 std::enable_shared_from_this 时,您的类里会(隐式地)包含一个 std::weak_ptr (弱指针)。

  2. 当您调用 std::make_shared<GoodClass>() 创建 p1 时,make_shared 的机制会检测到这个继承。

  3. 它不仅会创建 GoodClass 对象和控制块,还会自动将对象内部的那个 weak_ptr 设置为指向这个刚创建的控制块。

  4. 当您在 do_something() 内部调用 shared_from_this() 时:

    • 它实际上是去锁定 (lock) 内部的那个 weak_ptr
    • 锁定 weak_ptr 会返回一个 shared_ptr,这个 shared_ptr p1 共享同一个控制块,并使引用计数+1。
  5. 这样,就避免了创建第二个控制块的陷阱。


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 的核心特性:

  1. 不拥有对象 (Non-Owning): weak_ptr 指向一个由 std::shared_ptr 管理的对象,但它不会增加该对象的“引用计数”。
  2. 会失效 (Can Expire): 当最后一个 std::shared_ptr 销毁时,不管还有多少个 weak_ptr 指向它,该对象都会被delete。此时,所有的 weak_ptr 都会“失效”。
  3. 必须“锁定” (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_ptrshared_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++ 的其他方面吗?例如:

  1. 移动语义 (Move Semantics) (我们之前提过的 std::move 的深入原理)。
  2. C++ 的容器 (如 std::vector, std::map)。
  3. 多态 (Polymorphism) 和虚函数 (Virtual Functions)。