用 C 语言模拟 new / delete:基于回调的内存分配封装
背景
在 C 项目中,malloc / free 是最底层的内存分配接口,但直接使用存在两个常见痛点:
- 分配失败时只能返回
NULL,调用方必须逐一检查,容易遗漏。 - 缺少一个统一的"分配失败后尝试回收再重试"的机制。
C++ 的全局 operator new 在分配失败时会调用已注册的 new handler,给程序一次释放内存后重试的机会。本文用纯 C 实现了一个类似的封装,函数名直接沿用 operatorNew / operatorDelete,方便在 C 代码中形成与 C++ 一致的阅读习惯。
头文件声明
//include "util.h"
#ifndef __XM_LOGIC_WIN_UTIL_H__
#define __XM_LOGIC_WIN_UTIL_H__
#include "xm_logic_define.h"
#include "xm_logic_view_list.h"
#include "xm_logic_gdi.h"
typedef void (*NewHandler)();
void setNewHandler(NewHandler newHandler);
NewHandler getNewHandler();
void* operatorNew(int size);
void operatorDelete(void* ptr);
#endif要点:
NewHandler是一个无参、无返回值的函数指针类型,注册后在分配失败时被调用。operatorNew(int size)接收字节数,返回void*。operatorDelete(void* ptr)接收指针,内部调用free。
实现
operatorNew 的分配-重试循环
核心逻辑是一个 while 循环:
- 先尝试
malloc(size)。 - 若返回
NULL,取出已注册的NewHandler并调用它(期望该回调释放一些内存)。 - 回到循环顶部再次
malloc。
C++ 标准中全局 operator new 的失败路径与此类似:分配失败时调用 new handler,若 handler 正常返回则重试,如此循环直到分配成功或 handler 抛出异常(通常是 std::bad_alloc)。本 C 实现模拟了其中的重试循环,但 C 没有异常机制,因此若 handler 未能释放足够内存,循环将不会自行终止(详见下文"风险与边界")。
operatorDelete
对非空指针调用 free。注意末尾的 ptr = NULL; 只是修改了局部形参,不会影响调用方的指针变量,所以这行实际上没有效果。如果希望调用方指针也置空,需要传入 void**。
_new / _delete 包装
_new 在分配成功后用 memcpy 把源对象浅拷贝到新内存,_delete 直接转发给 operatorDelete。这两个函数适合在 C 中模拟"值语义"的对象复制,但 memcpy 是浅拷贝,若对象内含指针成员,调用方需自行处理深拷贝。
风险与边界
- 无限循环:如果
NewHandler没有真正释放内存(或忘记注册 handler),while循环会永远重试。生产代码中通常应加一个最大重试次数,或在 handler 中abort()。 - size == 0:代码将其修正为 1,避免部分平台
malloc(0)返回NULL(C 标准将malloc(0)的行为定义为 implementation-defined),从而与"分配失败"的语义混淆。这是合理的防御,但调用方仍应避免传入 0。 - 线程安全:
s_newHandler是全局静态变量,setNewHandler/getNewHandler没有加锁。多线程环境下并发读写该指针存在数据竞争风险。 - operatorDelete 的形参置空:如前所述,
ptr = NULL对调用方无效,不要依赖这行来防止悬空指针。 - 平台依赖:
malloc的行为(尤其是malloc(0))在不同 C 库实现中可能不同,上述size == 0的修正可以规避这一差异。
验证建议
- 单元测试中注册一个会释放预分配内存的 handler,验证分配失败后重试成功的路径。
- 注册一个空 handler(不释放任何内存),配合超时或重试上限,验证不会死循环。
- 多线程场景下对
setNewHandler加锁后再做并发分配测试。 - 对
_new传入含指针成员的结构体,确认浅拷贝后修改源对象指针不影响副本(或按预期影响)。
小结
这套封装把 C++ 全局 operator new 的"失败-回调-重试"语义搬到了纯 C 中,代码量很小,适合在嵌入式或遗留 C 项目中作为统一分配入口使用。使用时需要特别注意重试上限、线程安全和浅拷贝边界,避免把 C++ 的异常语义(new 失败抛 std::bad_alloc)直接套用到 C 的 NULL 返回模型上。
本文最初于 2023/6/19 发布在 CSDN。
相关文章
评论
正在读取评论…