1 C++ 栈、堆内存特征
在C++中,内存分成:堆(stack)、栈(heap)、全局/静态存储区(static)、程序代码区(code)。内存一般指计算机随机存储器(RAM),内存中的栈区和堆区有以下特征:
栈区(Stack memory):
- 内存空间由操作系统自动分配和释放。由程序自动向操作系统申请分配以及回收。函数执行时,栈空间自动分配,函数结束时,栈空间自动销毁。栈运算分配内置于处理器的指令集中,效率很高,使用方便,但程序员无法控制。
- 内存空间有限。栈内存属于执行期函数,编译时大小确定,
栈的最大容量是预先规定好的,栈容量参数可修改。win和linux下默认大小可能不同,例如win默认1~2M,Linux默认8M。只要栈的剩余空间大于所申请的空间,系统将为程序提供内存,否则将报异常提示栈溢出,即若分配失败,则提示栈溢出错误。 - 向下生长
- 存储内容:为运行函数而分配的局部变量、函数参数、返回地址等存放在栈区。
堆区(Heap Memory):
- 内存空间由手动申请和释放。由程序员申请,同时也必须由程序员负责销毁,否则导致内存泄露。
- 空间是很大。例如,在Linux中堆区小于3GB,Windows下小于2GB。
- 向上生长。
//存储在栈区,程序自动申请释放
int num = 1;//num储存在栈区
int arr_int[5] = {
1,2,3,4,5};//arr_int储存在栈区
char arr_char[] = "hello world!";//arr_char储存在栈区
//存储在堆区,需手动申请和释放
int* p1 = new int(100);//p1存储在堆区
int* p2 = new int[100];//数组指针p2存储在堆区
delete p1;
delete []p2;//注意 :new 带 [],则 delete 必须带[];否则,必须不带[]
//存储在可读写区
static long num1 = 500; //全局变量和静态变量存储在可读写区
2 栈溢出
2.1 栈溢出原因及处理措施
2.1.1 导致栈溢出的原因:
- 局部变量过大。当函数内部的数组过大时,有可能导致栈溢出。
- 递归调用层次太多函数调用深度过深:如果函数调用的层级太多,也会引起栈溢出。例如,递归函数在运行时会执行压栈操作,当压栈次数太多时,会导致栈溢出。
- 使用常发生栈溢出的危险函数:
输入:
gets()
,直接读取一行,到换行符’\n’为止,同时’\n’被转换为’\x00’;
scanf()
,格式化字符串中的%s不会检查长度;vscanf()
,同上。
输出:
sprintf()
,将格式化后的内容写入缓冲区中,但是不检查缓冲区长度
字符串:
strcpy()
,遇到’\x00’停止,不会检查长度,经常容易出现单字节写0(off by one)溢出;strcat()
,同上。
2.1.2 解决栈溢出的办法
主要有以下几种:
- 减少递归层级:尝试将递归调用改为迭代方式,以减少栈空间的消耗。
- 减少局部变量和临时数据的使用:可以将部分数据转移到堆上,或者使用全局变量、静态变量代替局部变量。
- 增加栈空间大小:可以通过编译器或操作系统提供的配置选项,增加程序使用的栈空间大小。
- 检查参数传递:对于函数调用时的参数,考虑是否需要传递大量数据,并尝试使用指针或引用等方式减少参数传递的开销。
2.1.3 检查程序堆栈溢出的方法、工具
待更新
2.2 栈溢出的应用:
- 病毒攻击
栈溢出是一种常见的计算机安全漏洞。当程序执行时向栈中不断压入数据,如果数据过多过大,超出了栈的存储空间,就会导致栈溢出。攻击者可以利用栈溢出漏洞向栈中注入恶意代码,从而控制程序的执行流程,达到攻击的目的。 具体来说,攻击者可以构造一段特定的输入,向程序输入这段输入后,就会导致栈溢出。历史上,栈溢出曾被用于实施攻击,如著名的“莫里斯蠕虫”病毒就利用了C语言标准库中的gets()
函数未限制输入数据长度的漏洞来实现栈溢出攻击。
3 内存泄漏(堆区)
3.1 内存泄露的原因
导致内存泄漏的原因内存创建和释放没有正确匹配。当确认已出现内存泄漏的情况后,需要继续进行判断:
- 是自己写的业务代码 造成的内存泄漏;
- 还是第三方库的内存泄漏。
3.2 检测内存泄露的方法、工具
内存泄漏是 C/C++ 应用程序中最微妙、最难以发现的 bug 。 内存泄漏是由于之前分配的内存未能正确解除分配而导致的。 最开始的少量内存泄漏可能没被发现,但随时间推移,会导致各种问题,从性能变差到程序由于内存不足而崩溃。
3.1.2.1 CRT库(win平台)
微软官方使用介绍:使用 CRT 库查找内存泄漏
本文中的CRT示例程序见:本文CRT示例程序及可直接拷贝利用的头文件
自写程序使用示例(debug模式下):
// debug_malloc.cpp
// compile by using: cl /EHsc /W4 /D_DEBUG /MDd debug_malloc.cpp
//请按照下述3行的顺序定义和包含
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#include <iostream>
int main()
{
_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );//多程序出口自动检测
std::cout << "Hello World!\n";
int* x1 = (int*)malloc(sizeof(int));
*x1 = 7;
printf("%d\n", *x1);
int x2 = (int*)calloc(3, sizeof(int));
x2[0] = 7;
x2[1] = 77;
x2[2] = 777;
printf("%d %d %d\n", x2[0], x2[1], x2[2]);
_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_DEBUG);
//_CrtDumpMemoryLeaks();//在应用出口点之前放置 _CrtDumpMemoryLeaks,
//从而在应用退出时显示内存泄漏报告,适用于单程序出口
}
【已解决】常见问题1:CRT输出没有具体函数名及不显示行号
在自写程序中,遇到这种输出仅包含内存申请不包含CRT输出没有具体函数名及行号。
内存泄漏报告解读:
- {183}、大括号里数字为内存分配编号,可在程序中增加
_CrtSetBreakAlloc(18);
来自动跳转内存分配数18的函数及行号; - 块类型,在示例中为 normal ;
- 块的大小,在示例种为20 bytes;
- 块中前 16 个字节的数据(十六进制形式)
解决方法:CRT输出具体函数名及行号需要包含下述代码):
#define _CRTDBG_MAP_ALLOC
#include<cstdlib>
#include<crtdbg.h>
#ifdef _DEBUG
#ifndef DBG_NEW
#define DBG_NEW new(_NORMAL_BLOCK,__FILE__,__LINE__)
#define new DBG_NEW
#endif // !DBG_NEW
#endif // _DEBUG
#ifdef _CRTDBG_MAP_ALLOC
#define malloc(malloc_a) _malloc_dbg(malloc_a, _NORMAL_BLOCK, __FILE__, __LINE__)
#define calloc(calloc_a,calloc_b) _calloc_dbg(calloc_a,calloc_b, _NORMAL_BLOCK, __FILE__, __LINE__)
#endif
添加上述代码后,此输出报告,包括泄露的分配的.cpp 的第n行。
右键即可直接跳转到具体位置。
值得注意的是,使用上述方法仍然不会输出第三方库的内存申请函数,第三方库内存泄露检测方法见下面回答。
【已解决】常见问题2:CRT大型程序及第三方库内存泄漏检测及定位困难
若要确定在某个代码部分中是否发生了内存泄漏,可以对这部分之前和之后的内存状态拍快照,然后使用 _CrtMemDifference
比较两个状态, 如果_CrtMemDifference
显示内存已泄漏,可以添加更多_CrtMemCheckpoint
调用使用二进制搜索来划分程序,直到你找到泄漏源(二分法)。
示例程序:
_CrtMemState s1;
_CrtMemCheckpoint( &s1 );//创建 _CrtMemState 结构并将其传递给_CrtMemCheckpoint 函数
// 这部分是待检测代码
int* mem1 = new int[5];
int* mem2 = (int *)malloc(sizeof(int) * 5);
int* mem3 = (int*)calloc(5,sizeof(int) );
_CrtMemState s2;
_CrtMemCheckpoint( &s2 );
_CrtMemState s3;
if ( _CrtMemDifference( &s3, &s1, &s2) )
_CrtMemDumpStatistics( &s3 );//_CrtMemDifference 比较内存状态s1和s2,
//并在 (s3) 中返回结果,它是 s1 与 s2 之间的差异。
输出结果:
输出结果格式解读:
- normal block(普通块):这是由你的程序分配的内存。
- client block(客户块):这是一种特殊类型的内存块,专门用于 MFC 程序中需要析构函数的对象。MFC new 操作符视具体情况既可以为所创建的对象建立普通块,也可以为之建立客户块
- CRT block(CRT 块):是由 C RunTime Library 供自己使用而分配的内存块。由 CRT 库自己来管理这些内存的分配与释放,我们一般不会在内存泄漏报告中发现 CRT 内存泄漏,除非程序发生了严重的错误(例如 CRT 库崩溃)。
除了上述的类型外,还有下面这两种类型的内存块,它们不会出现在内存泄漏报告中:
- free block(空闲块):已经被释放(free)的内存块。
- Ignore block(忽略块):这是程序员显式声明过不要在内存泄漏报告中出现的内存块。
3.1.2.2 VLD库(win平台)
VLD( Visual Leak Detector )最新版本是2.5.1,发布时间:2017-10-17
VLD2,5,1下载官网:VLD下载官网
本文中VLD示例程序见:VLD2.5.1配置程序
VLD的使用方法:直接#include“vld.h”
,没错,就是这么简单!
CRT与VLD的区别:
(1)CRT可在调试窗口右健直接跳转到内存泄漏行号,VLD的输出窗口不可以直接跳转;
(2)VLD自2017年未更新,目前最新版本检测不出来DLL函数内存泄漏;