学习定位:现代 C++ 核心知识。智能指针用于表达动态对象的所有权,日常开发应优先掌握 std::unique_ptr,再理解 std::shared_ptr / std::weak_ptr 的共享所有权场景。
智能指针是 C++11 引入的 RAII 类模板,用于自动管理动态内存,避免手动
new/delete导致的内存泄漏和悬挂指针。
1. 智能指针
传统裸指针的痛点:
- 需要手动
delete,容易遗忘 → 内存泄漏 - 不确定何时释放 → 悬挂指针(访问已释放的内存)
- 类内指针必须在析构函数中释放,若有多处
return或异常,极易遗漏 - 所有权不明确:调用者是否应该
delete返回的指针?
智能指针的解决方案:
- 在栈上创建智能指针对象,管理堆上的裸指针
- 智能指针过期时,自动调用析构函数释放所管理的对象
- 无需手动
delete,异常安全,所有权明确
2. 智能指针类型
头文件:#include <memory>
| 智能指针 | C++标准 | 所有权模型 | 特点 |
|---|---|---|---|
auto_ptr | C++98 | 独占(转移) | 已弃用(C++11起),存在语义缺陷 |
unique_ptr | C++11 | 独占 | 轻量、零开销,不可拷贝,可移动 |
shared_ptr | C++11 | 共享 | 引用计数,最后一个所有者释放对象 |
weak_ptr | C++11 | 弱引用 | 配合 shared_ptr 打破循环引用,不增加引用计数 |
现代 C++ 中,永远不要使用
auto_ptr,使用unique_ptr代替。
3.unique_ptr(独占所有权)
unique_ptr 独占其所管理的对象,同一时间只能有一个 unique_ptr 指向该对象。当 unique_ptr 被销毁时,所管理的对象也被销毁。
3.1 初始化方法
#include <memory>
// 方法1:直接使用 new(C++11)
std::unique_ptr<Person> up1(new Person);
// 方法2:使用 make_unique(C++14 起,推荐)
std::unique_ptr<Person> up2 = std::make_unique<Person>();
// 方法3:先创建裸指针,再交给 unique_ptr(不推荐)
Person* p = new Person;
std::unique_ptr<Person> up3(p);推荐:始终使用 std::make_unique(C++14 及以上)。它更安全(避免内存泄漏)、更高效(一次分配)。
3.2 基本用法
class Person {
public:
int age;
Person() { std::cout << "构造\n"; }
~Person() { std::cout << "析构\n"; }
};
int main() {
auto up = std::make_unique<Person>();
up->age = 25; // 使用 ->
(*up).age = 26; // 使用 *
Person* raw = up.get(); // 获取裸指针(不转移所有权)
// 无需 delete
} // 自动析构3.3 注意事项(unique_ptr)
title: unique_ptr 注意事项
- 不能将普通指针赋值给 `unique_ptr`(必须通过构造函数或 `reset`)
- **不能拷贝构造**:`std::unique_ptr<Person> up2(up1);` 错误
- **不能赋值**:`up2 = up1;` 错误
- 不要用同一个裸指针初始化多个 `unique_ptr`(会导致重复释放)
- 不要用 `unique_ptr` 管理非 `new` 分配的内存(如栈变量、 `malloc` 分配的)
- 不支持指针运算(`++`、`+` 等)
- 函数传参时建议传递引用或裸指针(值传递会调用拷贝构造,被禁止)3.4 常用成员函数
| 函数 | 说明 |
|---|---|
get() | 返回裸指针,不转移所有权 |
reset() | 释放当前管理的对象,可以替换为新对象 |
release() | 释放所有权,返回裸指针,unique_ptr 变为空 |
operator bool() | 判断是否非空 |
auto up = std::make_unique<Person>();
Person* p = up.release(); // up 放弃所有权,变为空,需手动 delete p
up.reset(new Person); // 释放原对象,接管新对象
if (up) { /* 非空 */ }3.5 转移所有权(移动语义)
unique_ptr 不可拷贝,但可以移动(转移所有权)。
std::unique_ptr<Person> up1 = std::make_unique<Person>();
std::unique_ptr<Person> up2 = std::move(up1); // 所有权转移,up1 变空函数返回值:编译器会自动应用移动语义(或 RVO)。
std::unique_ptr<Person> createPerson() {
return std::make_unique<Person>(); // 正确,不会调用拷贝
}
int main() {
auto up = createPerson(); // 移动构造(或 RVO)
}3.6 函数参数传递
// 推荐:传递引用(不转移所有权)
void usePerson(const std::unique_ptr<Person>& up) {
up->doSomething();
}
// 推荐:传递裸指针(更通用,不依赖智能指针类型)
void usePersonRaw(Person* p) {
if (p) p->doSomething();
}
// 转移所有权:传递 unique_ptr 值(调用者需 std::move)
void takeOwnership(std::unique_ptr<Person> up) {
// 函数结束时 up 析构,对象被释放
}
int main() {
auto up = std::make_unique<Person>();
takeOwnership(std::move(up)); // 转移所有权
}3.7 自定义删除器
对于非 new 分配的资源(如 malloc、fopen),可以自定义删除器。
// 管理 malloc 分配的内存
auto deleter = [](int* p) { std::free(p); };
std::unique_ptr<int, decltype(deleter)> up(static_cast<int*>(std::malloc(sizeof(int))), deleter);
// 管理 FILE*
auto fileDeleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(fileDeleter)> fp(fopen("test.txt", "r"), fileDeleter);C++17 起,可以使用 std::unique_ptr<T[], CustomDeleter> 管理数组,但更推荐使用 std::vector 或 std::array。
3.8 使用建议
title: unique_ptr 使用建议
- **默认使用 `unique_ptr`**,除非确实需要共享所有权。
- 始终使用 `std::make_unique`(C++14+)创建。
- 函数参数传递:仅观察时传递裸指针或引用;需要转移所有权时传递 `std::unique_ptr` 值(配合 `std::move`)。
- 作为类成员时,默认使用 `unique_ptr` 表示独占所有权。
- 不要使用 `auto_ptr`。4. shared_ptr(共享所有权)
shared_ptr 使用引用计数管理对象的生命周期。多个 shared_ptr 可以指向同一对象,当最后一个 shared_ptr 被销毁时,对象被释放。
4.1 初始化
#include <memory>
// 方法1:使用 new(不推荐)
// 方法2:使用 make_shared(推荐)
std::shared_ptr<Person> sp2 = std::make_shared<Person>();
// 方法3:从 unique_ptr 转移
std::unique_ptr<Person> up = std::make_unique<Person>();
std::shared_ptr<Person> sp3 = std::move(up);
// 方法4:拷贝构造(引用计数 +1)
std::shared_ptr<Person> sp4 = sp2;4.2 引用计数
auto sp1 = std::make_shared<Person>();
auto sp2 = sp1; // 引用计数变为 2
std::cout << sp1.use_count(); // 输出 2
sp2.reset(); // 引用计数减为 1
// sp1 离开作用域时,引用计数变为 0,对象被销毁4.3 常用成员函数
| 函数 | 说明 |
|---|---|
get() | 返回裸指针 |
reset() | 减少引用计数,若变为 0 则释放对象;可替换为新对象 |
use_count() | 返回引用计数(调试用,不应依赖业务逻辑) |
operator bool() | 判断是否非空 |
unique() | 判断是否独占(use_count() == 1,C++17 起废弃) |
4.4 注意事项
title: shared_ptr 注意事项
- **不要用同一个裸指针初始化多个 `shared_ptr`**(会导致重复释放)。
```cpp
Person* p = new Person;
std::shared_ptr<Person> sp1(p);
std::shared_ptr<Person> sp2(p); // 错误!两个独立控制块- 避免循环引用:两个
shared_ptr互相指向对方,会导致内存泄漏。 - 尽量使用
make_shared,而不是new+shared_ptr构造。 - 不要将
this直接交给shared_ptr(应使用enable_shared_from_this)。 - 性能开销:引用计数需要原子操作,比
unique_ptr稍慢。
#### 4.5 循环引用与 `weak_ptr`
```cpp
struct Node {
std::shared_ptr<Node> next; // 循环引用隐患
~Node() { std::cout << "~Node\n"; }
};
int main() {
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b; // a 引用 b
b->next = a; // b 引用 a,形成循环,引用计数永不归零 → 内存泄漏
}
解决方案:使用 weak_ptr 打破循环。
4.6 自定义删除器
shared_ptr 支持自定义删除器,类型擦除,无需在模板参数中指定类型。
auto deleter = [](Person* p) { delete p; };
std::shared_ptr<Person> sp(new Person, deleter);
// 管理 FILE*
std::shared_ptr<FILE> fp(fopen("test.txt", "r"), fclose);4.7 使用建议
title: shared_ptr 使用建议
- 仅当确实需要共享所有权时才使用 `shared_ptr`,默认应使用 `unique_ptr`。
- 始终使用 `std::make_shared` 创建。
- 使用 `weak_ptr` 打破循环引用。
- 避免将 `shared_ptr` 作为函数参数传递(除非需要共享所有权),传递裸指针或引用即可。
- 注意性能开销,不要滥用。5. weak_ptr(弱引用)
weak_ptr 是一种不控制对象生命周期的智能指针,它指向由 shared_ptr 管理的对象,但不增加引用计数。通常用于打破循环引用,或观察对象是否存活。
5.1 基本用法
std::shared_ptr<Person> sp = std::make_shared<Person>();
std::weak_ptr<Person> wp = sp; // 不增加引用计数
// 检查对象是否存活
if (auto sp2 = wp.lock()) { // 尝试提升为 shared_ptr
sp2->doSomething();
} else {
std::cout << "对象已销毁\n";
}
// 判断是否过期(对象是否已被释放)
if (wp.expired()) { /* ... */ }5.2 解决循环引用
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 使用 weak_ptr 打破循环
~Node() { std::cout << "~Node\n"; }
};
int main() {
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a; // weak_ptr 不增加引用计数
// 正确释放
}5.3 注意事项
title: weak_ptr 注意事项
- `weak_ptr` 不能直接解引用,必须通过 `lock()` 提升为 `shared_ptr`。
- 使用前务必检查提升后的 `shared_ptr` 是否为空。
- `weak_ptr` 不适用于需要确保对象存活的场景(应使用 `shared_ptr`)。6. 最佳实践总结
| 场景 | 推荐方案 |
|---|---|
| 明确独占所有权 | unique_ptr |
| 共享所有权(如多线程、缓存) | shared_ptr + weak_ptr |
| 打破循环引用 | weak_ptr |
| 观察对象,不延长生命周期 | weak_ptr |
| 类成员指针 | 根据所有权选择 unique_ptr 或 shared_ptr |
| 函数参数(仅使用,不转移所有权) | 传递裸指针或引用 |
| 函数参数(转移所有权) | 传 unique_ptr 值(调用者 std::move) |
| 返回堆对象 | 返回 unique_ptr(明确转移所有权) |
| 数组管理 | 优先 vector / array,而非 unique_ptr<T[]> |
黄金法则:
- 能用
unique_ptr就不要用shared_ptr。 - 永远不要手动
delete智能指针管理的对象。 - 使用
make_unique/make_shared,避免显式new。 - 在类成员中,优先使用
unique_ptr表示独占成员,shared_ptr表示共享成员。
7. 完整示例:综合应用
#include <iostream>
#include <memory>
#include <vector>
class Widget {
public:
Widget(int id) : id_(id) { std::cout << "Widget " << id_ << " created\n"; }
~Widget() { std::cout << "Widget " << id_ << " destroyed\n"; }
void show() const { std::cout << "Widget " << id_ << "\n"; }
private:
int id_;
};
// 工厂函数:返回独占所有权
std::unique_ptr<Widget> createWidget(int id) {
return std::make_unique<Widget>(id);
}
// 观察者:不持有所有权
void useWidget(Widget* w) {
if (w) w->show();
}
int main() {
// unique_ptr 示例
auto w1 = createWidget(1);
useWidget(w1.get());
// 转移所有权
auto w2 = std::move(w1);
useWidget(w2.get());
// w1 已空
// shared_ptr 示例
auto sw1 = std::make_shared<Widget>(2);
auto sw2 = sw1; // 共享所有权
std::cout << "use_count: " << sw1.use_count() << '\n';
// weak_ptr 示例
std::weak_ptr<Widget> wp = sw1;
if (auto locked = wp.lock()) {
locked->show();
}
return 0;
}8. 总结
| 智能指针 | 所有权 | 开销 | 使用场景 |
|---|---|---|---|
unique_ptr | 独占 | 零(与裸指针相同) | 默认选择,绝大多数情况 |
shared_ptr | 共享 | 引用计数(原子操作) | 多个所有者,如缓存、多线程共享 |
weak_ptr | 弱引用 | 与 shared_ptr 相同 | 打破循环引用,观察者模式 |
现代 C++ 中,不应该再编写
delete语句。智能指针 + RAII 彻底解决了内存管理问题。