在 C/C++ 编程中,内存管理是一个核心知识点,直接影响程序的性能和稳定性。本文将详细介绍 C/C++ 中的内存分布、动态内存管理方式及相关原理,帮助大家系统掌握这部分知识。
一、C/C++ 内存分布
程序运行时,内存主要分为以下几个区域:
- 栈(堆栈):存储非静态局部变量、函数参数、返回值等,栈是向下增长的。
- 内存映射段:高效的 I/O 映射方式,用于装载共享动态内存库,也可用于进程间通信(了解即可)。
- 堆:用于程序运行时动态内存分配,堆是向上增长的。
- 数据段:存储全局数据和静态数据。
- 代码段:存储可执行的代码和只读常量。
内存分布示例分析:
char char2[] = "abcd":char2数组在栈上,"abcd" 作为字符串常量在代码段,初始化时会将字符串内容拷贝到栈上的数组中。char* pChar3 = "abcd":指针pChar3在栈上,其指向的字符串常量 "abcd" 在代码段。int* ptr1 = new int[10]:指针ptr1在栈上,其指向的动态内存空间在堆上。
二、C 语言动态内存管理方式
C 语言通过malloc、calloc、realloc和free四个函数进行动态内存管理,它们的区别和特性如下:
| 函数 | 功能描述 | 初始化特性 | 使用方法 |
| malloc | 从堆区开辟指定字节数的连续空间 | 不初始化,空间内是随机值 | void* malloc (size_t size); |
| calloc | 从堆区开辟 n 个 “指定大小” 的连续空间(可看作malloc+初始化) | 自动将空间全部初始化为 0 | void* calloc (size_t num, size_t size); |
| realloc | 调整已开辟的堆空间大小(支持扩容 / 缩容 | 扩容时保留原数据,新空间未初始化;缩容时截断数据 | void* realloc (void* ptr, size_t size); |
注意事项:
使用realloc调整空间大小时,若扩容成功,原指针p会被新指针接管,无需单独free(p),只需释放新指针即可。例如:
void Test() { int* p2 = (int*)calloc(4, sizeof(int)); int* p3 = (int*)realloc(p2, sizeof(int) * 10); // 无需free(p2),只需释放p3 free(p3); }三、C++ 内存管理方式
C 语言的内存管理方式在 C++ 中仍可使用,但 C++ 新增了new和delete操作符,更适合处理自定义类型,使用更简洁。
1. 操作内置类型
- 申请单个元素空间:
new+ 类型,释放用delete。 - 申请连续空间:
new[]+ 类型,释放用delete[](必须匹配使用)。 - 支持初始化,未指定的元素默认置 0。
示例代码:
void Test() { // 动态申请int空间,不初始化 int* p1 = new int; // 动态申请int空间,初始化为10 int* p2 = new int(10); // 动态申请3个int的连续空间,不初始化 int* p3 = new int[3]; // 动态申请10个int的连续空间,全部初始化为0 int* p4 = new int[10] {0}; // 前5个初始化,后5个默认置0 int* p5 = new int[10] {1, 2, 3, 4, 5}; // 释放空间(必须匹配) delete p1; delete p2; delete[] p3; delete[] p4; delete[] p5; }2. 操作自定义类型
new/delete与malloc/free的核心区别:对自定义类型,new会调用构造函数初始化,delete会调用析构函数清理资源。
示例代码:
class A { public: A(int a1 = 0, int a2 = 0) : _a1(a1), _a2(a2) { cout << "A():" << endl; } ~A() { cout << "~A():" << endl; } private: int _a1, _a2; }; int main() { A* p1 = new A(1); // 调用构造函数 A* p2 = new A[3]{A(1,1), A(2,2), A(3,3)}; // 调用3次构造函数 delete p1; // 调用析构函数 delete[] p2; // 调用3次析构函数 return 0; }四、operator new 与 operator delete 函数
new和delete是操作符,底层通过全局函数operator new和operator delete实现内存申请与释放。operator new:内部通过malloc申请空间,失败时抛异常(而非返回 NULL)。operator delete:内部通过free释放空间。
五、new 和 delete 的实现原理
1. 内置类型
- 与
malloc/free功能类似,但new失败时抛异常,malloc返回 NULL;new[]/delete[]用于连续空间,需匹配使用。
2. 自定义类型
new的原理:调用
operator new申请空间;在空间上执行构造函数。
delete的原理:在空间上执行析构函数;
调用
operator delete释放空间。
new T[N]的原理:调用
operator new[]申请 N 个对象的空间;执行 N 次构造函数。
delete[]的原理:执行 N 次析构函数;
调用
operator delete[]释放空间。
注意:对有析构函数的自定义类型数组,delete必须配合[]使用,否则会因释放位置错误导致崩溃。
示例代码:
class A { public: A(int a1 = 0, int a2 = 0) : _a1(a1) , _a2(a2) { cout << "A():" << endl; } A(const A& aa) : _a1(aa._a1) { cout << "A(const A& aa)" << endl; } ~A() { cout << "~A():" << endl; } private: int _a1 = 0; int _a2 = 0; }; class B { private: int _b1 = 0; int _b2 = 0; }; int main() { //int* p1 = new int[10]; //free(p1); //内置类型可以这样写,不涉及析构 //B中没有显示写析构函数 //B* p2 = new B[10]; //申请10 * 8个字节 //delete p2; //A中显示写析构函数了 A* p3 = new A[10]; //申请8 + 10 * 8个字节,前8个字节放的是个数(64位系统),32位就是4+10 * 8个字节 //delete p3; //报错,因为只释放了一部分,一整段空间不能在中间释放 delete[] p3; // 有析构函数的类释放时要在delete后加上[],实际释放的位置要往前偏移8个字节(64位)或4个字节(32位) return 0; }六、定位 new 表达式(placement-new)
在已分配的原始内存中手动调用构造函数初始化对象,格式:new (place_address) type(initializer-list)
场景:配合内存池使用(内存池分配的空间未初始化,需手动调用构造函数)。
示例:
A* p = (A*)malloc(sizeof(A)); // 仅开辟空间,未初始化 new (p) A(1, 2); // 调用构造函数初始化 p->~A(); // 手动调用析构函数 free(p); // 释放空间七、malloc/free 与 new/delete 的区别
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。
不同的地方是:
1. malloc和free是函数,new和delete是操作符
2. malloc申请的空间不会初始化,new可以初始化
3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可, 如果是多个对象,[]中指定对象个数即可
4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new 在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理释放