☰
从 malloc/free 到 new/delete:真正理解 C++ 中的内存管理与对象生命周期
2026/9/30 6:30:51 网站建设 项目流程

文章目录

    • 1. 先分清两件事:内存放在哪里,对象是否已经存在
    • 2. C 的动态分配:拿到的是内存,不是 C++ 对象的构造过程
    • 3. `new` 与 `delete`:先从内置类型看语法和初始化
    • 4. 对自定义类型,分配和构造必须连起来看
    • 5. `new` 表达式和 `operator new` 函数,不是同一个东西
    • 6. 拓展:placement new(定位 new)
    • 7. 为何要严格匹配内存开辟与释放规则
    • 8. 小结

1. 先分清两件事:内存放在哪里,对象是否已经存在


在 C 中,我们常用“栈、堆、数据段、代码段”来描述一个典型程序的内存布局。下面用一段 C++ 代码来观察变量与它具体指向的内容:

#include<iostream>//定义全局(静态)变量intglobal_value=1;staticintfile_value=2;intmain(){//静态变量staticintlocal_static=3;//普通变量intlocal_value=4;intlocal_array[2]={5,6};//指针变量constchar*text="C++";//动态开辟内存int*dynamic_value=newint(7);std::cout<<"global_value: "<<global_value<<std::endl;std::cout<<"file_value: "<<file_value<<std::endl;std::cout<<"local_static: "<<local_static<<std::endl;std::cout<<"local_value: "<<local_value<<std::endl;std::cout<<"local_array[0]: "<<local_array[0]<<std::endl;std::cout<<"text: "<<text<<std::endl;std::cout<<"*dynamic_value: "<<*dynamic_value<<std::endl;deletedynamic_value;}

运行示例:

  • global_value、file_value和local_static具有静态存储期,通常放在静态区(数据段);
  • local_value、local_array、text和dynamic_value这些局部变量通常位于栈帧中。
  • 指针变量和它指向的内容不是同一块东西:dynamic_value自身是局部变量,*dynamic_value是动态创建的整数对象,空间占用在堆上;
  • text自身是局部指针,而字符串字面量具有静态存储期,通常位于只读区域。

2. C 的动态分配:拿到的是内存,不是 C++ 对象的构造过程


先把熟悉的四个函数放在一张表里进行对比:

C 函数主要作用须记住的边界
malloc(bytes)分配指定字节数内容未初始化;失败返回空指针
calloc(count, bytes)分配并将字节置零字节置零但不等于调用 C++ 构造函数
realloc(ptr, new_bytes)调整既有分配块成功后旧指针失效,地址可能改变;失败时原块仍有效
free(ptr)释放上述函数得到的分配块不调用 C++ 析构函数;free(nullptr)安全

下面是一个 C 语言的内存管理示例,只对整数数组使用calloc/realloc。:

#include<stdlib.h>#include<stdio.h>intmain(){int*values=(int*)calloc(3,sizeof(int));if(values==NULL){perror("calloc fail!");return1;}int*grown=(int*)realloc(values,5*sizeof(int));if(grown==NULL){perror("realloc fail!");free(values);return1;}values=grown;// 成功后只能继续使用新指针printf("%d %d\n",values[3],values[4]);values[3]=30;// 扩出来的字节不能假定已清零values[4]=40;printf("%d %d\n",values[3],values[4]);free(values);return0;}

运行示例:

前两次打印时,由于未给新分配好的内存空间赋值,所以打印随机值,正常分配后则输出30 40。如果直接写values = realloc(values, ...),失败返回的空指针会覆盖原地址,从而导致原块无法释放(内存泄漏)。

在 C++ 中调用这些函数时,若是用来开辟自定义类型的对象时,它们不会调用类的构造函数。
对已经由new创建的对象,也不能用realloc扩容。

3.new与delete:先从内置类型看语法和初始化


new T创建一个T对象;new T[n]创建一个含n个对象的数组。

相较于手写malloc(n * sizeof(T)),new根据类型和元素个数决定所需存储,并直接返回相应类型的指针;对应地,单个对象用delete,数组用delete[]。

// 完整示例:内置类型的不同初始化方式#include<iostream>intmain(){int*a=newint;// 值未初始化,先赋值再读取int*b=newint(10);// 初始化为 10int*c=newint();// 值初始化,得到 0int*d=newint[3];// 三个元素的值未初始化(随机值)int*e=newint[3]();// 三个元素均值初始化为 0int*f=newint[3]{1,2,3};//三个元素按照给定元素初始化int*g=newint[3]{1};//第一个元素按给定元素初始化,其余为0*a=5;d[0]=1;d[1]=2;d[2]=3;std::cout<<"*a: "<<*a<<std::endl;std::cout<<"*b: "<<*b<<std::endl;std::cout<<"*c: "<<*c<<std::endl;for(size_t i=0;i<3;i++){std::cout<<"d["<<i<<"]:"<<d[i]<<' ';}std::cout<<std::endl;std::cout<<std::endl;for(size_t i=0;i<3;i++){std::cout<<"f["<<i<<"]:"<<f[i]<<' ';}std::cout<<std::endl;std::cout<<std::endl;for(size_t i=0;i<3;i++){std::cout<<"g["<<i<<"]:"<<g[i]<<' ';}deletea;deleteb;deletec;delete[]d;delete[]e;delete[]f;delete[]g;}

