文章目录

C++ 智能指针:unique_ptr 与 shared_ptr 的区别

发布于 2026-04-04 17:32:48 · 浏览 14 次 · 评论 0 条

C++ 智能指针:unique_ptr 与 shared_ptr 的区别

在 C++ 开发中,内存管理是每位开发者必须面对的核心问题。手动管理 newdelete 容易引发内存泄漏、野指针等问题,而智能指针作为 RAII(资源获取即初始化)思想的最佳实践,能够自动管理对象的生命周期。本文将深入探讨 C++11 引入的两种最常用的智能指针——unique_ptrshared_ptr——从设计原理到实际应用,帮助你在项目中做出正确的选择。


1. 为什么需要智能指针

在传统 C++ 编程中,动态分配内存需要手动调用 delete 释放。以下这段代码存在明显的内存泄漏风险:

void processData() {
    int* data = new int[1000];
    // 如果这里抛出异常,delete 永远不会执行
    delete[] data;
}

智能指针通过将指针封装到类对象中,利用析构函数自动释放资源,从根本上避免了这类问题。无论函数是正常返回还是抛出异常,当智能指针对象离开其作用域时,析构函数都会确保内存被正确释放。


2. unique_ptr:独占所有权的智能指针

2.1 核心特性

unique_ptr 是一种独占式智能指针,同一时刻只能有一个 unique_ptr 指向特定对象。当 unique_ptr 被销毁或赋值时,它所管理的对象也会被自动销毁。这种"独占"语义确保了内存管理的确定性,不存在多个指针同时拥有同一对象的所有权问题。

2.2 基本用法

#include <memory>

void uniquePtrExample() {
    // 创建 unique_ptr,托管一个动态分配的整数
    std::unique_ptr<int> ptr1 = std::make_unique<int>(42);

    // 通过 operator* 访问对象
    std::cout << *ptr1 << std::endl;  // 输出 42

    // 通过 get() 获取原始指针(不推荐,破坏封装)
    int* raw = ptr1.get();

    // 移动语义:转移所有权
    std::unique_ptr<int> ptr2 = std::move(ptr1);
    // 现在 ptr1 为空,ptr2 拥有对象
    if (!ptr1) {
        std::cout << "ptr1 现在为空" << std::endl;
    }

    // 出作用域时,ptr2 自动 delete 对象
}

2.3 管理数组

// 管理数组需要使用 delete[]
std::unique_ptr<int[]> arr = std::make_unique<int[]>(10);
arr[0] = 100;  // 可以使用下标操作

3. shared_ptr:共享所有权的智能指针

3.1 核心特性

unique_ptr 不同,shared_ptr 允许多个智能指针同时拥有同一个对象。它通过引用计数(reference counting)机制实现这一点:每当一个新的 shared_ptr 拷贝或赋值给另一个时,引用计数增加;当引用计数归零时,对象才会被销毁。这种机制特别适合需要共享对象所有权的场景。

3.2 基本用法

#include <memory>

void sharedPtrExample() {
    // 创建 shared_ptr
    std::shared_ptr<int> ptr1 = std::make_shared<int>(100);

    // 引用计数为 1
    std::cout << ptr1.use_count() << std::endl;

    {
        // 拷贝构造,引用计数增加到 2
        std::shared_ptr<int> ptr2 = ptr1;
        std::cout << ptr2.use_count() << std::endl;  // 2

    }  // ptr2 销毁,引用计数回到 1

    std::cout << ptr1.use_count() << std::endl;  // 1

    // 出作用域后自动释放
}

3.3 控制块与线程安全

shared_ptr 的内部结构包含一个控制块(control block),用于存储引用计数和可能的弱计数。值得注意的是,引用计数的操作是原子性的,因此 shared_ptr 的拷贝操作是线程安全的。但是,对象的访问和修改本身并不自动具备线程安全性,你需要自行添加同步机制。


4. 核心区别对比

特性 unique_ptr shared_ptr
所有权模型 独占 共享
引用计数 有(原子操作)
内存开销 仅指针大小(通常 8 字节) 指针 + 控制块(约 16-24 字节)
拷贝构造 ❌ 不支持 ✅ 支持
移动构造 ✅ 支持 ✅ 支持
默认构造 ✅ 支持空指针 ✅ 支持空指针
数组支持 ✅ 需使用 unique_ptr<T[]> ❌ 不直接支持
性能 无开销,极快 原子操作,略有开销
循环引用风险 有,需配合 weak_ptr

从性能角度来看,unique_ptr 与原始指针的开销几乎相同,因为它不需要维护引用计数。而 shared_ptr 每次拷贝都需要原子递增引用计数,这会产生一定的性能开销,虽然在现代处理器上这种开销通常可以接受,但对于性能敏感的代码路径,需要谨慎考虑。


