10. new_delete 不是 malloc_free 的包装

文章目录

  • 引言
  • [一、`new` 做了三件事,`malloc` 只做了一件](#一、new 做了三件事,malloc 只做了一件)
    • [1.1 拆解 `new` 的执行步骤](#1.1 拆解 new 的执行步骤)
    • [1.2 用 C 模拟 `new` 的行为](#1.2 用 C 模拟 new 的行为)
  • [二、`new` vs `malloc` 的核心差异表](#二、new vs malloc 的核心差异表)
  • 三、混用的后果:未定义行为
    • [3.1 `new` + `free`:析构函数被跳过](#3.1 new + free:析构函数被跳过)
    • [3.2 `malloc` + `delete`:在垃圾数据上执行析构函数](#3.2 malloc + delete:在垃圾数据上执行析构函数)
    • [3.3 即使对"简单类型"也成立](#3.3 即使对"简单类型"也成立)
  • [四、`new`/`delete` 的数组版本](#四、new/delete 的数组版本)
    • [4.1 `new[]` 和 `delete[]`](#4.1 new[]delete[])
    • [4.2 `operator new[]` 在干什么](#4.2 operator new[] 在干什么)
  • [五、`new` 的失败处理:不是返回 `nullptr`](#五、new 的失败处理:不是返回 nullptr)
    • [5.1 默认行为:抛异常](#5.1 默认行为:抛异常)
    • [5.2 `nothrow` 版本:和 `malloc` 一样返回 `nullptr`](#5.2 nothrow 版本:和 malloc 一样返回 nullptr)
    • [5.3 `malloc` 失败返回 `NULL`](#5.3 malloc 失败返回 NULL)
  • [六、`operator new` 与 `new` 表达式](#六、operator newnew 表达式)
    • [6.1 `new` 表达式 vs `operator new` 函数](#6.1 new 表达式 vs operator new 函数)
    • [6.2 自定义 `operator new`](#6.2 自定义 operator new)
  • [七、placement new:在指定地址构造对象](#七、placement new:在指定地址构造对象)
    • [7.1 基本用法](#7.1 基本用法)
    • [7.2 placement new 的真实定义](#7.2 placement new 的真实定义)
  • [八、`new` 在底层是否调用了 `malloc`](#八、new 在底层是否调用了 malloc)
    • [8.1 GCC/Clang 的实际情况](#8.1 GCC/Clang 的实际情况)
    • [8.2 为什么不保证](#8.2 为什么不保证)
  • [九、`new`/`delete` 的现代 C++ 替代方案](#九、new/delete 的现代 C++ 替代方案)
  • 总结

本系列为《C++深度修炼:基础、STL源码与多线程实战》第10篇

前置条件:理解 C 的 malloc/free,了解 C++ 的构造函数(第3篇)和引用(第9篇)

引言

很多 C 程序员初学 C++ 时,会自然地认为 new/delete 就是 malloc/free 外面套了一层壳:

cpp 复制代码
// 朴素的误解:
// new ≈ malloc + 强转
// delete ≈ free

这个理解是错误的,而且是危险的。 混用 newfree,或 mallocdelete,会导致从内存泄漏到未定义行为的各种问题。

newmalloc 之间,隔着一个完整的对象生命周期。本文从底层细节出发,把这对差异说清楚。


一、new 做了三件事,malloc 只做了一件

1.1 拆解 new 的执行步骤

cpp 复制代码
#include <iostream>
#include <cstdlib>

class Tracer {
public:
    Tracer(int id) : id_(id) {
        std::cout << "Tracer(" << id_ << ") 构造\n";
    }
    ~Tracer() {
        std::cout << "~Tracer(" << id_ << ") 析构\n";
    }
private:
    int id_;
};

int main() {
    // new 表达式做了三件事:
    // 1. 调用 operator new 分配原始内存
    // 2. 在原始内存上调用构造函数
    // 3. 返回带类型的指针
    Tracer *p = new Tracer(42);

    // malloc 只做一件事:
    // 1. 分配原始内存(没有类型信息,不调用构造函数)
    void *raw = std::malloc(sizeof(Tracer));
    // 此时 raw 指向的内存里是垃圾------没有任何 Tracer 对象存在

    delete p;
    std::free(raw);
}
text 复制代码
$ g++ -std=c++17 new_steps.cpp && ./a.out
Tracer(42) 构造
~Tracer(42) 析构

注意:malloc 分配的那块内存没有任何构造/析构输出------它只是一块"裸地"。

1.2 用 C 模拟 new 的行为

如果你在 C 中手动模拟 new,你需要写:

cpp 复制代码
// 模拟 new Tracer(42) 的 C 等价写法
Tracer *p = (Tracer*)std::malloc(sizeof(Tracer));
if (!p) { /* 处理分配失败 */ }
// 在已分配的内存上手动调用构造函数(placement new)
new (p) Tracer(42);

// 模拟 delete p 的 C 等价写法
p->~Tracer();          // 手动调用析构函数
std::free(p);

这就是 new/delete 的真面目:内存分配 + 对象构造/析构 的组合。malloc/free 只处理内存,不处理对象。


二、new vs malloc 的核心差异表

维度 malloc/free new/delete
语言 C 标准库函数 C++ 运算符/表达式
头文件 <cstdlib> 内置,无需头文件
返回类型 void*,需要手动强转 目标类型的指针,类型安全
构造/析构 不调用 调用构造函数和析构函数
计算大小 需要手动 sizeof(T) 编译器自动计算
失败行为 返回 NULL(或 nullptr 抛出 std::bad_alloc 异常
重载 不能重载 可以重载 operator new
数组支持 malloc(n * sizeof(T)) new T[n] / delete[] p
释放时传大小 free(p) 不需要大小 delete p 不需要大小

三、混用的后果:未定义行为

混用 new/freemalloc/delete 是 C++ 中的高频错误。来看实际后果:

3.1 new + free:析构函数被跳过

cpp 复制代码
#include <iostream>
#include <string>

int main() {
    // ❌ 错误:new 分配,却用 free 释放
    std::string *s = new std::string("hello, world");
    std::cout << *s << '\n';
    std::free(s);  // 没有调用 ~string()------内部 char 缓冲区泄漏!
}

std::string 内部在堆上分配了字符缓冲区。free 只释放了 std::string 对象本身(24 或 32 字节),但 std::string 内部管理的堆缓冲区永远泄漏了------因为析构函数没有被调用。

3.2 malloc + delete:在垃圾数据上执行析构函数

cpp 复制代码
#include <iostream>
#include <string>

int main() {
    // ❌ 错误:malloc 分配,却用 delete 释放
    void *raw = std::malloc(sizeof(std::string));
    std::string *s = static_cast<std::string*>(raw);
    // raw 里的内存是未初始化的垃圾数据!
    // s->size() 可能是 42 亿,s->data() 可能是任意地址
    // 但我们现在不去碰它------我们直接 delete:

    delete s;  // delete 会调用 ~string()
    // ~string() 认为对象是活的,试图释放"内部指针"------
    // 但这个指针是垃圾值------崩溃或堆损坏!
}

3.3 即使对"简单类型"也成立

intdouble 等基本类型,实践中 new int + free 可能在大多数平台上"刚好能跑"------因为 int 没有析构函数。但这仍然是未定义行为,换一个平台、换一个编译器版本就可能崩溃。不要依赖。

铁律:new 必须配 deletemalloc 必须配 freenew[] 必须配 delete[]


四、new/delete 的数组版本

4.1 new[]delete[]

cpp 复制代码
// 分配单个对象
T *p = new T(args...);
delete p;

// 分配数组
T *arr = new T[n];     // 调用 n 次默认构造函数
delete[] arr;          // 调用 n 次析构函数

new[]delete[] 是一对。用 delete 释放 new[] 的数组同样是未定义行为:

cpp 复制代码
std::string *arr = new std::string[3]{"a", "b", "c"};
// delete arr;    // ❌ 未定义行为!只调用了 arr[0] 的析构函数
delete[] arr;     // ✅ 为三个元素都调用析构函数

4.2 operator new[] 在干什么

当你写 new T[3] 时,编译器生成的代码大致如下:

cpp 复制代码
// 伪代码:new T[3] 的底层行为
size_t total_size = sizeof(size_t) + sizeof(T) * 3;  // 前面多存一个"元素个数"
void *raw = operator new[](total_size);
*(size_t*)raw = 3;          // 在内存头部记录元素个数
T *arr = (T*)((char*)raw + sizeof(size_t));  // 对象从偏移 sizeof(size_t) 开始
for (size_t i = 0; i < 3; ++i)
    new (&arr[i]) T();      // placement new 构造每个元素

delete[] 时,编译器从内存头部读取元素个数,然后逆序析构:

cpp 复制代码
// 伪代码:delete[] arr 的底层行为
size_t *count_ptr = (size_t*)((char*)arr - sizeof(size_t));
size_t n = *count_ptr;
for (size_t i = n; i > 0; --i)
    arr[i-1].~T();          // 逆序析构
operator delete[](count_ptr);

这就是为什么 deletedelete[] 不能混用------delete 不会去读那个隐藏的元素计数字段,以为只有一个对象,只调一次析构。


五、new 的失败处理:不是返回 nullptr

5.1 默认行为:抛异常

cpp 复制代码
#include <iostream>
#include <new>  // std::bad_alloc

int main() {
    try {
        // 尝试分配 10 亿 GB------会失败
        int *p = new int[1'000'000'000'000ULL];
        std::cout << "分配成功(不可能)\n";
        delete[] p;
    } catch (const std::bad_alloc &e) {
        std::cout << "分配失败: " << e.what() << '\n';
    }
}
text 复制代码
$ g++ -std=c++17 bad_alloc.cpp && ./a.out
分配失败: std::bad_alloc

5.2 nothrow 版本:和 malloc 一样返回 nullptr

如果你确实想要 malloc 式的返回 nullptr 行为:

cpp 复制代码
#include <iostream>
#include <new>  // std::nothrow

int main() {
    int *p = new (std::nothrow) int[1'000'000'000'000ULL];
    if (!p) {
        std::cout << "分配失败,p 是 nullptr\n";
    } else {
        delete[] p;
    }
}

5.3 malloc 失败返回 NULL

cpp 复制代码
#include <cstdlib>
#include <cstdio>

int main() {
    void *p = std::malloc(SIZE_MAX);  // 不可能成功的大小
    if (!p) {
        std::printf("malloc 失败\n");
    }
}

一种常见的"防御性写法"病:在 C++ 中用 new 然后判 nullptr

cpp 复制代码
int *p = new int[100];
if (!p) {  // 这个分支永远不会执行!new 失败不会返回 nullptr
    // 白写的代码
}

六、operator newnew 表达式

这是最常见的概念混淆------C++ 中 "new" 这个词指两件不同的事。

6.1 new 表达式 vs operator new 函数

cpp 复制代码
// new 表达式(new-expression)
T *p = new T(args...);

// 它等价于:
void *raw = T::operator new(sizeof(T));   // 步骤 1:分配内存
// 或者:void *raw = ::operator new(sizeof(T));  如果 T 没有重载
p = new (raw) T(args...);                 // 步骤 2:在内存上构造对象

operator new 只是一个分配函数------它只负责分配内存,不负责构造对象。你可以像调用普通函数一样调用它:

cpp 复制代码
#include <iostream>
#include <new>

class Foo {
public:
    Foo() { std::cout << "Foo()\n"; }
};

int main() {
    // 调用 operator new------仅分配内存,不调用构造函数
    void *raw = Foo::operator new(sizeof(Foo));
    std::cout << "内存已分配,但没有 Foo 对象存在\n";

    // 手动构造
    Foo *f = new (raw) Foo();  // placement new------在指定地址构造

    // 手动析构
    f->~Foo();

    // 释放内存
    Foo::operator delete(raw);
}
text 复制代码
$ g++ -std=c++17 operator_new.cpp && ./a.out
内存已分配,但没有 Foo 对象存在
Foo()

6.2 自定义 operator new

这是 C++ 独有的能力------你可以在类级别或全局级别替换内存分配策略:

cpp 复制代码
#include <iostream>
#include <new>

class Instrumented {
public:
    Instrumented() { std::cout << "Instrumented()\n"; }
    ~Instrumented() { std::cout << "~Instrumented()\n"; }

    // 重载 operator new
    static void* operator new(size_t size) {
        std::cout << "operator new(" << size << ")\n";
        void *p = std::malloc(size);
        if (!p) throw std::bad_alloc();
        return p;
    }

    // 重载 operator delete(必须成对重载)
    static void operator delete(void *p, size_t size) noexcept {
        std::cout << "operator delete(" << size << ")\n";
        std::free(p);
    }

private:
    int data_[100];
};

int main() {
    auto *obj = new Instrumented();
    delete obj;
}
text 复制代码
$ g++ -std=c++17 custom_new.cpp && ./a.out
operator new(400)
Instrumented()
~Instrumented()
operator delete(400)

注意:你可以在 operator new 中实现内存池、对齐分配、日志记录等自定义行为。malloc 做不到这点。


七、placement new:在指定地址构造对象

7.1 基本用法

cpp 复制代码
#include <iostream>
#include <new>

class Widget {
public:
    Widget(int x) : x_(x) { std::cout << "Widget(" << x_ << ")\n"; }
    ~Widget() { std::cout << "~Widget(" << x_ << ")\n"; }
    int x() const { return x_; }
private:
    int x_;
};

int main() {
    // 在栈上预留一块对齐的内存作为"对象池"
    alignas(Widget) char buffer[sizeof(Widget)];

    // placement new------在 buffer 地址上构造 Widget
    Widget *w = new (buffer) Widget(10);
    std::cout << "w->x() = " << w->x() << '\n';

    // 必须手动调用析构函数------不能 delete
    w->~Widget();  // placement new 分配的对象,析构后不能 delete
    // delete w;  // ❌ w 指向的是栈上的 buffer,delete 会崩溃
}
text 复制代码
$ g++ -std=c++17 placement_new.cpp && ./a.out
Widget(10)
w->x() = 10
~Widget(10)

placement new 是 C++ 标准库提供的一个重载版本------它不做任何内存分配,只调用构造函数。

7.2 placement new 的真实定义

cpp 复制代码
// 标准库中 placement new 的定义(简化版):
inline void* operator new(size_t, void *place) noexcept {
    return place;  // 什么都没分配,直接返回传入的地址
}

八、new 在底层是否调用了 malloc

这个问题在面试中极为常见,答案是:大多数实现中,是的,但不保证。

8.1 GCC/Clang 的实际情况

在 GCC 和 Clang 中,默认的 ::operator new 底层确实调用了 malloc

cpp 复制代码
// libstdc++ 中 operator new 的简化实现:
void* operator new(std::size_t size) {
    if (size == 0) size = 1;  // 禁止零大小分配
    void *p;
    while ((p = std::malloc(size)) == nullptr) {
        // 调用 new_handler 如果用户设置了
        std::new_handler handler = std::get_new_handler();
        if (handler) {
            handler();
        } else {
            throw std::bad_alloc();
        }
    }
    return p;
}

但这不意味着你可以把 newmalloc 用------调用构造函数这一步骤是关键

8.2 为什么不保证

其他实现(如某些嵌入式平台的 C++ 运行时)的 operator new 可能直接管理系统页表,完全不经过 malloc。依赖 new 等价于 malloc 的代码是不可移植的。


九、new/delete 的现代 C++ 替代方案

到这里,你可能觉得 new/delete 已经比 malloc/free 强很多了。但现代 C++ 的建议是:new/delete 都尽量少用。

传统方式 现代替代 理由
new T / delete p std::make_unique<T>() / std::make_shared<T>() 自动管理生命周期
new T[n] std::vector<T> 自动扩容,自动释放
new char[n] std::string / std::vector<char> 自动管理缓冲区
placement new std::optional<T> / std::variant<T> 类型安全的延迟初始化
自定义 operator new std::allocator<T> / std::pmr::memory_resource 标准化的分配器接口

这就是本系列接下来两篇文章要展开的内容:智能指针和 RAII。


总结

new/delete 不是 malloc/free 的包装------它们是两个不同层次的工具:

  1. malloc/free 只管理原始内存:不调用构造/析构函数,不关心类型系统
  2. new/delete 管理完整对象生命周期:分配内存 + 调用构造函数 = 创造活的对象;调用析构函数 + 释放内存 = 消灭对象
  3. new 表达式 ≠ operator new 函数:前者是语言层面的构造+分配,后者是可重载的内存分配原语
  4. new 失败抛异常malloc 失败返回 nullptr------判空是白写的
  5. new[] 必须配 delete[] :编译器在 new[] 时偷偷存了元素个数,delete[] 才能正确析构
  6. placement new 只在已有内存上构造对象------不分配、不释放,必须手动析构
  7. 底层实现不保证 :大多数平台上 operator new 确实调 malloc,但不可移植地依赖这一点就是埋坑

第3章的第一个地基已经打好。下一篇------智能指针:unique_ptrshared_ptr 的基本用法 ------我们将看到如何告别手动 new/delete,让编译器替你管理对象生命周期。


📝 动手练习

  1. 写一个带构造/析构打印的类,分别用 new/deletemalloc/free 分配释放,对比输出差异
  2. 故意用 new 分配一个 std::string 数组但用 delete(不是 delete[])释放------观察只有第一个元素析构了
  3. 重载一个类的 operator newoperator delete,在每次分配/释放时打印日志和当前分配次数
  4. 用 placement new 在一个栈上的 char buffer[1024] 中构造和析构一个对象,确认没有堆分配发生
    . 底层实现不保证 :大多数平台上 operator new 确实调 malloc,但不可移植地依赖这一点就是埋坑

第3章的第一个地基已经打好。下一篇------智能指针:unique_ptrshared_ptr 的基本用法 ------我们将看到如何告别手动 new/delete,让编译器替你管理对象生命周期。


📝 动手练习

  1. 写一个带构造/析构打印的类,分别用 new/deletemalloc/free 分配释放,对比输出差异
  2. 故意用 new 分配一个 std::string 数组但用 delete(不是 delete[])释放------观察只有第一个元素析构了
  3. 重载一个类的 operator newoperator delete,在每次分配/释放时打印日志和当前分配次数
  4. 用 placement new 在一个栈上的 char buffer[1024] 中构造和析构一个对象,确认没有堆分配发生
相关推荐
IT_陈寒1 小时前
Vue的computed属性怎么突然不更新了?
前端·人工智能·后端
方向研究1 小时前
人类的核心能力
人工智能
测试员周周1 小时前
【Appium 系列】第18节-重试与容错 — 移动端测试的稳定性保障
人工智能·python·功能测试·ui·单元测试·appium·测试用例
l1t2 小时前
Hy-MT2-1.8B总结的pgvector 0.8.2解决了并行HNSW索引构建漏洞
数据库·人工智能·postgresql
太华2 小时前
学习AI Agent编程-第二天-LangGraph ReAct模式实现
人工智能
dayuOK63072 小时前
从“爆款复刻”到“个性化创作”:AI辅助写作的技术挑战与演进方向
人工智能·职场和发展·自动化·新媒体运营·媒体
Raink老师2 小时前
【AI面试临阵磨枪-58】AI 生成内容合规、版权、审核机制设计
人工智能·面试·职场和发展
lizhihai_992 小时前
股市学习心得-与英伟达核心 PCB 相关的八家关联企业
大数据·人工智能·学习
嗝o゚2 小时前
昇腾CANN ops-nn 仓的 Activation 算子:不只是 ReLU
人工智能·cann·ops-nn