Modern C++ 智能指针浅谈
-1. 前言
这篇文章本蒟蒻咕了好久,大多数时间都在查资料。
您可以将这篇文章看作 C-style 指针浅谈 的续集。
本文参考 cppreference,由 千问 AI 查找文章内容问题、找错别字与贡献结尾。
虽然有 AI 核查,但仍然可能有问题。若有,您可以私信我或在评论区指出。
1. 什么是智能指针?
在 C/C++ 中,动态内存通常通过 malloc/free 或 new/delete 手动管理。然而,这种模式极易引发内存泄漏、重复释放、野指针等问题。为解决这一痛点,C++ 引入了智能指针 —— 一种基于 RAII(Resource Acquisition Is Initialization,资源获取即初始化)原则的封装类,能够在对象生命周期结束时自动释放所管理的资源。
现代 C++(C++11 起)标准库提供了以下智能指针类型:
std::auto_ptr(C++98 引入,C++11 废弃,C++17 移除)std::unique_ptr(独占所有权)std::shared_ptr(共享所有权,引用计数)std::weak_ptr(弱引用,用于打破循环引用)
所有这些智能指针都定义在 <memory> 头文件中,使用时注意包含。
2. Modern C++ 指针与 C-Style 指针的区别是什么
1. 安全性
C-Style 指针类型转换灵活,但容易出现逻辑错误。
考虑下面一段代码:
#include <iostream>
int main() {
int x = 10;
void *ptr = &x;
float *fptr = (float *)ptr; // 编译通过但逻辑错误
std::cout << *fptr;
return 0;
}
在我的电脑上运行结果为
而智能指针就不会这样。他直接禁止了你这样做。
#include <memory>
#include <iostream>
int main() {
int x = 10;
std::unique_ptr<int> ptr{&x};
// std::unique_ptr<float> fptr{ptr};
// 会直接编译错误:no instance of constructor
// std::cout << *fptr;
return 0;
}
2. 内存管理
C-Style 指针需要手动管理内存(malloc() / free() 或 new / delete),容易出现内存泄漏、二次释放等问题。
虽然内存泄漏对 OI 的影响并不会很大,但在工程中就不一定了。
而智能指针则不会,他们在作用域后就会立即销毁。
3. std::auto_ptr
1. 基本用法
#include <memory>
int main() {
std::auto_ptr<int> p(new int(42));
// 作用域结束时自动调用 delete
}
2. 为何被废弃?
尽管初衷良好,auto_ptr 存在致命缺陷:
1. 复制即转移所有权
对 auto_ptr 执行复制(如赋值、函数传参)时,源指针会自动置空,这违背了“复制应保持原对象不变”的语义直觉。
std::auto_ptr<int> p1(new int(114514));
std::auto_ptr<int> p2 = p1; // p1 变为 0!
// *p1; // 未定义行为!
2. 无移动语义支持
C++11 引入了移动语义,而 auto_ptr 的设计早于该机制,无法很好的与现代 C++ 兼容。
std::auto_ptr 在 C++11 中被标记为废弃,并在 C++17 标准中被彻底移除。
不应在新代码中使用 auto_ptr。
4. std::unique_ptr
1. 基本特性
- 独占所有权:同一时间仅一个
unique_ptr拥有资源。 - 不可复制,但可移动:符合 C++11 移动语义。
- 完美替代
auto_ptr。
2. 基本用法
#include <memory>
#include <iostream>
int main() {
auto p1 = std::make_unique<int>(114514);
// 使用 C++14 make_unique 创建一个 unique_ptr
auto p2 = std::unique_ptr<int>(new int(1919810));
// 直接使用 new 创建(不推荐)
// std::unique_ptr 有意显式删除了
// operator=(unique_ptr&) 与 unique_ptr(unique_ptr&),
// 使其无法复制
// 感谢 gtafics dalao 指出了问题所在
auto p3 = std::move(p1); // 显式转移所有权
// p1 == nullptr, p2 拥有资源
std::cout /*<< *p1 << " "*/ << *p2 << " " << *p3;
// 若使用 p1 则会出现错误
// Assertion 'get() != pointer()' failed.
}
除非明确需要共享,否则优先使用 unique_ptr。
5. std::shared_ptr
1. 基本特性
- 多个
shared_ptr可同时指向同一对象。 - 内部维护原子引用计数,当计数归零时自动释放内存。
- 支持复制、赋值,适用于需要共享资源的场景。
2. 基本用法
#include <memory>
#include <iostream>
int main() {
auto p1 = std::make_shared<int>(100);
// 使用 C++11 make_shared 创建一个 shared_ptr
auto p2 = std::shared_ptr<int>(new int(100));
// 使用 new 直接创建(不推荐)
{
std::shared_ptr<int> p3 = p1; // 引用计数变为 2
std::cout << "use_count: " << p1.use_count() << "\n";
}
std::cout << "use_count: " << p1.use_count() << "\n";
// p2 超出作用域析构,计数减为 1
}
引用计数可通过 p.use_count() 查询。
6. std::weak_ptr
1. 为什么需要它?
当两个 shared_ptr 互相持有对方(例如双向链表节点),引用计数永不归零,导致内存泄漏。
weak_ptr 不增加引用计数,仅“观察”对象是否存在。
2. 基本用法
#include <memory>
#include <iostream>
struct node {
int val;
std::shared_ptr<node> next;
std::weak_ptr<node> prev; // 避免循环引用
};
int main() {
auto n1 = std::make_shared<node>(node{1});
auto n2 = std::make_shared<node>(node{2});
n1->next = n2;
n2->prev = n1;
if (auto p = n2->prev.lock()) { // .lock() 转为 `shared_ptr`
std::cout << "n2->prev = " << p->value << "\n";
}
}
weak_ptr不能直接解引用,必须通过.lock()转为shared_ptr(若对象仍存在)。
7. 创建智能指针的最佳方法
C++14 引入 std::make_unique,C++11 已有 std::make_shared。推荐使用它们而非直接 new:
- 异常安全:避免在复杂表达式中因异常导致内存泄漏。
- 性能优化:
make_shared一次性分配对象与控制块,减少一次内存分配。
// 推荐
auto p1 = std::make_unique<std::vector<int>>(1919, 810);
auto p2 = std::make_shared<std::string>("Hello shared_ptr!");
// 不推荐(除非需要自定义删除器)
std::unique_ptr<int> p3(new int(114));
std::unique_ptr<int> p3(new int(514));
8. 自定义删除器(Deleter)
std::unique_ptr 支持自定义资源释放逻辑,适用于管理非 new 分配的资源,例如文件句柄 FILE 等。
默认 Deleter 为 std::default_delete,内部实现就是调用关键字 delete。
#include <memory>
#include <iostream>
#include <cstdio>
int main() {
auto file_deleter = [](FILE* f) {
// 这个示例使用了 lambda 来定义 Deleter
// 也可以使用标准库的做法
// 写一个struct/class,在其中重载 operator()
if (f) {
std::fclose(f);
}
};
std::unique_ptr<FILE, decltype(file_deleter)> fp(
std::fopen("test.txt", "w"), file_deleter
);
fputs("Hello unique_ptr!", fp.get());
return 0;
}
9. 常见陷阱与调试建议
1. 循环引用
```cpp
#include <memory>
#include <iostream>
struct node {
int val;
std::shared_ptr<node> next;
std::shared_ptr<node> prev; // 循环引用
};
int main() {
auto n1 = std::make_shared<node>(node{1});
auto n2 = std::make_shared<node>(node{2});
n1->next = n2;
n2->prev = n1;
return 0;
}
- 现象:
shared_ptr相互引用导致内存永不释放。 - 解决:将其中一个改为
weak_ptr。
2. 混用裸指针初始化多个智能指针
int* raw = new int(10);
std::shared_ptr<int> p1(raw);
std::shared_ptr<int> p2(raw); // 双重 delete!
// return value 3221226356
- 后果:程序崩溃。
- 对策:裸指针只初始化一个智能指针,剩下的用智能指针初始化。
3. 调试技巧
- 使用
.use_count()查看引用计数。 - 对
weak_ptr,先用.expired()判断对象是否已销毁。 - 启用 AddressSanitizer(ASan)、UndefinedSanitizer(UBSan)检测内存错误、未定义行为。具体使用可见往期洛谷日报 Awesome Sanitizers by
\color{red}\text{yurzhang} 。
10. 总结
| 智能指针 | 内存大小(典型) | 引用计数 | 控制块 | 移动/复制语义 | 启用/弃用时间 |
|---|---|---|---|---|---|
auto_ptr |
否 | 无 | 复制即转移(非标准移动) | Since C++98, Deprecated in C++11, Removed in C++17 | |
unique_ptr |
否 | 无 | 可移动,不可复制 | Since C++11 | |
shared_ptr |
是 | 有 | 可复制,可移动 | Since C++11 | |
weak_ptr |
共享 | 共享 | 可复制,可移动 | Since C++11 |
auto_ptr虽无额外内存开销,但其“复制即转移”语义破坏了值语义一致性,导致行为不可预测。unique_ptr通过移动语义提供了安全的所有权转移。shared_ptr和weak_ptr共享控制块,包含强引用计数、弱引用计数,因此内存与运行时开销会更高。
11. The End
By Qwen AI.
从 auto_ptr 的失败教训,到 unique_ptr/shared_ptr/weak_ptr 的成熟体系,C++ 智能指针的演进体现了语言对安全性与性能的双重追求。
如今,Modern C++ 已为我们提供了强大而安全的工具。善用它们,让内存管理不再是“玄学”,而是可预测、可验证的工程实践。
总结:
auto_ptr是历史产物,不应使用;unique_ptr是默认选择;shared_ptr按需使用;weak_ptr专治循环引用。
—— 愿你在 Modern C++ 的世界里,写出既高效又安全的代码。
:::info[AI 使用说明]
本文由 千问 AI 查找文章内容问题、找错别字与贡献结尾。
:::