首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >面试周刊(4):类自定义 new/delete ?三阶段(编译/链接/运行)一口气讲透

面试周刊(4):类自定义 new/delete ?三阶段(编译/链接/运行)一口气讲透

作者头像
早起的鸟儿有虫吃
发布2025-11-24 18:43:00
发布2025-11-24 18:43:00
1260
举报

各位老师好!

这是CPP面试冲刺周刊 (c++ weekly) 陪你一起快速冲击大厂面试 第四期

周刊目标

不是成为C++专家,而是成为C++面试专家

本期内容

如何为一个类自定义new/delete

一页PPT 解释: (回答有逻辑)

维度

① 类内 new/delete

② 全局 new/delete(弱符号)

③ LD_PRELOAD + tcmalloc

生效时机

编译期(静态绑定)

链接期(符号强弱覆盖)

运行期(动态装载器优先级)

作用范围

仅该类/基类查找链

全局(除已绑定到类内的 new)

全局(动态库与主程序)

机制

作用域查找 + 非虚(无 vtable)

弱符号 vs 强符号

预加载库先解析同名符号

是否依赖弱符号

是(libstdc++ 默认全局 new 为 weak)

否(依赖 ld.so 解析顺序)

源码是否需改动

需要(在类内写函数)

需要(提供全局 operator new/delete)

不需要(设置环境变量即可)

常见用途

类级内存池、对齐、对象复用

全局策略(统计/替换分配器/统一注入)

线上快速切换分配器/排查内存/零侵入部署

已生成图片
已生成图片

题目来源C++ Primer 第 18 章节

Exercise 18.9: Declare members new and delete for the QueueItem class.

18.1.6 Class Specific new and delete

Exercise 18.6: Reimplement your Vector class to use operator new, operator delete, placement new, and direct calls to the destructor.

课本上的题目绝对经典
课本上的题目绝对经典

课本上的题目绝对经典

整体知识看板(看文末):

第一周:c++基础知识高频面试题解析【当前位置】

第二周: 专注分布式存储,数据库广告搜索 Ai 辅助驾驶 大厂热门后端开发岗位拆解。

第三周:系统架构设计,用未来 10 年发展目标,重新设计原来系统

开始

一、面试官:如何为一个类自定义new/delete

C++ 对象的内存分配默认依赖 全局 operator new / operator delete, 本质上调用 libc malloc/free,最终走 系统调用 brk/mmap

答案可能很简单(这绝对不是最终结果,面试官要反问的)

代码语言:javascript
复制
#include <iostream>
using namespace std;

struct A {
    void* operator new(size_t sz) {
        cout << "A::operator new, size = " << sz << endl;
        return ::operator new(sz);
    }
    void operator delete(void* p) {
        cout << "A::operator delete" << endl;
        ::operator delete(p);
    }
};
int main() {
    A* a = new A;  // 输出: A::operator new, size = 1
    delete a;      // 输出: A::operator delete
}

如何和已有知识,一步步结合起来,

下面是我推导过程,可能意想不到发现

二、小青回答(工作0-3 年 青铜)

如何如何为一个类自定义new/delete 完全之前从来没有遇到过问题, 很简单,平时不总结,别指望 面试当成超常发挥,

我猜 你可能这样回顾之前准备

new 和 malloc 有什么区别

new 申请失败返回 NULL 还是抛出异常

自定义一个类函数,虚函数可以吗,好像不行

operator new 还是选择 placement new

暂停 思绪无限发散

重新整理 历史题目的的关系

1.1 普通的函数:

函数特性 重载(overload),隐藏(hide),覆盖(override)

