使用 new 与 delete 进行动态内存分配

一、为何需要动态内存分配

C++ 支持三种基本的内存分配方式,其中两种您已见过:

  • 静态内存分配:用于 static 与全局变量。此类变量在程序启动时一次性分配内存,并在整个程序生命周期内保持有效。
  • 自动内存分配:用于函数形参和局部变量。进入相应作用域时分配,退出作用域时释放,可反复进行。
  • 动态内存分配:本文讨论的主题。

静态与自动分配的共同限制:

  1. 变量 / 数组的大小必须在编译期已知;
  2. 内存的分配与释放完全由系统自动完成。

多数情况下,这些限制并无大碍。然而,当处理外部输入(用户或文件)时,上述约束往往成为障碍。

示例:

  • 需要字符串保存人名,但名字长度需在用户输入后才能确定;
  • 需从磁盘读取若干记录,但事先不知记录条数;
  • 游戏中怪物数量随时间增减,无法在编译期固定。

若必须在编译期声明大小,只能猜测一个最大值并祈祷够用:

char name[25];             // 但愿名字不超过 25 字符!
Record record[500];        // 但愿记录不超过 500 条!
Monster monster[40];       // 最多 40 只怪物
Polygon rendering[30000];  // 3D 场景最好别超过 3 万个多边形!

这种方案至少有四大缺陷:

  1. 内存浪费:若实际使用远小于最大值,则多余内存闲置。
  2. 使用追踪困难:如何区分哪些元素真正有效?需额外状态标记,增加复杂度与内存开销。
  3. 栈空间受限:常规变量(含定长数组)位于栈上,而默认栈空间通常很小(Visual Studio 默认为 1 MB)。超出即触发栈溢出,操作系统可能直接终止程序。
  4. 人为限制与越界风险:用户若尝试读取 600 条记录,而程序仅预留 500 条,只能报错、截断或导致数组越界。

动态内存分配可彻底解决上述问题:运行期按需向操作系统申请内存。该内存并非来自受限制的栈,而是来自由操作系统管理的、容量可达 GB 级别的堆(heap)。

二、单个变量的动态分配

使用 new 的标量(非数组)形式可为单个变量申请动态内存:

new int;           // 动态分配一个 int,但丢弃返回地址

通常我们将返回地址存入指针以便后续访问:

int* ptr{ new int };   // 分配 int 并将地址赋给 ptr
*ptr = 7;              // 通过解引用写入值 7

由此可见指针的实用场景之一:若无指针保存地址,我们将无法访问刚获得的内存。

注意:访问堆对象通常比访问栈对象慢。栈对象地址在编译期即可确定,可直接寻址;堆对象需通过指针二次寻址。

三、动态分配底层机制

计算机拥有大量可供应用程序使用的内存。操作系统加载程序后,将其映射到若干区域:代码区、运行期数据区(记录函数调用、管理全局/局部变量等)。剩余空间则等待程序申请。

程序调用 new 时,向操作系统请求预留部分内存;若请求成功,系统返回该内存地址。程序使用完毕后,应显式归还,以便系统再分配。

关键洞察

  • 栈对象的分配与释放由编译器自动生成代码完成,无需程序员干预。
  • 堆对象的分配与释放需程序员介入,因此必须通过地址唯一标识对象,以便后续释放。
  • new 返回的地址通常存入指针,供后续访问与 delete 使用。

四、动态分配时的初始化

可在分配时直接初始化:

int* ptr1{ new int(5) };   // 直接初始化
int* ptr2{ new int{ 6 } }; // 统一初始化(列表初始化)

五、单个变量的释放

使用 delete 的标量形式释放:

delete ptr;   // 将 ptr 指向的内存归还系统
ptr = nullptr; // 置空指针,避免悬空

六、“删除”内存的含义

delete 并非销毁变量,而是将内存归还操作系统。指针变量本身作用域不变,可重新赋值(如置 nullptr)。

若 delete 非动态分配的地址,将产生未定义行为。

七、悬空指针(dangling pointer)

C++ 不保证释放后的内存内容,也不修改指针值。指向已释放内存的指针即为悬空指针。解引用或二次 delete 均导致未定义行为:

int* ptr{ new int };
*ptr = 7;
delete ptr;     // ptr 现为悬空指针
std::cout << *ptr; // 未定义行为
delete ptr;     // 重复释放,未定义行为

当多个指针指向同一块动态内存时,释放后所有指针均悬空:

int* ptr{ new int{} };
int* otherPtr{ ptr };
delete ptr;      // ptr 与 otherPtr 均悬空
ptr = nullptr;   // ptr 安全,otherPtr 仍悬空

最佳实践:

  • 尽量避免多指针共享同一动态内存;若必须共享,明确“所有者”。
  • delete 后立即将指针置 nullptr(除非指针立即离开作用域)。

八、new 可能失败

极端情况下,系统无法满足内存请求,默认行为是抛出 std::bad_alloc 异常。若未捕获,程序将异常终止。

可用 nothrow 版本让 new 失败时返回 nullptr:

int* value{ new (std::nothrow) int };
if (!value) {
    std::cerr << "内存分配失败\n";
}

九、空指针与动态内存

空指针(nullptr)表示“未分配内存”,可用于条件分配:

if (!ptr)
    ptr = new int;

删除空指针是安全的,不会产生任何效果,因此无需事先判断:

delete ptr; // 若 ptr 为 nullptr,则无操作

十、内存泄漏

动态内存必须显式释放,否则将持续占用直至程序结束(操作系统回收)。若指针超出作用域或被重新赋值,而原地址未释放,则发生内存泄漏:

void doSomething() {
    int* ptr{ new int{} }; // 未 delete
}   // ptr 离开作用域,地址丢失,内存泄漏

其他泄漏场景:

  • 重新赋值未先释放:
int value = 5;
int* ptr{ new int{} };
ptr = &value; // 原地址丢失,泄漏
  • 连续多次 new 而不释放:
int* ptr{ new int{} };
ptr = new int{}; // 第一次地址丢失,泄漏

修正方法:在重新赋值前 delete。

十一、结论

  • new 与 delete 支持单个变量的动态分配与释放。
  • 动态分配的内存具有动态存储期,直到显式释放或程序终止。
  • 切勿解引用悬空或空指针。
    本节我们将讨论使用 new 与 delete 分配与释放数组。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"