常见的【内存泄漏】姿势
关注公众号【高性能架构探索】,第一时间获取干货;回复【pdf】,免费获取计算机经典书籍
本文节选自文章:
内存泄漏-原因、避免以及定位
本文总结常见内存泄漏的几种方式,留意到这几点,可以避免95+%以上的内存泄漏
未释放
这种是很常见的,比如下面的代码:
int fun() {char * pBuffer = malloc(sizeof(char));/* Do some work */return 0;
}
上面代码是非常常见的内存泄漏场景(也可以使用new来进行分配),我们申请了一块内存,但是在fun函数结束时候没有调用free函数进行内存释放。
在C++开发中,还有一种内存泄漏,如下:
class Obj {public:Obj(int size) {buffer_ = new char;}~Obj(){}private:char *buffer_;
};int fun() {Object obj;// do sthreturn 0;
}
上面这段代码中,析构函数没有释放成员变量buffer_指向的内存,所以在编写析构函数的时候,一定要仔细分析成员变量有没有申请动态内存,如果有,则需要手动释放,我们重新编写了析构函数,如下:
~Object() {delete buffer_;
}
在C/C++中,对于普通函数,如果申请了堆资源,请跟进代码的具体场景调用free/delete进行资源释放;对于class,如果申请了堆资源,则需要在对应的析构函数中调用free/delete进行资源释放。
未匹配
在C++中,我们经常使用new操作符来进行内存分配,其内部主要做了两件事:
- 通过operator new从堆上申请内存(glibc下,operator new底层调用的是malloc)
- 调用构造函数(如果操作对象是一个class的话)
对应的,使用delete操作符来释放内存,其顺序正好与new相反:
- 调用对象的析构函数(如果操作对象是一个class的话)
- 通过operator delete释放内存
void* operator new(std::size_t size) {void* p = malloc(size);if (p == nullptr) {throw("new failed to allocate %zu bytes", size);}return p;
}
void* operator new[](std::size_t size) {void* p = malloc(size);if (p == nullptr) {throw("new[] failed to allocate %zu bytes", size);}return p;
}void operator delete(void* ptr) throw() {free(ptr);
}
void operator delete[](void* ptr) throw() {free(ptr);
}
为了加深多这块的理解,我们举个例子:
class Test {public:Test() {std::cout << "in Test" << std::endl;}// other~Test() {std::cout << "in ~Test" << std::endl;}
};int main() {Test *t = new Test;// do sthdelete t;return 0;
}
在上述main函数中,我们使用new 操作符创建一个Test类指针
- 通过operator new申请内存(底层malloc实现)
- 通过placement new在上述申请的内存块上调用构造函数
- 调用ptr->~Test()释放Test对象的成员变量
- 调用operator delete释放内存
上述过程,可以理解为如下:
// new
void *ptr = malloc(sizeof(Test));
t = new(ptr)Test// delete
ptr->~Test();
free(ptr);
好了,上述内容,我们简单的讲解了C++中new和delete操作符的基本实现以及逻辑,那么,我们就简单总结下下产生内存泄漏的几种类型。
new 和 free
仍然以上面的Test对象为例,代码如下:
Test *t = new Test;
free(t)
此处会产生内存泄漏,在上面,我们已经分析过,new操作符会先通过operator new分配一块内存,然后在该块内存上调用placement new即调用Test的构造函数。而在上述代码中,只是通过free函数释放了内存,但是没有调用Test的析构函数以释放Test的成员变量,从而引起内存泄漏
。
new[] 和 delete
int main() {Test *t = new Test [10];// do sthdelete t;return 0;
}
在上述代码中,我们通过new创建了一个Test类型的数组,然后通delete操作符删除该数组,编译并执行,输出如下:
in Test
in Test
in Test
in Test
in Test
in Test
in Test
in Test
in Test
in Test
in ~Test
从上面输出结果可以看出,调用了10次构造函数,但是只调用了一次析构函数,所以引起了内存泄漏
。这是因为调用delete t释放了通过operator new[]申请的内存,即malloc申请的内存块,且只调用了t[0]对象的析构函数,t[1…9]对象的析构函数并没有被调用。
虚析构
记得08年面谷歌的时候,有一道题,面试官问,std::string能否被继承,为什么?
当时没回答上来,后来过了没多久,进行面试复盘的时候,偶然看到继承需要父类析构函数为virtual
,才恍然大悟,原来考察点在这块。
下面我们看下std::string的析构函数定义:
~basic_string() { _M_rep()->_M_dispose(this->get_allocator());
}
这块需要特别说明下,std::basic_string是一个模板,而std::string是该模板的一个特化,即std::basic_string
typedef std::basic_string<char> string;
现在我们可以给出这个问题的答案:不能,因为std::string的析构函数不为virtual,这样会引起内存泄漏
。
仍然以一个例子来进行证明。
class Base {public:Base(){buffer_ = new char[10];}~Base() {std::cout << "in Base::~Base" << std::endl;delete []buffer_;}
private:char *buffer_;};class Derived : public Base {public:Derived(){}~Derived() {std::cout << "int Derived::~Derived" << std::endl;}
};int main() {Base *base = new Derived;delete base;return 0;
}
上面代码输出如下:
in Base::~Base
可见,上述代码并没有调用派生类Derived的析构函数,如果派生类中在堆上申请了资源,那么就会产生内存泄漏
。
为了避免因为继承导致的内存泄漏,我们需要将父类的析构函数声明为virtual
,代码如下(只列了部分修改代码,其他不变):
~Base() {std::cout << "in Base::~Base" << std::endl;delete []buffer_;}
然后重新执行代码,输出结果如下:
int Derived::~Derived
in Base::~Base
借助此文,我们再次总结下存在继承情况下,构造函数和析构函数的调用顺序。
派生类对象在创建时构造函数调用顺序:
- 调用父类的构造函数
- 调用父类成员变量的构造函数
- 调用派生类本身的构造函数
派生类对象在析构时的析构函数调用顺序:
- 执行派生类自身的析构函数
- 执行派生类成员变量的析构函数
- 执行父类的析构函数
为了避免存在继承关系时候的内存泄漏,请遵守一条规则:无论派生类有没有申请堆上的资源,请将父类的析构函数声明为virtual
。
循环引用
在C++开发中,为了尽可能的避免内存泄漏,自C++11起引入了smart pointer
,常见的有shared_ptr、weak_ptr以及unique_ptr等(auto_ptr已经被废弃),其中weak_ptr是为了解决循环引用而存在,其往往与shared_ptr结合使用。
下面,我们看一段代码:
class Controller {public:Controller() = default;~Controller() {std::cout << "in ~Controller" << std::endl;}class SubController {public:SubController() = default;~SubController() {std::cout << "in ~SubController" << std::endl;}std::shared_ptr<Controller> controller_;};std::shared_ptr<SubController> sub_controller_;
};int main() {auto controller = std::make_shared<Controller>();auto sub_controller = std::make_shared<Controller::SubController>();controller->sub_controller_ = sub_controller;sub_controller->controller_ = controller;return 0;
}
编译并执行上述代码,发现并没有调用Controller和SubController的析构函数,我们尝试着打印下引用计数,代码如下:
int main() {auto controller = std::make_shared<Controller>();auto sub_controller = std::make_shared<Controller::SubController>();controller->sub_controller_ = sub_controller;sub_controller->controller_ = controller;std::cout << "controller use_count: " << controller.use_count() << std::endl;std::cout << "sub_controller use_count: " << sub_controller.use_count() << std::endl;return 0;
}
编译并执行之后,输出如下:
controller use_count: 2
sub_controller use_count: 2
通过上面输出可以发现,因为引用计数都是2,所以在main函数结束的时候,不会调用controller和sub_controller的析构函数,所以就出现了内存泄漏
。
上面产生内存泄漏的原因,就是我们常说的循环引用
。
为了解决std::shared_ptr循环引用导致的内存泄漏,我们可以使用std::weak_ptr来单面去除上图中的循环。
class Controller {public:Controller() = default;~Controller() {std::cout << "in ~Controller" << std::endl;}class SubController {public:SubController() = default;~SubController() {std::cout << "in ~SubController" << std::endl;}std::weak_ptr<Controller> controller_;};std::shared_ptr<SubController> sub_controller_;
};
在上述代码中,我们将SubController类中controller_的类型从std::shared_ptr变成std::weak_ptr,重新编译执行,结果如下:
controller use_count: 1
sub_controller use_count: 2
in ~Controller
in ~SubController
从上面结果可以看出,controller和sub_controller均以释放,所以循环引用
引起的内存泄漏问题,也得以解决。
可能有人会问,使用std::shared_ptr可以直接访问对应的成员函数,如果是std::weak_ptr的话,怎么访问呢?我们可以使用下面的方式:
std::shared_ptr controller = controller_.lock();
即在子类SubController中,如果要使用controller调用其对应的函数,就可以使用上面的方式。
关注公众号【高性能架构探索】,第一时间获取干货;回复【pdf】,免费获取计算机经典书籍
常见的【内存泄漏】姿势相关推荐
- UWP开发入门(十六)——常见的内存泄漏的原因
原文:UWP开发入门(十六)--常见的内存泄漏的原因 本篇借鉴了同事翔哥的劳动成果,在巨人的肩膀上把稿子又念了一遍. 内存泄漏的概念我这里就不说了,之前<UWP开发入门(十三)--用Diagno ...
- 5 个 Android 开发中比较常见的内存泄漏问题及解决办法
Android开发中,内存泄漏是比较常见的问题,有过一些Android编程经历的童鞋应该都遇到过,但为什么会出现内存泄漏呢?内存泄漏又有什么影响呢? 在Android程序开发中,当一个对象已经不需要再 ...
- 常见的内存泄漏原因及解决方法
常见的内存泄漏原因及解决方法 参考文章: (1)常见的内存泄漏原因及解决方法 (2)https://www.cnblogs.com/leeego-123/p/12187677.html 备忘一下.
- 【Android 内存优化】垃圾回收算法 ( 内存优化总结 | 常见的内存泄漏场景 | GC 算法 | 标记清除算法 | 复制算法 | 标记压缩算法 )
文章目录 一. 内存优化总结 二. 常见的内存泄漏场景 三. 内存回收算法 四. 标记-清除算法 ( mark-sweep ) 五. 复制算法 六. 标记-压缩算法 一. 内存优化总结 内存泄漏原理 ...
- Android 系统(87)---常见的内存泄漏原因及解决方法
常见的内存泄漏原因及解决方法 (Memory Leak,内存泄漏) 为什么会产生内存泄漏? 当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被 ...
- Android常见的内存泄漏分析
内存泄漏原因 当应用不需要在使用某个对象时候,忘记释放为其分配的内存,导致该对象仍然保持被引用状态(当对象拥有强引用,GC无法回收),从而导致内存泄漏. 常见的内存泄漏源头 泄漏的源头有很多,有开源的 ...
- Dreamwear如何创建javascript_内存管理+如何处理4种常见的内存泄漏
JavaScript是如何工作的:内存管理+如何处理4种常见的内存泄漏 潮水自会来去,但心志得坚若磐石.即便成不了那根定海神针,也至少不是那随意被拍上岸的野鬼游魂.by 一枚热汤圆 几周前,我们开始了 ...
- JS中常见的内存泄漏及识别方式
JavaScript常见的内存泄漏及识别方式 1.什么是内存 2.什么是内存泄漏 3.内存泄漏导致的后果 4.常见的内存泄漏 (1)全局变量引起的内存泄漏 (2)闭包引起的内存泄漏 (3)被遗忘的定时 ...
- java内部类内存泄漏,Android中常见的内存泄漏和解决方案
什么是内存泄漏? 简单点说,就是指一个对象不再使用,本应该被回收,但由于某些原因导致对象无法回收,仍然占用着内存,这就是内存泄漏. 为什么会产生内存泄漏,内存泄漏会导致什么问题? 相比C++需要手动去 ...
- JS中4种常见的内存泄漏
一.什么是内存泄漏 本质上讲,内存泄漏是当一块内存不再被应用程序使用的时候,由于某种原因,这块内存没有返还给操作系统或空闲内存池的现象. 二.几种常见的内存泄漏 1.意外的全局变量 一个未声明变量的引 ...
最新文章
- 2021年大数据基础(一):大数据概念
- 【抬杠】在某些时候不希望用户缩小浏览器的宽度,因为咳咳~会导致你的布局混乱,那么这个代码就是帮助你如何限制浏览器宽度的
- 5.Ubuntu下的GIF录制软件peek安装
- mxnet 配置gpu
- 7.STM32中对DMA_Config()函数的理解(自定义)测试DMA传输数据时CPU还可继续工作其他的事
- js字符串slice_JavaScript子字符串示例-JS中的Slice,Substr和Substring方法
- Visual Studio .Net团队开发[转]
- 重学C++语言之路:C++语言学习工具和环境
- [设计] - 判断LOGO好坏的12条参考标准
- 职场这样发邮件,你死定了!
- iOS中Lua脚本应用笔记一:脚本概念相关
- 论文阅读_TASE: Reducing Latency of Symbolic Execution with Transactional Memory
- 电阻元件、电感元件、电容元件
- html按钮位置设置吗,html改变button按钮位置
- 统一网关Gateway、路由断言工厂、路由过滤器及跨域问题处理
- 半导体车间净化工程的空气洁净度划分等级
- wkhtmltopdf(thead)分页问题
- c语言中at指令的比较,AT指令(中文详解版)(二)
- ⚡️狂神Linux学习笔记
- 嵌入式基础01【转载】详解大端模式和小端模式