
🌟 大家好,我是小王同学,
今天带你们一起探秘内存分配的奥秘! 拉倒最有也彩蛋
本文主要描述了如何为一个类自定义new实现
Happy Coding~💖

工作8年的小王 同学 为了准备c++面试, 很早 就从脉脉上搜索过类似题目,信心满满而去, 什么返回值区别,重载区别?还有函数用法区别?

经典题目
结果吊打一顿回来,在中午吃饭的时候,被老王看到了。
哈哈哈,又拒绝一家公司是对吧? 来说说这次发生了什么,有什么意想不到事情。
面试官:请说说 new 和malloc区别?
小王脑中2小人开始不假思索推理 开来?
于是我回答了
new 是运算符,运算符就支持重载,因为operator new 。。。。(这个回答是错误的)

运算符
老王:
从你回答慢了 2-3秒 速度来看 , 你思路产生混乱,被题目本身限制住了 更没有和日常开发工作有效结合起来。
❝划重点: 你知道,别人也知道,你了解还是10年前认知。没长进呀。 这个才是重点探索内容。
小提示
(1)C libstdc++**: https://gcc.gnu.org/libstdc++/
在标准库实现中,你可以找到 std::allocator、std::unique_ptr 和 std::shared_ptr 的实现方式。
一些高性能的 C++ 项目也有自定义 new/delete 的实现,你可以参考它们的代码:
开始 干活
演示代码:
https://github.com/watchpoints/master-cpp/blob/main/cpp/03_new.cpp
#include <iostream>
class Foo
{
public:
int number;
Foo(int n)
{
number = n;
}
};
int main() {
Foo* ptr = new Foo(10);
return0;
}
老王说:借助https://godbolt.org/z/YWcecfG7b 这个工具

汇编实现
看不懂汇编没关系,直接让大模型来解释
以下是添加了详细注释的汇编代码,解释每一条指令的作用:
__cxx_global_var_init:
push rbp # 1. 保存当前栈帧指针
mov rbp, rsp # 2. 设置新的栈帧
sub rsp, 32 # 3. 在栈上分配 32 字节的局部变量空间
mov edi, 4 # 4. 设置 operator new 的参数,分配 4 字节的空间
call operator new(unsigned long)@PLT # 5. 调用 operator new 进行内存分配
mov rdi, rax # 6. 将分配的地址存入 rdi(作为 Foo::Foo(int) 构造函数的 this 指针)
mov rax, rdi # 7. 复制 rdi 到 rax
mov qword ptr [rbp - 24], rax # 8. 将 this 指针存入栈中的局部变量(用于后续操作)
mov esi, 10 # 9. 设置 Foo::Foo(int) 构造函数的参数,传递值 10
call Foo::Foo(int) [base object constructor] # 10. 调用 Foo 类的构造函数
jmp .LBB0_1 # 11. 跳转到 .LBB0_1 继续执行后续逻辑
call 指令 就是调用函数
new 实现调用2个函数
❝划重点: More Effective C++ Item 8: Understand the different meanings of new and delete.
This operator is built into the language and, like sizeof, you can’t change its meaning: it always does the same thing.
What it does is twofold.
The new operator always does those two things; you can’t change its behavior in any way


More Effective C++

