C++内存管理

2024-05-23

内存管理在C++中无处不在,内存泄漏更是每个C++程序中经常发生的错误,因此想要熟练掌握C++,内存管理是首先需要掌握的。

1. C++内存管理详解

在C++中,内存分为5个区域,分别是堆、栈、自由存储区、全局/静态存储区和常量存储区

  • ,由 new 分配的内存块,不会被编译器自动释放,由我们的程序去控制,一个 new 一定要对应一个 delete 。如果程序中没有释放内存,那么程序运行结束后,操作系统会自动回收;
  • ,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束后这些存储单元被自动释放;
  • 自由存储区,由 malloc 等分配的内存块,和堆十分相似,但是使用 free 来回收内存;
  • 全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化和未初始化的,在C++里没有这个区分了,共同占用一块内存区;
  • 常量存储区,这是一块比较特殊的存储区,里面存放常量,不允许修改。

任何在函数内部声明的非static变量,其变量地址本身在栈区;

任何全局变量或者静态变量,其变量地址本身在全局区;

任何指针变量,如果采用 malloc, realloc, calloc 或者 C++ 里的 new 分配,指针指向内存的堆区。

const int const_var = 0;
static int static_var1;
int var1 = 0;
void function()
{
    static int static_var2 = 0;
    int var2 = 0;
	int *arr = new int[10];
    delete []arr;
}

代码中,const_var位于常量存储区,static_var1、static_var2以及var1位于全局存储区,var2位于栈区,arr位于堆区。

2. 区分堆与栈

  1. 管理方式:栈的管理是由编译器来进行分配和管理,堆的管理一般是程序员通过 newdelete 来对内存进行分配和释放。
  2. 空间大小:堆在系统中一般可以分配几个G大小的内存,而栈一般分配几M大小的内存。
  3. 碎片问题:堆在不断地执行 newdelete 操作中,内存被逐渐碎片化,使的程序的执行效率变低;栈采用后进先出的策略,不会出现碎片化的问题。
  4. 增长方向:堆的方向是向着地址增大的方向进行的,而栈采用的是先进后出的策略,所以元素的增长方向是朝着地址减小的方向进行的。
  5. 分配方式:堆一般是动态分配的,而栈既有动态分配的方式也有静态分配的方式,静态分配使用 alloc 函数进行,也是由编译器分配,动态由编译器分配。
  6. 分配效率:一般情况下,栈的分配效率高于堆,因为栈分配的时候由编译器和操作系统底层将临时变量和相关地址放在特定的寄存器中,读写效率高,而堆有上层复杂的数据结构会造成效率低的问题。

若是需要占用大块内存还是分配给堆比较好。需要注意栈和堆的越界问题,会造成内存泄露。

3. 区分堆和自由存储区

  1. 从技术上来说,堆(heap)是 C/C++ 语言和操作系统的术语,堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,使用malloc()、free() 来申请/释放内存。
  2. 自由存储是 C++ 中通过 new 和 delete 动态分配和释放对象的抽象概念。基本上,所有的 C++ 编译器默认使用堆来实现自由存储。也就是说,默认的全局运算符 newdelete 也许会使用 mallocfree 的方式申请和释放存储空间,这时自由存储区就位于堆上。但程序员也可以通过重载操作符,改用其他内存来实现自由存储,例如全局变量做的对象池,这时自由存储区就不位于堆上了。

4. 区分指针与数组

在C/C++语言中,指针和数组可以替换使用,但是二者并不等价。

数组可以在全局区被创建(全局数组),也可以在栈区被创建,数组名称对应着一块内存,数组的地址和容量在生命周期内不变。

指针可以随时指向任意类型的数据块,特征是“可变”,所以我们常用指针来操作动态内存。

5. 区分malloc/free和new/delete

malloc和free是C/C++标准库函数,new和delete是C++的运算符,都可用于动态申请内存和释放内存。

C++是面向对象编程的语言,使用类来提供封装、多态和继承,但是对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求。对象在创建的同时需要自动执行构造函数,在销毁之前需要自动执行析构函数。由于malloc/free不是运算符,不在编译器的控制范围内,不能够把执行构造函数和析构函数的任务强加于malloc/free。

因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。

malloc/free不能完成动态对象的管理,应该用new/delete。由于内部数据类型创建和销毁时不需要构造和析构,所以对于它们来说malloc/free和new/delete是等价的。但是new/delete无法完全替代malloc/free,因为C++程序中可能会调用C程序,C语言中只能使用malloc/free管理内存。

5.1. malloc/free使用要点

函数malloc的原型如下:

void* malloc(size_t size);

用malloc申请一块长度为len的整型变量内存,程序如下:

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

使用malloc函数需要注意:

  • malloc函数的返回值类型事void*,所以在调用malloc函数时要显式地类型转换,将 void* 转换成所需要的指针类型;
  • malloc函数只管分配内存,并不会初始化,其内存空间可能是随机的,如果分配的这块空间没有使用过,那么其中每个值都可能是0。相反,空间里可能会遗留有各种各样的值;
  • malloc函数本身并不识别要申请的内存是什么类型,只关心内存的总字节数。我们使用 sizeof() 函数来计算数据类型单个变量的字节数,计算出内存所需的总字节数后申请。

函数free的原型如下:

void free(void* memblock);

因为指针p的类型以及它所指的内存的容量事先都是知道的,语句free(p)能正确地释放内存,所以free函数不像malloc函数那么复杂。

5.2 new/delete使用要点

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

int* p = new int[len];

在程序执行期间,申请用于存放对象类型的内存空间并按照初始化列表赋初值。

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

class Object {
    public:
    	Object(void);
    	Object(int);
    	...
}
void Test(void)
{
    Object* a = new Object;
    Object* b = new Object(1);
    ...
    delete a;
    delete b;
}

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

Object *objs = new Object[100]

使用delete释放对象数组:

delete []objs;

5.3 总结

特征 new/delete malloc/free
分配内存的位置 自由存储区
内存分配失败 抛出异常 返回NULL
分配内存的大小 编译器根据类型计算得出 显式指定字节数
处理数组 有处理数组的new版本new[] 需要用户计算数组的大小后进行内存分配
已分配内存的扩张 不支持 使用realloc完成
分配内存时内存不足 可以指定处理函数或重新制定分配器 无法通过用户代码进行处理
是否可以重载 可以 不可以
构造函数与析构函数 调用 不调用