跳至内容

shared_ptr、weak_ptr 与所有权图

理解使用 shared_ptr 的引用计数共享所有权,使用 weak_ptr 打破循环引用,了解控制块和别名构造,以及何时 shared_ptr 不是正确的选择。

shared_ptr:共享所有权

std::shared_ptr 实现共享所有权 ── 多个 shared_ptr 实例可以拥有同一个对象。当最后一个拥有者 shared_ptr 被销毁或重置时,对象才被销毁。

在内部,shared_ptr 维护一个控制块,包含:
- 强引用计数(shared_ptr 所有者的数量)
- 弱引用计数(weak_ptr 观察者的数量)
- 删除器和分配器

shared_ptr 比原始指针大(通常为 2 个指针:一个指向对象,一个指向控制块),并且引用计数在每次拷贝/销毁时有原子操作开销。仅在所有权确实需要共享时才使用它。

make_shared 与引用计数

std::make_shared(args...) 在单次分配中同时分配对象和控制块(而不是两次)。这对缓存更友好,并且避免了裸 new 的异常安全问题。

shared_ptr_basics.cpp
#include <iostream>
#include <memory>
#include <string>

struct Logger {
    std::string name;
    Logger(std::string n) : name(std::move(n)) {
        std::cout << name << " created\n";
    }
    ~Logger() { std::cout << name << " destroyed\n"; }
    void log(const std::string& msg) {
        std::cout << "[" << name << "] " << msg << '\n';
    }
};

int main() {
    auto logger = std::make_shared<Logger>("AppLogger");
    std::cout << "use_count: " << logger.use_count() << '\n';  // 1

    {
        auto copy = logger;  // shared ownership
        std::cout << "use_count: " << logger.use_count() << '\n';  // 2
        copy->log("inside scope");
    }  // copy destroyed, count drops to 1

    std::cout << "use_count: " << logger.use_count() << '\n';  // 1
    logger->log("still alive");
}  // logger destroyed — count drops to 0, Logger deleted

weak_ptr:打破循环

std::weak_ptr 观察 shared_ptr 管理的对象而不延长其生命周期。要访问对象,调用 lock(),它返回一个 shared_ptr ── 如果对象已被销毁则返回 nullptr。weak_ptr 对于打破可能导致泄漏的循环引用至关重要。

weak_ptr_example.cpp
#include <iostream>
#include <memory>
#include <string>

struct Employee;
struct Team {
    std::string name;
    std::vector<std::shared_ptr<Employee>> members;
    Team(std::string n) : name(std::move(n)) {}
    ~Team() { std::cout << "Team " << name << " destroyed\n"; }
};

struct Employee {
    std::string name;
    std::weak_ptr<Team> team;  // weak — does NOT own the team
    Employee(std::string n) : name(std::move(n)) {}
    ~Employee() { std::cout << "Employee " << name << " destroyed\n"; }

    void print_team() {
        if (auto t = team.lock()) {  // try to get shared_ptr
            std::cout << name << " belongs to " << t->name << '\n';
        } else {
            std::cout << name << " has no team\n";
        }
    }
};

int main() {
    auto team = std::make_shared<Team>("Engineering");
    auto alice = std::make_shared<Employee>("Alice");
    auto bob = std::make_shared<Employee>("Bob");

    team->members.push_back(alice);
    team->members.push_back(bob);
    alice->team = team;  // weak_ptr — no circular ownership
    bob->team = team;

    alice->print_team();  // "Alice belongs to Engineering"

    team.reset();  // destroy team
    alice->print_team();  // "Alice has no team" — weak_ptr expired
}
常见陷阱

如果两个对象互相持有 shared_ptr,两者都永远无法达到引用计数零 ── 它们互相保持存活。这是一个内存泄漏,即使你使用了智能指针。