More Effective C++
老王:根据上面分析结果 不妨大胆猜测一下。
+----------------------+
| new 关键字 (C++) | [libstdc++.so]
| 调用 operator new |
+----------------------+
|
v
+----------------------+
| std::operator new | [libstdc++.so]
| (分配内存, 调用 malloc) |
+----------------------+
|
v
+----------------------+
| malloc() (glibc) | [libc.so]
| 分配内存 根据大小判断 |
+----------------------+
|
+-----+------+
| |
v v
+----------------------+ +----------------------+
| brk() (堆扩展) | | mmap() (大块内存) |
| size ≤ 128 KB | | size > 128 KB |
| [syscall to kernel] | | [syscall to kernel] |
+----------------------+ +----------------------+
| |
v v
+----------------------+ +----------------------+
| Linux 内核管理 | | Linux 内核管理 |
| 分配物理内存 | | 分配独立映射区域 |
+----------------------+ +----------------------+
各函数对应动态库
函数 | 作用 | 所在动态库 |
|---|---|---|
new | 申请对象并调用构造函数 | libstdc++.so |
std::operator new | 内存分配函数(调用 malloc) | libstdc++.so |
malloc | 申请动态内存 | libc.so |
brk | 调整堆(小内存) | libc.so (syscall) |
mmap | 申请大块内存(独立映射) | libc.so (syscall) |
free | 释放内存 | libc.so |
请 回到 演示代码部分,编译代码 ldd 查看,new 确实调用 libstdc++.so.6
ldd a.out linux-vdso.so.1 (0x00007fff6f9c0000) libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f5117c52000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5117a29000) libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f5117942000) /lib64/ld-linux-x86-64.so.2 (0x00007f5117e8c000) libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f511792200
老王:格局没打开 看看这个



简单来说,GCC是编译器,它负责将C++源代码编译成可执行程序。 在这个过程中,GCC会链接到Libc和Libstdc++这两个库。 Libc为GCC提供了底层的C语言接口, 而Libstdc++则为GCC提供了C++标准库的实现
libc是Linux下原来的标准C库,也就是当初写hello world时包含的头文件#include < stdio.h> 定义的地方。
后来逐渐被glibc取代
那glibc都做了些什么呢? glibc是Linux系统中最底层的API,几乎其它任何的运行库都要依赖glibc。 glibc最主要的功能就是对系统调用的封装,你想想看,你怎么能在C代码中直接用fopen函数就能打开文件? 打开文件最终还是要触发系统中的sys_open系统调用,而这中间的处理过程都是glibc来完成的。
特性 | glibc (libc) | libc++ | libstdc++ |
|---|---|---|---|
用途 | 提供 C 语言标准库实现 | 提供 C++ 标准库实现 | 提供 C++ 标准库实现 |
支持平台 | 主要用于 Linux 操作系统 | 主要与 Clang 编译器配合使用 | 主要与 GCC 编译器配合使用 |
内容 | 包括所有 C 标准库函数,操作系统接口、线程支持等 | 提供 C++ 标准库的模板库(STL)、IO流等 | 提供 C++ 标准库的模板库(STL)、IO流等 |
库功能 | 系统调用、内存管理、线程库、文件操作等 | C++ STL,算法,IO流等 | C++ STL,算法,IO流等 |
重入性与线程安全 | 提供线程安全的库支持,包含锁等线程控制功能 | 提供线程安全支持(但不一定是内建的) | 提供线程安全支持,包含多线程功能 |
自然会: 在 C++ 开发中,大多数程序都会使用标准库。**g++ 自动链接 libstdc++**,(你不说我就不知道的事情) 可以减少用户的额外操作,提高编译的便捷性,同时避免因忘记手动链接库而导致的错误。因此,这是一种用户友好的设计选择
如果使用 `gcc` 编译 C++ 代码(不推荐),
就需要手动添加 `-lstdc++` 选项来链接标准库
gcc 03_new.cpp
/usr/bin/ld: /tmp/ccZKC4VY.o: in function `main':
03_new.cpp:(.text+0x13): undefined reference to `operator new(unsigned long)'
gcc 03_new.cpp -lstdc++(需要手工制定)
g++ -v test.cpp -o test COLLECT_GCC=g++ ... /usr/lib/gcc/x86_64-linux-gnu/9/collect2 ... ... -lstdc++ ...
std::operator new 有关系, 但是他真实一个函数吗?operator是C++的关键字,它和运算符一起使用,表示一个运算符函数。
再次表示 new 运算符是不可以重载的,网上说错误的。

