在第 9 节中,我们学习了 newdelete 来手动管理动态内存,在第 13 节中我们了解了异常处理和 RAII 原则。今天,我们来学习一个把这两者完美结合的工具——智能指针(Smart Pointer)

智能指针的核心理念很简单:让对象自动管理内存,程序员不需要手动调用 delete 它是现代 C++ 中最重要的特性之一,也是写出安全、不泄漏内存的代码的关键。

1. 为什么需要智能指针

1.1 裸指针的烦恼

回顾一下使用裸指针(raw pointer)的问题:

void risky() {
    int* ptr = new int(42);
    
    // 如果这里发生了异常...
    doSomething();  // 可能抛出异常
    
    delete ptr;     // 这行可能永远执行不到!
}

如果 doSomething() 抛出异常,delete ptr 不会被执行,内存就泄漏了。虽然我们可以用 try-catch 来解决,但代码会变得非常臃肿。

1.2 智能指针的解决方案

智能指针利用 RAII 原则:在构造时获取资源(new),在析构时释放资源(delete)。当智能指针离开作用域时,析构函数会自动被调用,即使是因为异常导致的离开。

#include <memory>

void safe() {
    std::unique_ptr<int> ptr = std::make_unique<int>(42);
    
    doSomething();  // 即使抛出异常,ptr 的析构函数也会自动调用
    
    // 不需要手动 delete!
}

2. unique_ptr:独占所有权

2.1 基本用法

unique_ptr 是最常用的智能指针。它表示独占所有权——同一时刻只有一个 unique_ptr 拥有某个对象。

#include <iostream>
#include <memory>
using namespace std;

int main() {
    // 创建 unique_ptr(推荐使用 make_unique)
    unique_ptr<int> ptr = make_unique<int>(42);
    
    // 通过 * 解引用访问值
    cout << "值:" << *ptr << endl;  // 输出:42
    
    // 通过 -> 访问成员(用于对象时)
    // ptr->member
    
    // 离开作用域时自动释放内存
    return 0;
}

2.2 不可复制,只能移动

unique_ptr 不允许复制(copy),因为复制会导致两个指针指向同一块内存,违反了「独占」的原则。但它允许移动(move):

#include <iostream>
#include <memory>
using namespace std;

int main() {
    unique_ptr<int> ptr1 = make_unique<int>(42);
    
    // unique_ptr<int> ptr2 = ptr1;  // 编译错误!不能复制
    unique_ptr<int> ptr2 = std::move(ptr1);  // 移动所有权
    
    // 移动后,ptr1 变为空指针
    cout << "ptr1:" << (ptr1 ? "有效" : "为空") << endl;  // 输出:为空
    cout << "ptr2:" << *ptr2 << endl;  // 输出:42
    
    return 0;
}

2.3 管理数组

unique_ptr 也可以管理动态数组:

#include <iostream>
#include <memory>
using namespace std;

int main() {
    unique_ptr<int[]> arr = make_unique<int[]>(5);
    
    for (int i = 0; i < 5; i++) {
        arr[i] = i * 10;
    }
    
    for (int i = 0; i < 5; i++) {
        cout << arr[i] << " ";  // 输出:0 10 20 30 40
    }
    cout << endl;
    
    // 自动调用 delete[] 释放
    return 0;
}

2.4 自定义删除器

有时候你需要在释放资源时执行自定义操作(比如关闭文件、释放网络连接):

#include <iostream>
#include <memory>
#include <cstdio>
using namespace std;

int main() {
    // 用 unique_ptr 管理 FILE*,自定义删除器调用 fclose
    unique_ptr<FILE, decltype(&fclose)> file(fopen("test.txt", "w"), &fclose);
    
    if (file) {
        fprintf(file.get(), "Hello, Smart Pointer!\n");
    }
    // file 离开作用域时自动调用 fclose
    
    return 0;
}

3. shared_ptr:共享所有权

3.1 基本用法

shared_ptr 表示共享所有权——多个 shared_ptr 可以同时拥有同一个对象。它内部维护一个引用计数,当最后一个 shared_ptr 被销毁时,才会释放内存。

#include <iostream>
#include <memory>
using namespace std;

int main() {
    shared_ptr<int> ptr1 = make_shared<int>(42);
    cout << "引用计数:" << ptr1.use_count() << endl;  // 输出:1
    
    {
        shared_ptr<int> ptr2 = ptr1;  // 复制,引用计数 +1
        cout << "引用计数:" << ptr1.use_count() << endl;  // 输出:2
    }
    // ptr2 离开作用域,引用计数 -1
    
    cout << "引用计数:" << ptr1.use_count() << endl;  // 输出:1
    
    return 0;
}
// ptr1 离开作用域,引用计数归零,自动释放内存

3.2 shared_ptr 的开销