struct Node {
    std::shared_ptr<Node> next;  // 如果 next 指回来,泄漏!
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a;  // 循环 ── a 和 b 都永远不会被销毁

修复方法:将一个方向改为 weak_ptr。在树结构中,子节点持有指向父节点的 weak_ptr(或原始观察者指针)。在双向链表中,prev 指针使用 weak_ptr。

别名构造与 enable_shared_from_this

别名构造函数创建一个与现有 shared_ptr 共享所有权但指向不同对象(通常是成员)的 shared_ptr。只要任何别名 shared_ptr 存在,被别名的对象就保持存活:

struct Pair { int first; int second; };
auto p = std::make_shared<Pair>(Pair{1, 2});
std::shared_ptr<int> sp_first(p, &p->first);  // 共享 p 的控制块
// p 可以离开作用域 ── Pair 保持存活因为 sp_first 仍然引用它

enable_shared_from_this 是一个 CRTP 基类,允许被 shared_ptr 管理的对象通过 shared_from_this() 获取指向自身的 shared_ptr。如果没有它,从 this 构造新的 shared_ptr 会创建第二个控制块,导致双重释放。

最佳实践

shared_ptr 被过度使用了。只有在所有权确实跨独立生命周期共享时才使用它。不要把 shared_ptr 作为不思考所有权的偷懒替代品。

避免在以下情况使用 shared_ptr:
- 存在单一所有者 ── 改用 unique_ptr(零开销)。
- 对象在栈上 ── 直接使用对象本身。
- 你只需要传递引用 ── 传递 T& 或 T*。
- 你用它「因为安全」 ── shared_ptr 有运行时开销(原子引用计数、额外分配)并且隐藏了所有权语义。
- 在性能关键路径中 ── 原子引用计数操作在竞争条件下出奇地昂贵。

合法的共享所有权场景:缓存(多次查找返回同一对象)、发布/订阅系统、共享配置对象,以及具有真正共享节点的图数据结构。

控制块内部原理

理解控制块有助于解释 shared_ptr 的开销。make_shared 将对象和控制块合并为一次分配。从裸 new 构造需要两次单独的分配。

control_block.cpp
#include <memory>
#include <iostream>

struct Widget {
    int data[4];
};

int main() {
    // Two allocations: one for Widget, one for control block
    std::shared_ptr<Widget> p1(new Widget{});

    // Single allocation: Widget + control block together
    auto p2 = std::make_shared<Widget>();

    // Sizes (typical 64-bit platform)
    std::cout << "sizeof(Widget*):          " << sizeof(Widget*) << '\n';   // 8
    std::cout << "sizeof(unique_ptr<Widget>): " << sizeof(std::unique_ptr<Widget>) << '\n';  // 8
    std::cout << "sizeof(shared_ptr<Widget>): " << sizeof(std::shared_ptr<Widget>) << '\n';  // 16

    // shared_ptr is 2 pointers: one to object, one to control block
    // Control block contains: strong count, weak count, deleter, allocator

    std::cout << "p2 use_count: " << p2.use_count() << '\n';  // 1
    std::weak_ptr<Widget> w = p2;
    std::cout << "p2 use_count: " << p2.use_count() << '\n';  // still 1
    // weak_ptr does NOT increase strong count
}
关键要点
  • shared_ptr 使用引用计数 ── 最后一个所有者消失时对象才被销毁
  • make_shared 在一次分配中同时分配对象和控制块(始终优先使用它)
  • weak_ptr 观察但不拥有 ── 使用 lock() 安全访问,打破循环引用
  • shared_ptr 宽度为 2 个指针并有原子引用计数开销 ── 不是免费的
  • 默认使用 unique_ptr;仅在所有权确实共享时才使用 shared_ptr
  • 永远不要从同一个原始指针构造两个 shared_ptr ── 使用 enable_shared_from_this 进行自引用

测验 — 检验你的知识

(15 经验值)

1. 当两个对象互相持有 `shared_ptr` 时会发生什么?

2. 为什么 `std::make_shared` 优于 `shared_ptr<T>(new T(...))`?

3. 如何通过 `weak_ptr` 安全地访问对象?