这篇主要介绍了内存泄漏的问题,并着重讨论了C/C++下内存泄漏的检测和避免的问题。
内存泄漏
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或者无法释放,造成系统内存的浪费,导致程序运行速度的减慢甚至系统崩溃等后果。
内存泄漏实际上就是未能正确释放分配的内存。程序运行中占用的内存未释放或无法释放,随着程序长时间运行或分配内存的语句反复执行,可以使用的内存越来越少,从而导致程序乃至整个系统运行效率降低。在C中用malloc等来分配内存,free来释放内存,C++中则用new和delete,而java 中由于有垃圾回收机制,所以不用手动释放。
同时内存泄漏比较难以检测,并不会像内存访问越界或者段错误等,编译器在编译或者程序在运行时会报错,内存泄漏并不会产生可以观察的现象,但是会逐渐降低整个系统的运行性能,甚至严重导致系统崩溃。如C下每次malloc几K的内存,然后不及时free,每次分配的内存相对于整个内存空间来说很小,但是长时间积累,会有大量的内存因为没有及时释放而无法再使用,最后导致了程序运行缓慢,系统崩溃。
内存泄漏通常有如下几种(感觉没啥用的分类):
- 常发性: 即发生内存泄漏的代码经常被执行,每次执行都会导致一块内存被泄漏。
- 偶发性:发生内存泄漏的代码只有在特定条件下才会被执行。
- 一次性:发生内存泄漏的代码只会被执行一次。
- 隐式: 实际上程序是会释放掉分配的内存,但是是在程序退出运行之后才会显示的释放,当程序长时间运行时,由于并没有及时的释放内存,依旧导致了内存泄漏。
检测内存泄漏
windows下 visual studio 和 visual C++
在windows下,visual C++ 可以使用CRT(C Runtime Library, C运行时库)来检测内存泄漏。
C运行库包含了在Debug下特殊版本的堆分配函数,这些函数与release版本相同,只是在函数名字前面加了_dbg。
当定义了_DEBUG(即在DEBUG模式下运行程序)后,CRT会将所有的malloc映射到_malloc_dbg,这个版本会跟踪内存分配和释放。而未定义_DEBUG时(release版本),则不会发生映射。
首先,要在所有的.c文件首行插入以下代码(代码顺序不能乱):
1 |
并且在main()开始的位置加入
1 |
|
然后按F5调试运行,然后在命令窗的输入窗口可以看到内存泄漏检测的结果。
内存泄漏报告显示:
- 发生泄漏的内存编号为90,(这个编号和实际你分配内存的编号不一定一致)
- 内存块的类型为normal
- 内存块的位置为0x0049BBF0
- 泄漏的内存块的大小为 2bytes
- 块中的数据为”a”
简单来说,就是在程序第90次分配内存时,分配两个字节”a”泄漏了。
此时,为了找到内存泄漏的源头,在主函数开始加上_CrtSetBreakAlloc(90);
,那么程序就会在第90次分配内存时触发中断,然后观察调用堆栈,就可以找到出问题的函数。(此处安利代码图,发现可以很直观的看到函数的调用关系)
触发中断
通过调用堆栈确定内存泄漏的位置
代码图,可以很清晰的看到函数的调用关系
这种方法适用于C中的malloc函数,当使用C++中的new,则需要重新定义new函数,才能看到文件和行号。
1 |
Linux、OSX下
Linux、OSX下,使用valgrind工具(apt-get install valgrind、 brew install valgrind),然后直接在命令行执行valgrind --leak-check=full ./xx
就可以看到内存泄漏的报告。同时valgrind还有检测未初始化变量等功能(这种行为在某些编译器里是会赋初值,而另一些则会随机赋值,这种行为是很危险的。)
避免内存泄漏
导致内存泄漏的原因通常如下:
- 分配的内存没有及时释放。(如new了没delete等,注意new的东西用free释放是未定义行为)
- 指向内存的指针丢失,这块内存没办法找到了。
- 在释放内存的语句执行前,因为某些异常,程序中断推出了,导致内存未释放(虽然用异常机制try-catch可以避免,但是有的地方不允许用异常机制,可以用构造函数和析构函数来完成内存管理的工作来避免内存泄漏)。
等等(以后慢慢补充吧)
如何避免内存泄漏(这里参考了轮子哥的博客):
- 如果构造函数new了一个对象并使用成员指针变量保存,必须在析构函数delete它,不能为了方便而把对象所有权转让出去(资源所有权的问题,并且析构函数在对象生存期结束时一定会执行,即使是发生了异常,析构函数也保证了new的东西都会被delete)。
- 尽量使用shared_ptr,只要shared_ptr不发生循环引用,无论怎么使用,放在哪个容器里,都能安全的传递。(实际上智能指针本身的引用技术等机智,很好的管理了内存。循环引用指如果两个对象互相使用一个shared_ptr成员变量直接或者间接指向对方)
- 不要在有构造函数和析构函数的对象上用memset(或者memcpy)如果一个对象需要memset,在这个对象的构造函数中memset自己,包括memset一个对象数组,也是在这个对象的构造函数中memset。如果你需要memset一个没有构造函数的复杂对象,那么请为他添加一个构造函数,除非那是别人的API提供的东西。具体的解释看博客。实际就是防止某些对象的构造函数中存在非空指针,当memset后,会让这些指针的变为空指针,从而发生内存泄漏,如vczh设计的简单的string类。
- 如果一个对象是继承了其他东西,或者某些成员被标记了virtual的话,绝对不要memset。对象是独立的,也就是说父类内部结构的演变不需要对子类负责。哪天父类里面加了一个string成员,被子类一memset,就欲哭无泪了。
- 如果需要为一个对象定义构造函数,那么连复制构造函数、operator=重载和析构函数都全部写全。如果不想写复制构造函数和operator=的话,那么用一个空的实现写在private里面,确保任何试图调用这些函数的代码都出现编译错误。
上面的这几条,在轮子哥的博客中有介绍。
其他
程序中的内存分配
程序中的各个部分在内存空间的分布:
名称 | 内容 |
---|---|
代码段(text) | 存放二进制代码和一些常量(只读) |
数据段 | 存放初始化变量,包括全局变量,局部静态变量,全局静态变量,常量 |
bss | 存放未初始化变量,包括全局变量,全局静态变量 |
堆 | 动态分配的内存 |
栈 | 局部变量,函数参数(调用栈) |
从上面的表和图就可以看出,堆和栈是相向生长的,栈和堆可能会发生内存上的冲突。
下面介绍一些常见的内存相关的错误。
内存溢出
内存溢出(out of memory)简单来说就是内存不够用了。
内存溢出,即内存越界,比如缓冲区溢出,这是安全领域里的一个例子,当未对用户的输入长度进行限制时,用户可以通过输入比较长的字符串,从而超过输入变量所占的内存,访问到别的变量的内存,即缓冲区溢出攻击。
又比如栈溢出。
注意,内存溢出和内存泄漏容易搞混,两者有一定关系,比如内存泄漏会导致内存溢出,但是两个是不一样的。
栈溢出
栈溢出(stackoverflow)是很常见的问题了。是函数在多级调用的时候未返回,导致调用栈无限增长。典型的如写递归函数没有写base condition,导致函数进入无限调用,最终发生栈溢出。
段错误
段错误(Segmentation fault)指访问的内存超出了系统所给这个程序的内存空间。比如访问数组越界,访问不可访问内存等。