image.png
代码:
https://github.com/gcc-mirror/gcc/blob/master/libstdc++-v3/libsupc++/new_op.cc
// A freestanding C runtime may not provide "malloc" -- but there is no
// other reasonable way to implement "operator new".
// 一些独立的 C 运行时环境可能不提供 "malloc",但没有其他合理的方法来实现 "operator new"。
// 因此,这里直接声明 `malloc` 函数,保证后续调用不会出错。
extern"C"void *malloc (std::size_t);
#endif
// `operator new` 的弱定义版本,可以被用户自定义版本覆盖。
// 当 new 关键字被调用时,会执行此函数以分配内存。
_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
void *p;
// `malloc(0)` 在不同的 C 标准库实现中可能会有不确定的行为(返回 nullptr 或者有效指针)。
// 为了避免这种不确定性,如果 `sz == 0`,则将其设为 1,确保 `malloc` 至少分配 1 字节。
if (__builtin_expect (sz == 0, false))
sz = 1;
// 循环尝试调用 `malloc`,直到成功分配内存或者 `new_handler` 处理失败
while ((p = malloc (sz)) == 0) // 如果 `malloc(sz)` 返回 `nullptr`,表示内存分配失败
{
// 获取当前的 new_handler(可以通过 `std::set_new_handler` 设定)
new_handler handler = std::get_new_handler ();
// 如果没有设置 `new_handler`,则直接抛出 `std::bad_alloc` 异常,终止程序。
if (!handler)
_GLIBCXX_THROW_OR_ABORT(bad_alloc());
// 调用用户自定义的 `new_handler`,该处理程序可能会尝试释放一些内存,
// 或者执行其他策略(如记录日志、延迟执行),以便下一次 `malloc` 可能成功。
handler ();
}
// 成功分配内存,返回指针
return p;
}
https://github.com/gcc-mirror/gcc/blob/master/libstdc++-v3/libsupc++/new_op.cc
// ------------------ BEGIN COPY ------------------
// Implement all new and delete operators as weak definitions
// in this shared library, so that they can be overridden by programs
// that define non-weak copies of the functions.
static void* operator_new_impl(std::size_t size) {
if (size == 0)
size = 1;
void* p;
while ((p = std::malloc(size)) == nullptr) {
// If malloc fails and there is a new_handler,
// call it to try free up memory.
std::new_handler nh = std::get_new_handler();
if (nh)
nh();
else
break;
}
return p;
}
❝划重点: 为什么
new被视为高级抽象?,operator new 人为第水平管理内存方式
malloc 或 calloc,然后需要手动管理内存,包括初始化对象。new 运算符提供了一个更高级的抽象,不仅仅是内存分配,还自动调用对象的构造函数来初始化对象。new 时,如果内存分配失败,它会抛出一个 std::bad_alloc 异常,这使得开发者不需要显式地检查 malloc 返回的 NULL 值。这样的异常处理提供了一种更加安全和优雅的内存管理方式。operator new 函数可以被重载,但 C++ 语言本身提供了透明的内存分配接口,允许开发者使用 new 运算符而不关心内存分配的细节。这种抽象化让开发者集中于高层次的程序逻辑,而不是低级的内存管理。https://en.cppreference.com/w/cpp/memory https://en.cppreference.com/w/cpp/memory/new

image.png
特性 | malloc (函数) | new (运算符) |
|---|---|---|
是否是函数? | ✅ 是普通函数 | 不是函数,是运算符 |
是否调用构造函数? | 不会调用 | ✅ 会调用构造函数 |
返回类型 | void*,需要强制转换 | 直接返回正确的指针类型 |
是否可以重载? | ❌ 不能重载 | ✅ operator new 可以被重载 |
是否支持类型安全? | ❌ 需要手动转换类型 | ✅ 自动匹配类型 |
释放方式 | free(ptr); | delete ptr; |
失败时返回 | nullptr / NULL | 抛出异常(std::bad_alloc) |
老王:

