😏https://share.note.youdao.com/s/2Tnw5HvW😏【笔记链接】
在 C/C++ 编程中,内存管理是绕不开的核心话题,它直接影响程序的性能、稳定性甚至安全性。很多开发者在初学时容易混淆不同变量的存储位置,也对malloc/freenew/delete的区别一知半解。本文将从内存区域划分、变量存储位置入手,逐步深入new/delete的底层实现,帮你彻底理清 C/C++ 内存管理的脉络。

一、先搞懂:内存区域到底怎么划分?

在程序运行时,内存会被划分为不同区域,每个区域承担不同职责。理解这些区域,是搞懂变量存储和内存分配的基础。以常见的 32 位系统为例,内存布局从高地址到低地址依次为:

内存区域 核心作用 关键特点
内核空间 操作系统内核使用 用户代码无法读写,属于系统级区域
栈区 存储局部变量、函数参数 自动管理(函数进栈分配、函数返回释放),向下增长
内存映射段 加载动态库、文件映射 用于实现进程间通信、动态链接等
堆区 动态内存分配 手动管理(需开发者申请 / 释放),向上增长
静态区(数据段) 存储全局变量、静态变量 程序启动时分配,程序结束后释放
常量区(代码段) 存储可执行代码、字符串常量 只读属性,尝试修改会导致运行时错误

二、实战辨析:变量都存在哪里?

光了解区域划分还不够,关键要能判断具体变量的存储位置。我们结合一段经典代码,逐个分析变量的存储区域(选项:A. 栈 B. 堆 C. 数据段 (静态区) D. 代码段 (常量区))。

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); // 动态内存指针
    free(ptr1);
}

关键变量存储位置解析

  1. globalVar(全局变量):C 选项(数据段)全局变量不属于任何函数,程序启动时在数据段分配空间,程序结束后释放。
  2. staticGlobalVar(静态全局变量):C 选项(数据段)静态变量(无论全局还是局部)均存储在数据段,区别仅在于作用域(静态全局变量作用域为整个文件)。
  3. staticVar(静态局部变量):C 选项(数据段)虽然定义在函数内,但static修饰使其脱离栈区,存储在数据段,生命周期与程序一致。
  4. localVar(局部变量):A 选项(栈)普通局部变量随函数调用进栈,函数返回时栈帧销毁,变量自动释放。
  5. num1(局部数组):A 选项(栈)数组是 “内存块实体”,局部数组的整个内存块都在栈区分配。
  6. char2(字符数组):A 选项(栈)char2[] = "abcd"是在栈区分配数组空间,并将 “abcd” 从常量区拷贝到栈区数组中。
  7. *char2(数组内容):A 选项(栈)char2是数组名,解引用后指向栈区数组的内容。
  8. pChar3(指针变量):A 选项(栈)指针变量本身是局部变量,存储在栈区,其值是 “abcd” 的地址。
  9. *pChar3(指针指向内容):D 选项(代码段)“abcd” 是字符串常量,存储在只读的常量区,指针pChar3仅指向这个常量的地址。
  10. ptr1(动态内存指针):A 选项(栈)指针变量ptr1在栈区,存储的是malloc分配的堆内存地址。
  11. *ptr1(动态内存内容):B 选项(堆)malloc从堆区申请空间,解引用ptr1访问的是堆区的内存。

三、易混点:数组名 vs 指针,到底有啥区别?

很多人会把数组名和指针画等号,但实际上二者本质不同。我们通过代码和sizeof验证,彻底分清二者差异:

1. 本质区别

