【C++】零基础入门 · 第 14 节:智能指针(unique_ptr、shared_ptr、weak_ptr)
在第 9 节中,我们学习了 new 和 delete 来手动管理动态内存,在第 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_ptr 比 unique_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_ptr 是 shared_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++ 的更多高级特性。加油!
更多推荐



所有评论(0)