C++中的shared_ptr和unique_ptr

在 C++ 中,std::shared_ptrstd::unique_ptr 是两种核心的智能指针,它们解决了不同的所有权管理问题。理解它们各自的使用场景对于编写安全、高效的现代 C++ 代码至关重要。

一、std::unique_ptr(独占指针)使用场景

1. 独占资源所有权

场景:当资源有单一明确的所有者时
特点

  • 资源只能被一个指针拥有
  • 所有权可以通过 std::move 转移
  • 不支持复制
std::unique_ptr<DatabaseConnection> createConnection() {
    return std::make_unique<DatabaseConnection>("server:3306");
}

void processData() {
    auto conn = createConnection(); // 获得连接所有权
    auto result = conn->query("SELECT * FROM users");
    // conn 离开作用域时自动关闭连接
}

2. 工厂模式返回值

场景:工厂函数创建对象并转移所有权

std::unique_ptr<Shape> createShape(ShapeType type) {
    switch(type) {
        case Circle: return std::make_unique<Circle>();
        case Square: return std::make_unique<Square>();
        default: return nullptr;
    }
}

auto shape = createShape(Circle); // 获得图形对象所有权
shape->draw();

3. 类成员资源管理

场景:类独占管理其成员资源

class Renderer {
public:
    Renderer() : context(std::make_unique<GLContext>()) {}
    
private:
    std::unique_ptr<GLContext> context; // 独占OpenGL上下文
};

4. 在容器中存储资源

场景:容器拥有其元素的所有权

std::vector<std::unique_ptr<Player>> players;

players.push_back(std::make_unique<Player>("Alice"));
players.push_back(std::make_unique<Player>("Bob"));

// 玩家对象生命周期与容器绑定

5. 实现 PIMPL 模式

场景:隐藏实现细节

// Widget.h
class Widget {
public:
    Widget();
    ~Widget(); // 必须声明析构函数
    
private:
    struct Impl;
    std::unique_ptr<Impl> pImpl;
};

// Widget.cpp
struct Widget::Impl {
    int data;
    std::string config;
};

Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default; // 在实现文件中定义

二、std::shared_ptr(共享指针)使用场景

1. 共享资源所有权

场景:多个实体需要访问同一资源
特点

  • 资源在所有使用者销毁后自动释放
  • 内部使用引用计数
class TextureCache {
public:
    std::shared_ptr<Texture> getTexture(const std::string& path) {
        if (!cache.count(path)) {
            cache[path] = std::make_shared<Texture>(path);
        }
        return cache[path];
    }
    
private:
    std::unordered_map<std::string, std::shared_ptr<Texture>> cache;
};

// 多个对象共享同一纹理
auto texture = cache.getTexture("grass.png");
auto obj1 = std::make_shared<GameObject>(texture);
auto obj2 = std::make_shared<GameObject>(texture);

2. 观察者模式

场景:主题与观察者共享状态

class Subject {
public:
    void addObserver(std::shared_ptr<Observer> obs) {
        observers.push_back(obs);
    }
    
    void notify() {
        for (auto& obs : observers) {
            obs->update();
        }
    }
    
private:
    std::vector<std::shared_ptr<Observer>> observers;
};

3. 缓存系统

场景:缓存条目共享访问

class DataCache {
public:
    std::shared_ptr<Data> fetch(int id) {
        std::lock_guard<std::mutex> lock(mutex);
        if (auto it = cache.find(id); it != cache.end()) {
            return it->second.lock(); // 从weak_ptr提升
        }
        
        auto data = std::make_shared<Data>(loadData(id));
        cache[id] = data; // 存储weak_ptr
        return data;
    }
    
private:
    std::unordered_map<int, std::weak_ptr<Data>> cache;
    std::mutex mutex;
};

4. 循环引用解决方案

场景:需要打破循环引用
组合shared_ptr + weak_ptr

class Node {
public:
    void setParent(std::shared_ptr<Node> parent) {
        this->parent = parent; // shared_ptr
        parent->children.push_back(shared_from_this());
    }
    
