动态内存分配:new 与 delete 的基本用法
在 C++ 编程中,内存管理是核心能力之一,而动态内存分配则是实现灵活内存使用的关键。前文我们学习了指针、const 指针、引用等知识点,了解到指针可通过地址操作内存,但这些操作多基于栈上的静态内存(如普通变量、数组)。静态内存的大小和生命周期由编译器自动管理,无法满足运行时动态调整内存的需求(如不确定数组长度、按需创建对象)。此时,就需要通过 new 和 delete 运算符手动管理堆内存,实现动态内存分配与释放。本文将从动态内存的核心意义入手,详细讲解 new/delete 的基本语法、单变量/数组的分配释放、配对原则及常见错误,帮你夯实动态内存管理基础。
一、前置认知:静态内存与动态内存的区别
在 C++ 中,程序运行时的内存主要分为栈(Stack)、堆(Heap)、全局/静态存储区等,其中栈和堆是最常用的内存区域,二者管理方式与特性差异显著,直接决定了静态内存与动态内存的使用场景。
1. 静态内存(栈内存)
静态内存通常用于存储普通局部变量、函数参数、固定大小的数组等,由编译器自动分配和释放,生命周期与作用域绑定:
-
分配时机:进入变量作用域时,编译器自动在栈上分配内存;
-
释放时机:离开作用域时,编译器自动释放内存,变量销毁;
-
特点:分配效率高,无需手动管理,但大小固定、生命周期不可控,栈空间有限(超出易导致栈溢出)。
cpp
#include <iostream>
using namespace std;
void func() {
int a = 10; // 栈上静态内存,进入func时分配
int arr[5] = {1,2,3,4,5}; // 固定大小数组,栈内存分配
} // 离开func作用域,a和arr自动释放,内存回收
int main() {
func();
return 0;
}
2. 动态内存(堆内存)
动态内存用于存储运行时按需分配的数据,由开发者通过 new 手动分配、delete 手动释放,生命周期完全由开发者控制:
-
分配时机:执行
new语句时,向操作系统申请堆内存; -
释放时机:执行
delete语句时,释放堆内存,归还给操作系统; -
特点:大小灵活(运行时确定)、生命周期可控,但分配效率低于栈,需手动管理,遗漏释放易导致内存泄漏。
核心关联:动态内存的分配结果需通过指针接收(指针存储堆内存地址),本质是通过指针操作堆上的数据,这也是前文指针知识与动态内存分配的核心衔接点。
二、new 运算符:动态内存分配的核心
new 是 C++ 用于动态分配堆内存的运算符,其核心作用是向操作系统申请一块指定类型的堆内存,并返回该内存的首地址(需用对应类型的指针接收)。根据分配对象的不同,new 的用法分为"单变量动态分配"和"数组动态分配"两类。
1. 单变量动态分配
(1)语法格式
Plain
// 格式1:分配内存,不初始化
数据类型 *指针名 = new 数据类型;
// 格式2:分配内存并初始化(C++11及以上支持)
数据类型 *指针名 = new 数据类型(初始化值);
// 格式3:分配内存,若失败抛出bad_alloc异常(默认行为)
// 格式4:分配内存,若失败返回nullptr(nothrow版本,需包含<new>头文件)
数据类型 *指针名 = new(nothrow) 数据类型;
(2)实战示例
cpp
#include <iostream>
#include <new> // nothrow版本所需头文件
using namespace std;
int main() {
// 1. 分配int类型内存,不初始化
int *p1 = new int;
*p1 = 10; // 通过指针解引用赋值
cout << "p1指向的值:" << *p1 << endl; // 输出10
// 2. 分配int类型内存并初始化
int *p2 = new int(20);
cout << "p2指向的值:" << *p2 << endl; // 输出20
// 3. nothrow版本,分配失败返回nullptr
int *p3 = new(nothrow) int(30);
if (p3 != nullptr) { // 需判断是否分配成功
cout << "p3指向的值:" << *p3 << endl; // 输出30
} else {
cout << "内存分配失败" << endl;
}
// 手动释放内存(后续讲解delete)
delete p1;
delete p2;
delete p3;
return 0;
}
(3)关键说明
-
new 分配的内存是匿名的,必须通过指针接收地址,否则会导致内存泄漏(无法找到并释放该内存);
-
默认情况下,new 分配失败会抛出
std::bad_alloc异常,可通过 nothrow 版本避免异常,改为返回 nullptr; -
分配的内存位于堆上,指针变量本身位于栈上,指针生命周期结束后,堆内存若未释放仍会存在(内存泄漏)。
2. 数组动态分配
当需要动态调整数组大小时,可通过 new[] 分配数组内存,其语法与单变量分配略有差异,需指定数组长度。
(1)语法格式
Plain
// 格式1:分配数组内存,不初始化
数据类型 *指针名 = new 数据类型[数组长度];
// 格式2:分配数组内存并初始化(C++11及以上支持,仅对基本类型有效)
数据类型 *指针名 = new 数据类型[数组长度]{初始化列表};
// 格式3:nothrow版本,分配失败返回nullptr
数据类型 *指针名 = new(nothrow) 数据类型[数组长度];
(2)实战示例
cpp
#include <iostream>
using namespace std;
int main() {
int n = 5; // 运行时确定数组长度,静态数组无法实现
// 1. 分配int数组内存,不初始化
int *arr1 = new int[n];
for (int i = 0; i < n; i++) {
arr1[i] = i + 1; // 手动赋值
cout << arr1[i] << " "; // 输出1 2 3 4 5
}
cout << endl;
// 2. 分配数组并初始化
int *arr2 = new int[3]{10, 20, 30};
for (int i = 0; i < 3; i++) {
cout << arr2[i] << " "; // 输出10 20 30
}
cout << endl;
// 释放数组内存(需用delete[])
delete[] arr1;
delete[] arr2;
return 0;
}
(3)关键说明
-
new[] 分配的数组内存是连续的,指针指向数组首元素地址,可通过指针偏移或下标访问元素(与静态数组一致);
-
数组长度可在运行时确定(如通过变量指定),这是静态数组(长度需编译时确定)无法实现的核心优势;
-
分配数组时,new[] 会额外存储数组长度信息(用于 delete[] 释放时确定释放范围),因此不可用 delete 替代 delete[] 释放数组内存。
三、delete 运算符:动态内存释放的核心
动态内存分配后,若不手动释放,会一直占用堆空间,直到程序结束(操作系统回收),这会导致内存泄漏(可用内存逐渐减少,影响程序性能甚至崩溃)。delete 运算符的作用是手动释放 new 分配的堆内存,将内存归还给操作系统,避免内存泄漏。delete 的用法需与 new 严格配对,分为"单变量释放"和"数组释放"两类。
1. 单变量内存释放
(1)语法格式
Plain
delete 指针名; // 仅用于释放new分配的单变量内存
(2)实战示例与注意事项
cpp
#include <iostream>
using namespace std;
int main() {
int *p = new int(10);
cout << *p << endl; // 输出10
delete p; // 释放p指向的堆内存
// 释放后,p变为野指针(仍存储原地址,但地址已无效)
p = nullptr; // 建议释放后将指针置空,避免野指针
// *p = 20; // 错误:访问已释放的内存,行为未定义
return 0;
}
关键注意:释放内存后,指针本身不会被销毁(仍存在于栈上),会成为野指针(指向无效内存)。因此,释放后需将指针置为 nullptr,避免后续误操作。
2. 数组内存释放
(1)语法格式
Plain
delete[] 指针名; // 仅用于释放new[]分配的数组内存
(2)实战示例与注意事项
cpp
#include <iostream>
using namespace std;
int main() {
int *arr = new int[5]{1,2,3,4,5};
delete[] arr; // 正确:释放数组内存
arr = nullptr; // 置空指针,避免野指针
// 错误用法:用delete释放数组内存
// int *arr2 = new int[3];
// delete arr2; // 行为未定义,可能导致内存泄漏或程序崩溃
return 0;
}
核心原则:new 与 delete 配对,new[] 与 delete[] 配对,不可混用。混用会导致内存释放不彻底(内存泄漏)或程序崩溃,这是动态内存管理的高频错误点。
四、new/delete 的进阶用法:const 动态内存与对象分配
1. const 修饰的动态内存
前文我们学习了 const 指针,动态内存也可通过 const 修饰,实现"只读堆内存",禁止通过指针修改堆上的数据。
cpp
#include <iostream>
using namespace std;
int main() {
// 分配const动态内存,必须初始化(只读,无法后续赋值)
const int *p = new const int(10);
cout << *p << endl; // 合法:仅可读取
// *p = 20; // 错误:const动态内存不可修改
delete p; // 正常释放,const不影响内存释放
p = nullptr;
return 0;
}
关键说明:const 动态内存必须在分配时初始化,因为后续无法通过指针修改值,其本质与 const 普通变量一致,仅存储位置从栈变为堆。
2. 类对象的动态分配与释放
new/delete 不仅可分配基本数据类型,还可分配类对象,分配时会自动调用类的构造函数初始化对象,释放时会自动调用析构函数清理对象资源(这是 C++ 与 C 语言 malloc/free 的核心区别)。
cpp
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
// 构造函数(初始化对象)
Person(string n, int a) : name(n), age(a) {
cout << "Person构造函数调用" << endl;
}
// 析构函数(清理资源)
~Person() {
cout << "Person析构函数调用" << endl;
}
void showInfo() {
cout << "姓名:" << name << ",年龄:" << age << endl;
}
private:
string name;
int age;
};
int main() {
// 动态分配Person对象,自动调用构造函数
Person *p = new Person("张三", 20);
p->showInfo(); // 输出姓名和年龄
// 释放对象内存,自动调用析构函数
delete p;
p = nullptr;
return 0;
}
运行结果:
关键说明:动态分配对象数组时,同样需用 new[] 和 delete[],构造函数和析构函数会为每个对象分别调用。
五、避坑指南:new/delete 常见错误与规避
1. 内存泄漏(最常见错误)
cpp
void func() {
int *p = new int(10);
// 遗漏delete,函数结束后p销毁,堆内存无法释放(内存泄漏)
}
规避方案:① 牢记"谁分配谁释放"原则,new 后务必对应 delete/delete[];② 释放后将指针置空,避免重复释放;③ 复杂场景可使用智能指针(如 unique_ptr、shared_ptr)自动管理内存。
2. 重复释放内存
cpp
int *p = new int(10);
delete p;
delete p; // 错误:重复释放同一内存,导致程序崩溃
规避方案:释放内存后立即将指针置为 nullptr,重复释放 nullptr 是安全的(无任何操作)。
3. new/new[] 与 delete/delete[] 混用
cpp
int *arr = new int[5];
delete arr; // 错误:用delete释放数组内存
// int *p = new int;
// delete[] p; // 错误:用delete[]释放单变量内存
规避方案:严格遵循配对原则,new 对应 delete,new[] 对应 delete[],可在代码中添加注释区分,避免混淆。
4. 访问已释放的内存(野指针操作)
cpp
int *p = new int(10);
delete p;
cout << *p << endl; // 错误:访问已释放的野指针
规避方案:释放内存后,立即将指针置为 nullptr,使用指针前先判断是否为 nullptr。
5. new 分配失败未处理
cpp
int *p = new int[1000000000000]; // 内存分配失败,默认抛出异常
*p = 10; // 若未捕获异常,程序直接崩溃
规避方案:① 使用 nothrow 版本 new,分配失败返回 nullptr,手动判断;② 捕获 bad_alloc 异常,优雅处理分配失败场景。
六、总结
动态内存分配是 C++ 灵活管理内存的核心手段,new/delete 运算符实现了堆内存的手动分配与释放,其核心价值在于"运行时动态调整内存大小、控制生命周期",弥补了静态内存的局限性。本文核心知识点可概括为:
-
静态内存(栈)由编译器自动管理,大小固定;动态内存(堆)由开发者通过 new/delete 手动管理,灵活可控。
-
new 用于分配堆内存(单变量用 new,数组用 new[]),返回内存地址,需用指针接收;delete 用于释放堆内存(单变量用 delete,数组用 delete[]),必须与 new 配对。
-
动态内存分配后,需及时释放并置空指针,避免内存泄漏、野指针、重复释放等错误。
-
new/delete 分配释放类对象时,会自动调用构造函数和析构函数,这是其与 C 语言 malloc/free 的核心差异。