C++11 shared_ptr 原理与详细教程

一、shared_ptr

std::shared_ptr 是 C++11 引入的另一种智能指针,与 unique_ptr 的独占所有权不同,它实现了共享所有权语义。多个 shared_ptr 实例可以同时管理同一个对象,当最后一个持有该对象的 shared_ptr 被销毁时,对象才会被自动释放。这种机制通过引用计数(reference counting)实现,是解决资源共享问题的理想选择。

核心优势

  • 共享所有权:支持多指针共同管理同一资源
  • 自动释放:最后一个所有者销毁时自动释放资源
  • 灵活性:可与标准容器和算法无缝配合
  • 线程安全:引用计数的修改是原子操作,支持多线程环境
  • 自定义删除器:支持自定义资源释放逻辑

二、shared_ptr 原理

2.1 引用计数机制

shared_ptr 的核心是引用计数(reference count):

  • 每个被管理的对象都有一个关联的引用计数器,记录当前有多少个 shared_ptr 指向它
  • 当创建新的 shared_ptr 拷贝时,引用计数增加 1
  • shared_ptr 被销毁或重置时,引用计数减少 1
  • 当引用计数变为0时,对象被自动删除

2.2 控制块结构

shared_ptr 内部通过控制块(control block)管理引用计数和其他资源。一个典型的控制块包含:

  • 共享引用计数:跟踪管理对象的 shared_ptr 数量
  • 弱引用计数:跟踪观察对象的 weak_ptr 数量(后续讲解)
  • 删除器:用于释放管理对象的函数或函数对象
  • 分配器:用于内存管理的分配器
  • 管理对象指针:指向实际管理的对象

2.3 内存布局

shared_ptr 通常包含两个指针大小的成员:

  1. 指向管理对象的指针(get() 返回的值)
  2. 指向控制块的指针

![shared_ptr内存布局示意图]

当使用 std::make_shared 创建 shared_ptr 时,控制块和管理对象会在同一块内存中分配,减少内存碎片并提高缓存效率。

2.4 简化版 shared_ptr 实现

下面通过实现简化版 shared_ptr 理解其工作原理:

#include <atomic>
#include <utility>

// 前向声明
template <typename T>
class WeakPtr;

// 控制块基类
class ControlBlockBase {
public:
    std::atomic<int> shared_count;  // 共享引用计数
    std::atomic<int> weak_count;    // 弱引用计数

    ControlBlockBase() : shared_count(1), weak_count(0) {}
    virtual ~ControlBlockBase() = default;
    virtual void destroy() = 0;     // 销毁管理对象
    virtual void deallocate() = 0;  // 释放控制块
};

// 针对原始指针的控制块
template <typename T, typename Deleter>
class ControlBlock : public ControlBlockBase {
private:
    T* ptr;          // 管理对象指针
    Deleter deleter; // 删除器

public:
    ControlBlock(T* p, Deleter d) : ptr(p), deleter(std::move(d)) {}

    void destroy() override {
        deleter(ptr); // 使用删除器销毁对象
    }

    void deallocate() override {
        delete this;  // 释放控制块自身
    }
};

// 简化版 shared_ptr 实现
template <typename T>
class SharedPtr {
private:
    T* ptr;                  // 存储的指针
    ControlBlockBase* control_block; // 控制块指针

    // 增加引用计数
    void increase_count() {
        if (control_block) {
            control_block->shared_count.fetch_add(1, std::memory_order_relaxed);
        }
    }

    // 减少引用计数并检查是否需要销毁
    void decrease_count() {
        if (control_block) {
            if (control_block->shared_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
                control_block->destroy();       // 销毁管理对象
                if (control_block->weak_count == 0) {
                    control_block->deallocate(); // 释放控制块
                }
            }
        }
    }

public:
    // 默认构造函数
    SharedPtr() : ptr(nullptr), control_block(nullptr) {}

    // 接受原始指针和删除器的构造函数
    template <typename Deleter = std::default_delete<T>>
    explicit SharedPtr(T* p, Deleter deleter = Deleter()) {
        if (p) {
            ptr = p;
            control_block = new ControlBlock<T, Deleter>(p, std::move(deleter));
        } else {
            ptr = nullptr;
            control_block = nullptr;
        }
    }

