1. C/C++ 内存分布
1.1 内存分区图示
程序运行时的虚拟地址空间分层如下:
| 分区 | 存储内容 | 特点 |
|---|---|---|
| 栈 (Stack) | 非静态局部变量、函数参数、返回地址等 | 向下增长,自动管理 |
| 内存映射段 | 动态库、共享内存 | 用于高效 I/O 和进程通信 |
| 堆 (Heap) | 动态分配的内存 (malloc/new) |
向上增长,手动管理 |
| 数据段 (Static) | 已初始化的全局变量、静态变量 | 程序启动时分配,结束时释放 |
| 代码段 (Text) | 可执行代码、只读常量(如字符串字面量) | 只读,共享 |
1.栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。
3. 堆用于程序运行时动态内存分配,堆是可以上增长的。
4. 数据段--存储全局数据和静态数据。
5. 代码段--可执行的代码/只读常量。
1.2示例代码与变量位置
cpp
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);
}
cpp
选择题:
选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
globalVar在哪里?____
staticGlobalVar在哪里?____
staticVar在哪里?____
localVar在哪里?____
num1 在哪里?____
char2在哪里?____
*char2在哪里?___
pChar3在哪里?____
*pChar3在哪里?____
ptr1在哪里?____
*ptr1在哪里?____
下面我们来看看这些都选什么呢?
前两个都是全局变量,所以都选C没问题吧,那么第三个是局部静态变量,所以也选C对不对,那么对于localVar,普通局部变量,函数调用时在栈上分配,所以选A
num1,局部数组,整个数组空间都在栈上,所以选A,那么前五个是不是完事了呀,让我们看看后面的。
那么char2应该在哪里呢?局部字符数组,是不是内容从常量区拷贝到栈上啊,所以选A,那么我们对于char2进行解引用,是不是就拿到数组首元素啦,数组在栈上,元素自然在栈上,所以选A,
pchar3是不是为指向数组首元素的地址呀,局部指针变量,存储地址值,所以选A,
对于pchar3解引用,很多人会在这里掉入陷阱,认为这里的*pchar3在栈上,就跟上面对char2进行解引用一样,实则不然,这里其实应该选D,为什么呢?因为这里指针是指向字符串字面量 "abcd",存储在只读常量区,我们的abcd并没有被拷贝到栈上,所以存在代码段,那么下面两个就不用多说了吧,一个A,一个B。
综上所述:
| 变量 | 位置 | 原因 |
|---|---|---|
globalVar |
C. 数据段 | 全局变量,程序启动时分配,程序结束时释放 |
staticGlobalVar |
C. 数据段 | 全局静态变量,链接属性为内部,内存位置同全局变量 |
staticVar |
C. 数据段 | 局部静态变量,生命周期提升为全局,但作用域限于函数内 |
localVar |
A. 栈 | 普通局部变量,函数调用时在栈上分配 |
num1 |
A. 栈 | 局部数组,整个数组空间都在栈上 |
char2 |
A. 栈 | 局部字符数组,内容从常量区拷贝到栈上 |
*char2 |
A. 栈 | 数组首元素,数组在栈上,元素自然在栈上 |
pChar3 |
A. 栈 | 局部指针变量,存储地址值 |
*pChar3 |
D. 代码段 | 指针指向字符串字面量 "abcd",存储在只读常量区 |
ptr1 |
A. 栈 | 局部指针变量 |
*ptr1 |
B. 堆 | malloc 分配的内存在堆上 |
2. C 语言动态内存管理 ------ malloc/calloc/realloc/free
c
malloc/calloc/realloc的区别?
void Test ()
{
// 1.malloc/calloc/realloc的区别是什么?
int* p2 = (int*)calloc(4, sizeof (int));
int* p3 = (int*)realloc(p2, sizeof(int)*10);
// 这里需要free(p2)吗?
free(p3 );
}
c
int* ptr1 = (int*)malloc(sizeof(int) * 4);
- 功能:在堆上分配指定字节数的未初始化内存。
- 参数:需要分配的字节数。
- 返回值:void*,需要强制类型转换。
- 失败:返回 NULL。
c
malloc(16) 后:
堆:┌─────────────────┐
│ ? │ ? │ ? │ ? │ (垃圾值)
└─────────────────┘
↑
ptr1
c
int* ptr2 = (int*)calloc(4, sizeof(int));
- 功能:分配 num * size 字节的内存,并初始化为 0。
- 参数:元素个数,每个元素大小。
- 优点:自动清零,适合数组。
c
calloc(4, sizeof(int)) 后:
堆:┌─────────────────┐
│ 0 │ 0 │ 0 │ 0 │
└─────────────────┘
↑
ptr2
c
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
-
功能:调整已分配内存块的大小。
-
行为:
如果原内存后有足够空间,直接扩展并返回原地址。
否则,另找一块足够大的内存,将原数据拷贝过去,释放原内存,返回新地址。
-
注意:ptr3 可能等于 ptr2,也可能不同。如果 realloc 失败,返回 NULL,但原内存不会被释放。
c
free(ptr1);
free(ptr3); // 注意:原代码中 ptr2 被 realloc 后不应再 free(ptr2),因为 realloc 可能已释放
- 功能:释放 malloc/calloc/realloc 分配的内存。
- 释放后:指针变成悬垂指针,建议置为 NULL。
3. C++内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
3.1 为什么需要 new/delete?
malloc/free 只能分配内存,不能构造对象。对于类类型,这远远不够:
cpp
class A {
public:
A() { cout << "构造" << endl; } // malloc 不会调用这个!
~A() { cout << "析构" << endl; } // free 不会调用这个!
};
3.2 内置类型的 new/delete
cpp
void Test()
{
// 动态申请一个int类型的空间
int* ptr4 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
// 动态申请10个int类型的空间
int* ptr6 = new int[3];
delete ptr4;
delete ptr5;
delete[] ptr6;
} // 注意:数组释放必须用 delete[]
3.3 自定义类型的 new/delete
cpp
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
}
private:
int _a;
};
int main()
{
// new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间还会调用构造函数和析构函数
A* p1 = (A*)malloc(sizeof(A));
A* p2 = new A(1);
free(p1);
delete p2;
// 内置类型是几乎是一样的
int* p3 = (int*)malloc(sizeof(int)); // C
int* p4 = new int;
free(p3);
delete p4;
A* p5 = (A*)malloc(sizeof(A)*10);
A* p6 = new A[10];
free(p5);
delete[] p6;
return 0;
}
cpp
A* p1 = (A*)malloc(sizeof(A)); // 只分配内存,不调用构造
A* p2 = new A(1); // 分配内存 + 调用构造函数
free(p1); // 只释放内存,不调用析构
delete p2; // 调用析构函数 + 释放内存
关键区别:new 将内存分配和对象构造一体化,delete 将对象析构和内存释放一体化。
cpp
new A(1) 的执行流程:
│
├─ 1. 调用 operator new(size) 申请内存
│ └─ 内部调用 malloc
│
├─ 2. 在申请的内存上调用构造函数 A::A(1)
│ └─ 对象初始化完成
│
└─ 3. 返回指向该对象的指针
delete p2 的执行流程:
│
├─ 1. 调用析构函数 p2->~A()
│ └─ 对象清理资源
│
├─ 2. 调用 operator delete(p2) 释放内存
│ └─ 内部调用 free
│
└─ 完成
4. operator new 与 operator delete 函数(底层机制)
4.1 它们是函数,不是运算符重载
虽然名字里有 operator,但它们是全局函数,可以被用户替换(重载)。
cpp
void* operator new(size_t size) {
void* p = malloc(size);
if (p == nullptr) throw std::bad_alloc(); // 失败抛异常,不返回 NULL
return p;
}
void operator delete(void* p) noexcept {
free(p);
}
new 表达式与 operator new 的关系:
- new 是关键字,编译时会被展开为:先调用 operator new,再调用构造函数。
- operator new 只负责分配内存。
为什么 new 失败抛异常而 malloc 返回 NULL?
这是 C++ 设计哲学:用异常处理错误,而不是检查返回值。这样代码更简洁,但需要配合 try-catch。
cpp
try {
int* p = new int[1000000000000];
} catch (const std::bad_alloc& e) {
std::cerr << "内存不足!" << std::endl;
}
5. new 和 delete 的实现原理
对于 int、char 等,new 和 malloc 行为几乎一致(只是语法不同),因为不需要构造/析构。
new T 的原理:
- 调用 operator new(sizeof(T)) 分配内存。
- 在分配的内存上调用 T 的构造函数。
delete p 的原理:
- 调用 p->~T() 析构对象。
- 调用 operator delete§ 释放内存。
new T[N] 的原理:
- 调用 operator new[](N * sizeof(T) + 额外空间)。额外空间用于存储对象个数(以便 delete[]知道要调用多少次析构)。
- 循环调用构造函数 N 次。
delete[] p 的原理:
- 从额外存储中读取对象个数 N。
- 逆序调用析构函数 N 次。
- 调用 operator delete[] 释放内存。
cpp
new A[3] 分配的内存布局:
┌─────────────────────────────────────────────┐
│ 对象个数 (size_t) │ A[0] │ A[1] │ A[2] │ │
└─────────────────────────────────────────────┘
↑
└── delete[] 通过这里知道要析构 3 次
返回给用户的指针指向 A[0] 的位置
6. malloc/free和new/delete的区别
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new可以初始化
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可, 如果是多个对象,[]中指定对象个数即可
- malloc的返回值为
void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型 - malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
- 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理释放
cpp
内存分区:栈(局部)、堆(动态)、数据段(全局/静态)、代码段(常量)
│
├─ C 语言动态内存:malloc/calloc/realloc/free
│ ├─ malloc:分配不初始化
│ ├─ calloc:分配并清零
│ └─ realloc:调整大小
│
└─ C++ 动态内存:new/delete
├─ new:operator new(分配内存) + 构造函数
├─ delete:析构函数 + operator delete(释放内存)
├─ new[]/delete[]:处理数组,额外存储个数
└─ placement new:在已有内存上构造对象