家用电脑可以做网站服务器/抖音营销软件
工作中最常用的编程语言是python和C++,通常是完成一个算法原型开发,再交由负责工程优化的同学来进行优化、集成。因此平时代码中很少注意编程规范、内存泄漏、高性能编程等问题。本来C++基础就很不扎实,平时使用少了就更生疏了。最近被同事提醒,new出来的指针一定要delete,或者使用智能指针,否则会造成内存泄漏。所以在培养写技术blog习惯的开头,就从智能指针谈起。
以下内容是对一些书籍、技术博客的知识点整合。所有参考资料链接附在文章末尾。
什么是内存泄漏
内存泄漏指的是程序未能释放一块不再使用的内存,也就是说程序分配某段内存后,但在释放该段内存之前就失去了对该段内存的控制,造成了内存的浪费。程序不断分配新内存但不再使用的内存没有被释放,最终造成内存用尽。
new/delete
在写程序时,有时会有动态分配内存的需求,在C++中通常使用new/delete来上动态分配一段内存。
怎么理解“动态内存分配“呢?与之对应的是静态内存分配。
静态内存分配:编译器在编译的时候就知道所需的内存空间。
动态内存分配:只有在程序运行时才能确定所需空间,编译器无法在编译时预定存储空间
malloc/free和new/delete的区别
它们都是管理动态内存的方式。
malloc/free只是分配和释放内存空间,而new/delete除了分配空间外,还会调用构造函数和析构函数。
malloc需要手动指定所分配的空间字节数并返回(void*),new/delete根据元素个数自动计算字节数,返回对应类型的指针
malloc/free管理内存失败会返回0,new/delete管理内存失败会抛出异常
malloc从堆上分配内存空间,new从自由存储区(free store) 上分配内存。自由存储区是C++基于new操作符的一个抽象概念。而堆是对操作系统而言的,是操作系统维护的一块内存区域。自由存储区是否是堆,取决于new的实现细节(是不同编译器实现不同吗?)。自由存储区不仅可以是堆,也可以改用其他内存在实现自由存储。
怎样会造成内存泄漏?
有以下几种常见情况会造成内存泄漏,根本原因都是new出来的内存没有通过delete合理地释放。
强行内存泄漏-1. new出来的对象没有及时delete:疯狂new,但是这些new出来的内存空间并没有实际使用,最终导致内存耗尽。
int main() { while(1) { new int[1000]; } return 0; }
强行内存泄漏-2. delete掉一个void*类型的指针,导致没有调用对象的析构函数,析构函数内的相关内存清理都没有被执行。
class Object {private: char* data; const int size; const char id; public: Object(int sz, char c):size(sz), id(c) { data = new char[size]; cout << "Object() " << id << " size = " << size << endl; } ~Object() { cout << "~Object() " << id << endl; delete []data; } };int main() { Object* a = new Object(10, 'A');//Object*指针指向一个Object对象; void* b = new Object(20, 'B');//void*指针指向一个Object对象; delete a; //执行delete,编译器自动调用析构函数; delete b; //执行delete,编译器不会调用析构函数,导致data占用内存没有得到回收; return 0;}
delete void* 补充:
delete void* 并不是什么都不干。如果是普通指针char*, short*, int*, long*,是会被正确释放的。如果void* 是一个类类型指针,系统会认为void*指向普通内存空间,不会调用指向对象的析构函数。解决方法:把void*转换为原类型指针,再调用deletetemplate <typename T>inline void safe_delete_void_ptr(void *&target) { if (nullptr != target) { T* temp = static_cast(target); delete temp; temp = nullptr; target = nullptr; }}int *psample = new int(100);safe_delete_void_ptr<int>(psample);
强行内存泄漏-3. new创建一个数组指针,回收内存时只调用了delete而没有调用delete[],导致只有数组第一个对象的析构函数被执行,其他对象的内存都没有回收。
智能指针
概念:智能指针主要用于管理动态分配的内存(通常是分配在堆上),它将普通的指针封装为一个栈对象。当栈对象的生命周期结束后,会自动调用该栈对象的析构函数,会在析构函数中释放掉申请的内存。因此智能指针的“智能”之处就在于能够自动正确地释放内存,而不需显示调用delete进行释放,也无需人为考虑该什么时候进行释放。
C++11中支持3种智能指针:unqiue_ptr, shared_ptr, weak_ptr. 这里主要介绍最常用的shared_ptr.
shared_ptr
shared_ptr实现共享式拥有的概念。shared_ptr可以通过new出来的指针进行初始化。多个智能指针可以指向相同的对象,使用计数机制来记录指针指向的内存被多少个智能指针所引用。新增一个引用时,计数+1. 调用release()时,当前智能指针会释放资源的所有权,即引用-1,计数-1. 当计数等于0时才会真正释放堆上(严格来说是自由存储区)的内存。
shared_ptr的简单例子
int main(){ string *s1 = new string("s1"); // 自由存储区上动态分配内存 shared_ptr<string> ps1(s1); // 初始化智能指针 shared_ptr<string> ps2; ps2 = ps1; // 引用+1 cout <endl; cout<endl; cout << ps1.unique()<<endl; //0 string *s3 = new string("s3"); shared_ptr<string> ps3(s3); cout <endl; cout << ps3.get() << endl; //033B2C50 swap(ps1, ps3); //交换所拥有的对象 cout << (ps1.get())<<endl; //033B2C50 cout << ps3.get() << endl; //033AEB48 cout << ps1.use_count()<<endl; //1 cout <endl; ps2 = ps1; cout << ps1.use_count()<<endl; //2 cout <endl; ps1.reset(); //放弃ps1的拥有权,引用计数的减少 cout <endl; cout <endl; }
weak_ptr
介绍weak_ptr之前,先看看shared_ptr的缺点。如果两个对象都有一个shared_ptr成员变量指向对方,造成循环引用。请看如下例子,退出作用域时,先释放的是pa(后构造的对象先释放)。而pa的引用次数为2,减去1后变为了1,所以pa指向的自由存储区内存没有被释放。但pa这个栈对象已经被销毁了。
接下来释放pb,pb的计数也是2,减1后变为1,pb指向的自由存储区内存也没有被释放。
class B; //声明class A {public: shared_ptr pb_; ~A() { cout << "A delete\n"; }};class B {public: shared_ptr pa_; ~B() { cout << "B delete\n"; }};void fun(){ shared_ptr pb(new B()); shared_ptr pa(new A()); cout << pb.use_count() << endl; //1 cout << pa.use_count() << endl; //1 pb->pa_ = pa; pa->pb_ = pb; cout << pb.use_count() << endl; //2 cout << pa.use_count() << endl; //2}int main() { fun(); return 0;}
这时候就使用weak_ptr来辅助。把以上例子A或B成员变量类型其中一个shared_ptr改为weak_ptr就可以了。例如把B类中的shared_ptr改为weak_ptr。weak_ptr的构造析构不会影响shared_ptr引用计数的大小,因此下面第7行不会增加pa的引用计数。而在退出函数时,pa首先被析构,计数由1减为0,内存被释放。因为pa被析构了,pb的引用计数就会自动减1,由2变为1。最后再执行pb的析构,计数由1减为0,内存被释放。
void fun(){ shared_ptr pb(new B()); shared_ptr pa(new A()); cout << pb.use_count() << endl; //1 cout << pa.use_count() << endl; //1 pb->pa_ = pa; // 不会增加pa的计数 pa->pb_ = pb; cout << pb.use_count() << endl; //2 cout << pa.use_count() << endl; //1}
参考资料:
C++——动态内存分配
https://www.cnblogs.com/southcyy/p/10271981.html
malloc与new分配内存
https://blog.csdn.net/xijiacun/article/details/53150363
自由存储区与堆
https://www.cnblogs.com/QG-whz/p/5060894.html
C++造成内存泄漏的原因汇总
https://blog.csdn.net/qq_18824491/article/details/78902636
如何优雅地释放void*
https://blog.csdn.net/SweetTool/article/details/77688337
详解C++11智能指针
https://www.cnblogs.com/WindSun/p/11444429.html
浅谈shared_ptr及shared_ptr涉及到的循环问题
https://blog.csdn.net/qq_34992845/article/details/69218843