    void addChild(std::shared_ptr<Node> child) {
        child->parent = weak_from_this(); // weak_ptr
        children.push_back(child);
    }
    
private:
    std::weak_ptr<Node> parent; // 弱引用打破循环
    std::vector<std::shared_ptr<Node>> children;
};

5. 异步操作共享状态

场景:多个异步操作访问共享数据

void asyncProcess(std::shared_ptr<Data> data) {
    std::thread([data] {
        // 在独立线程中使用数据
        process(*data);
    }).detach();
}

auto sharedData = std::make_shared<Data>();
asyncProcess(sharedData);
asyncProcess(sharedData); // 多个操作共享同一数据

三、两种指针对比与决策指南

特性std::unique_ptrstd::shared_ptr
所有权独占共享
复制禁止允许
移动支持支持
开销接近原始指针(极小)引用计数(原子操作)
循环引用无风险需配合 weak_ptr
空状态支持 (nullptr)支持 (nullptr)
定制删除器支持支持
典型大小8 字节(单指针)16-24 字节(控制块+指针)

决策树:如何选择智能指针

graph TD
    A[需要管理动态资源?] --> B{所有权是否可共享?}
    B -->|单一明确所有者| C[unique_ptr]
    B -->|多个实体需要访问| D{存在循环引用风险?}
    D -->|是| E[shared_ptr + weak_ptr]
    D -->|否| F[shared_ptr]
    A -->|仅观察不拥有| G[原始指针或 weak_ptr]

四、性能考量与最佳实践

1. 性能影响

  • unique_ptr:运行时开销几乎为零,适合性能关键路径
  • shared_ptr
    • 引用计数操作是原子的(线程安全但开销较大)
    • 控制块分配额外内存(make_shared 可优化)
    • 不适合高频创建/销毁的场景

2. 创建最佳实践

// 优先使用 make_ 系列函数
auto uptr = std::make_unique<MyClass>(args...);
auto sptr = std::make_shared<MyClass>(args...);

// 避免这种用法(潜在内存泄漏风险)
std::shared_ptr<MyClass> bad(new MyClass(args...));

3. 参数传递指南

// unique_ptr 作为参数(所有权转移)
void takeOwnership(std::unique_ptr<Resource> res);

// shared_ptr 作为参数
void shareResource(const std::shared_ptr<Resource>& res); // 不获取所有权
void storeResource(std::shared_ptr<Resource> res);       // 共享所有权

4. 避免常见陷阱

错误1:不必要的共享所有权

// 不好:使用 shared_ptr 但实际只需 unique_ptr
std::shared_ptr<Logger> logger = std::make_shared<Logger>();

// 更好:单一所有权
std::unique_ptr<Logger> logger = std::make_unique<Logger>();

错误2:混合使用智能指针和原始指针

Resource* raw = new Resource();
std::shared_ptr<Resource> sptr(raw); // 危险!

// 如果其他地方使用相同的 raw 指针
std::shared_ptr<Resource> sptr2(raw); // 双重释放!

错误3:忽略循环引用

class Parent {
    std::shared_ptr<Child> child; // 应使用 weak_ptr
};

class Child {
    std::shared_ptr<Parent> parent; // 循环引用!
};

五、总结:核心使用原则

  1. 默认使用 unique_ptr

    • 适用于大多数单一所有权场景
    • 性能最优,语义最清晰
  2. 需要共享时使用 shared_ptr

    • 配合 weak_ptr 避免循环引用
    • 用于缓存、观察者等共享场景
  3. 避免裸 new/delete

    • 始终使用智能指针管理动态资源
  4. 优先使用 make_shared/make_unique

    • 提供异常安全保证
    • 更高效的内存布局(特别是 make_shared
  5. 接口设计明确所有权

    • 函数参数和返回值清晰表达所有权转移意图

通过合理选择智能指针类型,可以显著提高 C++ 代码的安全性、可读性和可维护性,同时减少内存管理相关的错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

递归书房

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值