引言
在 C++ 世界里,内存是一切的舞台。无论是创建一个对象、调用一个函数,还是操作一块数组,背后都离不开内存的分配与释放。相比于 C 的
malloc / free
,C++ 又引入了更高级的new / delete
,它们不仅分配原始的字节空间,还会调用构造与析构函数,为对象的生命周期保驾护航。理解这些机制,能让我们更清晰地把握程序运行的底层逻辑,避免常见的内存泄漏与野指针问题。
内存布局
文章开始之前我们先通过一段程序了解一下不同的变量分别存储在内存中的哪块区域
代码示例
c
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
static int staticVar = 1;
int localVar = 1;
int num1[10] = { 1, 2, 3, 4 };
char char2[] = "abcd";
const char* pChar3 = "abcd";
int* ptr1 = (int*)malloc(sizeof(int) * 4);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
free(ptr1);
free(ptr3);
}
通过这段C语言的程序,试着选择各变量存放在哪个内存区域:
选项:A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
globalVar
在哪里? ____staticGlobalVar
在哪里?____staticVar
在哪里?____localVar
在哪里?____num1
在哪里?____char2
在哪里?____*char2
在哪里?___pChar3
在哪里?____*pChar3
在哪里?____ptr1
在哪里?____*ptr1
在哪里?____
解答
变量 | 存放区域 | 说明 |
---|---|---|
globalVar |
数据段(静态区) | 已初始化的全局变量,存放在数据段 |
staticGlobalVar |
数据段(静态区) | 已初始化的静态全局变量,和全局变量一样放在数据段 |
staticVar |
数据段(静态区) | 函数内的静态局部变量,生命周期与程序同在,存放在数据段 |
localVar |
栈 | 普通局部变量,函数调用时压栈 |
num1 |
栈 | 局部数组,内存在栈上分配 |
char2 |
栈 | 局部数组,存在栈上,存放拷贝出来的 "abcd" |
*char2 |
栈 | 数组元素 'a' 'b' 'c' 'd' '\0' 都在栈上 |
pChar3 |
栈 | 局部指针变量,本身在栈上 |
*pChar3 |
常量区 | "abcd" 字符串字面量存放在常量区,指针指向这里 |
ptr1 |
栈 | 局部指针变量,存放在栈上 |
*ptr1 |
堆 | malloc 分配的空间在堆上 |
说明
- 栈 (
Stack
):存放局部变量、函数调用的栈帧;当前函数运行完毕,自动释放 - 堆 (
Heap
):使用malloc/realloc/calloc/new
动态分配的区域;需手动释放 - 数据段/静态区 (
Static
):存放全局变量和static
修饰的局部变量;程序运行结束,自动释放 - 代码段/常量区 :使用
const
修饰的变量、字符串常量
易错点
1. const常量和字符常量
-
const char* pChar3 = "abcd";
,为什么pChar3
在栈区,*pChar3
在常量区?const
修饰的是pChar3
指向的值,char* const pChar3
修饰的才是pChar3
本身pChar3
其实指向的是字符常量"abcd"
-
char char2[] = "abcd";
,"abcd"是字符常量
,char2指向它,为什么*char2
却是在栈区- 编译器将常量区的"abcd"拷贝了一份到栈区,
char2
就指向了这块栈区地址
- 编译器将常量区的"abcd"拷贝了一份到栈区,
2. 堆和栈
-
堆和栈都是动态增长的,栈是向下增长,堆是向上增长,中间是一块"空旷的虚拟空间",两者可能相遇(冲突) 内存布局,如下图:
拓展
malloc
,realloc
,calloc
的区别
- malloc :按字节在堆上申请空间;
- 初始化都是随机值
- calloc :分配一块大小为 num * size 字节的连续内存;
size
:元素字节大小,num
:元素个数;- 初始化为
0
- realloc :调整一块已经分配的内存空间大小(
malloc/realloc/calloc
分配过的)- 新空间
>
旧空间,使用malloc
申请新空间,旧空间的数据拷贝到新空间,再将旧空间释放 - 新空间
<
旧空间,释放旧空间多余的部分 - 扩展的新空间,初始化的是随机值
- 新空间
都是 成功,返回void*
新地址;失败,返回NULL
二、new和delete
在C++中使用new
和delete
关键字进行内存管理,和C语言中的malloc
和free
功能相同,都是在堆上申请/释放内存,注意弥补了malloc和free的一些缺陷,使用更灵活,更智能;
new
申请成功后返回的是申请类型的指针,所以要使用相同类型的指针接收;
1.基本操作
语法:
申请: 单块内存:类型* 变量名 =new 类型(初始化值); 连续内存:类型* 变量名 =new 类型[元素个数] 释放: 单块内存:delete 变量名; 连续内存:delete[ ] 变量名;
new T
→ 必须用delete
;new T[n]
→ 必须用delete[]
内置类型
对于内置类型,new
和malloc
基本没有什么差异;使用new
申请内存可以进行初始化,而malloc
不能初始化 代码示例:
cpp
void Test1()
{
//申请一个int(4字节)类型的空间
int* p1 = new int;
//申请一个int类型的空间,并初始化
int* p2 = new int(1);
//申请一段int类型的连续空间(数组)
int* p3 = new int[10];
//释放内存
delete p1;
delete p2;
delete[] p3;
}
自定义类型
new
和malloc
主要的区别就是在申请自定义类型内存时,new
会调用该只定义类型的构造函数,而malloc
只会按字节大小直接申请内存;
代码示例:
cpp
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
int main()
{
Test1();
//申请一块A类型的空间
//调用一次构造函数
A* p1 = new A;
//申请三块连续的A类型空间
//调用三次构造函数
A* p2 = new A[3];
delete p1;
delete[] p2;
return 0;
}
2. new/delete底层原理
new
的底层是调用系统的全局函数void* operator new(size_t _Size)
new[]
的底层是调用系统的全局函数void* operator new[](size_t _Size)
delete
的底层是调用系统的全局函数void* operator delete(void* _Block)
delete[]
的底层是调用系统的全局函数void operator delete[](void* _Block)
operator new() / operator new[] ()
operator new
函数底层调用就是malloc()
申请内存;
operator new
在申请自定义类型 内存空间时,会在申请成功的内存空间上调用当前自定义类型的构造函数使用
new [N]
申请N
个连续的自定义类型时,会调用N
次operator new
;并且,如果显示实现了析构函数,编译器还会自动在申请的内存前面额外申请4个字节,用于存放数字N
,如果没有显示实现,则不会记录;这样为了在delete
时,记录需要调用析构函数多少次;
malloc
申请成功,operator new
就返回申请的内存地址,失败则抛出异常错误bad_alloc
operator new源码(简化版)
cpp
void* __CRTDECL operator new(size_t const size)
{
for (;;)
{
if (void* const block = malloc(size))
{
return block;
}
if (_callnewh(size) == 0)
{
if (size == SIZE_MAX)
{
__scrt_throw_std_bad_array_new_length();
}
else
{
__scrt_throw_std_bad_alloc();
}
}
// The new handler was successful; try to allocate again...
}
}
可以看到源码中直接调用了malloc
函数(p=malloc(size)
)
operator delete() / operator delete[] ()
operator delete
函数底层调用就是free()
去释放内存;(对于内置类型的空间,free也可以替代delete释放内存,但是不建议)- 使用
delete[]
释放对象时,会调用operator delete
去释放内存,调用次数根据额外空间记录的个数
总结
特性 | 内置类型 (int ) |
自定义类型 (class A ) |
---|---|---|
构造函数调用 | 无 | 有(逐个构造) |
析构函数调用 | 无 | 有(逆序逐个析构) |
new 初始化方式 |
默认随机值/零初始化 | 按构造函数逻辑执行 |
delete[] 行为 |
仅释放内存 | 析构+释放内存 |
写到这里,关于 new/delete 和 malloc/free 的小故事就先告一段落啦。本文只是抛砖引玉,难免有遗漏和偏差,还请大家多多指正。希望这些内容能帮你在和内存"斗智斗勇"的路上少踩几个坑,多几分笃定,如果对你有帮助,麻烦你 👍点赞 ⭐收藏 ❤️关注 吧~