    // 拷贝构造函数
    SharedPtr(const SharedPtr& other) : ptr(other.ptr), control_block(other.control_block) {
        increase_count();
    }

    // 移动构造函数
    SharedPtr(SharedPtr&& other) noexcept : ptr(other.ptr), control_block(other.control_block) {
        other.ptr = nullptr;
        other.control_block = nullptr;
    }

    // 析构函数
    ~SharedPtr() {
        decrease_count();
    }

    // 拷贝赋值运算符
    SharedPtr& operator=(const SharedPtr& other) {
        if (this != &other) {
            decrease_count();          // 减少当前引用计数
            ptr = other.ptr;
            control_block = other.control_block;
            increase_count();          // 增加新的引用计数
        }
        return *this;
    }

    // 移动赋值运算符
    SharedPtr& operator=(SharedPtr&& other) noexcept {
        if (this != &other) {
            decrease_count();          // 减少当前引用计数
            ptr = other.ptr;
            control_block = other.control_block;
            other.ptr = nullptr;
            other.control_block = nullptr;
        }
        return *this;
    }

    // 解引用运算符
    T& operator*() const {
        return *ptr;
    }

    // 成员访问运算符
    T* operator->() const {
        return ptr;
    }

    // 数组访问运算符 (C++17)
    T& operator[](size_t index) const {
        return ptr[index];
    }

    // 获取原始指针
    T* get() const {
        return ptr;
    }

    // 获取引用计数
    long use_count() const {
        if (control_block) {
            return control_block->shared_count.load(std::memory_order_relaxed);
        }
        return 0;
    }

    // 检查是否是唯一所有者
    bool unique() const {
        return use_count() == 1;
    }

    // 显式转换为 bool
    explicit operator bool() const {
        return ptr != nullptr;
    }

    // 重置指针
    template <typename Deleter = std::default_delete<T>>
    void reset(T* p = nullptr, Deleter deleter = Deleter()) {
        SharedPtr<T>(p, std::move(deleter)).swap(*this);
    }

    // 交换指针
    void swap(SharedPtr& other) noexcept {
        std::swap(ptr, other.ptr);
        std::swap(control_block, other.control_block);
    }

    // 友元类声明
    friend class WeakPtr<T>;
};

// 辅助函数:创建 shared_ptr
template <typename T, typename... Args>
SharedPtr<T> make_shared(Args&&... args) {
    return SharedPtr<T>(new T(std::forward<Args>(args)...));
}

2.5 关键技术点解析

  1. 引用计数的原子操作

    • 使用 std::atomic 确保引用计数的线程安全
    • 增加计数使用 memory_order_relaxed(仅需原子性)
    • 减少计数使用 memory_order_acq_rel(确保资源释放的正确顺序)
  2. 控制块的多态设计

    • 基类 ControlBlockBase 定义通用接口
    • 派生类 ControlBlock 处理具体类型和删除器
    • 支持不同类型的删除器和分配器
  3. 拷贝与移动语义

    • 拷贝操作增加引用计数
    • 移动操作转移所有权,不修改引用计数
    • 析构时减少计数,为0时销毁对象

三、shared_ptr 使用详解

3.1 基本用法

#include <memory>
#include <iostream>

struct MyClass {
    MyClass(int id) : id(id) { 
        std::cout << "MyClass(" << id << ") constructed\n"; 
    }
    ~MyClass() { 
        std::cout << "MyClass(" << id << ") destroyed\n"; 
    }
    int id;
};

int main() {
    // 创建 shared_ptr (三种方式)
    auto sp1 = std::make_shared<MyClass>(1);       // 推荐:高效,异常安全
    std::shared_ptr<MyClass> sp2(new MyClass(2));  // 不推荐:二次分配
    auto sp3 = std::shared_ptr<MyClass>(sp2);      // 拷贝构造,引用计数变为2

    std::cout << "sp1 use count: " << sp1.use_count() << "\n"; // 1
    std::cout << "sp2 use count: " << sp2.use_count() << "\n"; // 2
    std::cout << "sp3 use count: " << sp3.use_count() << "\n"; // 2

    // 共享所有权
    {
        auto sp4 = sp2;  // 新的拷贝,引用计数变为3
        std::cout << "Inside scope, use count: " << sp4.use_count() << "\n"; // 3
    } // sp4 销毁,引用计数回到2

    // 重置指针
    sp2.reset();        // sp2 不再拥有对象,引用计数变为1
    std::cout << "After sp2 reset, sp3 use count: " << sp3.use_count() << "\n"; // 1

    return 0;
} // sp1, sp3 销毁,引用计数变为0,对象被销毁

