文章目录
5. shared_ptr和weak_ptr
5.1 shared_ptr循环引用问题
- shared_ptr大多数情况下管理资源非常合适,支持RAII,也支持拷贝。但是在循环引用的场景下会导致资源没得到释放内存泄漏,所以我们要认识循环引用的场景和资源没释放的原因,并且学会使用weak_ptr解决这种问题。
- 如下图所述场景,n1和n2析构后,管理两个节点的引用计数减到1
- 右边的节点什么时候释放呢,左边节点中的
_next
管着呢,_next
析构后,右边的节点就释放了。_next
什么时候析构呢,_next
是左边节点的的成员,左边节点释放,_next
就析构了。- 左边节点什么时候释放呢,左边节点由右边节点中的
_prev
管着呢,_prev
析构后,左边的节点就释放了。_prev
什么时候析构呢,_prev
是右边节点的成员,右边节点释放,_prev
就析构了。
- 至此逻辑上成功形成回旋镖似的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏
- 把
ListNode
结构体中的_next
和_prev
改成weak_ptr
,weak_ptr
绑定到shared_ptr
时不会增加它的引用计数,_next
和_prev
不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题
struct ListNode
{
int _data; // 节点数据
// 方案1:使用shared_ptr(会导致循环引用)
std::shared_ptr<ListNode> _next; // 指向下一个节点的智能指针
std::shared_ptr<ListNode> _prev; // 指向前一个节点的智能指针
// 方案2:使用weak_ptr(解决循环引用)
/*std::weak_ptr<ListNode> _next; // 弱引用指向下一节点
std::weak_ptr<ListNode> _prev; // 弱引用指向前一节点*/
// weak_ptr特点:
// 1. 当n1->_next = n2绑定shared_ptr时不增加n2的引用计数
// 2. 不参与资源释放的管理
// 3. 成功避免循环引用问题
~ListNode()
{
cout << "~ListNode()" << endl; // 析构时输出提示
}
};
int main()
{
// 演示循环引用导致的内存泄露问题
std::shared_ptr<ListNode> n1(new ListNode); // 创建第一个节点
std::shared_ptr<ListNode> n2(new ListNode); // 创建第二个节点
cout << n1.use_count() << endl; // 输出1(初始引用计数)
cout << n2.use_count() << endl; // 输出1(初始引用计数)
// 建立双向链接,使用shared_ptr会导致循环引用
n1->_next = n2; // n2的引用计数加1
n2->_prev = n1; // n1的引用计数加1
cout << n1.use_count() << endl; // 输出2(因为n2->_prev也指向n1)
cout << n2.use_count() << endl; // 输出2(因为n1->_next也指向n2)
// 注意:weak_ptr的限制
// 1. weak_ptr不支持直接管理资源
// 2. 不支持RAII(资源获取即初始化)
// 3. weak_ptr只能绑定到shared_ptr上
//std::weak_ptr<ListNode> wp(new ListNode); // 错误!weak_ptr不能直接管理资源
// 程序结束时:
// 1. 由于循环引用,n1和n2的引用计数永远不会降到0
// 2. 导致内存泄露
// 3. 析构函数不会被调用
return 0;
}
5.2 weak_ptr
- weak_ptr 不支持RAII,也不支持访问资源,所以我们看文档发现weak_ptr构造时不支持绑定到资源,只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数,那么就可以解决上述的循环引用问题。
- weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的shared_ptr已经释放了资源,那么他去访问资源就是很危险的。weak_ptr支持expired检查指向的资源是否过期,use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用lock返回一个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是一个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
int main()
{
// 创建一个shared_ptr,管理string对象"111111"
std::shared_ptr<string> sp1(new string("111111"));
// sp2和sp1指向相同的对象,引用计数增加到2
std::shared_ptr<string> sp2(sp1);
// 创建weak_ptr,观察sp1指向的对象,不增加引用计数
std::weak_ptr<string> wp = sp1;
cout << wp.expired() << endl; // 输出0(false),wp观察的对象还存在
cout << wp.use_count() << endl; // 输出2,表示被观察对象的引用计数
// sp1指向新对象"222222",原对象的引用计数减1(变为1)
sp1 = make_shared<string>("222222");
cout << wp.expired() << endl; // 输出0(false),因为sp2还在引用原对象
cout << wp.use_count() << endl; // 输出1,sp2仍在引用原对象
// sp2也指向新对象"333333",原对象引用计数减为0并释放
sp2 = make_shared<string>("333333");
cout << wp.expired() << endl; // 输出1(true),原对象已被释放
cout << wp.use_count() << endl; // 输出0,没有shared_ptr引用原对象
// wp重新绑定到sp1当前指向的对象("222222")
wp = sp1;
// 通过weak_ptr获取一个shared_ptr
//std::shared_ptr<string> sp3 = wp.lock();
auto sp3 = wp.lock(); // lock()返回一个shared_ptr,引用计数加1
cout << wp.expired() << endl; // 输出0(false),对象存在
cout << wp.use_count() << endl; // 输出2,sp1和sp3都指向对象
// 通过sp3修改对象,因为sp1和sp3指向同一对象,所以sp1看到的值也改变
*sp3 += "###";
cout << *sp1 << endl; // 输出"222222###"
return 0;
// 程序结束时sp1、sp3析构,对象被释放
}
6. shared_ptr的线程安全问题
- shared_ptr的引用计数对象在堆上,如果多个shared_ptr对象在多个线程中,进行shared_ptr的拷贝析构时会访问修改引用计数,就会存在线程安全问题,所以shared_ptr引用计数是需要加锁或者原子操作保证线程安全的。
- shared_ptr指向的对象也是有线程安全的问题的,但是这个对象的线程安全问题不归shared_ptr管,它也管不了,应该有外层使用shared_ptr的人进行线程安全的控制。
- 下面的程序会崩溃或者A资源没释放,bit::shared_ptr引用计数从
int*
改成atomic<int>*
就可以保证引用计数的线程安全问题,或者使用互斥锁加锁也可以。
struct AA
{
int _a1 = 0; // 第一个计数器,初始化为0
int _a2 = 0; // 第二个计数器,初始化为0
~AA()
{
cout << "~AA()" << endl; // 析构时输出提示
}
};
int main()
{
// 创建一个shared_ptr管理AA对象
bit::shared_ptr<AA> p(new AA);
// 定义循环次数
const size_t n = 100000;
// 创建互斥锁用于同步
mutex mtx;
// 定义线程要执行的函数
auto func = [&]() {
for (size_t i = 0; i < n; ++i)
{
// 创建智能指针的副本,此时会原子地增加引用计数
// 这个操作本身是线程安全的,不需要加锁
bit::shared_ptr<AA> copy(p);
{
// 作用域加锁,保护对象成员的修改
unique_lock<mutex> lk(mtx);
copy->_a1++; // 增加第一个计数器
copy->_a2++; // 增加第二个计数器
}
// 作用域结束,锁自动释放
// copy析构,引用计数自动减1
}
};
// 创建两个线程执行相同的任务
thread t1(func);
thread t2(func);
// 等待两个线程完成
t1.join();
t2.join();
// 输出最终结果
cout << p->_a1 << endl; // 应该输出200000 (100000 * 2)
cout << p->_a2 << endl; // 应该输出200000 (100000 * 2)
cout << p.use_count() << endl;// 输出1,因为只有p一个引用了
return 0;
// 程序结束时p析构,AA对象被删除
}
7. C++11和boost中智能指针的关系
- Boost库是为C++语言标准库提供扩展的一些C++程序库的总称,Boost社区建立的初衷之一就是为C++的标准化工作提供可供参考的实现,Boost社区的发起人Dawes本人就是C++标准委员会的成员之一。在Boost库的开发中,Boost社区也在这个方向上取得了丰硕的成果,C++11及之后的新语法和库有很多都是从Boost中来的。
- C++ 98 中产生了第一个智能指针auto_ptr。
- C++ boost给出了更实用的scoped_ptr/scoped_array和shared_ptr/shared_array和weak_ptr等.
- C++ TR1,引入了shared_ptr等,不过注意的是TR1并不是标准版。
- C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。
8. 内存泄漏
8.1 什么是内存泄漏,内存泄漏的危害
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存,一般是忘记释放或者发生异常释放程序未能执行导致的。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:普通程序运行一会就结束了出现内存泄漏问题也不大,进程正常结束,页表的映射关系解除,物理内存也可以释放。长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务、长时间运行的客户端等等,不断出现内存泄漏会导致可用内存不断变少,各种功能响应越来越慢,最终卡死。
int main()
{
// 申请一个1G未释放,这个程序多次运行也没啥危害
// 因为程序马上就结束,进程结束各种资源也就回收了
char* ptr = new char[1024 * 1024 * 1024];
cout << (void*)ptr << endl;
return 0;
}
8.2 如何检测内存泄漏(了解)
- linux下内存泄漏检测:linux下几款内存泄漏检测工具
- windows下使用第三方工具:windows下的内存泄露检测工具VLD使用_windows内存泄漏检测工具-CSDN博客
8.3 如何避免内存泄漏
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
- 尽量使用智能指针来管理资源,如果自己场景比较特殊,采用RAII思想自己造个轮子管理。
- 定期使用内存泄漏工具检测,尤其是每次项目快上线前,不过有些工具不够靠谱,或者是收费。
- 总结一下:内存泄漏非常常见,解决方案分为两种:
- 1、事前预防型。如智能指针等。
- 2、事后查错型。如泄漏检测工具。
9. 定制删除器
namespace bit
{
template<class T>
class shared_ptr
{
public:
// 构造函数,传入指针,初始化引用计数为1
// 默认参数nullptr,支持无参构造
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
{}
// 支持自定义删除器的构造函数
// 模板参数D表示删除器类型,可以接收各种可调用对象
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new int(1))
, _del(del) // 存储删除器
{}
// 析构函数
// 引用计数减1,如果减到0释放资源
~shared_ptr()
{
if (--(*_pcount) == 0)
{
cout << "delete:" << _ptr << endl;
//_del替代了直接delete,更加灵活
_del(_ptr); // 使用删除器释放资源
delete _pcount; // 释放计数器
}
}
// 重载操作符,让智能指针用起来像普通指针
T& operator*() { return *_ptr; } // 解引用
T* operator->() { return _ptr; } // 箭头运算符
// 拷贝构造,共享管理同一资源
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount); // 引用计数加1
}
// 赋值运算符重载
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//检查自赋值
if (_ptr == sp._ptr)
return *this;
// 释放原来的资源
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
// 共享新资源
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
return *this;
}
// 获取引用计数
int use_count() const { return *_pcount; }
// 获取原始指针
T* get() const { return _ptr; }
private:
T* _ptr; // 指向管理的资源
int* _pcount; // 引用计数
// 默认删除器使用delete,可以被构造函数中传入的删除器替换
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
}
// 自定义数组删除器
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
delete[] ptr; // 使用delete[]释放数组
}
};
/*
使用示例:
1. 管理数组:
bit::shared_ptr<A> sp1(new A[10], DeleteArray<A>());
2. 管理malloc内存:
bit::shared_ptr<A> sp2((A*)malloc(sizeof(A)), [](A* ptr) {free(ptr); });
3. 管理文件指针:
bit::shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
});
*/
// 基本使用
int main()
{
bit::shared_ptr<A> sp1(new A[10], DeleteArray<A>());
bit::shared_ptr<A> sp2((A*)malloc(sizeof(A)), [](A* ptr) {free(ptr); });
bit::shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
});
bit::shared_ptr<A> sp1(new A); // 使用默认删除器
return 0;
}
// 错误示例:可能导致内存泄漏
int main()
{
char* ptr = new char[1024 * 1024 * 1024]; // 分配1GB内存但没有释放
return 0;
}