文章目录
为什么需要智能指针?
在C++开发中,手动管理内存资源(如new
/delete
)容易导致:
- 内存泄漏:忘记释放资源
- 悬垂指针:访问已释放的内存
- 双重释放:多次删除同一对象
智能指针通过RAII(Resource Acquisition Is Initialization)技术自动管理资源生命周期:
- 在构造函数中获取资源
- 在析构函数中释放资源
- 确保异常安全(即使发生异常也能正确释放资源)
// 传统方式 - 有内存泄漏风险
void unsafe_example() {
int* raw_ptr = new int(42);
// 如果这里抛出异常...
delete raw_ptr; // 可能不会执行
}
// 智能指针方式 - 异常安全
void safe_example() {
std::unique_ptr<int> smart_ptr = std::make_unique<int>(42);
// 即使抛出异常,资源也会自动释放
}
三大智能指针使用场景
1. unique_ptr:独占所有权指针
- 适用场景:
- 工厂函数返回对象
- 作为类成员(明确所有权关系)
- 局部临时对象管理
// 工厂函数示例
std::unique_ptr<Connection> create_connection() {
return std::make_unique<Connection>("db://localhost");
}
// 类成员示例
class DeviceController {
private:
std::unique_ptr<Device> device_; // 明确设备所有权
public:
DeviceController()
: device_(std::make_unique<Device>()) {}
};
2. shared_ptr:共享所有权指针
- 适用场景:
- 多对象共享同一资源
- 缓存系统
- 观察者模式(需配合weak_ptr)
// 共享配置数据
class AppConfig {
public:
static std::shared_ptr<AppConfig> load() {
static auto config = std::make_shared<AppConfig>();
return config;
}
};
// 多模块共享配置
void module1() {
auto config = AppConfig::load();
// 使用配置...
}
void module2() {
auto config = AppConfig::load();
// 使用相同配置...
}
3. weak_ptr:解决循环引用问题
- 适用场景:
- 打破shared_ptr循环引用
- 实现缓存和观察者模式
- 临时访问共享资源
class Observer {
std::weak_ptr<Subject> subject_; // 避免循环引用
public:
void observe(std::shared_ptr<Subject> subject) {
subject_ = subject;
}
void notify() {
if (auto subject = subject_.lock()) {
// 安全访问
subject->update();
}
}
};
智能指针最佳实践
1. 优先使用make_unique/make_shared
// 好:单次内存分配(控制块+对象)
auto ptr1 = std::make_shared<Widget>();
// 不好:两次内存分配
std::shared_ptr<Widget> ptr2(new Widget);
2. 避免裸指针转换
// 危险:可能造成双重释放
Widget* raw = new Widget;
std::shared_ptr<Widget> p1(raw);
std::shared_ptr<Widget> p2(raw); // 错误!
// 正确方式
auto p1 = std::make_shared<Widget>();
auto p2 = p1; // 共享所有权
3. 自定义删除器处理特殊资源
// 文件句柄管理
std::unique_ptr<FILE, decltype(&fclose)>
file_ptr(fopen("data.txt", "r"), &fclose);
// 数组类型
std::shared_ptr<int[]> arr(new int[10],
[](int* p) { delete[] p; });
4. 多线程注意事项
std::shared_ptr<Counter> global_counter;
void thread_work() {
// 线程安全:引用计数原子操作
auto local_counter = global_counter;
// 非线程安全:需要额外同步
if (!local_counter) {
std::lock_guard<std::mutex> lock(mtx);
if (!global_counter) {
global_counter = std::make_shared<Counter>();
}
local_counter = global_counter;
}
}
常见陷阱与调试技巧
陷阱1:循环引用
struct Node {
std::shared_ptr<Node> next;
// std::weak_ptr<Node> prev; // 正确方式
std::shared_ptr<Node> prev; // 导致循环引用
};
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2;
node2->prev = node1; // 引用计数永远不会归零
解决方案:使用weak_ptr打破循环
陷阱2:返回unique_ptr的原始指针
class ResourceHolder {
std::unique_ptr<Resource> resource_;
public:
Resource* get() const { return resource_.get(); } // 危险!
};
// 外部可能delete指针导致双重释放
解决方案:返回weak_ptr或const引用
调试技巧:使用enable_shared_from_this
class Session : public std::enable_shared_from_this<Session> {
public:
void process() {
// 安全获取自身的shared_ptr
auto self = shared_from_this();
queue_.push(self); // 放入任务队列
}
};
C++17/20智能指针增强
C++17特性
// 数组支持
std::unique_ptr<int[]> arr = std::make_unique<int[]>(10);
// 结构化绑定
auto [ptr1, ptr2] = std::make_tuple(
std::make_unique<int>(1),
std::make_unique<int>(2)
);
C++20特性
// 原子shared_ptr
std::atomic<std::shared_ptr<Config>> atomic_config;
// make_shared支持对齐内存
auto aligned_ptr = std::make_shared<AlignedStruct>();
总结:何时使用哪种智能指针?
场景 | 推荐指针 | 说明 |
---|---|---|
独占资源 | unique_ptr | 明确所有权,零开销 |
共享资源 | shared_ptr | 需要共享访问权 |
缓存/观察 | weak_ptr | 避免循环引用 |
特殊资源 | 自定义删除器 | 文件句柄、数组等 |
多线程共享 | atomic<shared_ptr> | C++20起可用 |