重载(overload):在相同访问内(一个类),函数名相同,参数不同(c 语言不支持这样重载

隐藏(hide): 不同范围内 ,派生类定义与基类同名非虚函数时,基类同名函数被隐藏

覆盖 / 重写(Override):基类虚函数被派生类虚函数覆盖,运行期判断

C++多态必要条件 ① 继承(this 指针) ② 虚函数重写 , ③ 父类指针/引用指向子类对象

画外音:目前就是动态绑定无法解决这个问题,

operator new 是默认 static 函数,static 函数无法访问 this 指针,不支持运行时多态

1.2 普通函数,库函数,系统调用

参考:序员的自我修养:链接、装载与库

new 是c++运算符,不能重载,语法规定的

operator new 是c++ libstdc++ 标准库函数,静态函数,静态函数自然是不能是虚函数

代码语言:javascript
复制
_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)

malloc 是 c 语言 glibc 标准库的库函数

brk是系统调用

演示:

new 实现调用2个函数

1

内存分配 调用全局的 operator new 函数为对象分配足够大小的内存。(4字节)

2

调用 Foo构造对象

3

返回对象指针 构造完毕后返回分配并初始化后的对象指针。

代码语言:javascript
复制

 Foo* ptr = new Foo();
   │
   ▼
查找类内 operator new
   │
   ├─存在 → 调用类内 new
   └─不存在 → 调用全局 ::operator new
   │
   ▼
内存分配(malloc / 内存池 / 用户态栈)
   │
   ▼
调用构造函数
   │
   ▼
返回对象指针



1.3 c++角度无法解决这个问题了,c 语言不支持重载,如何解决重载问题 弱符号,编译期静态绑定

概念

实现方式

发生时机

影响范围

函数重载 (overloading)

同一作用域内多个函数签名不同

编译期

仅 C++ 语法

虚函数覆盖 (overriding)

子类重写父类虚函数

运行时

仅多态场景

符号覆盖 (symbol overriding)

通过弱符号 + 链接器选择强符号

链接期

整个可执行文件/动态库

如何验证 libstdc++ 的 operator new 是弱符号

我们可以直接用 nm 命令验证:

nm -C /usr/lib/x86_64-linux-gnu/libstdc++.so | grep "operator new"

典型输出类似:

0000000000098e80 W operator new(unsigned long) 0000000000098ed0 W operator new[](unsigned long) 0000000000098f20 W operator delete(void*)

这里的 W 就表示 weak symbol

为什么 libstdc++ 把 operator new/delete 定义为弱符号

来看 libstdc++ 的源码(以 GCC 13.x 为例):

注意到 _GLIBCXX_WEAK_DEFINITION,在 GCC 平台上一般展开为:

#define _GLIBCXX_WEAK_DEFINITION __attribute__ ((weak))

这意味着 libstdc++ 提供的 operator new 是弱符号

原因:为了支持用户自定义重载

在 Linux 下,符号解析遵循 ELF 链接规则

弱符号(weak):如果存在同名的强符号(strong),那么最终可执行文件会绑定到强符号。

强符号(strong):用户自己实现的 operator new 会被视为强符号。

因此:

如果用户自己实现了全局 operator new会自动覆盖 libstdc++ 提供的版本。

如果用户没有实现,则默认使用 libstdc++ 的弱符号实现。

这就是为什么 libstdc++ 必须把它们标记为弱符号,否则用户无法重载

到这里你是否感觉找到最终答案 NO,c++继承复杂之处在这里, 上面operator new是全局符号,如果有继承的类呢?

三 、小白回答(工作 3-5 年 白银)

如果面试官问:

为什么类内 operator new/delete 是静态绑定,而全局 operator new/delete 是弱符号?

可以回答:

1

全局版本是由 libstdc++ 提供的默认实现,在源码中标记了 __attribute__((weak)),用户可以通过定义自己的全局版本来覆盖默认实现 → 弱符号机制

2

类内版本是一个 普通静态成员函数,绑定过程在 编译期完成,属于 静态绑定,不会走虚函数表,也不会受弱符号影响。

3

当类内和全局版本同时存在,类内优先

30 秒总结自定义类实现 new 和/delete:

第一是类内 operator new/delete,纯编译期静态绑定,作用域查找,不涉及弱符号;

第二层是全局 operator new/delete,libstdc++ 默认实现是弱符号,你提供强符号即可链接期覆盖;