类型 本质 存储位置 核心特点
数组名(如int num1[10] 内存块标识符 栈区(局部数组)/ 数据段(全局数组) 代表整个数组,sizeof(num1)返回数组总字节数(如 10 个 int 为 40 字节)
指针变量(如int* ptr1 存储地址的变量 栈区 独立变量,仅存储地址,sizeof(ptr1)返回指针大小(32 位系统 4 字节,64 位系统 8 字节)

2. 代码验证

int num1[10];
int* ptr1 = (int*)malloc(sizeof(int)*10);
printf("数组大小:%zu\n", sizeof(num1));  // 输出40(10*4)
printf("指针大小:%zu\n", sizeof(ptr1));  // 输出4(32位)或8(64位)

3. 关键结论

  • 数组名在表达式中会退化为首元素地址(如num1 == &num1[0]),但它本身不是指针。
  • 堆内存的访问必须通过指针:堆内存没有绑定 “标识符”,指针是连接开发者与堆内存的唯一桥梁,丢失指针会导致堆内存成为 “幽灵内存”(内存泄漏)。

四、进阶:const 与指针的三种组合

const修饰指针时,容易混淆 “指针本身不可变” 和 “指针指向内容不可变”。记住一个原则:每加一个const,就限制一个 “可变项”,共三种组合:

1. 指向常量的指针(Pointer to Constant)

  • 语法:const T* ptr 或 T const* ptr(二者等价)
  • 特点:指针指向的内容不可变,但指针本身可以指向其他地址。
  • 代码示例:
    const int* p;
    int a=10, b=20;
    p = &a;    // ✅ 合法:指针可重定向
    *p = 30;   // ❌ 非法:不能修改指向的值
    

2. 常量指针(Constant Pointer)

  • 语法:T* const ptr
  • 特点:指针本身的地址不可变,但可修改指向的值。
  • 代码示例:
    int* const p = &a;  // 指针指向固定
    *p = 30;            // ✅ 合法:可修改指向的值
    p = &b;             // ❌ 非法:不能修改指针地址
    

3. 指向常量的常量指针

  • 语法:const T* const ptr
  • 特点:指针地址和指向的值均不可变
  • 代码示例:
    const int* const p = &a;
    *p = 30;  // ❌ 非法:值不可变
    p = &b;   // ❌ 非法:指针地址不可变
    

五、字符串两种创建方式:常量 vs 数组

字符串的存储是内存管理的高频考点,两种创建方式的差异直接影响 “是否可修改” 和 “存储区域”:

特性维度 字符串常量(如char* p = "abc" 字符数组(如char arr[] = "abc"
存储区域 常量区(只读) 栈区(局部)/ 数据段(全局 / 静态)
内容可修改? ❌ 不可修改(修改会导致运行时错误) ✅ 可修改(如arr[0] = 'x'
生命周期 整个程序运行期 取决于作用域(函数结束 / 程序结束)
常见用途 存储不变的提示信息、配置字 存储需处理、拼接的字符串数据

六、重点:strlensizeof的区别

strlensizeof都能计算 “长度”,但适用场景完全不同,误用会导致严重 bug:

1. 核心差异

  • sizeof:运算符,计算变量 / 类型占用的内存字节数,编译时确定结果。
  • strlen:函数,计算字符串的有效长度(从起始地址到\0前的字符数),运行时确定结果。

2. 代码示例(基于前文 Test 函数)

int num1[10] = {1,2,3,4};    // 数组
char char2[] = "abcd";       // 字符数组
char* pChar3 = "abcd";       // 字符串常量指针
int* ptr1 = (int*)malloc(sizeof(int)*4); // 堆指针

sizeof(num1) = 40;    // 10个int,10*4=40字节
sizeof(char2) = 5;    // 包含'\0',"abcd"+'\0'共5字节
strlen(char2) = 4;    // 不包含'\0',有效字符4个
sizeof(pChar3) = 4;   // 指针大小(32位系统)
strlen(pChar3) = 4;   // 字符串有效长度
sizeof(ptr1) = 4;     // 指针大小(32位系统)

七、底层原理:new/deletemalloc/free的区别

malloc/free是 C 标准库函数,new/delete是 C++ 操作符,二者都是从堆区申请内存,但new/deletemalloc/free的 “升级版”,解决了很多痛点。

1. 核心区别对比

对比维度 malloc/free new/delete
本质 函数 操作符
初始化 不支持(申请的空间是 “脏内存”) 支持(内置类型可直接初始化,自定义类型调用构造函数)
空间大小计算 需手动计算(如malloc(sizeof(int)*10) 自动计算(如new int[10],无需写sizeof
返回值类型 void*,需强转 对应类型指针,无需强转
失败处理 返回NULL,需手动判空 bad_alloc异常,需捕获处理
自定义类型支持 仅开辟 / 释放空间,不调用构造 / 析构函数 开辟空间后调用构造函数,释放前调用析构函数

2. new/delete的底层实现

newdelete本质是通过operator newoperator delete函数实现的:

  • operator new:内部调用malloc申请空间,申请失败时尝试执行用户自定义的 “空间不足应对措施”,若没有则抛异常。
  • operator delete:内部调用free释放空间,释放前会先调用对象的析构函数(自定义类型)。
自定义类型的new/delete流程
  • new A(单个对象)

    1. 调用operator new(sizeof(A))申请空间;
    2. 在申请的空间上调用A的构造函数,初始化对象。
  • delete p(单个对象)

    1. 调用p指向对象的析构函数,清理资源;
    2. 调用operator delete(p)释放空间。
  • new A[5](对象数组)

    1. 调用operator new[](内部调用operator new)申请空间,额外多开辟 4 字节存储对象个数
    2. 调用 5 次A的构造函数,初始化每个对象。
  • delete[] p(对象数组)

    1. 根据多开辟的 4 字节确定对象个数,调用 5 次析构函数;
    2. 调用operator delete[](内部调用operator delete)释放空间。

八、避坑指南:这些错误千万别犯!

内存管理的坑很多,尤其是new[]delete的搭配错误,会导致内存泄漏或程序崩溃。

1. 错误 1:用delete释放new[]的空间

A* p = new A[10];  // new[]申请对象数组
delete p;          // ❌ 错误:应使用delete[]
  • 后果:delete不会读取 “多开辟的 4 字节”,仅调用 1 次析构函数,导致 9 个对象的资源未清理(内存泄漏),且释放空间时少释放 4 字节。

2. 错误 2:用realloc修改new申请的空间

new开辟的空间不支持reallocrealloc是 C 标准库函数,仅与malloc搭配使用。

3. 错误 3:频繁申请 / 释放堆内存

频繁的堆内存分配会导致内存碎片(堆区被分割成多个小的空闲块,无法满足大内存申请),而栈区由于自动管理,不存在此问题。

九、高级用法:定位 new(Placement new)

定位 new 是 C++ 的特殊语法,用于在已分配的原始内存上调用构造函数初始化对象,主要配合内存池使用(内存池分配的内存未初始化,需手动调用构造函数)。

1. 语法格式

new (place_address) type;          // 无参构造
new (place_address) type(init_val); // 带参构造
  • place_address:已分配的原始内存地址(指针)。

2. 代码示例(内存池场景)

int main() {
    // 步骤1:从内存池分配原始内存(仅分配,不构造)
    A* p1 = (A*)operator new(sizeof(A));
    
    // 步骤2:定位new调用构造函数,初始化对象
    new(p1) A(10);  // 在p1指向的内存上构造A对象
    
    // 步骤3:使用对象...
    // p1->doSomething();
    
    // 步骤4:显式调用析构函数,清理资源(内存池不释放内存)
    p1->~A();
    
    // 步骤5:释放原始内存(返还给内存池)
    operator delete(p1);
    
    return 0;
}

十、总结

C/C++ 内存管理看似复杂,但只要理清 “内存区域→变量存储→动态分配→底层原理” 的逻辑,就能逐步掌握:

  1. 变量存储位置:全局 / 静态变量在数据段,局部变量 / 数组在栈区,动态内存在堆区,字符串常量在常量区。
  2. 数组名≠指针:数组名是内存块标识符,指针是存储地址的变量,sizeof结果差异是关键区分点。
  3. new/deletemalloc/free的升级版:支持初始化、自动调用构造 / 析构函数,失败抛异常。
  4. 避坑核心:new[]必须搭配delete[],定位 new 用于内存池,避免频繁分配堆内存。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