前言:
在C / C++ 中,内存主要分为五个区域:栈(Stack)、堆(Heap)、全局/静态存储区、常量存储区和代码区,我们通常讨论的"管理",主要集中在栈和堆。

一、内存分布
1.1 内存区域分布
在C 和 C++ 程序中,内存被划分为几个关键区域,每个区域都有其特定的用途和管理方式

详情说明:
1. 栈:用于存储非静态局部变量、函数参数和返回值等数据,其内存空间采用向下增长的方式分配。
2. 内存映射段:是高效的I/O映射方式,用于装载一个共享的动态内存库,用户可使用系统接口创建共享共享内存,做进程间通信。
3. 堆:用于程序运行时动态内存分配,其内存空间采用向上增长的方式分配。
4. 数据段: 用于存放全局变量和静态变量。
5. 代码段:用于存储可执行的代码/只读常量。
1.2 实战演示
cpp
#include <iostream>
#include <cstdlib>
using namespace std;
int globalVar = 1; // 全局变量
static int staticGlobalVar = 1; // 静态全局变量
void Test()
{
static int staticVar = 1; // 静态局部变量
int localVar = 1; // 局部变量
int num1[10] = {1, 2, 3, 4}; // 局部数组
}
题组一:
选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
① globalVar在哪里?____
② staticGlobalVar在哪里?____
③ staticVar在哪里?____
④ localVar在哪里?____
⑤ num1 在哪里?____
解答:
① globalVar 为全局变量,存储在数据段,故而选择C。
② staticGlobalVar 为静态全局变量,存储在数据段,故而选择C。
③ staticVar 为静态局部变量,存储在数据段,故而选择C。
④ localVar 为局部变量,存储在栈,故而选择A
⑤ num1 为数组名,num1 是一个局部数组,它的生命周期随函数的调用而开始,随函数的结束而销毁,其内存空间完全位于函数的栈帧中,故而选择A
cpp
#include <iostream>
#include <cstdlib>
using namespace std;
void Test()
{
char char2[] = "abcd"; // 局部字符数组
const char* pChar3 = "abcd"; // 指向常量字符串的指针
int* ptr1 = (int*)malloc(sizeof(int) * 4); // 动态分配
int* ptr2 = (int*)calloc(4, sizeof(int)); // 动态分配并初始化为0
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4); // 重新分配
free(ptr1);
free(ptr3);
}
题组二:
选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
① char2在哪里?____
② *char2在哪里?___
③ pChar3在哪里?____
④ *pChar3在哪里?____
⑤ ptr1在哪里?____
⑥ *ptr1在哪里?____
解答:
① char2 : 为局部数组,数组本身在栈上,字符串内容被拷贝到了栈中,故而选择A。
② *char2 : char2为数组名,在非sizeof(char2) 和 &char2时,指向数组的首元素,所以解引用指向的是数组的首元素,因为数组在栈上,所以元素也在栈上, 故而选择A。
③ pChar3:指向常量字符串的指针,局部指针变量本身,存放在栈上,故而选择A。
注意:const放在指针名左边,指的是不能修改指针所指向的内容。
④ *pChar3: pChar3存储的是 "abcd" 位于常量区首个字符的地址,当我们对它解引用时,访问的就是它指向的那个数据,也就是位于代码段 (常量区) 的字符 'a',故而选择D。
⑤ ptr1: 局部指针变量本身,存放在栈上,故而选择A。
⑥ *ptr1: ptr指向的是malloc在堆上分配的内存空间的地址,所以*ptr1解引用得到是**
malloc分配的内存上存储的元素,其存储在堆上,故而选择B。**
二、C++ 内存管理方式
2.1 简单回顾C语言内存管理
C语言动态内存分配,它的核心价值在于:打破了"数组大小必须在写代码时确定"的限制,允许程序在运行时按需申请和释放内存。
三种函数的区别:
| 函数 | 初始化 | 参数形式 | 使用场景 |
|---|---|---|---|
malloc |
不初始化 | malloc(size) |
普通内存分配 |
calloc |
初始化为0 | calloc(count, size) |
需要零初始化的数组 |
realloc |
保持原数据 | realloc(ptr, new_size) |
调整已分配内存大小 |
代码示例:
cpp
#include <iostream>
#include <cstdlib>
using namespace std;
void TestCMemory()
{
// malloc - 分配指定字节数的内存,不进行初始化
int* p1 = (int*)malloc(sizeof(int) * 4);
// calloc - 分配并初始化为0
int* p2 = (int*)calloc(4, sizeof(int)); // 分配4个int,初始化为0
// realloc - 重新分配内存大小
int* p3 = (int*)realloc(p2, sizeof(int) * 10);
// 注意:p2已被realloc处理,不需要再free
free(p1);
free(p3); // 释放重新分配后的内存
}
2.2 C++的新内存管理方式
C语言内存管理方式在C++中可以继续使用,因为C++兼容C语言,但是有些地方就无能为力了,而且使用起来比较麻烦。
因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
2.2.1 new 和 delete 的基本语法
在 C++ 中,new 和 delete 是运算符,而不是函数,它们不仅负责分配内存,更重要的是负责对象的生命周期管理(构造与析构)。
基本语法如下代码所示:
1. 单个对象的分配与释放
cpp
// 1.在堆上申请一个int 大小的空间
int *p = new int; // 分配空间,值未初始化(可能是垃圾值)
int *q = new int(10); // 分配空间,并初始化为 10
// 2. 使用
*p = 5;
// 3. 释放
delete p;
delete q;
2. 数组的分配与释放 (注意方括号 [])
cpp
//1. 在堆上申请10个整形的空间
int* ptr1 = new int[10];
//2.申请多个对象+初始化
int* ptr2 = new int[10] {0};
//3.申请多个对象+部分初始化,剩余未指定的部分默认初始化未0。
int* ptr3 = new int[10] {1, 2, 3, 4, 5};
//释放多个整形的空间
delete[] ptr1;
delete[] ptr2;
delete[] ptr3;
2.2.2 核心区别:new vs malloc
这是面试和实际开发中最常问的问题,为什么 C++ 要发明 new,而不使用 malloc?
最根本的原因是:类 (Class) 和对象 (Object)。
1. 在C++中,对于自定义类型使用 关键字new 和 delete ,会自动调用默认构造函数和析构函数。
2. 在C语言中,对于malloc 、calloc 和 free 函数,不会自动调用构造函数和析构函数。
代码示例:使用new时构造函数的自动调用,使用delete时析构函数的自动调用。
cpp
class Student
{
public:
Student() { cout << "构造函数:学生出生了!" << endl; }
~Student() { cout << "析构函数:学生毕业了(销毁)!" << endl; }
};
int main()
{
cout << "--- 使用 malloc ---" << endl;
// malloc 只分配了内存,是一块"死"的内存,没有任何初始化
Student* s1 = (Student*)malloc(sizeof(Student));
// 仅仅归还内存,没有做任何清理工作
free(s1);
cout << "\n--- 使用 new ---" << endl;
// new 分配内存 + 自动调用构造函数
Student* s2 = new Student();
// delete 自动调用析构函数 + 释放内存
delete s2;
return 0;
}
打印结果如下:

