文章目录
- 引言
- [一、`new` 做了三件事,`malloc` 只做了一件](#一、
new做了三件事,malloc只做了一件) -
- [1.1 拆解 `new` 的执行步骤](#1.1 拆解
new的执行步骤) - [1.2 用 C 模拟 `new` 的行为](#1.2 用 C 模拟
new的行为)
- [1.1 拆解 `new` 的执行步骤](#1.1 拆解
- [二、`new` vs `malloc` 的核心差异表](#二、
newvsmalloc的核心差异表) - 三、混用的后果:未定义行为
-
- [3.1 `new` + `free`:析构函数被跳过](#3.1
new+free:析构函数被跳过) - [3.2 `malloc` + `delete`:在垃圾数据上执行析构函数](#3.2
malloc+delete:在垃圾数据上执行析构函数) - [3.3 即使对"简单类型"也成立](#3.3 即使对"简单类型"也成立)
- [3.1 `new` + `free`:析构函数被跳过](#3.1
- [四、`new`/`delete` 的数组版本](#四、
new/delete的数组版本) -
- [4.1 `new[]` 和 `delete[]`](#4.1
new[]和delete[]) - [4.2 `operator new[]` 在干什么](#4.2
operator new[]在干什么)
- [4.1 `new[]` 和 `delete[]`](#4.1
- [五、`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 new与new表达式) -
- [6.1 `new` 表达式 vs `operator new` 函数](#6.1
new表达式 vsoperator new函数) - [6.2 自定义 `operator new`](#6.2 自定义
operator new)
- [6.1 `new` 表达式 vs `operator new` 函数](#6.1
- [七、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
这个理解是错误的,而且是危险的。 混用 new 和 free,或 malloc 和 delete,会导致从内存泄漏到未定义行为的各种问题。
new 和 malloc 之间,隔着一个完整的对象生命周期。本文从底层细节出发,把这对差异说清楚。
一、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/free 或 malloc/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 即使对"简单类型"也成立
对 int、double 等基本类型,实践中 new int + free 可能在大多数平台上"刚好能跑"------因为 int 没有析构函数。但这仍然是未定义行为,换一个平台、换一个编译器版本就可能崩溃。不要依赖。
铁律:new 必须配 delete,malloc 必须配 free,new[] 必须配 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);
这就是为什么 delete 和 delete[] 不能混用------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 new 与 new 表达式
这是最常见的概念混淆------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;
}
但这不意味着你可以把 new 当 malloc 用------调用构造函数这一步骤是关键。
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 的包装------它们是两个不同层次的工具:
malloc/free只管理原始内存:不调用构造/析构函数,不关心类型系统new/delete管理完整对象生命周期:分配内存 + 调用构造函数 = 创造活的对象;调用析构函数 + 释放内存 = 消灭对象new表达式 ≠operator new函数:前者是语言层面的构造+分配,后者是可重载的内存分配原语new失败抛异常 ,malloc失败返回nullptr------判空是白写的new[]必须配delete[]:编译器在new[]时偷偷存了元素个数,delete[]才能正确析构- placement new 只在已有内存上构造对象------不分配、不释放,必须手动析构
- 底层实现不保证 :大多数平台上
operator new确实调malloc,但不可移植地依赖这一点就是埋坑
第3章的第一个地基已经打好。下一篇------智能指针:unique_ptr 与 shared_ptr 的基本用法 ------我们将看到如何告别手动 new/delete,让编译器替你管理对象生命周期。
📝 动手练习:
- 写一个带构造/析构打印的类,分别用
new/delete和malloc/free分配释放,对比输出差异- 故意用
new分配一个std::string数组但用delete(不是delete[])释放------观察只有第一个元素析构了- 重载一个类的
operator new和operator delete,在每次分配/释放时打印日志和当前分配次数- 用 placement new 在一个栈上的
char buffer[1024]中构造和析构一个对象,确认没有堆分配发生
. 底层实现不保证 :大多数平台上operator new确实调malloc,但不可移植地依赖这一点就是埋坑
第3章的第一个地基已经打好。下一篇------智能指针:unique_ptr 与 shared_ptr 的基本用法 ------我们将看到如何告别手动 new/delete,让编译器替你管理对象生命周期。
📝 动手练习:
- 写一个带构造/析构打印的类,分别用
new/delete和malloc/free分配释放,对比输出差异- 故意用
new分配一个std::string数组但用delete(不是delete[])释放------观察只有第一个元素析构了- 重载一个类的
operator new和operator delete,在每次分配/释放时打印日志和当前分配次数- 用 placement new 在一个栈上的
char buffer[1024]中构造和析构一个对象,确认没有堆分配发生