深蓝黑背景封面。左侧上下两条内存分配路径对照:上路一张代码卡写着「Foo* p = new Foo(42);」,箭头指向一块被切成两段的堆内存块,左段灰色标注「operator new:分配 sizeof(Foo) 字节」,右段青色高亮标注「构造函数:Foo(42) 就地初始化」,下方结论「拿到的是 Foo*,类型明确、对象已可用」;下路代码卡写着「void* p = malloc(sizeof(Foo));」,箭头指向一块虚线描边的灰色内存块,标注「malloc:只给 n 字节原始内存」,内部画着杂点表示未初始化的脏字节,下方结论「返回 void* 需强转,没有构造函数,内容是脏的」。右侧大标题两行「new/delete 与 malloc/free」「区别只在写法吗」,副标题「一个构造对象,一个只给字节」,小注「构造析构 · 类型安全 · 内存来源 · 失败处理」。左上角标注「面试经典 · C/C++」,右上角圆角胶囊标注「Day 04 · 12-01」。 图 1:本文封面,两条分配路径的对照。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料整理,与任何公司的当前招聘无关。「new/delete 和 malloc/free 有什么区别」是 C++ 面试的钉子户,很多人能背出「new 会调构造函数」就停了,但题目问的是「底层实现」,真正要考的是你能不能把差异拆成构造析构、类型安全、内存来源、失败处理四个层面,每一层都落到机制上。

一句话结论:new/delete 是「分配内存 + 构造析构对象」的完整对象生命周期操作符,malloc/free 只是原始字节的搬运工。

第一个区别:构造与析构,这是最本质的一条。

new 是个操作符,编译器把它翻译成两步:先调 operator new 向堆要 sizeof(Foo) 字节的内存,再在这块内存上就地调用构造函数完成初始化;delete 正好反过来,先调析构函数,再调 operator delete 归还内存。malloc 和 free 是 C 标准库函数,眼里只有字节——malloc 从堆分配器的空闲链表里找一块够大的原始内存返回,free 把它挂回去,全程不知道、也不关心这块内存里装的是什么对象,构造析构一概不触发。所以用 malloc 拿到的 Foo 只是一段脏字节,直接解引用读成员是未定义行为。

第二个区别:类型安全。

new 表达式自带类型信息,返回的是 Foo*,编译期就能做类型检查;malloc 的签名是 void* malloc(size_t),返回无类型指针,必须强制转换才能用,字节数还得自己算——sizeof 写错、强转转错类型,编译器都不会拦你。一个是语言层面的语义,一个是库函数层面的约定。

第三个区别:内存来源与可定制性。

operator new 是可以重载的——全局重载可以统一接管所有 new 的分配行为(比如接进自己的内存池),也可以按类重载只接管某个类型;new 还衍生出定位 new(placement new),在调用方给定的地址上构造对象。malloc 走的是 C 库分配器的固定路径,没有对应的钩子。底层上多数实现的 operator new 就是包了一层 malloc 并加了失败时的处理逻辑,但「能重载」这件事让 new 成为工程上可扩展的分配入口。

第四个区别:失败处理。

new 分配失败默认抛出 std::bad_alloc 异常(也有不抛异常的 nothrow 版本),调用方用 try/catch 接住;malloc 失败返回 NULL,全靠调用方记得判空——忘了判,下一次解引用就是段错误。下面这段代码把四个区别一次跑给你看(Clang 17,g++ -std=c++17 mem.cpp -o mem && ./mem):

