C++栈内存和堆内存管理

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 解决栈溢出的办法

主要有以下几种:

  1. 减少递归层级:尝试将递归调用改为迭代方式,以减少栈空间的消耗。
  2. 减少局部变量和临时数据的使用:可以将部分数据转移到堆上,或者使用全局变量、静态变量代替局部变量。
  3. 增加栈空间大小:可以通过编译器或操作系统提供的配置选项,增加程序使用的栈空间大小
  4. 检查参数传递:对于函数调用时的参数,考虑是否需要传递大量数据,并尝试使用指针或引用等方式减少参数传递的开销

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输出没有具体函数名及行号。
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行。
CRT输出
右键即可直接跳转到具体位置。
右键直接跳转
值得注意的是,使用上述方法仍然不会输出第三方库的内存申请函数,第三方库内存泄露检测方法见下面回答。

【已解决】常见问题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函数内存泄漏;

相关推荐

  1. C语言内存管理以及空间空间区别

    2024-01-27 19:36:01       37 阅读
  2. C++中的内存定义以及区别

    2024-01-27 19:36:01       5 阅读
  3. 内存内存

    2024-01-27 19:36:01       17 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-01-27 19:36:01       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-01-27 19:36:01       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-01-27 19:36:01       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-01-27 19:36:01       18 阅读

热门阅读

  1. MyBatis学习笔记

    2024-01-27 19:36:01       37 阅读
  2. Vue3生命周期 VS Vue2生命周期(小记)

    2024-01-27 19:36:01       37 阅读
  3. 【leetcode100-063到068】【二分】六题合集

    2024-01-27 19:36:01       31 阅读
  4. 【C语言】(4)数组

    2024-01-27 19:36:01       32 阅读
  5. MySQL数据库备份的相关命令-运维面试常问

    2024-01-27 19:36:01       29 阅读
  6. SQL 优化建议

    2024-01-27 19:36:01       29 阅读
  7. MySQL运维实战(4.8) SQL_MODE之NO_ENGINE_SUBSTITUTION

    2024-01-27 19:36:01       30 阅读
  8. 使用scyllaDb 或者cassandra存储聊天记录

    2024-01-27 19:36:01       32 阅读
  9. 天梯赛 L3-020 至多删三个字符

    2024-01-27 19:36:01       36 阅读