Skip to content

智能指针原理

智能指针是 C++ RAII 的典型应用,彻底解决了裸指针的内存管理问题。理解其底层实现,才能用对、用好。

为什么需要智能指针

cpp
// 裸指针的问题
void bad() {
    int* p = new int(42);
    if (error()) return;   // 泄漏!
    throw std::exception(); // 泄漏!
    delete p;
}

// 智能指针:自动管理
void good() {
    auto p = std::make_unique<int>(42);
    if (error()) return;   // 自动释放
    throw std::exception(); // 自动释放
}

unique_ptr:独占所有权

cpp
#include <memory>

// 基本用法
auto p = std::make_unique<int>(42);
std::cout << *p << "\n";  // 42

// 不可拷贝,只能移动
auto p2 = std::move(p);   // p 变为 nullptr
// auto p3 = p2;           // 编译错误!

// 自定义删除器
auto file_deleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(file_deleter)> fp(fopen("a.txt", "r"), file_deleter);

// 数组版本
auto arr = std::make_unique<int[]>(10);
arr[0] = 1;  // 支持下标访问

// 释放所有权(返回裸指针,需手动管理)
int* raw = p2.release();
delete raw;

// 重置
p2.reset(new int(99));  // 释放旧资源,持有新资源
p2.reset();             // 释放,变为 nullptr

unique_ptr 底层实现

cpp
// 简化版 unique_ptr 实现
template<typename T, typename Deleter = std::default_delete<T>>
class UniquePtr {
    T*      ptr_ = nullptr;
    Deleter del_;

public:
    explicit UniquePtr(T* p = nullptr) : ptr_(p) {}

    ~UniquePtr() {
        if (ptr_) del_(ptr_);
    }

    // 禁止拷贝
    UniquePtr(const UniquePtr&) = delete;
    UniquePtr& operator=(const UniquePtr&) = delete;

    // 允许移动
    UniquePtr(UniquePtr&& other) noexcept : ptr_(other.ptr_) {
        other.ptr_ = nullptr;
    }

    T& operator*()  const { return *ptr_; }
    T* operator->() const { return ptr_; }
    T* get()        const { return ptr_; }
    explicit operator bool() const { return ptr_ != nullptr; }
};
// 零开销:sizeof(UniquePtr<T>) == sizeof(T*)(空基类优化)

shared_ptr:共享所有权

cpp
#include <memory>

// 基本用法
auto sp1 = std::make_shared<int>(42);
auto sp2 = sp1;  // 共享所有权,引用计数 = 2
auto sp3 = sp1;  // 引用计数 = 3

std::cout << sp1.use_count() << "\n";  // 3

sp2.reset();  // 引用计数 = 2
{
    auto sp4 = sp1;  // 引用计数 = 3
}  // sp4 析构,引用计数 = 2

// 最后一个 shared_ptr 析构时,释放对象

shared_ptr 底层实现

cpp
// shared_ptr 的内存布局
// make_shared 版本(推荐):一次分配,控制块和对象在一起
//
// ┌─────────────────────────────────────┐
// │  控制块 Control Block               │
// │  ├── strong_count (引用计数)         │
// │  ├── weak_count (弱引用计数)         │
// │  └── deleter / allocator            │
// ├─────────────────────────────────────┤
// │  对象数据 T                          │
// └─────────────────────────────────────┘
//
// shared_ptr<T> 本身包含两个指针:
// ├── ptr_  → 指向对象
// └── ctrl_ → 指向控制块

// 简化实现
template<typename T>
class SharedPtr {
    struct ControlBlock {
        std::atomic<int> strong_count{1};
        std::atomic<int> weak_count{1};  // 包含一个隐式弱引用
        virtual void destroy() = 0;
        virtual ~ControlBlock() = default;
    };

    T*            ptr_  = nullptr;
    ControlBlock* ctrl_ = nullptr;

public:
    SharedPtr(const SharedPtr& other) : ptr_(other.ptr_), ctrl_(other.ctrl_) {
        if (ctrl_) ctrl_->strong_count.fetch_add(1);
    }

