文章目录
- 前言
- 一、C/C++ 内存区域划分
- 1. 五大内存区域
- 2. 变量位置判断
- 二、C 语言动态内存管理:malloc/calloc/realloc
- 1. 函数对比
- 三、C++ 内存管理:new 和 delete
- 1. new/delete 操作内置类型
- 2. new/delete 操作自定义类型
- 四、底层揭秘:operator new 与 operator delete
- 1. operator new 是什么?
- 2. operator delete 是什么?
- 3. 关系链:
- 五、new 和 delete 的实现原理
- 1. 内置类型
- 2. 自定义类型
- 总结
前言
本文从 C/C++ 内存分区讲起,简单回顾 C 语言malloc/calloc/realloc,再深入讲解 C++ new/delete的用法、底层原理。
一、C/C++ 内存区域划分
1. 五大内存区域
| 区域 | 存储内容 | 特点 |
|---|---|---|
| 栈(Stack) | 非静态局部变量、函数参数、返回值等 | 函数结束自动释放,向下增长,空间较小 |
| 内存映射段 | 动态库、文件映射、共享内存 | 高效 I/O 映射,了解即可 |
| 堆(Heap) | 动态申请的内存(malloc/new) | 手动申请手动释放,向上增长,空间大 |
| 数据段(静态区) | 全局变量、静态变量(static) | 程序整个生命周期都存在,程序结束释放 |
| 代码段(常量区) | 可执行代码、只读常量(字符串常量) | 只读,不可修改 |
2. 变量位置判断
结合以下的代码,理解各个变量的存储位置。
intglobalVar=1;// 全局变量 → 数据段(静态区)staticintstaticGlobalVar=1;// 全局静态变量 → 数据段(静态区)voidTest(){staticintstaticVar=1;// 局部静态变量 → 数据段(静态区)intlocalVar=1;// 局部变量 → 栈intnum1[10]={1,2,3,4};// 局部数组 → 栈charchar2[]="abcd";// char2数组本身在栈,字符串常量"abcd"在代码段,数组拷贝了常量内容constchar*pChar3="abcd";// 指针pChar3在栈,指向的字符串"abcd"在代码段(常量区)int*ptr1=(int*)malloc(sizeof(int)*4);// ptr1指针在栈,指向的空间在堆int*ptr2=(int*)calloc(4,sizeof(int));// ptr2指针在栈,指向的空间在堆int*ptr3=(int*)realloc(ptr2,sizeof(int)*4);// ptr3指针在栈,指向的空间在堆free(ptr1);free(ptr3);}易错点:
1. 栈 vs 堆
栈:函数调用自动分配,结束自动回收,速度快,空间小;
堆:手动申请手动释放,速度慢,空间大,使用灵活。
2. 字符串常量"abcd"这种字符串字面量存在代码段(常量区),只读不可修改;char char2[] = "abcd"是把常量拷贝到栈上的数组里,数组内容可以修改。
3. static 变量
不管是全局还是局部 static,都存在数据段,生命周期贯穿整个程序。
二、C 语言动态内存管理:malloc/calloc/realloc
C 语言管理堆内存靠四个函数:malloc、calloc、realloc、free。
1. 函数对比
| 函数 | 作用 | 初始化 | 参数 |
|---|---|---|---|
| malloc | 申请指定字节数的堆空间 | 不初始化,内容随机 | 总字节数 |
| calloc | 申请指定个数指定大小的空间 | 自动初始化为 0 | 个数 + 单个大小 |
| realloc | 对已有空间扩容 | 不初始化新空间 | 原指针 + 新总大小 |
| free | 释放堆空间 | - | 指向堆空间的指针 |
知识点1:realloc:给已有空间扩容,分两种情况:
原地扩容:原空间后面有足够空位,直接扩大,返回原地址;
异地扩容:原空间后面不够,找一块更大的新空间,拷贝数据,释放旧空间,返回新地址。
知识点2:realloc 后原来的指针需要 free 吗?
int*p2=(int*)calloc(4,sizeof(int));int*p3=(int*)realloc(p2,sizeof(int)*10);// 这里需要 free(p2) 吗?不需要!因为:
如果是原地扩容,p3和p2地址一样,free(p3)就释放了;
如果是异地扩容,realloc内部会自动释放旧空间p2,不用手动 free。
三、C++ 内存管理:new 和 delete
C 语言的 malloc/free 在 C++ 里依然能用,但对付自定义类型就很麻烦 —— malloc只开空间,不会调用构造函数;free只释放空间,不会调用析构函数。
于是,C++ 推出了自己的内存管理操作符:new和delete。
1. new/delete 操作内置类型
对于int、char等内置类型,new/delete和malloc/free功能差不多,只是写法更简洁。
voidTest(){// 1. 申请单个int空间,不初始化int*ptr1=newint;// 2. 申请单个int空间,初始化为10int*ptr2=newint(10);// 3. 申请连续10个int的数组int*ptr3=newint[10];// 数组初始化:C++11支持列表初始化int*ptr4=newint[5]{1,2,3,4,5};// 释放单个空间deleteptr1;deleteptr2;// 释放数组空间,必须加 []delete[]ptr3;delete[]ptr4;}重要匹配规则:
new对应deletenew[]对应delete[]
不匹配使用会导致内存泄漏甚至程序崩溃。
2. new/delete 操作自定义类型
这才是 new/delete 真正的价值:申请空间 + 调用构造函数 / 释放空间前调用析构函数。
#include<iostream>usingnamespacestd;classA{public:A(inta=0):_a(a){cout<<"A():"<<this<<endl;}~A(){cout<<"~A():"<<this<<endl;}private:int_a;};intmain(){// C语言方式:只开空间,不调用构造A*p1=(A*)malloc(sizeof(A));free(p1);// 只释放空间,不调用析构// C++方式:开空间 + 调用构造函数A*p2=newA(1);deletep2;// 先调用析构,再释放空间cout<<"----------------"<<endl;// 数组版本A*p3=newA[2];// 调用2次构造函数delete[]p3;// 调用2次析构函数return0;}
核心结论
- malloc/free:只负责开 / 释放空间,不管对象的构造和析构;
- new/delete:
new:先开空间,再调用构造函数初始化对象;
delete:先调用析构函数清理资源,再释放空间。
这也是为什么 C++ 推荐用new/delete管理自定义类型。
四、底层揭秘:operator new 与 operator delete
new不是直接调用malloc,其实中间还有一层:operator new 和 operator delete 全局函数。
下面我们通过反汇编窗口查看:
operator new :
operator delete 还需要继续深入:
反汇编比较难理解,这里只是让大家看一下底层确实是调用了operator new 和 operator delete 全局函数,更多细节就不讨论了。
1. operator new 是什么?
operator new 是 C++ 提供的全局函数,不是运算符重载,它的本质就是封装了 malloc + 失败抛异常。
可以把源码简单理解为:
void*operatornew(size_t size){void*p;// 循环调用malloc申请空间while((p=malloc(size))==0){// 申请失败,调用用户设置的应对措施if(_callnewh(size)==0){// 没有应对措施,抛出 bad_alloc 异常throwbad_alloc();}}returnp;}关键区别:malloc 失败 VS new 失败
- malloc 失败:返回
NULL,使用前必须判空; - new 失败:抛出
bad_alloc异常,不需要判空,需要捕获异常。
2. operator delete 是什么?
operator delete 同样是全局函数,底层封装了free,专门用来释放operator new申请的空间。
3. 关系链:
简单把关系捋一下,就形成了:
new 操作符 → 调用 operator new 全局函数 → 底层调用 malloc 申请空间 → 申请成功返回地址,失败抛异常
delete 操作符 → 先调用析构函数 → 调用 operator delete 全局函数 → 底层调用 free 释放空间
五、new 和 delete 的实现原理
1. 内置类型
对于int、double等内置类型:
new和malloc几乎一样,只是语法不同、失败处理不同;delete和free几乎一样;new[]申请连续空间,delete[]释放连续空间。
2. 自定义类型
new 的执行流程
调用operator new函数申请堆空间;
在申请好的空间上执行构造函数,完成对象初始化。
delete 的执行流程
在对象上执行析构函数,清理对象内部资源;
调用operator delete函数释放堆空间。
new T [N] 的执行流程
调用operator new[],底层调用operator new申请N个对象的总空间;
在空间上执行N次构造函数,逐个初始化每个对象。
delete [] 的执行流程
在空间上执行N次析构函数,逐个清理每个对象;
调用operator delete[],底层调用operator delete释放总空间。
这也解释了为什么
new[]必须配delete[]: 编译器要知道调用多少次析构函数,不匹配会导致少调用析构或越界访问。
总结
欢迎点赞 + 收藏 + 关注!