本文章1.3w字警告,需耐心阅读。
目录
[1. malloc:最基础的内存申请函数](#1. malloc:最基础的内存申请函数)
[2. calloc:带初始化的内存申请函数](#2. calloc:带初始化的内存申请函数)
[3. realloc:动态调整内存大小的函数](#3. realloc:动态调整内存大小的函数)
[4. free:动态内存的释放函数](#4. free:动态内存的释放函数)
[new/delete 操作内置类型(基础用法)](#new/delete 操作内置类型(基础用法))
[1. 动态申请单个内置类型空间](#1. 动态申请单个内置类型空间)
[2. 动态申请多个内置类型空间(数组)](#2. 动态申请多个内置类型空间(数组))
[3. 申请空间并初始化(C++ 专属便捷性)](#3. 申请空间并初始化(C++ 专属便捷性))
[1. 动态申请单个内置类型空间](#1. 动态申请单个内置类型空间)
[2. malloc/free:仅分配内存,无构造 / 析构](#2. malloc/free:仅分配内存,无构造 / 析构)
[new []/delete []:批量构造 + 批量析构](#new []/delete []:批量构造 + 批量析构)
[malloc/free:仅分配连续内存,无任何构造 / 析构](#malloc/free:仅分配连续内存,无任何构造 / 析构)
[new/delete 与 malloc/free 的核心差异](#new/delete 与 malloc/free 的核心差异)
[定位 new(placement new)](#定位 new(placement new))
[Test* p1 = new Test;和Test* p1 = new Test()有什么区别?](#Test* p1 = new Test;和Test* p1 = new Test()有什么区别?)
[核心差异:new Test是「默认初始化」(无构造时成员为垃圾值),new Test()是「值初始化」(无构造时成员置 0,有构造时调用构造);](#核心差异:new Test是「默认初始化」(无构造时成员为垃圾值),new Test()是「值初始化」(无构造时成员置 0,有构造时调用构造);)
为什么用new[]创建的数组,释放内存时必须使用delete[]?
[1. 最直接:析构函数调用不完整(针对有析构函数的类型)](#1. 最直接:析构函数调用不完整(针对有析构函数的类型))
[2. 隐性风险:内存释放异常(可能崩溃 / 堆损坏)](#2. 隐性风险:内存释放异常(可能崩溃 / 堆损坏))
[3. 极端情况:程序行为完全不可预测](#3. 极端情况:程序行为完全不可预测)
[用new[]创建数组时,编译器会在数组内存的 "头部" 额外存储一个元素个数的计数,你怎么知道的?用vs2026可以查看吗?](#用new[]创建数组时,编译器会在数组内存的 “头部” 额外存储一个元素个数的计数,你怎么知道的?用vs2026可以查看吗?)
[步骤 1:编写测试代码](#步骤 1:编写测试代码)
[步骤 2:VS2026 调试配置](#步骤 2:VS2026 调试配置)
[步骤 3:查看内存中的计数](#步骤 3:查看内存中的计数)
先看代码
试着想想变量以及所指向的内容在虚拟程序空间的哪个部分
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";
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);
}
内存分布如下

- 栈又叫堆栈,用于存储非静态局部变量/函数参数/返回值等等,栈是向下增长的。
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。
- 堆用于存储运行时动态内存分配,堆是向上增长的。
- 数据段又叫静态区,用于存储全局数据和静态数据。
- 代码段又叫常量区,用于存放可执行的代码和只读常量。
为什么说栈是向下增长的,而堆是向上增长的?
从上面的图片可以发现,栈是向下增长,堆是向上增长,在一般情况下,在栈区开辟空间,先开辟的空间地址较高,而在堆区开辟空间,先开辟的空间地址较低。

例如,下面代码中,变量a和变量b存储在栈区,指针c和指针d指向堆区的内存空间:
cpp
#include <iostream>
using namespace std;
int main()
{
//栈区开辟空间,先开辟的空间地址高
int a = 10;
int b = 20;
cout << &a << endl;
cout << &b << endl;
//堆区开辟空间,先开辟的空间地址低
int* c = (int*)malloc(sizeof(int)* 10);
int* d = (int*)malloc(sizeof(int)* 10);
cout << c << endl;
cout << d << endl;
return 0;
}
栈区开辟空间,先开辟的空间地址较高,所以打印出来a的地址大于b的地址;在堆区开辟空间,先开辟的空间地址较低,所以c指向的空间地址小于d指向的空间地址。
不过需要注意的是,内存分配器(如 glibc 的 ptmalloc)会优先复用已释放的「内存空洞」,而非直接扩展堆顶,因此堆区开辟空间,后开辟的空间地址不一定比先开辟的空间地址高。
C语言动态内存的管理
1. malloc:最基础的内存申请函数
函数原型
cpp
void* malloc(size_t size);
核心功能
开辟指定字节数 的堆内存空间,成功返回内存首地址,失败返回NULL;开辟的内存未初始化,内容为随机的 "脏数据"。
关键说明
- 传参规则 :仅需传入 "需要开辟的字节个数"(如
malloc(4)表示开辟 4 字节空间); - 返回值 :
void*类型,需根据存储的数据类型强制类型转换; - 失败场景 :堆区剩余空间不足、申请的字节数过大(如
malloc(1024*1024*1024*2))。
示例代码
cpp
#include <stdio.h>
#include <stdlib.h> // 包含malloc/free声明
int main() {
// 申请能存放1个int的内存(int占4字节)
int* p = (int*)malloc(sizeof(int));
if (p == NULL) { // 必须检查是否申请成功
perror("malloc failed");
return 1;
}
*p = 10; // 未初始化的内存,赋值前是随机值
printf("*p = %d\n", *p); // 输出:10
free(p); // 释放内存
p = NULL; // 避免野指针
return 0;
}
2. calloc:带初始化的内存申请函数
函数原型
cpp
void* calloc(size_t num, size_t size);
核心功能
开辟能存放num个 "大小为size字节" 的元素的内存空间,成功返回首地址,失败返回NULL;关键区别:开辟后会将内存中每个字节初始化为 0。
关键说明
- 传参规则 :第一个参数是 "元素个数",第二个参数是 "单个元素的字节大小"(如
calloc(5, sizeof(int))表示开辟能存 5 个 int 的空间); - 适用场景:需要内存初始化为 0 的场景(如数组、结构体),避免脏数据导致的逻辑错误;
- 性能提示 :因多了初始化步骤,效率略低于
malloc,无需初始化时优先用malloc。
示例代码
cpp
#include <stdio.h>
#include <stdlib.h>
int main() {
// 申请能存放5个int的内存,并初始化为0
int* arr = (int*)calloc(5, sizeof(int));
if (arr == NULL) {
perror("calloc failed");
return 1;
}
// 未赋值的情况下,所有元素都是0
for (int i = 0; i < 5; i++) {
printf("arr[%d] = %d ", i, arr[i]); // 输出:0 0 0 0 0
}
free(arr);
arr = NULL;
return 0;
}
3. realloc:动态调整内存大小的函数
函数原型
cpp
void* realloc(void* ptr, size_t new_size);
核心功能
调整已开辟的动态内存大小(可扩容、可缩容),第一个参数是原内存首地址,第二个参数是调整后的新大小(字节数);成功返回新内存首地址,失败返回NULL(原内存不会被释放)。
内存调整的三种场景
realloc 的底层逻辑决定了其返回地址的三种情况,也是面试高频考点:
| 场景 | 条件 | 返回值 | 原内存处理 |
|---|---|---|---|
| 原地扩 | 原内存后方有足够连续空间 | 原内存首地址 | 直接在原空间后扩展,无需拷贝数据 |
| 异地扩 | 原内存后方无足够空间 | 新的内存首地址 | 拷贝原数据到新空间,自动释放原内存 |
| 扩充失败 | 堆区无满足新大小的连续空间 | NULL | 原内存保留,不会被释放 |
关键注意事项
- 禁止直接覆盖原指针 :若
realloc返回NULL,原指针会被覆盖为NULL,导致原内存无法释放(内存泄漏); - 缩容逻辑:新大小小于原大小时,仅调整内存管理标记,不会主动清空超出新大小的部分(数据可能残留)。
cpp
#include <stdio.h>
#include <stdlib.h>
int main() {
// 初始申请4字节(1个int)
int* p = (int*)malloc(sizeof(int));
if (p == NULL) {
perror("malloc failed");
return 1;
}
*p = 10;
// 扩容为能存5个int的空间(20字节)
int* new_p = (int*)realloc(p, 5 * sizeof(int));
if (new_p == NULL) { // 扩容失败,原p仍有效
perror("realloc failed");
free(p); // 释放原内存
p = NULL;
return 1;
}
p = new_p; // 扩容成功,更新指针
// 赋值并验证扩容后的数据
for (int i = 0; i < 5; i++) {
p[i] = i + 1;
printf("p[%d] = %d ", i, p[i]); // 输出:1 2 3 4 5
}
free(p);
p = NULL;
return 0;
}
4. free:动态内存的释放函数
函数原型
cpp
void free(void* ptr);
核心功能
释放malloc/calloc/realloc申请的堆内存,将内存归还给操作系统,供后续内存申请复用。
关键规则(避坑重点)
- 释放范围:仅释放指针指向的内存空间,不会修改指针本身的值(指针仍指向原地址,成为 "野指针");
- 空指针安全 :
free(NULL)不会报错(可放心调用); - 禁止重复释放 :对同一块内存多次
free会触发未定义行为(程序崩溃、内存错乱); - 禁止释放非动态内存 :如栈区变量(
int a; free(&a);)、已释放的内存、NULL 以外的无效指针。
cpp
// 错误1:重复释放
int* p = (int*)malloc(sizeof(int));
free(p);
free(p); // 错误:重复释放,程序崩溃
// 错误2:释放栈内存
int a = 10;
free(&a); // 错误:栈内存无需手动释放
// 正确写法
int* p = (int*)malloc(sizeof(int));
if (p != NULL) {
free(p);
p = NULL; // 置空,避免野指针
}
四大函数核心对比表
| 函数 | 核心特点 | 初始化 | 传参方式 | 适用场景 |
|---|---|---|---|---|
| malloc | 基础内存申请 | 否 | 仅传入总字节数 | 无需初始化的内存分配 |
| calloc | 带初始化的内存申请 | 是(0) | 元素个数 + 单个元素大小 | 需要初始化为 0 的数组 / 结构体 |
| realloc | 调整已分配内存的大小 | 否 | 原指针 + 新大小(字节) | 动态扩容 / 缩容(如动态数组) |
| free | 释放动态内存 | - | 待释放的内存首地址 | 所有动态内存使用完毕后释放 |
C++动态内存的管理
首先,new/delete是操作符,不是函数。
C 语言的malloc/calloc/realloc/free虽能在 C++ 中继续使用,但面对类对象、异常安全、初始化等场景时显得笨拙。C++ 因此引入new/delete操作符,不仅兼容原生内存管理的核心逻辑,还适配面向对象特性,实现更简洁、更贴合 C++ 语法的动态内存管理。
new/delete 操作内置类型(基础用法)
内置类型(int/char/double等)无构造 / 析构函数,new/delete对其的操作可看作malloc/free的 "语法糖",但支持更便捷的初始化,且无需手动计算字节数、强制类型转换。
1. 动态申请单个内置类型空间
cpp
// C++ 写法(new/delete)
////////////////////////////////////////////////
#include <iostream>
using namespace std;
int main() {
// 申请单个int空间(未初始化)
int* p1 = new int;
*p1 = 20; // 手动赋值
cout << *p1 << endl; // 输出:20
delete p1; // 释放单个空间
p1 = nullptr; // 置空避免野指针
return 0;
}
//////////////////////////////////////////////
// 等价 C 语言写法(malloc/free)
/////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
int main() {
// 需计算字节数 + 强制类型转换
int* p2 = (int*)malloc(sizeof(int));
*p2 = 20;
printf("%d\n", *p2);
free(p2);
p2 = NULL;
return 0;
}
/////////////////////////////////////////////
2. 动态申请多个内置类型空间(数组)
cpp
// C++ 写法(new/delete)
////////////////////////////////////////////////
#include <iostream>
using namespace std;
int main() {
// 申请10个int的连续空间(未初始化)
int* p3 = new int[10];
// 手动赋值
for (int i = 0; i < 10; i++) {
p3[i] = i;
}
delete[] p3; // 释放数组空间,必须加[]
p3 = nullptr;
return 0;
}
//////////////////////////////////////////////
// 等价 C 语言写法(malloc/free)
/////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
int main() {
int* p4 = (int*)malloc(sizeof(int) * 10);
for (int i = 0; i < 10; i++) {
p4[i] = i;
}
free(p4);
p4 = NULL;
return 0;
}
/////////////////////////////////////////////
3. 申请空间并初始化(C++ 专属便捷性)
cpp
// C++ 写法(new/delete)
////////////////////////////////////////////////
#include <iostream>
using namespace std;
int main() {
// 单个元素初始化
/////////////////////////////////////
// 申请int空间并直接初始化为10(无需单独赋值)
int* p5 = new int(10); // 初始化列表的方式为int* p5 = new int{10};
cout << *p5 << endl; // 输出:10
delete p5;
p5 = nullptr;
/////////////////////////////////////
// 数组元素初始化(C++11+)
////////////////////////////////////
// 申请10个int空间,初始化0~9(批量赋值一步到位)
int* p7 = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; //初始化列表
for (int i = 0; i < 10; i++) {
cout << p7[i] << " "; // 输出:0 1 2 3 4 5 6 7 8 9
}
delete[] p7;
p7 = nullptr;
////////////////////////////////////
}
//////////////////////////////////////////////
// 等价 C 语言写法(malloc/free)
/////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
int main() {
int* p8 = (int*)malloc(sizeof(int) * 10);
for (int i = 0; i < 10; i++) {
p8[i] = i;
}
free(p8);
p8 = NULL;
}
/////////////////////////////////////////////
new和delete操作自定义类型
对于自定义类型Test示例
cpp
#include <iostream>
using namespace std;
class Test {
public:
// 构造函数:对象创建时初始化成员,可执行资源申请(如new、open)
Test() : _a(0) {
cout << "Test() 构造函数" << endl;
}
// 析构函数:对象销毁时清理资源(如delete、close)
~Test() {
cout << "~Test() 析构函数" << endl;
}
private:
int _a; // 类成员变量
};
1. 动态申请单个内置类型空间
new/delete:构造+析构自动调用
new不只是 "开空间",而是 "创建一个完整的对象";delete不只是 "释放空间",而是 "销毁对象后再释放空间"。
cpp
int main() {
// new:两步操作 → 1. 分配堆内存 2. 调用Test的构造函数初始化对象
Test* p1 = new Test;
// 控制台输出:Test() 构造函数
// delete:两步操作 → 1. 调用Test的析构函数清理对象 2. 释放堆内存
delete p1;
// 控制台输出:~Test() 析构函数
p1 = nullptr; // 置空避免野指针
return 0;
}
2. malloc/free:仅分配内存,无构造 / 析构
cpp
int main() {
// malloc:仅分配sizeof(Test)字节的裸内存,不调用构造函数
Test* p2 = (Test*)malloc(sizeof(Test));
// 控制台无输出(构造函数未执行)
// free:仅释放内存,不调用析构函数
free(p2);
// 控制台无输出(析构函数未执行)
p2 = NULL;
return 0;
}
致命问题:
p2指向的内存仅为 "裸内存",并非 "合法的 Test 对象",直接访问_a或调用类成员函数会触发未定义行为(崩溃 / 数据错乱)。- 若类中包含动态资源(如
char* _buf = new char[100];),free仅释放 Test 对象的内存,_buf指向的内存会永久泄漏。
这两个致命问题,前者可以用定位new初始化来解决,后者可以手动释放资源或者对operator free进行重载来决绝,但是违背了C++的设计原则,得不偿失。
2.动态申请多个自定义类型对象(数组)
new []/delete []:批量构造 + 批量析构
cpp
int main() {
// new[]:1. 分配10个Test大小的连续内存 2. 调用10次构造函数
Test* p3 = new Test[10];
// 控制台输出10次:Test() 构造函数
// delete[]:1. 调用10次析构函数 2. 释放连续内存
delete[] p3;
// 控制台输出10次:~Test() 析构函数
p3 = nullptr;
return 0;
}
malloc/free:仅分配连续内存,无任何构造 / 析构
cpp
int main() {
// malloc:仅分配10*sizeof(Test)字节的裸内存
Test* p4 = (Test*)malloc(sizeof(Test) * 10);
// 控制台无输出
// free:仅释放内存,无析构调用
free(p4);
// 控制台无输出
p4 = NULL;
return 0;
}
核心规则(必记)
| 场景 | C++ 写法 | 注意事项 |
|---|---|---|
| 单个元素申请 / 释放 | new 类型 / delete 指针 |
无需 [],释放后指针置空 |
| 数组申请 / 释放 | new 类型[个数] / delete[] 指针 |
释放必须加 [],否则内存泄漏 |
new/delete 与 malloc/free 的核心差异
这是面试高频考点,也是理解 C++ 内存管理的关键,差异不仅是 "语法糖",更是面向对象的本质区别:
| 维度 | new/delete(C++) | malloc/free(C) |
|---|---|---|
| 本质 | 操作符(编译器支持,可重载) | 库函数(需包含 <stdlib.h>) |
| 类型检查 | 编译期类型安全(无需强制转换) | 无类型检查(返回 void*,需手动转换) |
| 初始化 | 支持直接初始化(单个 / 数组) | 仅申请空间,初始化需手动赋值 |
| 类对象支持 | 自动调用构造函数(申请)+ 析构函数(释放) | 仅分配内存,无法调用构造 / 析构 |
| 内存失败处理 | 默认抛异常(bad_alloc),可指定 nothrow 返回 NULL | 返回 NULL,需手动检查 |
| 数组处理 | 需配套 new []/delete [],编译器记录数组长度 | 仅分配连续内存,不记录数组长度 |
| 重载 / 定制 | 可重载 operator new/delete,自定义内存分配逻辑 | 无法重载,逻辑固定 |
定位 new(placement new)
这是new的一个特殊用法,仅调用构造函数,不分配内存(用已有的内存),能更直观体现 "分配" 和 "构造" 的分离
- 核心作用是将对象构造与内存分配
解耦,常用于内存池、嵌入式系统、高性能编程等场景。 - 在实际开发中,定位 new 表达式通常会与内存池 配合使用。
- 由于内存池分配的内存并未经过初始化,
- 因此若要在这块内存上创建自定义类型的对象,就需要借助定位 new 表达式来显式调用构造函数,
- 从而完成对象的初始化工作。
定位new的语法 :new (内存地址) 类型(构造参数);
cpp
#include <iostream>
#include <new> // 必须包含此头文件
using namespace std;
class A
{
public:
A(int a = 0) //构造函数
:_a(a)
{}
~A() //析构函数
{}
private:
int _a;
};
int main()
{
// 1. 预分配内存(可通过 malloc、new 或栈内存)
char* buffer = new char[sizeof(A)]; // 堆上分配
// 或:char buffer[sizeof(A)]; // 栈上分配
// 2. 使用定位new在buffer上构造对象
A* p = new (buffer) A(10);
// 3. 使用对象
cout << "p->a: " << p->_a << endl;
// 4. 手动调用析构函数(定位new不会自动调用析构)
p->~A();
// 5. 释放预分配的内存(如果是堆上分配的)
delete[] buffer;
return 0;
}
Test* p1 = new Test;和Test* p1 = new Test()有什么区别?
核心区别:初始化规则不同
这两种写法的本质差异在于是否触发 "值初始化(value-initialization)" ,具体表现会根据Test类的成员类型、是否有自定义构造函数而不同。
如果Test没有自定义构造函数(仅含内置类型成员,如 int、char 等),两种写法的初始化行为完全不同:
new Test:仅分配内存,成员变量为「未初始化状态(垃圾值)」(默认初始化);new Test():会对所有成员变量做「值初始化」------ 内置类型(int/char 等)置 0,指针置 nullptr。
核心差异: new Test是「默认初始化」(无构造时成员为垃圾值),new Test()是「值初始化」(无构造时成员置 0,有构造时调用构造);
有自定义构造:两者无区别,都会调用构造函数;
无自定义构造 / 内置类型 :new Test()会把所有内置类型成员置 0,new Test则保留垃圾值;
实践建议 :如果需要确保成员被初始化(避免垃圾值),优先用new Test()(或 C++11 后的new Test{}),尤其是无自定义构造的类 / 内置类型。
为什么new和delete支持自动调用构造和析构?
new和delete是用户进行动态内存申请和释放的操作符,operator new和operator delete是系统提供的全局函数,new和delete在底层是通过调用全局函数operator new和operator delete来申请和释放空间的。
operator new和operator delete的用法和malloc和free的用法完全一样,其功能都是在堆上申请和释放空间。
cpp
int* p1 = (int*)operator new(sizeof(int)* 10); //申请
operator delete(p1); //销毁
等价于
cpp
int* p2 = (int*)malloc(sizeof(int)* 10); //申请
free(p2); //销毁
实际上,operator new的底层是通过调用malloc函数来申请空间的,当malloc申请空间成功时直接返回;若申请空间失败,则尝试执行空间不足的应对措施,如果该应对措施用户设置了,则继续申请,否则抛异常。而operator delete的底层是通过调用free函数来释放空间的。
注意:虽然说operator new和operator delete是系统提供的全局函数,但是我们也可以针对某个类,重载其专属的operator new和operator delete函数,进而提高效率。
为什么用new[]创建的数组,释放内存时必须使用delete[]?
为什么必须配对使用?
new/delete:用于单个对象的内存分配和释放new[]/delete[]:用于数组对象的内存分配和释放
当你用new[]创建数组时,编译器会在内存中额外存储数组的元素个数 (用于后续逐个调用元素的析构函数)。而delete[]会读取这个计数,遍历数组并调用每个元素的析构函数,最后释放整块内存;但普通的delete不会读取这个计数,只会释放内存,导致:
- 对于内置类型(int、char 等,无析构函数):表面可能 "正常",但仍属于未定义行为(不同编译器 / 平台可能崩溃)
- 对于自定义类型(如 string、类对象):会跳过大部分元素的析构函数,导致内存泄漏、资源泄露(比如文件句柄、网络连接未释放),甚至程序崩溃。
new[]必须配delete[]------ 本质是delete[]需要读取new[]存储的计数,才能完成所有析构函数的调用,而delete会忽略这个计数,导致未定义行为。
用delete而不是delete[]的后果是什么
delete替代delete[]的本质是破坏了 C++ 内存管理的配对规则,会触发 "未定义行为"(Undefined Behavior)------ 这意味着标准没有规定程序会如何表现,不同编译器、平台、场景下的后果可能不同,但核心问题集中在以下几类:
1. 最直接:析构函数调用不完整(针对有析构函数的类型)
当你用new[]创建数组时,编译器会在数组内存的 "头部" 额外存储一个元素个数的计数(这个计数对开发者不可见):
delete[]:会先读取这个计数,遍历数组的每一个元素,逐个调用析构函数,再释放整块内存;delete:完全忽略这个计数,只会把指针当成 "单个对象" 处理 ------ 只调用第一个元素的析构函数,然后直接释放整块内存。
cpp
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass() { cout << "构造:" << this << endl; }
~MyClass() { cout << "析构:" << this << endl; } // 有自定义析构函数
};
int main() {
MyClass* arr = new MyClass[3]; // 创建3个对象,输出3次构造
cout << "-------- 错误释放 --------" << endl;
delete arr; // 仅调用第一个元素的析构,跳过第2、3个
return 0;
}
//输出结果
构造:0x55e8b867aeb0
构造:0x55e8b867aeb1
构造:0x55e8b867aeb2
-------- 错误释放 --------
析构:0x55e8b867aeb0 // 仅第一个元素析构,后两个"失联"
未被析构的元素如果持有资源(比如堆内存、文件句柄、网络连接、锁),这些资源会永久泄露, 程序运行期间无法回收,直到进程结束。
2. 隐性风险:内存释放异常(可能崩溃 / 堆损坏)
- 对于内置类型 (int、char、double 等,无析构函数):表面上可能 "运行正常",但这只是编译器的 "容错",本质仍是未定义行为。比如在 Windows 的 MSVC 编译器下,可能触发
HEAP CORRUPTION DETECTED(堆损坏);在 Linux 的 GCC 下,可能暂时无感知,但后续内存操作会随机崩溃。 - 对于复杂类型 (如
std::string数组):std::string的析构函数会释放其内部的字符缓冲区,若跳过析构,不仅会泄露字符缓冲区的内存,还可能导致堆管理结构被破坏 ------ 后续的new/delete操作会出现 "双重释放""野指针访问" 等致命错误,表现为程序崩溃(Segmentation Fault)或随机闪退。
3. 极端情况:程序行为完全不可预测
未定义行为的可怕之处在于 "不可复现":
- 测试环境下可能毫无问题,上线后随机崩溃;
- 小数据量时正常,数据量增大后触发堆溢出;
- 不同编译器表现不同(比如 Clang 严格检查,直接报错;GCC 宽松,隐性泄露)。

用new[]创建数组时,编译器会在数组内存的 "头部" 额外存储一个元素个数的计数,你怎么知道的?用vs2026可以查看吗?
VS2026(包括 VS2019/2022)支持通过内存窗口 + 调试直接看到这个隐藏的计数,以下是完整的验证步骤(以 32 位程序为例,64 位逻辑一致,仅内存地址宽度不同):
步骤 1:编写测试代码
cpp
#include <iostream>
using namespace std;
// 自定义类(必须有析构函数,编译器才会存储计数;内置类型可能优化掉计数)
class Test {
public:
int val;
Test() : val(0) {}
~Test() {} // 析构函数必须存在,触发计数存储
};
int main() {
// 1. 分配包含5个Test对象的数组
Test* arr = new Test[5];
// 2. 断点打在这里,查看arr的内存
cout << "数组首地址:" << (void*)arr << endl;
// 3. 错误释放(仅为演示,实际不要这么写)
// delete arr;
// 正确释放
delete[] arr;
return 0;
}
步骤 2:VS2026 调试配置
- 打开 VS2026,创建 "空项目",添加上述代码;
- 右键项目 → 属性 → 配置属性 → C/C++ → 所有选项 → 确保 "平台" 是x86(32 位)(32 位更易观察,计数占 4 字节);
- 按
F5启动调试,程序会停在cout那行的断点处。
步骤 3:查看内存中的计数
- 在调试窗口中,先记录
arr的地址(比如输出是0x00785240); - 打开 "内存窗口":调试 → 窗口 → 内存 → 内存 1(若没显示,确保程序处于中断状态);
- 在内存窗口的地址栏输入:
arr - 4(因为 32 位下计数占 4 字节,存储在数组首地址前 4 字节); - 此时内存窗口会显示一个 4 字节的数值,这个数值就是5(数组元素个数)。
假设调试时arr的地址是0x00785240,内存布局如下:
| 内存地址 | 存储内容(4 字节) | 说明 |
|---|---|---|
| 0x0078523C | 0x00000005 | 数组元素个数(计数) |
| 0x00785240 | 0x00000000 | 第一个 Test 对象的 val |
| 0x00785244 | 0x00000000 | 第二个 Test 对象的 val |
| ... | ... | ... |
delete[]会先读取0x0078523C的数值 5,然后遍历 5 个对象调用析构,再释放从0x0078523C开始的整块内存;而delete会直接把0x00785240当成单个对象地址,只调用 1 次析构,且释放内存时可能破坏堆结构。
特别说明
- 如果数组是
int* arr = new int[5](内置类型,无析构),MSVC 会优化掉这个计数,直接分配连续内存,此时delete和delete[]表面无差异(但仍属于未定义行为)。 - 64 位下计数占 8 字节,需查看
arr - 8的地址。 - GCC/Clang 也会存储计数,但计数的位置 / 格式可能略有不同(比如有的存在数组尾部),但核心逻辑一致。