# 如何在 C++ 中实现“智能指针”——std::shared_ptr 的细节与实践

在现代 C++ 开发中,智能指针已成为内存管理的核心工具。相比手动使用 new / delete,智能指针可以自动管理资源,防止内存泄漏和悬空指针。本文将聚焦 std::shared_ptr,从内部实现原理到实际使用技巧,帮助你在项目中更安全、高效地使用共享所有权。

1. 什么是 std::shared_ptr

std::shared_ptr 是一种引用计数式智能指针,它允许多个指针实例共享同一块资源,并在最后一个指针销毁时自动释放内存。核心特性:

  • 引用计数:记录有多少 shared_ptr 指向同一对象。
  • 线程安全:计数器自增/自减操作采用原子操作。
  • 自定义删除器:可以为不同类型的资源指定不同的销毁方式。

2. 内部实现细节

2.1 控制块(Control Block)

每个 shared_ptr 实例实际上引用了一个 控制块control_block),该块包含:

成员 作用
`std::atomic
use_count` 活跃引用计数
`std::atomic
weak_count| 弱引用计数(与weak_ptr` 相关)
T* ptr 实际指向的对象
Deleter deleter 自定义删除器

控制块在第一个 shared_ptr 创建时分配,并在 use_count 归零后,先销毁对象再释放自身。

2.2 内存布局

struct ControlBlock {
    std::atomic <size_t> use_count{1};
    std::atomic <size_t> weak_count{0};
    T* ptr;
    Deleter deleter;
};

use_count 变为 0 时:

  1. 调用 deleter(ptr) 释放对象。
  2. weak_count 也为 0,则 delete this 释放控制块。

2.3 线程安全实现

bool use_count_dec() {
    return use_count.fetch_sub(1, std::memory_order_acq_rel) == 1;
}

使用 acq_rel(获取/释放)保证在多线程中对计数器的正确顺序。

3. 常见使用场景

3.1 共享资源

std::shared_ptr <Worker> p1 = std::make_shared<Worker>();
std::shared_ptr <Worker> p2 = p1; // 引用计数 +1

3.2 循环引用解决方案

class A;
class B {
public:
    std::shared_ptr <A> a;  // 如果 A 也持有 B 的 shared_ptr,导致循环引用
};
class A {
public:
    std::weak_ptr <B> b;    // 用 weak_ptr 断开循环
};

3.3 自定义删除器

auto deleter = [](int* p){ std::cout << "freeing\n"; delete p; };
std::shared_ptr <int> sp(new int(5), deleter);

3.4 与 C API 交互

std::shared_ptr <FILE> file(fopen("log.txt", "r"), fclose);

4. 性能注意事项

  1. 控制块分配make_shared 在一次内存分配中同时创建对象和控制块,减少碎片。
  2. 弱引用计数:如果不需要 weak_ptr,不必使用 shared_ptr,否则 weak_count 仍会占用额外内存。
  3. 避免大量临时shared_ptr 复制代价较大,尽量传递引用(`const std::shared_ptr &`)或使用 `std::move`。

5. 常见陷阱与解决方案

陷阱 说明 解决方案
悬空指针 shared_ptr 复制后,旧指针仍然有效 reset() 立即释放,或使用 unique_ptr
资源泄漏 循环引用导致计数 never zero 使用 weak_ptr 解除循环
多线程 race use_count 变更不原子 依赖标准库实现,或使用 atomic 计数器

6. 代码示例:实现一个简单的图形系统

#include <memory>
#include <iostream>
#include <vector>

struct Shape {
    virtual void draw() const = 0;
    virtual ~Shape() { std::cout << "Shape destroyed\n"; }
};

struct Circle : Shape {
    void draw() const override { std::cout << "Circle\n"; }
    ~Circle() { std::cout << "Circle destroyed\n"; }
};

struct Group {
    std::vector<std::shared_ptr<Shape>> members;
    void add(const std::shared_ptr <Shape>& s){ members.push_back(s); }
    void drawAll() const { for(auto& m: members) m->draw(); }
    ~Group() { std::cout << "Group destroyed\n"; }
};

int main() {
    auto circle1 = std::make_shared <Circle>();
    auto circle2 = std::make_shared <Circle>();
    auto group = std::make_shared <Group>();
    group->add(circle1);
    group->add(circle2);

    group->drawAll(); // Circle Circle

    // 共享圆形
    circle1->draw();  // Circle

    return 0;
}

运行结束后,控制台会输出:

Circle
Circle
Group destroyed
Circle destroyed
Circle destroyed
Shape destroyed

显示了 shared_ptr 的引用计数机制:只有在所有 shared_ptr 失效后才销毁对象。

7. 小结

  • std::shared_ptr 是 C++11 之后安全、方便的共享所有权实现。
  • 内部通过控制块实现引用计数,线程安全且支持自定义删除器。
  • 正确使用 weak_ptr 可以避免循环引用,保证资源及时释放。
  • 在性能敏感代码中,优先使用 make_shared 并减少临时对象。

掌握这些细节后,你可以在复杂项目中更放心地使用 shared_ptr,让内存管理变得透明、可维护。祝编码愉快!

发表评论