C++---智能指针和内存泄露
文章目录
- 智能指针
- **发展历史**
- **C++11前**
- **C++11引入的智能指针**
- **智能指针的使用及原理**
- shared_ptr的循环引用
- **循环引用的场景**
- 通过weak_ptr来解决shared_ptr的循环引用
- 智能指针的模拟实现
- 内存泄露
- 内存泄露的概念和危害
- 内存泄露分类
- 如何检测内存泄露
- 如何避免内存泄露
智能指针
智能指针在<memory>
头文件中。
发展历史
C++11前
-
C++标准库提供了一个auto_ptr
-
**auto_ptr提供了拷贝,但是其拷贝是 管理权转移 **
管理权转移:把原智能指针的资源转移到要拷贝到的智能指针中去,原智能指针置为空struct Date { int _year; int _month; int _day; Date(int year = 1, int month = 1, int day = 1) :_year(year) , _month(month) , _day(day) {} ~Date() { cout << "~Date()" << endl; } }; int main() { auto_ptr<Date> ap1(new Date); // 拷贝时,管理权限转移,被拷贝悬空 auto_ptr<Date> ap2(ap1); //ap1->_year++; 这里会直接崩,因为ap1的管理权被转移到ap2中,ap1中的指针被置为空了。 return 0; }
-
-
Boost库提供了一些智能指针
Boost库是为C++语言标准库提供扩展的一些C++程序库的总称。
Boost库由Boost社区组织开发、维护。其目的是为C++程序员提供免费、同行审查的、可移植的程序库。Boost库可以与C++标准库共同工作,并且为其提供扩展功能。Boost库使用Boost License来授权使用,根据该协议,商业或非商业的使用都是允许并鼓励的。
Boost社区是由C++标准委员会的成员Dawes发起的,目的是C++的标准化工作提供可供参考的实现。因此Boost库也被很多人当作C++的准标准库,C++后续的更新有很多是将Boost库中一些改造后录入的
- scoped_ptr/scoped_array:防拷贝(即禁止拷贝),也是C++11中unique_ptr的前身
- shared_ptr/shared_array:通过引用计数的方式支持拷贝。也是C++11中shared_ptr的前身
- weak_ptr:不同于上面的智能指针,不支持直接管理资源,配合解决shared_ptr的一个缺陷(循环引用,循环引用会导致内存泄露)。 也是C++11中weak_ptr的前身
C++11引入的智能指针
-
unique_ptr:防拷贝(即禁止拷贝),支持移动构造,但是其拷贝构造 和 赋值 被delete,实现防拷贝
-
当new多个数据的时候,进行析构的方法
-
通过语法中,第二个 声明语法:
template < class T , class D = default_delete<T> > class unique_ptr;
。将定制删除器传入第二个模板参数中,这个定制删除器是在析构函数中被调用
定制删除器其实是 可调用对象。struct Date { int _year; int _month; int _day; Date(int year = 1, int month = 1, int day = 1) :_year(year) , _month(month) , _day(day) {} ~Date() { cout << "~Date()" << endl; } }; template<class T> class DeleteArray { public: void operator()(T* ptr) { delete[] ptr; } }; class Fclose { public: void operator()(FILE* ptr) { cout << "fclose:" << ptr << endl; fclose(ptr); } }; int main() { // 定制删除器 unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]); unique_ptr<FILE, Fclose> up3(fopen("test.cpp", "r")); return 0; }
-
通过语法中,第二个 声明语法:
template <class T , class D> class unique_ptr<T[] , D>;
这样的声明在析构时,会析构多个数据struct Date { int _year; int _month; int _day; Date(int year = 1, int month = 1, int day = 1) :_year(year) , _month(month) , _day(day) {} ~Date() { cout << "~Date()" << endl; } }; int main() { unique_ptr<Date[]> up2(new Date[5]); return 0; }
-
-
一些接口
int main() { unique_ptr<FILE, Fclose> up3(fopen("test.cpp", "r")); }
-
-
shared_ptr:通过引用计数的方式支持拷贝。 引用计数和存储数据是分离的。引用计数需要一小块内存去存储,因此大量使用shared_ptr会有内存碎片的风险,因此shared_ptr的构造函数也支持自己传内存池来管理这些东西。
-
管理多个对象时
int main() { shared_ptr<Date[]> sp4(new Date[5]); }
-
shared_ptr
的定制删除器是传入构造函数里,然后在析构函数中调用int main() { shared_ptr<FILE> sp5(fopen("test.cpp", "r"), Fclose()); //上面代码相当于下面代码,shared_ptr是将定制删除器传入构造函数中,unique_ptr是将定制删除器传入模板参数中 unique_ptr<FILE, Fclose> up3(fopen("test.cpp", "r")); }
-
一些接口
![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=memory_1_6.png&pos_id=img-h4fVHSuy-1731928899472)int main() { shared_ptr<Date> up1(new Date); shared_ptr<Date> up2(up1); up2->_year++; up1->_year++; up1->_day = 10; }
-
make_shared
函数:make_shared创建shared_ptr的时候,是同时分配对象和引用计数控制块,可以减少内存碎片化的问题传入 要构造的对象(支持隐式类型转化),返回 对应的shared_ptr
int main() { shared_ptr<Date> sp6 = make_shared<Date>(2024, 8, 5);//此处的Date有3个参数的构造函数,先构造出Date类型的对象,sp6中的指针指向该对象 }
-
-
weak_ptr:不支持RAII,weak_ptr不支持管理资源。
其构造函数
如图所示,
weak_ptr
不支持通过资源进行构造,支持shared_ptr
进行构造,shared_ptr
进行构造时,不会增加引用计数。operator=支持shared_ptr赋值给weak_ptr
weak_ptr有自己的引用计数的,weak_ptr的引用计数用来监督-
weak_ptr 的 lock 函数用于创建一个新的 shared_ptr 对象,该对象共享被管理对象的所有权。如果 weak_ptr 指向的对象已经被释放,则返回一个空的shared_ptr
struct ListNode { int _data; weak_ptr<ListNode> _next; weak_ptr<ListNode> _prev; ~ListNode() { cout << "~ListNode()" << endl; } }; int main() { std::weak_ptr<ListNode> wp; std::shared_ptr<ListNode> sp; { std::shared_ptr<ListNode> n1(new ListNode); wp = n1; //expired()是返回weak_ptr指向的资源是否过期。 过期返回true,不过期返回false cout << wp.expired() << endl; n1->_data++; sp = wp.lock(); } cout << wp.expired() << endl; return 0; }
-
智能指针的使用及原理
-
RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内 存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在 对象析构的时候释放资源。
借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做 法有两大好处:
- **不需要显式地释放资源。 **
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
-
使用RAIII思路设计的smartPtr类。
这里没有提供拷贝,实际上智能指针的拷贝也是一个十分麻烦的事情,因为直接拷贝会导致多次析构的情况// 使用RAII思想设计的SmartPtr类 template<class T> class SmartPtr { public: SmartPtr(T* ptr = nullptr) : _ptr(ptr) {} ~SmartPtr() { if (_ptr) delete _ptr; } private: T* _ptr; }; int div() { int a, b; cin >> a >> b; if (b == 0) throw invalid_argument("除0错误"); return a / b; } void Func() { SmartPtr<int> sp1(new int); SmartPtr<int> sp2(new int); //SmartPtr<int> sp3(sp2); 当Func函数栈帧销毁时,这里会导致两次调用析构,导致程序崩溃 cout << div() << endl; }
shared_ptr的循环引用
循环引用的场景
struct ListNode
{
int _data;
shared_ptr<ListNode> _next;
shared_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
// 循环引用 -- 内存泄露
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
//n2赋值给n1->_next,使得n2的引用计数++
n1->_next = n2;
//n1赋值给n2->_next,使得n1的引用计数++
n2->_prev = n1;
//上面使得当n2销毁时,n2的引用计数--,但是析构中没有销毁new出来的ListNode,因为引用计数没有到0。然后轮到n1销毁,销毁时n1的引用计数--,同样,也没有销毁其new出来的ListNode,因为其引用计数没有到0
return 0;
}
通过weak_ptr来解决shared_ptr的循环引用
struct ListNode
{
int _data;
/*bit::shared_ptr<ListNode> _next;
bit::shared_ptr<ListNode> _prev;*/
weak_ptr<ListNode> _next;
weak_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
// 循环引用 -- 内存泄露
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
// weak_ptr不支持管理资源,不支持RAII
//std::weak_ptr<ListNode> wp(new ListNode);
return 0;
}
智能指针的模拟实现
1
内存泄露
内存泄露的概念和危害
**内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。**内 存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对 该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现 内存泄漏会导致响应越来越慢,最终卡死。
void MemoryLeaks() { // 1.内存申请了忘记释放 int* p1 = (int*)malloc(sizeof(int)); int* p2 = new int; // 2.异常安全问题 int* p3 = new int[10]; Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放 delete[] p3; }
内存泄露分类
C/C++程序中一般我们关心两种方面的内存泄漏:
- 堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一 块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分 内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。 - 系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放 掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
如何检测内存泄露
- 在linux下内存泄漏检测:linux下几款内存泄漏检测工具
- 在windows下使用第三方工具: VLD工具说明
- VLD工具说明 其他工具: 内存泄漏工具比较
如何避免内存泄露
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps: 这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智 能指针来管理才有保证。
- 采用RAII思想或者智能指针来管理资源。
- 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
- 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总结一下: 内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具