输出结果:

MyClass(1) constructed
MyClass(2) constructed
sp1 use count: 1
sp2 use count: 2
sp3 use count: 2
Inside scope, use count: 3
After sp2 reset, sp3 use count: 1
MyClass(1) destroyed
MyClass(2) destroyed

3.2 自定义删除器

shared_ptr 支持自定义删除器,用于非默认的资源释放逻辑:

#include <cstdio>

// 自定义文件删除器
struct FileDeleter {
    void operator()(FILE* fp) const {
        if (fp) {
            std::fclose(fp);
            std::cout << "File closed\n";
        }
    }
};

int main() {
    // 使用自定义删除器管理文件指针
    std::shared_ptr<FILE> file_ptr(
        std::fopen("example.txt", "w"), 
        FileDeleter()
    );
    
    if (file_ptr) {
        std::fputs("Hello, shared_ptr!", file_ptr.get());
    }
    // 离开作用域时自动调用 FileDeleter 关闭文件
    
    return 0;
}

也可使用 lambda 表达式作为删除器:

auto deleter = [](MyClass* p) {
    std::cout << "Custom deleter for MyClass(" << p->id << ")\n";
    delete p;
};

std::shared_ptr<MyClass> sp(new MyClass(3), deleter);

3.3 数组支持 (C++17)

C++17 开始支持管理动态数组:

// C++17 数组支持
auto arr_ptr = std::make_shared<int[]>(5); // 创建包含5个int的数组

// 访问数组元素
for (int i = 0; i < 5; ++i) {
    arr_ptr[i] = i * 10;
}

// 数组版本会自动使用 delete[] 释放资源

3.4 与 weak_ptr 配合使用

shared_ptr 的最大问题是可能产生循环引用,此时需要 weak_ptr 打破循环:

#include <memory>

struct Node {
    int data;
    std::shared_ptr<Node> next;  // 共享指针导致循环引用
    // std::weak_ptr<Node> next;  // 使用弱指针打破循环
};

int main() {
    auto node1 = std::make_shared<Node>(1);
    auto node2 = std::make_shared<Node>(2);
    
    node1->next = node2;
    node2->next = node1;  // 形成循环引用
    
    // 此时 node1 和 node2 的引用计数都是2,离开作用域后不会减为0
    // 导致内存泄漏!
    
    return 0;
}

解决方法是将其中一个 shared_ptr 改为 weak_ptr

struct Node {
    int data;
    std::weak_ptr<Node> next;  // 弱指针不增加引用计数
};

weak_ptr 特性:

  • 不拥有对象所有权,不增加引用计数
  • 可通过 lock() 方法获取 shared_ptr(如果对象存在)
  • 用于解决循环引用问题
  • 可通过 expired() 检查对象是否已被销毁

四、高级应用场景

4.1 多线程共享资源

shared_ptr 的引用计数操作是线程安全的,可以安全地在多线程间传递:

#include <thread>
#include <vector>

void func(std::shared_ptr<MyClass> sp) {
    // 线程安全:引用计数的增减是原子操作
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::cout << "Thread " << std::this_thread::get_id() 
              << ", use count: " << sp.use_count() << "\n";
}

int main() {
    auto sp = std::make_shared<MyClass>(1);
    std::vector<std::thread> threads;
    
    // 创建5个线程共享 sp
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back(func, sp);
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    std::cout << "Main thread, use count: " << sp.use_count() << "\n";
    return 0;
}

4.2 作为容器元素

shared_ptr 可安全存储在标准容器中:

#include <vector>
#include <algorithm>

int main() {
    std::vector<std::shared_ptr<MyClass>> objects;
    
    // 添加元素
    objects.push_back(std::make_shared<MyClass>(1));
    objects.push_back(std::make_shared<MyClass>(2));
    objects.push_back(std::make_shared<MyClass>(3));
    
    // 遍历并访问元素
    std::for_each(objects.begin(), objects.end(), [](const auto& sp) {
        std::cout << "MyClass id: " << sp->id << "\n";
    });
    
    return 0;
}