第三层是LD_PRELOAD + tcmalloc,通过动态装载器在运行时优先解析同名符号,实现零改码劫持。

三者分别对应编译期、链接期、运行期三个阶段,

优先级依次是:类内静态绑定 > 运行期预加载 > 链接期弱符号

2.1 深入理解 C++ new/delete:类内静态绑定 vs 全局弱符号机制

维度

类内 operator new/delete

全局 ::operator new/delete

作用域

限于该类及其派生类

全局可见

绑定时机

编译期静态绑定

链接期符号解析

多态性

❌ 无多态

❌ 无多态

弱符号

❌ 不依赖弱符号

✅ 是弱符号

优先级

优先于全局 ::operator new

最后兜底

代码语言:javascript
复制
#include <iostream>
#include <new>
struct Base
{
	static void* operator new(std::size_t size)
	{
		std::cout << "Base new\n";
		return ::operator new(size);
	}
};

struct Derived : Base {

};

int main()
{
	//A class-specific operator new is looked up in the scope of the class and is not virtual.

	Derived* p = new Derived; // 调用 Base::operator new 还是 Derived::operator new?
}

编译期静态绑定 new Derived 的查找顺序是:

1、Derived 中查找 operator new

2、如果没找到,查找 Base

3、如果都没找到,最后使用全局 ::operator new

Derived 没有定义 operator new,所以直接用到了 Base::operator new

•这不是多态,不需要虚函数表。

最终结论

•类内 operator new/delete

是静态成员函数

编译期静态绑定

和弱符号无关

•全局 operator new/delete

libstdc++ 提供默认实现

被标记为 weak symbol

用户可覆盖

2.2 编译期,链接期,运行期

维度

① 类内 new/delete

② 全局 new/delete(弱符号)

③ LD_PRELOAD + tcmalloc

生效时机

编译期(静态绑定)

链接期(符号强弱覆盖)

运行期(动态装载器优先级)

作用范围

仅该类/基类查找链

全局(除已绑定到类内的 new)

全局(动态库与主程序)

机制

作用域查找 + 非虚(无 vtable)

弱符号 vs 强符号

预加载库先解析同名符号

是否依赖弱符号

是(libstdc++ 默认全局 new 为 weak)

否(依赖 ld.so 解析顺序)

源码是否需改动

需要(在类内写函数)

需要(提供全局 operator new/delete)

不需要(设置环境变量即可)

常见用途

类级内存池、对齐、对象复用

全局策略(统计/替换分配器/统一注入)

线上快速切换分配器/排查内存/零侵入部署

① 类内 operator new/delete(静态绑定)
代码语言:javascript
复制
源码:
struct Base {
  static void* operator new(std::size_t);
};
struct Derived : Base {};

new Derived
   │
   ├─ 编译器做“名字查找”(先 Derived,后 Base)
   │
   ├─ 若 Derived 未定义 → 绑定到 Base::operator new   ←←← 静态绑定(非多态)
   │
   └─ 生成直接调用指令(无 vtable,无符号覆盖参与)


要点

编译期决定,不走弱符号、不走 vtable。

仅影响该类(及查找到的基类作用域),优先级高于全局

② 全局 operator new/delete(弱符号覆盖,链接期)

代码语言:javascript
复制
源文件们  ──(编译)──► 目标文件们(含符号表) ──(链接)──► 可执行文件 / so

libstdc++ 提供:
  _GLIBCXX_WEAK_DEFINITION
  void* ::operator new(std::size_t);     ← 弱符号(weak)

你的工程若提供:
  void* ::operator new(std::size_t);     ← 强符号(strong)

链接器规则:

  若同名强符号存在 → 选择强符号(你的全局 new)

  否则 → 选择弱符号(libstdc++ 默认 new)


LD_PRELOAD + tcmalloc(动态链接优先级,运行期)

代码语言:javascript
复制
运行命令:
  LD_PRELOAD=/usr/lib/libtcmalloc.so ./app

动态装载器 ld.so 加载顺序:
  1) 先装载 LD_PRELOAD 指定的 so(优先级最高)
  2) 再装载主程序与其依赖的其它 so(libc, libstdc++, ...)

