C++中的内存管理、内存泄漏和内存回收

一、内存管理

1、C++内存管理详解

1.1、内存组成

栈区(stack):由编译器自动分配与释放,存放为运行时函数分配的局部变量、函数参数、返回数据、返回地址等。其操作类似于数据结构中的栈。
堆区(heap):一般由程序员自动分配,如果程序员没有释放,程序结束时可能有OS回收。其分配类似于链表。
全局/静态区(static):存放全局变量、静态数据、常量。程序结束后由系统释放。全局区分为已初始化全局区(data)和未初始化全局区(bss)。
常量区(文字常量区):存放常量字符串,程序结束后有系统释放。
代码区:存放函数体(类成员函数和全局区)的二进制代码。
![![![在这里插入图片描述](https://img-blog.csdnimg.cn/20200703181934524.png?](https://img-blog.csdnimg.cn/20200703184718719.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2NzIxMDMy,size_16,color_FFFFFF,t_70](https://img-blog.csdnimg.cn/20200703184805604.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2NzIxMDMy,size_16,color_FFFFFF,t_70
Code: 即代码域,它指的是编译器生成的机器指令。
RO_data: ReadOnly data,即只读数据域,它指程序中用到的只读数据,全局变量,例如C语言中const关键字定义的全局变量就是典型的RO-data。
RW_data: ReadWrite data,即可读写数据域,它指初始化为“非0值”的可读写数据,程序刚运行时,这些数据具有非0的初始值,且运行的时候它们会常驻在RAM区,因而应用程序可以修改其内容。例如全局变量或者静态变量,且定义时赋予“非0值”给该变量进行初始化。
ZI_data: ZeroInitialie data,即0初始化数据,它指初始化为“0值”的可读写数据域,它与RW_data的区别是程序刚运行时这些数据初始值全都为0,而后续运行过程与RW-data的性质一样,它们也常驻在RAM区,因而应用程序可以更改其内容。包括未初始化的全局变量,和初始化为0的全局变量。
RO: 只读区域,包括RO_data和code。

1.2、堆与栈的区别

void f() { int* p=new int[5]; }

这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?他分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。在程序会先确定在堆中分配内存的大小,然后调用operator new分配内存,然后返回这块内存的首地址,放入栈中。

堆与栈主要的区别由以下几点:

1、管理方式不同;
对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。

2、空间大小不同;
一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改:打开工程,依次操作菜单如下:Project->Setting->Link,在Category 中选中Output,然后在Reserve中设定堆栈的最大值和commit。注意:reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。

3、能否产生碎片不同;
对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。

4、生长方向不同;
对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

5、分配方式不同;
堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

6、分配效率不同;
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。

虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。

无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难的:)

2、指针与数组

2.1、修改内容

下面示例中,字符数组a的容量是6个字符,其内容为hello。a的内容可以改变,如a[0]= ‘X’。指针p指向常量字符串“world”(位于静态存储区,内容为world),常量字符串的内容是不可以被修改的。从语法上看,编译器并不觉得语句p[0]= ‘X’有什么不妥,但是该语句企图修改常量字符串的内容而导致运行错误。

char a[] = “hello”;

a[0] = ‘X’;

cout << a << endl;

char *p = “world”; // 注意p指向常量字符串

p[0] = ‘X’; // 编译器不能发现该错误

cout << p << endl;

2.2、 内容复制与比较

不能对数组名进行直接复制与比较。若想把数组a的内容复制给数组b,不能用语句 b = a ,否则将产生编译错误。应该用标准库函数strcpy进行复制。同理,比较b和a的内容是否相同,不能用if(b == a) 来判断,应该用标准库函数strcmp进行比较。
语句p = a 并不能把a的内容复制指针p,而是把a的地址赋给了p。要想复制a的内容,可以先用库函数malloc为p申请一块容量为strlen(a)+1个字符的内存,再用strcpy进行字符串复制。同理,语句if (p==a) 比较的不是内容而是地址,应该用库函数strcmp来比较。

// 数组…

char a[] = "hello";

char b[10];

strcpy(b, a); // 不能用 b = a;

if(strcmp(b, a) == 0) // 不能用 if (b == a)// 指针…

int len = strlen(a);

char *p = (char *)malloc(sizeof(char)*(len+1));

strcpy(p,a); // 不要用 p = a;

if(strcmp(p, a) == 0) // 不要用 if (p == a)

2.3、 计算内存容量

用运算符sizeof可以计算出数组的容量(字节数)。如下示例中,sizeof(a)的值是12(注意别忘了’’)。指针p指向a,但是sizeof§的值却是4。这是因为sizeof§得到的是一个指针变量的字节数,相当于sizeof(char*),而不是p所指的内存容量。C++/C语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。

char a[] = "hello world";

char *p = a;

cout<< sizeof(a) << endl; // 12字节

cout<< sizeof(p) << endl; // 4字节

注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。如下示例中,不论数组a的容量是多少,sizeof(a)始终等于sizeof(char *)。

void Func(char a[100])

{

 cout<< sizeof(a) << endl; // 4字节而不是100字节

}

3、 有了malloc/free为什么还要new/delete?

malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。我们先看一看malloc/free和new/delete如何实现对象的动态内存管理,见示例:

class Obj

{

 public :

  Obj(void){ cout << “Initialization” << endl; }

  ~Obj(void){ cout << “Destroy” << endl; }

  void Initialize(void){ cout << “Initialization” << endl; }

  void Destroy(void){ cout << “Destroy” << endl; }

};

void UseMallocFree(void)

{

 Obj *a = (obj *)malloc(sizeof(obj)); // 申请动态内存

 a->Initialize(); // 初始化

 //…

 a->Destroy(); // 清除工作

 free(a); // 释放内存

}

void UseNewDelete(void)

{

 Obj *a = new Obj; // 申请动态内存并且初始化

 //…

 delete a; // 清除并且释放内存

}

类Obj的函数Initialize模拟了构造函数的功能,函数Destroy模拟了析构函数的功能。函数UseMallocFree中,由于malloc/free不能执行构造函数与析构函数,必须调用成员函数Initialize和Destroy来完成初始化与清除工作。函数UseNewDelete则简单得多。

所以我们不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete。由于内部数据类型的“对象”没有构造与析构的过程,对它们而言malloc/free和new/delete是等价的。

既然new/delete的功能完全覆盖了malloc/free,为什么C++不把malloc/free淘汰出局呢?这是因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。

如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放“malloc申请的动态内存”,结果也会导致程序出错,但是该程序的可读性很差。所以new/delete必须配对使用,malloc/free也一样。

4、内存耗尽怎么办?

如果在申请动态内存时找不到足够大的内存块,malloc和new将返回NULL指针,宣告内存申请失败。通常有三种方式处理“内存耗尽”问题。
(1)判断指针是否为NULL,如果是则马上用return语句终止本函数。例如:

void Func(void)

{

 A *a = new A;

 if(a == NULL)

 {

  return;

 }

 …

}

(2)判断指针是否为NULL,如果是则马上用exit(1)终止整个程序的运行。例如:

void Func(void)

{

 A *a = new A;

 if(a == NULL)

 {

  cout << “Memory Exhausted” << endl;

  exit(1);

 }

 …

}

(3)为new和malloc设置异常处理函数。例如Visual C++可以用_set_new_hander函数为new设置用户自己定义的异常处理函数,也可以让malloc享用与new相同的异常处理函数。详细内容请参考C++使用手册。

上述(1)(2)方式使用最普遍。如果一个函数内有多处需要申请动态内存,那么方式(1)就显得力不从心(释放内存很麻烦),应该用方式(2)来处理。

很多人不忍心用exit(1),问:“不编写出错处理程序,让操作系统自己解决行不行?”

不行。如果发生“内存耗尽”这样的事情,一般说来应用程序已经无药可救。如果不用exit(1) 把坏程序杀死,它可能会害死操作系统。道理如同:如果不把歹徒击毙,歹徒在老死之前会犯下更多的罪。

5、malloc/free以及new/delete的使用要点

5.1、malloc/free

用malloc申请一块长度为length的整数类型的内存,程序如下:

int *p = (int *) malloc(sizeof(int) * length);

我们应当把注意力集中在两个要素上:“类型转换”和“sizeof”。

  • malloc返回值的类型是void *,所以在调用malloc时要显式地进行类型转换,将void * 转换成所需要的指针类型。

  • malloc函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。我们通常记不住int, float等数据类型的变量的确切字节数。例如int变量在16位系统下是2个字节,在32位下是4个字节;而float变量在16位系统下是4个字节,在32位下也是4个字节。

函数free的原型如下:

void free( void * memblock );

5.2、new/delete

运算符new使用起来要比函数malloc简单得多,例如:

int *p1 = (int *)malloc(sizeof(int) * length);

int *p2 = new int[length];

这是因为new内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new的语句也可以有多种形式。例如

class Obj

{

 public :

  Obj(void); // 无参数的构造函数

  Obj(int x); // 带一个参数的构造函数

  …

}

void Test(void)

{

 Obj *a = new Obj;

 Obj *b = new Obj(1); // 初值为1

 …

 delete a;

 delete b;

}

如果用new创建对象数组,那么只能使用对象的无参数构造函数。例如:

Obj *objects = new Obj[100]; // 创建100个动态对象

不能写成:

Obj *objects = new Obj[100](1);// 创建100个动态对象的同时赋初值1

在用delete释放对象数组 时,留意不要丢了符号‘[]’。例如:

delete []objects; // 正确的用法

delete objects; // 错误的用法

后者有可能引起程序崩溃和内存泄漏。

二、内存泄漏、内存溢出、内存越界

1、内存泄漏

一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显示释放的内存。应用程序一般使用malloc,realloc,new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。以下这段小程序演示了堆内存发生泄漏的情形:

void MyFunction(int nSize)

{

 char* p= new char[nSize];

 if( !GetStringFrom( p, nSize ) ){

  MessageBox(“Error”);

  return;

 }

 …//using the string pointed by p;

 delete p;

}

当函数GetStringFrom()返回零的时候,指针p指向的内存就不会被释放。这是一种常见的发生内存泄漏的情形。程序在入口处分配内存,在出口处释放内存,但是c函数可以在任何地方退出,所以一旦有某个出口处没有释放应该释放的内存,就会发生内存泄漏。

广义的说,内存泄漏不仅仅包含堆内存的泄漏,还包含系统资源的泄漏(resource leak),比如核心态HANDLE,GDI Object,SOCKET, Interface等,从根本上说这些由操作系统分配的对象也消耗内存,如果这些对象发生泄漏最终也会导致内存的泄漏。而且,某些对象消耗的是核心态内存,这些对象严重泄漏时会导致整个操作系统不稳定。所以相比之下,系统资源的泄漏比堆内存的泄漏更为严重。

2、内存溢出

指程序在申请内存时,没有足够的内存空间供其使用。简单理解,内存溢出就是要求分配的内存超出了系统所给的。

缓冲区溢出(栈溢出)
程序为了临时存取数据的需要,一般会分配一些内存空间称为缓冲区。如果向缓冲区中写入缓冲区无法容纳的数据,机会造成缓冲区以外的存储单元被改写,称为缓冲区溢出。而栈溢出是缓冲区溢出的一种,原理也是相同的。分为上溢出和下溢出。其中,上溢出是指栈满而又向其增加新的数据,导致数据溢出;下溢出是指空栈而又进行删除操作等,导致空间溢出。

堆栈溢出一般是由于什么原因导致的?

  • 内存泄漏/没有回收垃圾资源,导致存储空间垃圾太多,内存已满。
  • 没有设置合理的停止条件,递归调用次数太多,重复调用自己导致内存不足。

3、内存越界

内存越界是软件系统主要错误之一,其后果往往不可预料且非常严重。更麻烦的是,它出现的时机是随机的,表现出来的症状是随机的,而且造成的后果也是随机的,这会使程序员很难找出这些 Bug 的现象和本质之间的联系,从而给 Bug 的定位带来极大的困难。一般情况下,内存越界访问可分如下两种:

  • 读越界,即读了不属于自己的数据。如果所读的内存地址是无效的,程序立刻崩溃;如果所读内存地址是有效的,在读的时候不会马上出现问题,但由于读到的数据是随机的,因此它会造成不可预料的后果。
  • 写越界,又称为缓冲区溢出,所写入的数据对别的程序来说是随机的,它也会造成不可预料的后果。
  • 9
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C 内存管理是指在编程过程,对内存资源的分配和释放进行管理的技术。在C语言,程序员需要手动管理内存的分配和释放,以便有效地使用系统的内存资源。 C语言内存管理主要包括以下几个方面: 1. 内存的分配:C语言使用malloc()函数来分配堆内存。该函数接收一个整数参数,表示要分配的字节数。分配的内存块是在程序运行时动态分配的,并且在堆区。分配完成后,malloc()函数会返回一个指向分配内存块起始位置的指针。 2. 内存的释放:C语言使用free()函数来释放之前分配的内存。释放内存的操作是手动进行的,当不再需要使用之前分配的内存块时,程序员需要显式调用free()函数来释放该内存块。释放内存后,该内存块将被系统回收,可以被其他程序使用。 3. 内存泄漏:在使用malloc()函数分配内存后,如果程序员忘记调用free()函数来释放内存,就会导致内存泄漏内存泄漏会导致程序运行过程逐渐占用更多的内存,最终耗尽系统的内存资源,导致程序崩溃。 4. 指针和数组:C语言的指针与数组密切相关。指针是变量,保存了内存地址。数组也是一种特殊的指针,指向一段连续的内存空间。程序员可以通过指针和数组操作内存,实现数据的动态存储和访问。 5. 内存的动态分配:C语言,动态分配内存可以根据程序运行时的需要,动态地调整内存的大小。例如,在需要存储不确定数量的数据时,可以使用realloc()函数重新调整之前分配的内存大小。 总之,C语言内存管理是程序员需要关注的重要问题。合理地进行内存的分配和释放,可以避免内存泄漏内存溢出等问题,提高程序的效率和稳定性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值