shared_ptrunique_ptr 有更多的内存和性能开销:

  • 每个 shared_ptr 需要额外存储一个指向控制块的指针
  • 控制块中包含引用计数、弱引用计数、删除器等信息
  • 引用计数的增减是原子操作,有微小的性能开销

所以:能用 unique_ptr 就不要用 shared_ptr

3.3 make_shared vs new

推荐使用 make_shared 而不是 new 来创建 shared_ptr

// 推荐:一次内存分配(对象和控制块一起分配)
auto ptr = make_shared<int>(42);

// 不推荐:两次内存分配(一次 new 对象,一次分配控制块)
shared_ptr<int> ptr(new int(42));

make_shared 不仅代码更简洁,而且性能更好(减少一次内存分配)。

4. weak_ptr:打破循环引用

4.1 循环引用的问题

shared_ptr 有一个致命的陷阱——循环引用

#include <iostream>
#include <memory>
using namespace std;

struct B;  // 前向声明

struct A {
    shared_ptr<B> b_ptr;
    ~A() { cout << "A 被销毁" << endl; }
};

struct B {
    shared_ptr<A> a_ptr;
    ~B() { cout << "B 被销毁" << endl; }
};

int main() {
    auto a = make_shared<A>();
    auto b = make_shared<B>();
    
    a->b_ptr = b;  // A 指向 B
    b->a_ptr = a;  // B 指向 A
    
    // 离开作用域后,a 的引用计数 = 1(被 b->a_ptr 引用)
    // b 的引用计数 = 1(被 a->b_ptr 引用)
    // 两者都不会归零,内存泄漏!
    return 0;
}

运行这段代码,你会发现 A 和 B 的析构函数都不会被调用——内存泄漏了。

4.2 weak_ptr 的解决方案

weak_ptrshared_ptr 的「观察者」,它不增加引用计数。通过 weak_ptr 可以打破循环引用:

#include <iostream>
#include <memory>
using namespace std;

struct B;

struct A {
    shared_ptr<B> b_ptr;
    ~A() { cout << "A 被销毁" << endl; }
};

struct B {
    weak_ptr<A> a_ptr;  // 改用 weak_ptr!
    ~B() { cout << "B 被销毁" << endl; }
};

int main() {
    auto a = make_shared<A>();
    auto b = make_shared<B>();
    
    a->b_ptr = b;
    b->a_ptr = a;
    
    return 0;
}
// 输出:
// A 被销毁
// B 被销毁

4.3 使用 weak_ptr 访问对象

weak_ptr 不能直接访问对象,需要先调用 lock() 获取一个 shared_ptr

weak_ptr<int> wp = ...;

// 使用前先检查对象是否还存在
if (auto sp = wp.lock()) {
    cout << "对象存在,值为:" << *sp << endl;
} else {
    cout << "对象已被销毁" << endl;
}

lock() 的行为:

  • 如果对象还存在(引用计数 > 0),返回一个有效的 shared_ptr
  • 如果对象已被销毁,返回一个空的 shared_ptr

5. 三种智能指针对比

特性 unique_ptr shared_ptr weak_ptr
所有权 独占 共享 不拥有(观察)
可复制
可移动 -
引用计数 不增加计数
内存开销 最小 较大(控制块) 较小
典型场景 独占资源管理 共享资源 打破循环引用
创建方式 make_unique make_shared shared_ptr 创建

6. 使用建议

6.1 优先使用 unique_ptr

如果你的场景中只有一个指针拥有对象,用 unique_ptr。它没有额外开销,语义也最清晰。

6.2 需要共享时用 shared_ptr

当多个地方需要共享同一个对象,且对象的生命周期不确定时,用 shared_ptr

6.3 用 weak_ptr 打破循环

如果两个 shared_ptr 互相引用,把其中一个改为 weak_ptr

6.4 函数参数传递

// 只读访问:传引用或裸指针
void process(const Widget& widget);
void process(Widget* widget);

// 接收所有权:传 unique_ptr(按值)
void takeOwnership(unique_ptr<Widget> widget);

// 共享所有权:传 shared_ptr(按值或按引用)
void share(shared_ptr<Widget> widget);

6.5 不要混用裸指针和智能指针

// 错误!两个独立的智能指针管理同一块内存
int* raw = new int(42);
unique_ptr<int> p1(raw);
unique_ptr<int> p2(raw);  // p1 和 p2 都会 delete raw,双重释放!

// 正确:只通过智能指针创建
auto p1 = make_unique<int>(42);

7. 总结

这一节我们学习了 C++ 的三种智能指针:

  • unique_ptr:独占所有权,不可复制,推荐首选。
  • shared_ptr:共享所有权,引用计数管理,有额外开销。
  • weak_ptr:观察者,不增加引用计数,用于打破循环引用。

智能指针是现代 C++ 内存管理的基石。掌握了它们,你就再也不需要担心内存泄漏和悬垂指针的问题。下一节我们将继续探索 C++ 的更多高级特性。加油!

更多推荐