动态内存分配:new 与 delete 的基本用法

动态内存分配:new 与 delete 的基本用法

在 C++ 编程中,内存管理是核心能力之一,而动态内存分配则是实现灵活内存使用的关键。前文我们学习了指针、const 指针、引用等知识点,了解到指针可通过地址操作内存,但这些操作多基于栈上的静态内存(如普通变量、数组)。静态内存的大小和生命周期由编译器自动管理,无法满足运行时动态调整内存的需求(如不确定数组长度、按需创建对象)。此时,就需要通过 newdelete 运算符手动管理堆内存,实现动态内存分配与释放。本文将从动态内存的核心意义入手,详细讲解 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 运算符实现了堆内存的手动分配与释放,其核心价值在于"运行时动态调整内存大小、控制生命周期",弥补了静态内存的局限性。本文核心知识点可概括为:

  1. 静态内存(栈)由编译器自动管理,大小固定;动态内存(堆)由开发者通过 new/delete 手动管理,灵活可控。

  2. new 用于分配堆内存(单变量用 new,数组用 new[]),返回内存地址,需用指针接收;delete 用于释放堆内存(单变量用 delete,数组用 delete[]),必须与 new 配对。

  3. 动态内存分配后,需及时释放并置空指针,避免内存泄漏、野指针、重复释放等错误。

  4. new/delete 分配释放类对象时,会自动调用构造函数和析构函数,这是其与 C 语言 malloc/free 的核心差异。

相关推荐
yangminlei2 小时前
SpringSecurity核心源码剖析+jwt+OAuth(一):SpringSecurity的初次邂逅(概念、认证、授权)
java·开发语言·python
小张快跑。2 小时前
【SpringBoot进阶指南(一)】SpringBoot整合MyBatis实战、Bean管理、自动配置原理、自定义starter
java·开发语言·spring boot
资深web全栈开发2 小时前
JS防爬虫3板斧
开发语言·javascript·爬虫
CDA数据分析师干货分享2 小时前
【CDA干货】客户分群建模——RFM+K-Means用户画像——电商用户数据分析全流程:从数据到增长决策
算法·机器学习·数据挖掘·数据分析·kmeans·cda证书
机器学习之心2 小时前
MATLAB基于GA-BP神经网络与NSGA-Ⅱ多目标优化算法结合,用于优化42CrMo钢表面激光熔覆工艺参数
神经网络·算法·matlab
Ulyanov2 小时前
三维战场可视化核心原理(一):从坐标系到运动控制的全景指南
开发语言·前端·python·pyvista·gui开发
java1234_小锋2 小时前
Java项目中如何选择垃圾回收器?
java·开发语言
zhangjin11202 小时前
java线程的阻塞和等待的区别
java·开发语言
养军博客2 小时前
C语言五天算法速成(可用于备考蓝桥杯)
c语言·算法·蓝桥杯