在C++11之后,标准库提供了三种主要的智能指针:std::unique_ptr、std::shared_ptr和std::weak_ptr。它们的出现极大地简化了内存管理,避免了大量手写new/delete的错误,并让资源获取即初始化(RAII)理念得以充分实践。本文将逐一介绍这三种智能指针的特性、适用场景,并给出一些实用技巧。
1. std::unique_ptr
1.1 特性
- 独占所有权:同一时间只能有一个
unique_ptr指向同一个资源。 - 移动语义:支持移动构造和移动赋值,但不支持拷贝。
- 析构自动释放:当
unique_ptr离开作用域时,自动调用对应的删除器释放资源。
1.2 用法示例
#include <memory>
#include <iostream>
struct Node {
int value;
Node(int v) : value(v) { std::cout << "Node(" << value << ") constructed\n"; }
~Node() { std::cout << "Node(" << value << ") destroyed\n"; }
};
int main() {
std::unique_ptr <Node> ptr1 = std::make_unique<Node>(10); // 推荐使用 make_unique
// std::unique_ptr <Node> ptr2(ptr1); // 编译错误:不能拷贝
std::unique_ptr <Node> ptr2 = std::move(ptr1); // 转移所有权
if (!ptr1) std::cout << "ptr1 is now empty\n";
}
1.3 小技巧
- 自定义删除器:如果需要特殊释放逻辑(如文件句柄、网络连接),可以传入第二个模板参数或构造时提供删除器。
auto customDel = [](int* p){ std::cout << "Custom delete\n"; delete p; };
std::unique_ptr<int, decltype(customDel)> up(new int(5), customDel);
- 非堆分配:
unique_ptr可以指向栈上对象,但需使用自定义删除器防止错误删除。
int arr[10];
auto arrPtr = std::unique_ptr<int[], decltype(&std::free)>(arr, [](int*){ /* no op */ });
2. std::shared_ptr
2.1 特性
- 共享所有权:多个
shared_ptr实例可以指向同一资源。 - 引用计数:内部维护计数,计数为0时才释放资源。
- 线程安全:对计数的增减操作是原子操作。
2.2 用法示例
#include <memory>
#include <iostream>
#include <vector>
struct User {
std::string name;
User(const std::string& n) : name(n) { std::cout << name << " created\n"; }
~User(){ std::cout << name << " destroyed\n"; }
};
int main() {
std::shared_ptr <User> p1 = std::make_shared<User>("Alice");
{
std::shared_ptr <User> p2 = p1; // 计数+1
std::cout << "ref count: " << p1.use_count() << '\n';
} // p2离开作用域,计数-1
std::cout << "ref count after block: " << p1.use_count() << '\n';
}
2.3 小技巧
- 避免循环引用:若两个对象互相持有
shared_ptr,会导致内存泄漏。此时使用std::weak_ptr打破循环。
class B; // 前向声明
class A {
public:
std::shared_ptr <B> ptrB;
~A(){ std::cout << "A destroyed\n"; }
};
class B {
public:
std::weak_ptr <A> ptrA; // 注意使用 weak_ptr
~B(){ std::cout << "B destroyed\n"; }
};
-
自定义计数器:在性能敏感场景,可自定义内存分配器或计数实现。
-
配合
std::make_shared:一次性分配对象和控制块,减少内存碎片。
3. std::weak_ptr
3.1 特性
- 非拥有指针:不影响引用计数,不持有资源的所有权。
- 避免循环引用:与
shared_ptr配合使用,解除循环依赖。 - 访问资源:通过
lock()获取对应的shared_ptr,若资源已被释放返回空指针。
3.2 用法示例
std::shared_ptr <int> sp = std::make_shared<int>(42);
std::weak_ptr <int> wp = sp; // 只记录弱引用
sp.reset(); // 资源被释放
if (auto locked = wp.lock()) {
std::cout << "value: " << *locked << '\n';
} else {
std::cout << "resource expired\n";
}
3.3 小技巧
-
常用在观察者模式:观察者持有
weak_ptr,避免强引用导致生命周期被拉长。 -
定期检查:在循环或定时器中使用
weak_ptr检查资源是否已消亡,避免悬空指针。
4. 实用技巧与最佳实践
| 场景 | 推荐智能指针 | 说明 |
|---|---|---|
| 单一所有者,栈或堆资源 | unique_ptr |
自动释放,移动语义 |
| 多个所有者共享 | shared_ptr |
需要时使用 make_shared |
| 循环引用场景 | weak_ptr |
打破循环,避免泄漏 |
| 自定义删除器 | unique_ptr/shared_ptr |
可自定义释放逻辑 |
| 大量短生命周期资源 | unique_ptr |
性能更好,计数开销低 |
| 线程共享资源 | shared_ptr |
计数线程安全,轻量 |
| 资源监控、观察者 | weak_ptr |
观察者不拥有资源,防止生命周期拉长 |
4.1 注意事项
- 不要混用:在同一对象上既用
unique_ptr又用shared_ptr,会产生未定义行为。 - 避免裸指针:尽量使用智能指针,若必须使用裸指针,记得使用
get()而不是operator->直接操作。 - 自定义删除器:若使用数组,需要传入
std::default_delete<T[]>或自己实现。 - 拷贝与移动:
unique_ptr只能移动,shared_ptr可拷贝;使用std::move时注意变量状态。 - 计数溢出:在极端情况下,
shared_ptr计数可能溢出;可通过weak_ptr或分配器进行管理。
5. 小结
C++智能指针是现代C++编程不可或缺的工具,正确使用可以大幅降低内存泄漏、悬空指针等错误的发生概率。熟悉它们的语义、生命周期以及最佳实践,是成为优秀C++开发者的必经之路。希望本文能帮助你快速掌握智能指针的核心概念,并在实际项目中灵活运用。祝编码愉快!