运行示例:

普通new分配失败时通常抛出std::bad_alloc而中断程序,而不是像malloc那样返回空指针;所以写if (p == nullptr)并不能处理普通new的分配失败。

后续可以使用异常处理中的try...catch来进行异常的捕捉。

4. 对自定义类型,分配和构造必须连起来看


在C++中,动态开辟类对象时比整数时多了一段关键过程:new自动调用构造函数让对象可用,delete自动调用析构函数在对象结束前清理资源。这也是与 C 的内存管理有差别的地方,malloc是函数,而new是操作符。

下面的Cell类在构造时申请一个整数内存,在析构时释放它。

// 完整示例:单个对象与对象数组的生命周期#include<iostream>classCell{public:explicitCell(intvalue=0):_value(newint(value)){std::cout<<"构造 "<<*_value<<std::endl;}~Cell(){std::cout<<"析构 "<<*_value<<std::endl;delete_value;}private:int*_value;};intmain(){Cell*one=newCell(7);deleteone;Cell*three=newCell[3];delete[]three;}

运行示例:

正常执行时,先输出一次“构造 7、析构 7”,再输出三次“构造 0”和三次“析构 0”。

  • new Cell[3]要能逐个构造,因此这里的Cell需要有可不传参数的构造函数(默认构造函数);
  • delete[]要结束全部数组元素的生命,再释放数组占用的存储。


如果把delete one改成free(one),析构函数不会执行,_value指向的资源无法经由析构函数释放;所以就可能导致崩溃、破坏分配器状态。

单个元素也可能暂时看不出异常。但还是建议尽量配对使用,并且尤其注意释放开辟的数组时使用方括号delete[]。

5.new表达式和operator new函数,不是同一个东西


名字接近,职责却不同:

写法做什么是否构造或析构对象
new Cell(7)new表达式:取得存储并初始化一个对象调用构造函数
delete pdelete表达式:结束对象生命并释放存储调用析构函数
::operator new(sizeof(Cell))分配函数:只取得原始存储不调用构造函数
::operator delete(raw)释放函数:只释放原始存储不调用析构函数

在通常的路径中,可以把过程想成:

new Cell(7) : 取得足够且对齐的存储 -> 在其中构造 Cell -> 得到 Cell* delete p : 对 Cell 执行析构 -> 释放其占用的存储

operator new≠new表达式

全局operator new常由运行库在底层借助malloc或其他分配器实现,但标准并不要求它必须调用malloc。

  1. 不要混淆:调用operator new不会产生对象,只是一块未初始化内存,直接当成Cell *去使用,属于未定义行为。
  2. operator new/operator delete可以被类重写(类内重载);new表达式语法本身不能重载。

6. 拓展:placement new(定位 new)


如果你已经用operator new拿到裸内存,想要手动执行构造函数,就用定位 new:

void*raw=::operatornew(sizeof(Cell));// 只分配内存,没有对象Cell*obj=new(raw)Cell(7);// placement new:在raw这块内存上调用构造函数

释放的时候也要手动两步:

obj->~Cell();// 手动调用析构::operatordelete(raw);// 再释放原始内存

7. 为何要严格匹配内存开辟与释放规则


先看以下规则,不只看一次运行的表象:

取得方式正确释放错误配对
malloc/calloc/reallocfreedelete、delete[]
new Tdeletefree、delete[]
new T[n]delete[]free、delete

这些错误配对都是未定义行为,包括“内置类型数组用delete并没发生崩溃”。对于有析构函数的对象数组,后果往往更明显(对象析构不完全导致内存泄漏):delete并不能按数组规则逐个结束所有元素的生命,释放时还可能把不合适的地址交给分配器。

那么,delete[]又怎样知道该析构多少个对象?有的实现会在分配块附近保存元素数目或相关元数据,常称为array cookie;某些类型、实现或优化下也可能不需要这样的记录。

cookie 是编译器实现细节,C++ 标准没有强制要求必须存在、放哪里、多大。
但记录的位置、大小、是否存在,都不能拿来随意编写可移植代码,要根据平台与编译器的选择具体决定。

但只要严格遵守配对使用,就能很大程度避免一些问题的发生,尤其对于一些底层实现很复杂的情况。

8. 小结


  • new/delete是表达式,分配释放内存同时自动调用构造、析构;new[]必须配delete[],错配属于未定义行为。
  • operator new/operator delete只操作裸内存,不执行构造析构,是new表达式底层调用的函数。
  • malloc/free只管理原始内存,不处理对象;和new/delete指针严禁交叉释放。
  • 对象完整生命周期:分配内存 -> 构造 -> 使用 -> 析构 -> 释放内存。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询