4.3 类型转换

shared_ptr 提供专门的类型转换函数,类似于 C++ 的强制类型转换:

struct Base { virtual ~Base() = default; };
struct Derived : Base { int value = 42; };

int main() {
    std::shared_ptr<Base> base_ptr = std::make_shared<Derived>();
    
    // 动态转换(安全检查)
    if (auto derived_ptr = std::dynamic_pointer_cast<Derived>(base_ptr)) {
        std::cout << "Derived value: " << derived_ptr->value << "\n";
    }
    
    // 静态转换(无安全检查)
    auto derived_ptr = std::static_pointer_cast<Derived>(base_ptr);
    std::cout << "Derived value: " << derived_ptr->value << "\n";
    
    return 0;
}

五、注意事项与最佳实践

5.1 避免的操作

  1. 不要将原始指针交给多个 shared_ptr

    // 错误示例:多个 shared_ptr 独立管理同一资源
    int* raw_ptr = new int(10);
    std::shared_ptr<int> sp1(raw_ptr);
    std::shared_ptr<int> sp2(raw_ptr); // 严重错误!两个独立控制块,导致双重释放
    
  2. 避免循环引用

    // 循环引用导致内存泄漏
    struct A { std::shared_ptr<B> b; };
    struct B { std::shared_ptr<A> a; };
    
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b = b;
    b->a = a; // 循环引用,引用计数永远不为0
    

    解决:将其中一个改为 std::weak_ptr

  3. 不要用 shared_ptr 管理栈上对象

    // 错误示例:管理栈上对象
    int x = 10;
    std::shared_ptr<int> sp(&x); // 析构时会调用 delete &x,导致未定义行为
    

5.2 最佳实践

  1. 优先使用 std::make_shared

    • 优点1:一次分配内存(控制块+对象),效率更高
    • 优点2:异常安全,避免资源泄漏
    auto sp1 = std::make_shared<MyClass>(); // 推荐
    auto sp2 = std::shared_ptr<MyClass>(new MyClass()); // 不推荐
    
  2. 避免使用 get() 返回的原始指针

    • 不要将 get() 返回的指针交给另一个智能指针
    • 不要手动释放 get() 返回的指针
  3. 正确处理数组(C++17前)

    • C++17 前 shared_ptr 不直接支持数组,需提供自定义删除器
    // C++17 前管理数组的正确方式
    std::shared_ptr<int> arr_ptr(new int[5], std::default_delete<int[]>());
    
  4. 使用 weak_ptr 解决循环引用

    • 在双向链表、树等数据结构中,父节点持有子节点的 shared_ptr,子节点持有父节点的 weak_ptr
  5. 线程安全注意事项

    • shared_ptr 本身的引用计数操作是线程安全的
    • 但管理的对象不是线程安全的,仍需同步机制保护

六、shared_ptr 与 unique_ptr 对比

特性shared_ptrunique_ptr
所有权共享所有权独占所有权
大小两个指针大小一个指针大小
引用计数
拷贝操作允许(增加计数)禁止
移动操作允许允许
循环引用可能(需 weak_ptr 解决)不可能
数组支持C++17 开始支持原生支持
性能引用计数操作有开销无额外开销

选择建议

  • 当需要共享资源时,使用 shared_ptr
  • 当资源仅需独占时,使用 unique_ptr(性能更优)
  • 优先考虑 unique_ptr,仅在必要时才使用 shared_ptr

七、总结

std::shared_ptr 是 C++ 中实现共享所有权的智能指针,通过引用计数机制允许多个指针共同管理同一资源。其核心特点包括:

  • 共享所有权:多个 shared_ptr 可指向同一对象
  • 自动释放:最后一个所有者销毁时释放资源
  • 线程安全:引用计数操作是原子的,支持多线程环境
  • 灵活性:支持自定义删除器和分配器
  • 配合 weak_ptr:可解决循环引用问题

在实际开发中,应根据资源所有权需求选择合适的智能指针,优先考虑 unique_ptr 以获得更好的性能,仅在需要共享资源时使用 shared_ptr

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码事漫谈

感谢支持,私信“已赏”有惊喜!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值