    ~SharedPtr() {
        if (!ctrl_) return;
        if (ctrl_->strong_count.fetch_sub(1) == 1) {
            ctrl_->destroy();  // 析构对象
            if (ctrl_->weak_count.fetch_sub(1) == 1) {
                delete ctrl_;  // 释放控制块
            }
        }
    }
};

shared_ptr 的陷阱

cpp
// 陷阱 1:循环引用 → 内存泄漏
struct Node {
    std::shared_ptr<Node> next;  // 循环引用!
    std::shared_ptr<Node> prev;
};

auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a;  // a 和 b 互相持有,引用计数永远不为 0

// 解决:用 weak_ptr 打破循环
struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node>   prev;  // 弱引用,不增加引用计数
};

// 陷阱 2:从裸指针构造多个 shared_ptr
int* raw = new int(42);
std::shared_ptr<int> sp1(raw);
std::shared_ptr<int> sp2(raw);  // 两个独立控制块!double free!

// 正确:用 make_shared 或从已有 shared_ptr 拷贝
auto sp = std::make_shared<int>(42);
auto sp2 = sp;  // 共享同一控制块

// 陷阱 3:在类内部获取 this 的 shared_ptr
class Foo : public std::enable_shared_from_this<Foo> {
public:
    std::shared_ptr<Foo> get_self() {
        return shared_from_this();  // 正确!
        // return std::shared_ptr<Foo>(this);  // 错误!
    }
};

weak_ptr:弱引用

cpp
#include <memory>

auto sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;  // 不增加引用计数

std::cout << wp.use_count() << "\n";  // 1(只有 sp)
std::cout << wp.expired() << "\n";    // false

// 使用前必须 lock(),获取临时 shared_ptr
if (auto locked = wp.lock()) {
    std::cout << *locked << "\n";  // 42
} else {
    std::cout << "对象已销毁\n";
}

sp.reset();  // 释放对象
std::cout << wp.expired() << "\n";  // true
auto locked = wp.lock();            // 返回空 shared_ptr

缓存场景

cpp
// weak_ptr 经典用途:缓存(不阻止对象被销毁)
class Cache {
    std::unordered_map<int, std::weak_ptr<Data>> cache_;

public:
    std::shared_ptr<Data> get(int id) {
        auto it = cache_.find(id);
        if (it != cache_.end()) {
            if (auto sp = it->second.lock()) {
                return sp;  // 缓存命中
            }
        }
        // 缓存未命中或已过期,重新加载
        auto sp = load_data(id);
        cache_[id] = sp;
        return sp;
    }
};

性能对比

cpp
// unique_ptr:零开销,与裸指针完全等价
// sizeof(unique_ptr<T>) == sizeof(T*)

// shared_ptr:有开销
// sizeof(shared_ptr<T>) == 2 * sizeof(void*)(两个指针)
// 引用计数操作是原子操作(线程安全但有开销)
// make_shared 比 shared_ptr(new T) 少一次内存分配

// 性能建议:
// 1. 优先用 unique_ptr,需要共享时再用 shared_ptr
// 2. 用 make_shared/make_unique,不要直接 new
// 3. 函数参数传 T& 或 T*(不转移所有权时),不要传智能指针
// 4. 热路径避免 shared_ptr 拷贝(原子操作有开销)

void process(const Widget& w);      // 推荐:不涉及所有权
void process(Widget* w);            // 可以:可能为 nullptr
void process(std::unique_ptr<Widget> w);  // 转移所有权
void process(std::shared_ptr<Widget> w);  // 共享所有权(有开销)

关键认知

unique_ptr 是默认选择,零开销;shared_ptr 用于真正需要共享所有权的场景;weak_ptr 打破循环引用。函数参数传引用或裸指针,不要无谓地传智能指针。

系统学习 C++ 生态,深入底层架构