符号解析:
  当需要解析 "malloc/free/new/delete" 时
  ├─ 如果在预加载的 tcmalloc.so 中已定义 → 直接绑定到 tcmalloc 版本
  └─ 否则继续在后续库中查找(如 libc)

效果:
  不改源码,即可把 malloc/free/new/delete 劫持到 tcmalloc

要点

•运行时生效,由动态装载器决定符号优先级。

对主程序和动态库均可生效(除非静态链接/受限环境)。

常用于快速切换分配器/排查内存问题

四、小王回答( 工作 5-10 年 ):用起来

工作 10 年和工作 3 年 在知识不会任何新增 ,结合 3fs 代码说明

4.1 DeepSeek 3FS 灵活内存池实现策略

自定义:用默认系统的

自定义:用第三方库的

自定义:自己实现,这个不是分布式存储重点,没有实现。

代码分析:

1. 定义宏开关 CMakeLists.txt
代码语言:javascript
复制
option(OVERRIDE_CXX_NEW_DELETE "Override C++ new/delete operator" OFF)
2. 重载全局函数 operator new/delete

src\memory\common\OverrideCppNewDelete.h

代码

代码语言:javascript
复制

#ifdef OVERRIDE_CXX_NEW_DELETE

// 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); }

#endif

3. dlopen方式加载 动态库

src/memory/common/GlobalMemoryAllocator.cc

代码语言:javascript
复制

static void loadMemoryAllocatorLib()
	void *mallocLib = nullptr;
	GetMemoryAllocatorFunc getMemoryAllocatorFunc = nullptr;
	mallocLib = ::dlopen(mallocLibPath, RTLD_NOW | RTLD_GLOBAL);

	gAllocator = getMemoryAllocatorFunc();

//
//这里通过环境变量 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
void *allocate(size_t size)

	if (gAllocator == nullptr)

		mem = std::malloc(allocateSize);

	else
		mem = gAllocator->allocate(allocateSize)

提供 GetMemoryAllocatorFunc 函数指针类型用于通过固定导出符号加载实现(如 dlsym("getMemoryAllocator")):

代码语言:javascript
复制

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 *(*)();

} // namespace hf3fs::memory


4.2 疑问:智能指针在自定义分配器情况不能调用默认的 delete 操作

要点

谁分配,谁释放;分配与释放函数必须匹配。

对象池/自定义分配接口返回的内存,务必使用自定义 deleter;默认 delete 不适用。

启用 3FS 全局 new/delete 重载时,默认智能指针 deleter 可直接用

代码语言:javascript
复制
  void* mem = hf3fs::memory::memalign(alignof(My), sizeof(My));
  My* obj = new (mem) My(...);

  auto deleter = [](My* p){
    p->~My(); //可以直接调用析构函数
    hf3fs::memory::deallocate(p); //归还到内存池
  };

  std::unique_ptr<My, decltype(deleter)> up(obj, deleter);
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-08-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 后端开发成长指南 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、面试官:如何为一个类自定义new/delete
  • 二、小青回答(工作0-3 年 青铜)
    • 1.1 普通的函数:
    • 1.2 普通函数,库函数,系统调用
      • 1.3 c++角度无法解决这个问题了,c 语言不支持重载,如何解决重载问题 弱符号,编译期静态绑定
  • 三 、小白回答(工作 3-5 年 白银)
    • 2.1 深入理解 C++ new/delete:类内静态绑定 vs 全局弱符号机制
    • 2.2 编译期,链接期,运行期
      • ② 全局 operator new/delete(弱符号覆盖,链接期)
      • ③ LD_PRELOAD + tcmalloc(动态链接优先级,运行期)
  • 四、小王回答( 工作 5-10 年 ):用起来
    • 4.1 DeepSeek 3FS 灵活内存池实现策略
      • 3. dlopen方式加载 动态库
      • 4.2 疑问:智能指针在自定义分配器情况不能调用默认的 delete 操作
      • 要点
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档