5. 常见使用场景

5.1 选择 unique_ptr 的场景

当你确定某个对象只需要被一个所有者持有时,unique_ptr 是最佳选择。这种设计不仅性能最优,而且语义清晰,避免了所有权混乱的问题。

函数返回动态创建的对象

std::unique_ptr<Config> loadConfig(const std::string& path) {
    auto config = std::make_unique<Config>();
    config->loadFromFile(path);
    return config;  // 移动语义,零拷贝
}

类成员变量管理资源

class FileHandler {
private:
    std::unique_ptr<FILE, decltype(&fclose)> file_;
public:
    FileHandler(const char* filename)
        : file_(fopen(filename, "r"), fclose) {}
};

工厂模式的返回值

std::unique_ptr<Shape> createCircle(double radius) {
    return std::make_unique<Circle>(radius);
}

5.2 选择 shared_ptr 的场景

当多个对象或函数需要共享同一资源的所有权时,shared_ptr 是合适的工具。

观察者模式中的主题共享

class Subject {
    std::vector<std::shared_ptr<Observer>> observers_;
public:
    void addObserver(std::shared_ptr<Observer> obs) {
        observers_.push_back(obs);
    }
};

需要在多个地方共享的配置对象

auto config = std::make_shared<Config>();
auto cache = std::make_shared<ConfigCache>(config);
auto validator = std::make_shared<ConfigValidator>(config);

对象需要被容器长期持有

std::vector<std::shared_ptr<DatabaseConnection>> connections;
connections.push_back(std::make_shared<DatabaseConnection>());

6. 常见陷阱与注意事项

6.1 避免循环引用

使用 shared_ptr 时最大的陷阱是循环引用。假设对象 A 持有指向 B 的 shared_ptr,而 B 又持有指向 A 的 shared_ptr,则两者的引用计数永远无法归零,导致内存泄漏。

// 错误示例:循环引用
class B;  // 前向声明

class A {
public:
    std::shared_ptr<B> b;
};

class B {
public:
    std::shared_ptr<A> a;
};

void circularReference() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b = b;
    b->a = a;
    // 引用计数都是 2,永远不会释放
}

解决方案:使用 weak_ptr 打破循环。weak_ptr 不参与引用计数,只在需要时尝试获取所有权。

// 正确示例:使用 weak_ptr
class B;

class A {
public:
    std::weak_ptr<B> b;  // 改为 weak_ptr
};

class B {
public:
    std::shared_ptr<A> a;
};

void noCircularRef() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b = b;
    b->a = a;
    // 现在可以正确释放
}

6.2 不要混用原始指针和智能指针

将同一个原始指针同时传递给多个 shared_ptr 会导致重复释放:

// 错误示例
int* raw = new int(100);
std::shared_ptr<int> p1(raw);
std::shared_ptr<int> p2(raw);  // 重复释放!

正确做法是让智能指针直接管理 new 的结果,或者使用 enable_shared_from_this

6.3 自定义删除器

unique_ptrshared_ptr 都支持自定义删除器,这对于管理非内存资源(如文件句柄、网络连接)非常有用:

// 使用自定义删除器管理 FILE*
auto filePtr = std::unique_ptr<FILE, decltype(&fclose)>(
    fopen("data.txt", "r"), fclose);

// 使用 shared_ptr 管理网络连接
struct NetworkDeleter {
    void operator()(Connection* conn) const {
        conn->disconnect();
        delete conn;
    }
};
std::shared_ptr<Connection> conn(new Connection(), NetworkDeleter());

6.4 优先使用 make_unique 和 make_shared

尽可能使用 std::make_uniquestd::make_shared 创建智能指针,而非直接使用 new 表达式。这不仅语法更简洁,还能避免异常安全问题,并减少内存分配次数:

// 推荐
auto ptr = std::make_unique<int>(42);
auto ptr2 = std::make_shared<int>(100);

// 不推荐
std::unique_ptr<int> ptr3(new int(42));
std::shared_ptr<int> ptr4(new int(100));

7. 选择指南

在大多数场景下,优先考虑 unique_ptr。它具有更低的内存开销、更清晰的语义和更好的性能。只有在确实需要共享所有权时,才使用 shared_ptr。如果你发现自己在使用 shared_ptr 后频繁遇到循环引用或性能问题,这通常是设计需要重新审视的信号。

unique_ptr 几乎可以替代所有需要独占所有权的情况,而 shared_ptr 则应该谨慎使用。理解这两种智能指针的设计哲学,将帮助你在代码可维护性和性能之间找到最佳平衡点。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文