struct Foo {
    int v;
    Foo(int x) : v(x) { std::printf("  Foo(%d) 构造函数被调用\n", x); }
    ~Foo() { std::printf("  ~Foo() 析构函数被调用\n"); }
};
int main(int argc, char**) {
    std::printf("[new/delete]\n");
    Foo* p = new Foo(42);                      // operator new 分配 + 构造
    std::printf("  p->v = %d(对象已初始化,可直接使用)\n", p->v);
    delete p;                                  // 析构 + operator delete 释放

    std::printf("[malloc/free]\n");
    Foo* q = static_cast<Foo*>(std::malloc(sizeof(Foo)));  // 只给原始字节
    std::printf("  malloc 返回 void*,上面没有任何构造输出,字节是未初始化的\n");
    std::free(q);                              // 只归还内存,不触发析构

    unsigned long long huge = 1ull << (60 + argc);  // 约 8 EB,必然失败
    try {
        char* h = new char[huge]; (void)h;
    } catch (const std::bad_alloc&) {
        std::printf("[失败处理] new 失败:抛出 std::bad_alloc 异常\n");
    }
    char* m = static_cast<char*>(std::malloc(huge));
    std::printf("[失败处理] malloc 失败:返回 %s\n",
                m == nullptr ? "NULL,必须手动判空" : "非空指针");
    std::free(m);
}

实测输出:

[new/delete]
  Foo(42) 构造函数被调用
  p->v = 42(对象已初始化,可直接使用)
  ~Foo() 析构函数被调用
[malloc/free]
  malloc 返回 void*,上面没有任何构造输出,字节是未初始化的
[失败处理] new 失败:抛出 std::bad_alloc 异常
[失败处理] malloc 失败:返回 NULL,必须手动判空

new/delete 一行代码驱动了构造和析构两条打印;malloc/free 全程静默;最后一组对比里,同样申请一个必然失败的天文数字,new 抛出 bad_alloc 被 catch 接住,malloc 安静地返回了 NULL。

最后一个考点:搭配与混用。

规则很死:new 配 delete、new[] 配 delete[]、malloc 只配 free。混用——比如 new 出来的对象用 free 释放——是未定义行为:轻则析构函数被跳过造成资源泄漏,重则因为两套分配器的堆块管理结构不同而直接踩坏堆。这条规则反过来也能当排错线索:程序在释放时崩溃,先查是不是指针的出身和释放方式对不上。

深蓝黑背景概念图,顶部小字「概念图 · new 与 malloc 的底层差异」,主标题「new = 分配 + 构造,delete = 析构 + 释放」。上半部分左右两条执行链:左链青色系,「new Foo」依次经过「operator new 向堆要内存」「构造函数就地初始化」到达「Foo* 可用对象」,其下方反向链「delete p」先走「析构函数」再走「operator delete 归还内存」;右链灰色系,「malloc(n)」经过「堆分配器找空闲块」直接到达「void* 原始字节」,下方反向「free(p) 把内存挂回空闲链表,不触发析构」。中部是一张三列五行对照表,维度依次为:构造与析构(自动调用 对 完全不调用)、类型安全(返回具体类型指针 对 返回 void* 需强转)、内存来源(operator new 可重载 对 分配器空闲链表)、失败处理(抛 std::bad_alloc 异常 对 返回 NULL 需手动判空)、搭配规则(new 配 delete、new[] 配 delete[] 对 malloc 只配 free)。底部总结条强调「混用是未定义行为:new 来的必须 delete,malloc 来的必须 free」,下方小字延伸「operator new 可以全局或按类重载,malloc 不行」。 图 2:两条执行链与五维对照。

面试现场的答题结构。

先否定题目的暗示:「区别不只是写法,是对象生命周期和原始字节的差别」。然后按四层展开:构造析构(编译器把 new 翻译成 operator new 加构造、delete 翻译成析构加 operator delete)、类型安全(Foo* 对 void* 强转)、内存来源(operator new 可全局或按类重载、有 placement new,malloc 走分配器空闲链表)、失败处理(bad_alloc 异常对 NULL 判空)。最后补搭配规则与混用的未定义行为收尾。如果被追问「C++ 里为什么还要保留 malloc」,可以答:兼容 C 代码与 C 库接口、realloc 这类原地扩缩容的语义 new 体系没有直接对应物。你调试时遇到过 new 和 free 混用导致的诡异崩溃吗?最后怎么定位的?欢迎分享。