13.c++11的智能指针有哪些。weak_ptr的使用场景。什么情况下会产生循环引用
🔬 C++11智能指针详解:weak_ptr与循环引用
📖 内容概览
- 了解C++11中三种智能指针的特点和使用场景
- 掌握weak_ptr的核心原理和应用
- 理解循环引用的产生原因
- 学习如何使用weak_ptr解决循环引用问题
- 学会智能指针的最佳实践
🔍 C++11智能指针类型
C++11引入了三种智能指针,用于自动管理动态内存,避免内存泄漏:
| 智能指针类型 | 所有权模型 | 核心特点 | 适用场景 |
|---|---|---|---|
| unique_ptr | 独占所有权 | 不可拷贝,可移动 零开销抽象 自动释放资源 | 单一对象独占资源 工厂函数返回值 容器元素 |
| shared_ptr | 共享所有权 | 引用计数 线程安全引用计数 可拷贝,可移动 | 多个对象共享资源 长期存在的资源 跨作用域共享 |
| weak_ptr | 弱引用 | 不影响引用计数 需lock()获取shared_ptr 可检测对象是否存活 | 解决循环引用 观察者模式 缓存和资源管理 |
🔄 智能指针详解
🔒 unique_ptr
核心特点:
- 独占资源所有权,同一时间只能有一个unique_ptr指向同一对象
- 不可拷贝,但可以通过
std::move()转移所有权 - 析构时自动释放资源
- 支持自定义删除器
- 零额外开销(与裸指针性能相同) 使用示例:
#include <memory>#include <iostream>int main() { // 创建unique_ptr std::unique_ptr<int> p1 = std::make_unique<int>(42);
// 转移所有权 std::unique_ptr<int> p2 = std::move(p1); // p1现在为空 if (!p1) { std::cout << "p1 is null" << std::endl; } // p2持有资源 std::cout << *p2 << std::endl; // 输出: 42 return 0; // p2自动释放资源}🤝 shared_ptr
- 共享资源所有权,多个shared_ptr可以指向同一对象
- 内部维护引用计数,记录有多少个shared_ptr指向同一对象
- 当引用计数为0时,自动释放资源
- 引用计数是线程安全的
- 有一定的性能开销(引用计数的原子操作)
void func(std::shared_ptr<int> p) { std::cout << "引用计数: " << p.use_count() << std::endl;}int main() { // 创建shared_ptr std::shared_ptr<int> p1 = std::make_shared<int>(100); std::cout << "引用计数: " << p1.use_count() << std::endl; // 输出: 1 // 拷贝shared_ptr,引用计数增加 std::shared_ptr<int> p2 = p1; std::cout << "引用计数: " << p1.use_count() << std::endl; // 输出: 2 // 函数传参,引用计数增加 func(p1); // 输出: 3 return 0; // p1和p2销毁,引用计数变为0,资源释放}🔗 weak_ptr
- 弱引用,不影响引用计数
- 不能直接访问对象,需要通过
lock()方法获取shared_ptr - 可以检测对象是否还存活
- 用于解决shared_ptr的循环引用问题
#include <memory>#include <iostream>int main() { std::shared_ptr<int> sp = std::make_shared<int>(50); std::weak_ptr<int> wp = sp; // 弱引用,引用计数不变 std::cout << "shared_ptr引用计数: " << sp.use_count() << std::endl; // 输出: 1 // 通过lock()获取shared_ptr if (auto p = wp.lock()) { std::cout << "对象值: " << *p << std::endl; // 输出: 50 std::cout << "lock后引用计数: " << sp.use_count() << std::endl; // 输出: 2 } // 销毁shared_ptr sp.reset(); // 检测对象是否存活 if (auto p = wp.lock()) { std::cout << "对象仍然存活" << std::endl; } else { std::cout << "对象已被销毁" << std::endl; // 输出 } return 0;}🔬 weak_ptr的使用场景
🛠️ 解决循环引用
问题:两个或多个对象互相持有shared_ptr,导致引用计数永远不为0,内存无法释放 解决方案:将其中一个shared_ptr改为weak_ptr
🔄 观察者模式
应用:观察者对象持有被观察对象的弱引用,避免影响被观察对象的生命周期 示例:
class Subject { std::vector<std::weak_ptr<Observer>> observers;public: void addObserver(std::shared_ptr<Observer> obs) { observers.push_back(obs); }
void notify() { for (auto& wp : observers) { if (auto sp = wp.lock()) { sp->update(); } }};💾 缓存和资源管理
应用:缓存对象时,使用weak_ptr避免缓存对象影响原对象的生命周期
class Cache { std::unordered_map<std::string, std::weak_ptr<Resource>> cache; std::shared_ptr<Resource> get(const std::string& key) { if (auto it = cache.find(key); it != cache.end()) { if (auto resource = it->second.lock()) { return resource; // 对象已被销毁,从缓存中移除 cache.erase(it); // 创建新资源并缓存 auto resource = std::make_shared<Resource>(key); cache[key] = resource; return resource; }};⚠️ 循环引用的产生
🧩 循环引用示例
#include <memory>#include <iostream>class B; // 前置声明class A {public: std::shared_ptr<B> b; // A持有B的shared_ptr
~A() { std::cout << "A被销毁" << std::endl; }};class B {public: std::shared_ptr<A> a; // B持有A的shared_ptr ~B() { std::cout << "B被销毁" << std::endl; }};int main() { { // 新作用域 auto a = std::make_shared<A>(); auto b = std::make_shared<B>();
// 互相引用,形成循环 a->b = b; b->a = a; std::cout << "a引用计数: " << a.use_count() << std::endl; // 输出: 2 std::cout << "b引用计数: " << b.use_count() << std::endl; // 输出: 2 } // 作用域结束,a和b销毁 // 但A和B的析构函数不会被调用! std::cout << "作用域结束,检查是否内存泄漏" << std::endl; return 0;}输出结果: a引用计数: 2 b引用计数: 2 作用域结束,检查是否内存泄漏 问题分析:
- 当
a和b离开作用域时,它们的引用计数从2减为1 - 但A和B对象仍然互相持有对方的shared_ptr,引用计数永远不会减为0
- 因此A和B的析构函数不会被调用,内存永远不会释放,造成内存泄漏
💡 使用weak_ptr解决循环引用
🛠️ 解决方案
将其中一个shared_ptr改为weak_ptr,打破循环引用:
#include <memory>#include <iostream>class B; // 前置声明class A {public: std::shared_ptr<B> b; // A仍然持有B的shared_ptr
~A() { std::cout << "A被销毁" << std::endl; }};class B {public: std::weak_ptr<A> a; // B持有A的weak_ptr,不影响引用计数 ~B() { std::cout << "B被销毁" << std::endl; }};int main() { { // 新作用域 auto a = std::make_shared<A>(); auto b = std::make_shared<B>();
a->b = b; b->a = a; // 弱引用,a的引用计数仍为1 std::cout << "a引用计数: " << a.use_count() << std::endl; // 输出: 1 std::cout << "b引用计数: " << b.use_count() << std::endl; // 输出: 2 } // 作用域结束,a和b销毁 // A和B的析构函数都会被调用! std::cout << "作用域结束,内存正常释放" << std::endl; return 0;}输出结果: a引用计数: 1 b引用计数: 2 A被销毁 B被销毁 作用域结束,内存正常释放 解决方案分析:
B类使用weak_ptr<A>而不是shared_ptr<A>- 当
a离开作用域时,其引用计数从1减为0,A对象被销毁 - A对象销毁时,会自动销毁其成员
b,导致B对象的引用计数从2减为1 - 当
b离开作用域时,其引用计数从1减为0,B对象被销毁 - 循环引用被打破,内存正常释放
🛠️ 最佳实践
- 优先使用unique_ptr:
- 对于独占资源,优先使用
unique_ptr,它是最高效的智能指针 - 只有当需要共享资源时,才使用
shared_ptr
- 对于独占资源,优先使用
- 使用make_unique和make_shared:
- 更安全:避免内存泄漏(当创建对象和赋值之间发生异常时)
- 更高效:
make_shared只分配一次内存(对象和引用计数一起) - 更简洁:代码更易读
- 避免循环引用:
- 设计类时,注意避免互相持有shared_ptr
- 必要时使用weak_ptr打破循环
- 不要混合使用裸指针和智能指针:
- 避免将裸指针赋值给智能指针
- 避免从智能指针获取裸指针后长期持有
- 选择合适的智能指针:
- 短暂使用的资源:unique_ptr
- 长期共享的资源:shared_ptr
- 解决循环引用:weak_ptr
- 考虑自定义删除器:
- 对于特殊资源(如文件句柄、网络连接),可以为智能指针指定自定义删除器
📊 性能对比
| 智能指针类型 | 内存开销 | 拷贝开销 | 线程安全性 | 适用场景 |
|---|---|---|---|---|
| unique_ptr | 与裸指针相同 | 禁止拷贝,移动开销小 | 不涉及共享,线程安全 | 独占资源 |
| shared_ptr | 额外的引用计数 | 原子操作,开销较大 | 引用计数线程安全 | 共享资源 |
| weak_ptr | 额外的控制块指针 | 拷贝开销小 | 引用计数线程安全 | 解决循环引用 |
📋 总结
- C++11智能指针类型:
unique_ptr:独占所有权,零开销,不可拷贝shared_ptr:共享所有权,引用计数,线程安全weak_ptr:弱引用,不影响引用计数,解决循环引用
- weak_ptr的核心作用:
- 解决shared_ptr的循环引用问题
- 实现观察者模式
- 用于缓存和资源管理
- 检测对象是否存活
- 循环引用的产生:
- 两个或多个对象互相持有shared_ptr
- 导致引用计数永远不为0,内存无法释放
- 是shared_ptr最常见的陷阱
- 解决方案:
- 将其中一个shared_ptr改为weak_ptr
- 打破循环引用,让引用计数能正常减为0
- 最佳实践:
- 优先使用unique_ptr
- 使用make_unique和make_shared
- 避免循环引用
- 不要混合使用裸指针和智能指针 通过合理使用C++11智能指针,可以有效避免内存泄漏,编写更安全、更可靠的C++代码。理解每种智能指针的特点和适用场景,是掌握现代C++内存管理的关键。