在现代 C++ 开发中,手动管理内存已经不再是最佳选择。C++11 及以后版本提供了三种主要的智能指针:std::unique_ptr、std::shared_ptr 和 std::weak_ptr。它们各自承担不同的责任,适用于不同的场景。本文将对这三种智能指针进行对比,并给出一套最佳实践,帮助你在项目中更好地使用它们。
1. 语义对比
| 智能指针 | 所有权 | 复制与移动 | 线程安全 | 用途 |
|---|---|---|---|---|
unique_ptr |
独占 | 只能移动,复制被删除 | 线程安全但不互斥 | 核心资源管理、RAII |
shared_ptr |
共享 | 可复制,引用计数 | 线程安全 | 需要多方共享所有权 |
weak_ptr |
非拥有 | 只能观察 | 与 shared_ptr 共享计数 |
避免循环引用,观察对象生命周期 |
- unique_ptr:严格的独占所有权,任何对象只能被一个
unique_ptr持有。它在离开作用域时会自动调用delete,适合资源拥有者。 - shared_ptr:共享所有权,内部维护一个引用计数。当最后一个
shared_ptr被销毁时,对象才会被释放。适合需要多方持有同一资源的场景,但会带来一定的性能和内存开销。 - weak_ptr:观察型指针,不影响引用计数。常与
shared_ptr搭配使用,用来打破循环引用或实现缓存。
2. 典型使用场景
2.1 unique_ptr
class FileHandler {
public:
explicit FileHandler(const std::string& path) : file_(std::fopen(path.c_str(), "r")) {
if (!file_) throw std::runtime_error("Cannot open file");
}
~FileHandler() { std::fclose(file_); }
// 通过 unique_ptr 传递所有权
void process(std::unique_ptr <FileHandler> other) {
// 这里可以安全地使用 other
}
private:
std::FILE* file_;
};
- 适合包装系统资源(文件、网络连接、线程等)。
- 通过 `std::make_unique (args…)` 创建,保证了异常安全。
2.2 shared_ptr
struct Node {
int value;
std::vector<std::shared_ptr<Node>> children;
};
std::shared_ptr <Node> root = std::make_shared<Node>();
root->value = 0;
root->children.emplace_back(std::make_shared <Node>(Node{1}));
- 适合树形结构、图结构等需要共享节点的情况。
std::make_shared先分配一次内存,存放对象和计数,效率更高。
2.3 weak_ptr
class Observable {
public:
void registerObserver(const std::shared_ptr <Observer>& obs) {
observers_.push_back(obs);
}
void notifyAll() {
for (auto it = observers_.begin(); it != observers_.end(); ) {
if (auto obs = it->lock()) { // 转换为 shared_ptr
obs->update();
++it;
} else {
// 已被销毁的观察者,从列表中移除
it = observers_.erase(it);
}
}
}
private:
std::vector<std::weak_ptr<Observer>> observers_;
};
- 通过
weak_ptr::lock()尝试获取对应的shared_ptr。若已被销毁,返回nullptr。 - 防止
Observable与Observer之间形成强引用循环。
3. 性能与安全注意事项
| 细节 | 说明 |
|---|---|
| 内存布局 | shared_ptr 的计数器会与对象分配在不同的内存块;make_shared 通过一次分配减少碎片。 |
| 对象生命周期 | weak_ptr 本身不影响对象销毁;在使用 lock() 前确保 shared_ptr 仍存在。 |
| 自定义删除器 | unique_ptr 与 shared_ptr 均可通过模板参数或构造函数传入自定义删除器。 |
| 线程安全 | 对同一个 shared_ptr 的引用计数操作是原子操作,但对内部对象的操作不是。需要自行同步。 |
| 循环引用 | 两个对象互相持有 shared_ptr 会导致内存泄漏。改用 weak_ptr 解决。 |
4. 最佳实践
-
默认使用
unique_ptr
在不需要共享所有权的地方,首选unique_ptr。它更轻量,提供更强的所有权语义。 -
使用
make_unique/make_shared
直接调用构造函数会导致两次分配,使用make_可以一次完成。 -
避免裸指针传递
如果需要在函数中只观察对象生命周期,使用const T&或T*是安全的;若要传递所有权,明确使用unique_ptr或shared_ptr。 -
处理循环引用
在父子关系中,子节点使用shared_ptr指向父节点,但父节点使用weak_ptr指向子节点(或相反),取决于生命周期控制。 -
异常安全
使用智能指针可以大幅降低内存泄漏风险;但在涉及多指针互相操作时,仍需谨慎。 -
自定义资源
如需要管理非内存资源(文件、句柄),可以为unique_ptr/shared_ptr提供自定义删除器,例如:struct FileCloser { void operator()(FILE* f) const { std::fclose(f); } }; std::unique_ptr<FILE, FileCloser> file(std::fopen("data.txt", "r")); -
在 STL 容器中使用
vector<std::shared_ptr<T>>等容器常见,但若不需要共享,优先考虑vector<std::unique_ptr<T>>,减少拷贝。
5. 代码示例:一个简单的插件系统
#include <iostream>
#include <memory>
#include <vector>
#include <string>
class Plugin {
public:
virtual void execute() = 0;
virtual ~Plugin() = default;
};
class LoggerPlugin : public Plugin {
public:
void execute() override { std::cout << "LoggerPlugin executed\n"; }
};
class AnalyticsPlugin : public Plugin {
public:
void execute() override { std::cout << "AnalyticsPlugin executed\n"; }
};
class PluginManager {
public:
void addPlugin(std::shared_ptr <Plugin> p) { plugins_.push_back(p); }
void runAll() {
for (auto& p : plugins_) p->execute();
}
private:
std::vector<std::shared_ptr<Plugin>> plugins_;
};
int main() {
PluginManager mgr;
mgr.addPlugin(std::make_shared <LoggerPlugin>());
mgr.addPlugin(std::make_shared <AnalyticsPlugin>());
mgr.runAll(); // 输出两条信息
}
- 通过
shared_ptr管理插件实例,便于多处使用。 PluginManager并不拥有插件的最终所有权,可在需要时通过weak_ptr观察插件生命周期。
6. 小结
- unique_ptr:单一所有权、最高性能、最安全。适合所有权明确的场景。
- shared_ptr:共享所有权、自动销毁、稍微开销大。适合需要多方共享同一对象的情况。
- weak_ptr:非拥有、用于观察、打破循环引用。常与
shared_ptr搭配使用。
掌握这三种智能指针的语义与最佳实践后,你将能写出更安全、更高效、更易维护的 C++ 代码。祝编码愉快!