image.png
推荐看一本书 程序员的自我修养—链接、装载与库 补一补。

最喜欢一句话 Any problem in computer science can be solved by anotherlayer of indirection.”
这句话几乎概括了计算机系统软件体系结构的设计要点,整个体系结构从上到下都是按照严格的层次结构设计的

PLT 是 ELF 文件中的一块代码区域,用于动态链接时的函数调用延迟绑定。
也就是说,当程序第一次调用一个外部函数(如来自共享库的函数)时, 实际调用的是 PLT 中的一个“跳板”入口,经过解析后再跳转到真正的函数地址;
而后续调用则直接通过更新过的 GOT 表项(全局偏移表)跳转到目标函数。
假设有下面的 C 程序
#include <stdio.h>
int main() {
printf("Hello, PLT!\n"); //使用libc这个动态库
return 0;
}
当你编译这个程序(使用动态链接库,例如 libc)后,编译器不会直接把 printf 的真实地址写进代码中,而是生成一条调用“printf@PLT”的指令。
符号(Symbol)这个概念随着汇编语言的普及迅速被使用,它用来表示一个地址,这个地址可能是一段子程序(后来发展成函数)的起始地址,也可以是一个变量的起始地址。
也就是说,程序在运行时会先跳转到 PLT 表中的一个“跳板”入口,而不是直接跳转到 libc 中的 printf 函数。
#动态链接中PLT与GOT工作流程
❝大局观
首先, 我们要知道, GOT和PLT只是一种重定向的实现方式. 所以为了理解他们的作用, 就要先知道什么是重定向, 以及我们为什么需要重定向
比如某个公司开发完成了某个产品,它按照一定的规则制定好程序的接口,其他公司或开发者可以按照这种接口来编写符合要求的动态链接文件。
该产品程序可以动态地载入各种由第三方开发的模块,在程序运行时动态地链接,实现程序功能的扩展
弱符号主要用于:
operator new 就是一个典型案例,标准库提供 默认的弱定义,用户可以 提供自己的强定义 进行替换。动手验证:
#include <iostream>
#include <cstdlib>
// 自定义 operator new(强定义)
void* operator new(std::size_t size) {
std::cout << "Custom operator new called, size: " << size << std::endl;
returnmalloc(size);
}
int main() {
int* p = newint; // 这里会调用自定义的 operator new
delete p;
return0;
}
输出
Custom operatornew called, size: 4
- `operatornew` 在标准库中是弱符号(`__attribute__((weak))`)。
- 当程序员提供了自己的 `operatornew`,它作为 **强符号** 替换了标准库的实现。
- 因此,`new` 关键字调用的是自定义的 `operatornew`,而不是标准库的版本。
阅读代码:3FS\src\memory\common\OverrideCppNewDelete.h
// Override global new/delete with custom memory allocator.
// 用强符号代替标准库的弱符号
void *operator new(size_t size) { return hf3fs::memory::allocate(size); }
void operator delete(void *mem) noexcept { hf3fs::memory::deallocate(mem); }
inline void *allocate(size_t size) { return std::malloc(size); }
上面三个章节 解释了 int* ptr =new int(10) 这个高级抽象
调用c++标准库 operaotr new函数-->c语言标准库 malloc --glibc 和os底层先不考虑。
+----------------------+
| new 关键字 (C++) | [c++语法]
| 调用 operator new |
+----------------------+
|
v
+----------------------+
| std::operator new | [libstdc++.so]
| (分配内存, 调用 malloc) |
+----------------------+
|
v
+----------------------+
| malloc() (glibc) | [glibc.so]
| 分配内存 根据大小判断 |
+----------------------+
|
+-----+------+
| |
v v
+----------------------+ +----------------------+
| brk() (堆扩展) | | mmap() (大块内存) |
| size ≤ 128 KB | | size > 128 KB |
| [syscall to kernel] | | [syscall to kernel] |
+----------------------+ +----------------------+
| |
v v
+----------------------+ +----------------------+
| Linux 内核管理 | | Linux 内核管理 |
| 分配物理内存 | | 分配独立映射区域 |
C++ new 操作的完整调用路径
new 操作
├── operator new(size) // 调用标准库中的 operator new
│ ├── malloc(size) // 调用 C 语言的 malloc
│ │ ├── _int_malloc() // glibc 内部分配
│ │ │ ├── sbrk() // 小块分配(扩展 heap)
│ │ │ ├── mmap() // 大块分配(独立映射)
│ │ └── 返回分配的内存地址
│ └── 返回指针
├── 构造函数初始化对象
└── 返回对象指针
代码位置: https://github.com/lattera/glibc/blob/master/malloc/malloc.c
老王:
在使用GCC编译器时,如果不想工程使用系统的库函数, 例如在自己的工程中可以根据选项来控制是否使用系统中提供的malloc/free函数,可以有两种方法:
(1). 使用LD_PRELOAD环境变量:可以设置共享库的路径,并且该库将在任何其它库之前加载,即这个动态库中符号优先级是最高的。
(2). 使用GCC的–wrap选项:
下面用简单的语言解释一下 GCC 的 –wrap 选项是如何工作的,以及如何用它来替换(拦截)一个函数,比如 malloc:
--wrap=symbol 是一个链接选项。它的作用是在链接阶段“拦截”对某个函数(symbol)的调用,让这些调用转而去调用另一个函数(包装函数)。Use a wrapper function for symbol. Any undefined reference to symbol will be resolved to __wrap_symbol
--wrap=malloc 时,所有对 malloc 的调用都会被重定向到 __wrap_malloc。这就好像你给 malloc 装上了一个“包装纸”,让调用者实际使用的是包装后的版本。实际操作:
malloc 时,链接器把它转换成对 __wrap_malloc 的调用。malloc,你可以直接调用 __real_malloc。链接器会把 __real_malloc 转换回真正的 malloc 实现。简单示例:
假设我们想对 malloc 进行包装:
// wrap.c
#include <stdio.h>
#include <stdlib.h>
void* __real_malloc(size_t size); // 只声明不定义__real_malloc
void* __wrap_malloc(size_t size) // 定义__wrap_malloc
{
printf("__wrap_malloc called, size:%zd\n", size); // log输出
return __real_malloc(size); // 通过__real_malloc调用真正的malloc
}
下面是测试用例:
// test.c
#include <stdio.h>
#include <stdlib.h>
int main()
{
char* c = (char*)malloc(sizeof(char)); // 调用malloc
printf("c = %p\n", c);
free(c); // 调用free,防止内存泄漏
return 0;
}
下面是编译链接过程:
gcc -c wrap.c test.c
gcc -Wl,--wrap,malloc -o test wrap.o test.o // 链接参数-Wl,--wrap,malloc12
结果查看:
./test
### 3. C++ 中的特殊注意
- **C++ 名字修饰问题:**
如果你用 C++ 来实现包装函数(比如 __wrap_malloc),一定要在函数声明前加上 `extern "C"`。
这样做可以防止 C++ 的名字修饰(name mangling),确保链接器能正确找到 `__wrap_malloc`。
示例:
```cpp
extern "C" void* __wrap_malloc(size_t size) {
// 包装逻辑
return __real_malloc(size);
}
```
---
## 3FS 是怎么做的
- 定义宏开关
CMakeLists.txt
option(OVERRIDE_CXX_NEW_DELETE "Override C++ new/delete operator" OFF)
- 宏开关开启不同特性(c语言没有c++函数重载这个特性)
```c
src\memory\common\OverrideCppNewDelete.h
#ifdef OVERRIDE_CXX_NEW_DELETE
void *allocate(size_t size)
#else
inline void *allocate(size_t size) { return std::malloc(size); }
gAllocator = getMemoryAllocatorFunc();
void *allocate(size_t size) {
//这里通过环境变量 MEMORY_ALLOCATOR_LIB_PATH 来指定要加载的内存分配器库。
//例如,如果你想使用jemalloc.tcmalloc:
//set MEMORY_ALLOCATOR_LIB_PATH=D:\path\to\jemalloc.dll
//set MEMORY_ALLOCATOR_LIB_PATH=D:\path\to\tcmalloc.dll
if (!gAllocatorInited) {
std::call_once(gInitOnce, loadMemoryAllocatorLib);
gAllocatorInited = true;
}
//- 首次调用内存分配相关函数时触发加载
void *mem;
if (gAllocator == nullptr)
mem = std::malloc(allocateSize);
// 使用系统默认分配器
else
mem = gAllocator->allocate(allocateSize);
// 使用自定义分配器
static MemoryAllocatorInterface *gAllocator = nullptr
#define GET_MEMORY_ALLOCATOR_FUNC_NAME "getMemoryAllocator"
//这种设计允许在运行时动态切换不同的内存分配器实现,提供了很好的灵活性和可扩展性。
namespace hf3fs::memory {
class MemoryAllocatorInterface {
public:
virtual ~MemoryAllocatorInterface() = default;
virtual void *allocate(size_t size) = 0;
virtual void deallocate(void *mem) = 0;
virtual void *memalign(size_t alignment, size_t size) = 0;
virtual void logstatus(char *buf, size_t size) = 0;
virtual bool profiling(bool active, const char *prefix) = 0;
};
using GetMemoryAllocatorFunc = MemoryAllocatorInterface *(*)();
//这种设计允许在运行时动态切换不同的内存分配器实现,提供了很好的灵活性和可扩展性。
//这种设计允许在运行时动态切换不同的内存分配器实现,提供了很好的灵活性和可扩展性。
代码位置:3FS\src\memory\jemalloc
class JemallocMemoryAllocator : public MemoryAllocatorInterface
void *allocate(size_t size) override { return je_malloc(size); }
bool profiling(bool active, const char *prefix) override
extern "C" {
hf3fs::memory::MemoryAllocatorInterface *getMemoryAllocator() {
static hf3fs::memory::JemallocMemoryAllocator jemalloc;
return &jemalloc;
}
class CustomMemoryAllocator :public MemoryAllocatorInterface
void *allocate(size_t size) override {
// 实现内存分配逻辑
returnmalloc(size);
}
// 导出获取分配器的函数
extern"C"MemoryAllocatorInterface* getMemoryAllocator() {
static CustomMemoryAllocator allocator;
return &allocator;
}
基本概念:https://www.cnblogs.com/Anker/p/3746802.html
static void loadMemoryAllocatorLib() {
GetMemoryAllocatorFunc getMemoryAllocatorFunc = nullptr;
constchar *mallocLibPath = std::getenv("MEMORY_ALLOCATOR_LIB_PATH");
mallocLib = ::dlopen(mallocLibPath, RTLD_NOW | RTLD_GLOBAL);
//dlsym 函数每次调用只能查找和返回一个符号(函数或变量)的地址。
//# 获取函数的运行时地址
getMemoryAllocatorFunc = (GetMemoryAllocatorFunc)::dlsym(mallocLib, GET_MEMORY_ALLOCATOR_FUNC_NAME);
//GetMemoryAllocatorFunc
工作流程是:
1. 通过 dlsym 获取函数指针
2. 调用该函数指针获取分配器实例
3. 将实例保存到全局变量 gAllocator [疑问 这个我没看懂 怎么关联的]
4. 后续的内
要优缺点:
编译时链接 :
生命周期
我是小王同学,
希望帮你深入理解分布式存储系统3FS更进一步 , 为了更容易理解设计背后原理,这里从一个真实面试场故事开始的。