2.3 深入解析new和delete的重要性
为什么使用new和delete申请动态内存或释放资源时,调用构造函数和析构函数重要的原因?
简单来说:malloc 和 free 只管内存字节,而构造和析构函数管的是对象的状态和资源。
2.3.1. 核心层面:维护"不变量"
在 C 语言的 struct 中,数据是公开的,你可以随便改。但在 C++ 的 class 中,我们强调"封装",一个对象必须处于"合法状态"才能使用。
场景: 假设你有一个 BankAccount(银行账户)类。
如果不调构造函数 (使用malloc): 内存里的值是随机的垃圾值。余额可能是 -842150451,账号可能是乱码,如果不小心用了这个对象,系统直接逻辑错误。
如果调构造函数 (使用new): 构造函数会把余额初始化为 0.0,这保证了你拿到对象的那一刻,它就是可用的、安全的。
2.3.2. 资源层面:资源获取即初始化
这是 C++ 最核心的设计哲学,对象通常不仅仅包含 int 或 float,它们往往持有外部资源,如果不调用构造函数和析构函数,这些外部资源将永远无法初始化和释放!
假设你有一个类 Student,它的名字是动态分配的:
cpp
class Student
{
char* name; // 指针,指向另一块堆内存
public:
Student(const char* n)
{
// 构造函数:向系统申请 100 字节存名字
name = new char[100];
strcpy(name, n);
}
~Student()
{
// 析构函数:归还那 100 字节
delete[] name;
}
};
现在我们看看 new/delete 和 malloc/free 的区别:
A. 正确做法 (new + delete):
Student* s = new Student("Tom");
delete s;
①分配 Student 对象本身的内存(比如 8 字节存指针)。
②自动调用构造函数 -> 分配 name 的 100 字节。
③自动调用析构函数 -> 释放 name 的 100 字节。
④释放 Student 对象本身的内存。
结果:完美,无泄漏。
B. 灾难做法 (malloc + free):
Student* s = (Student*)malloc(sizeof(Student));
free(s);
①分配 Student 对象本身的内存。
②构造函数没执行! s->name 是一个随机的野指针。如果你尝试打印名字,程序崩溃。
③析构函数没执行! 假设你手动修复了 name 指针让程序跑下去了,现在你调用 free。
④它只回收了 Student 对象那 8 字节。
结果 :name 指向的那 100 字节(如果分配了的话)没人管了,变成了孤儿内存(内存泄漏)。
2.4 注意事项
2.4.1 易错点1
如果一个类没有默认构造函数(即必须带参数才能创建对象),你直接写 new ClassName[10],编译器不仅仅是不调用构造函数,而是直接报错(编译失败)。
面对这种情况,正如你所说,我们需要"手动分配空间,再手动创建对象"。
方案一:手动创建多个对象
cpp
class B
{
public:
B(int b1, int b2)
:_b1(b1)
, _b2(b2)
{
cout << "B(int _b1,int _b2)" << endl;
}
// 新增:显式拷贝构造函数
B(const B& other)
: _b1(other._b1)
, _b2(other._b2)
{
cout << "Copy B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
private:
int _b1;
int _b2;
};
int main()
{
//1.方法一:手动创建三个对象
B b1(1, 1);
B b2(2, 2);
B b3(3, 3);
//通过调用拷贝构造函数,将创建的对象拷贝到堆上
B* pb1 = new B[3]{ b1,b2,b3 };
delete[] pb1;
return 0;
}
方案二:使用匿名对象
cpp
class B
{
public:
B(int b1, int b2)
:_b1(b1)
, _b2(b2)
{
cout << "B(int _b1,int _b2)" << endl;
}
// 新增:显式拷贝构造函数
B(const B& other)
: _b1(other._b1)
, _b2(other._b2)
{
cout << "Copy B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
private:
int _b1;
int _b2;
};
int main()
{
//2.方法二:使用匿名对象
// 通过调用拷贝构造函数,将匿名对象拷贝到堆上,但编译器会进行优化到调用直接构造
B* pb2 = new B[3]{ B(1,1),B(2,2),B(3,3) };
delete[] pb2;
return 0;
}
2.4.2 易错点2
如果当动态内存申请失败时,通常需要进行异常处理,但是一般对于日常使用来说,动态内存开辟不会失败。
代码示例:动态内存申请失败,进行捕获异常操作。
cpp
int main()
{
try
{
void* p = new char[0xFFFFFFFFFFFFFFFFULL];
}
catch(const exception &e)
{
cout << e.what()<<endl;
}
return 0;
}
三、new 和 delete 的底层
要深入理解 new 和 delete,你必须把它们拆解开。在编译器眼里,new 和 delete 并不是一个单一的操作,而是一套组合拳。
3.1 了解两个关键函数
operator new 与 operator delete函数
operator new
函数原型:void* operator new(size_t size);
职责:只负责分配字节,不负责初始化(不调构造函数)。
地位:它是 malloc 的 C++ 封装版。
operator delete:
函数原型:void operator delete(void* p);
职责:只负责归还字节,不负责清理(不调析构函数)。
地位:它是 free 的 C++ 封装版。
3.2 new和delete的实现原理
new的原理
1. 调用operator new函数申请空间
2. 在申请的空间上执行构造函数,完成对象的构造
delete的原理
1. 在空间上执行析构函数,完成对象中资源的清理工作
2. 调用operator delete函数释放对象的空间
new T[N]的原理
1. 调用operator new[]函数,在operator new[] 中实际调用operator new函数完成N个对象空间的申请
2. 在申请的空间上执行N次构造函数
delete []的原理
1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释 放空间
核心部分理解:
①new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数。
②new在底层调用operator new全局函数来申请空间,通过构造函数来初始化资源
③delete在底层通过 operator delete全局函数来释放空间,通过析构函数来释放资源。
四、new/delete 和 malloc/free 的区别
malloc/free和new/delete的共同点在于:它们都是从堆内存中动态申请空间,并且都需要用户手动释放内存,二者的主要区别如下:
①语法差异:malloc/free是标准库函数,而new/delete是C++操作符
②初始化处理:malloc不会初始化申请的内存空间,而new可以自动完成初始化
③空间计算:使用malloc时需要手动计算并传入所需空间大小,而new只需指定类型即可;对于数组对象,new只需在[]中指明元素数量
④返回值类型:malloc返回void*指针,使用时需要强制类型转换;new直接返回对应类型的指针
⑤错误处理:malloc失败时返回NULL,需要检查返回值;new失败时会抛出异常,需要进行异常捕获
⑥对象处理:对于自定义类型对象,malloc/free仅分配/释放内存,不调用构造/析构函数;new会调用构造函数初始化对象,delete会先调用析构函数再释放内存
既然看到这里了,不妨关注+点赞+收藏,感谢大家,若有问题请指正。

