首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >在一个模块里分配、在另一个模块里释放:跨模块内存管理的经典坑

在一个模块里分配、在另一个模块里释放:跨模块内存管理的经典坑

原创
作者头像
PC电脑医生
发布于 2026-09-24 12:44:50
发布于 2026-09-24 12:44:50
1440
举报

模块拆分之后,有一类崩溃特别难查:代码看起来完全对称,但就是会崩,而且崩得随机。

典型写法是这样的:模块 A 分配一块内存,把指针交给模块 B,由 B 在合适的时机释放。

逻辑上很整齐。但它隐含了一个前提——两边必须使用同一套运行时库、同一份堆。而这个前提在很多工程里并不成立。

一、前提为什么容易不成立

第一种情况:运行时库的链接方式不同。

如果模块各自静态链接了运行时库,那么每个模块都带着自己的一份——各自维护各自的堆。

这时候,在 A 的堆上分配的内存,用 B 的释放函数去释放,行为是未定义的。不是"可能出错",而是没有任何保证。

第二种情况:运行时库版本不同。

不同版本、不同编译工具链的运行时,不保证可以互相操作内存。混用是隐患。

第三种情况:调试版本与发布版本混用。

两套运行时并存时,这个问题更容易显形。

所以“谁分配谁释放”不是代码风格偏好,而是一条正确性要求。 也正因为它是设计层面的问题,DLL修复工具 这类补齐文件的工具无从下手。

二、会造成什么后果

后果一:直接在释放处崩溃。

堆校验失败,程序立刻结束。这一类还算"友好",至少位置明确。

后果二:随机崩溃,位置与原因相距很远。

堆的内部结构被破坏后,崩溃往往延迟发生——破坏发生在释放那一刻,但报错出现在之后某次无关的分配上。这是最难查的一类,也解释了为什么这类崩溃用 DLL修复工具 扫描不出任何异常。

后果三:内存缓慢泄漏。

释放调用去了错误的堆,等于没释放。程序长期运行后内存持续增长。

三、正确做法:把所有权写进接口

第一条,谁分配谁释放。

这不是一句口号,要落到接口设计上:提供"返回内存"的接口,就同时提供对应的"销毁内存"接口,由同一个模块负责两端。

代码语言:cpp
复制
// allocate and free inside the same module; pass only the pointer across
extern "C" MYLIB_API char* CreateBuffer(int size);
extern "C" MYLIB_API void  DestroyBuffer(char* buf);

第二条,跨模块只传数据与句柄,不传"需要对方释放的内存"。

句柄是标识,谁持有都能用,但它背后的资源仍由原模块管理——这样所有权不会跨过边界。

第三条,统一运行时库。

同一进程内,各模块要么都用同一版本的动态运行时,要么都做一致的处理。避免"一个静态、一个动态"这种组合。

第四条,接口成对提供。

只提供创建、不提供销毁的接口,等于强制调用方去猜该由谁释放。这是设计缺陷,不是使用者的错。

四、一个常见误解:传结构体就安全了吗

很多人知道"不要跨模块传裸指针",于是改成传结构体。这只解决了一半。

按值传递的结构体是拷贝——拷贝出来的内存由被调方自己管理,不涉及跨模块释放。这部分是安全的。

但如果结构体里有指针成员,那个指针指向的内存的归属问题依旧存在。

判断标准要看跨边界的是什么:

  • 一个数值、一个按值结构体:否,拷贝
  • 一个句柄(标识):否,资源仍由原模块持有
  • 一块内存的指针 / 分配出来的缓冲区:是,必须明确谁释放
  • 一个对象(含虚函数):是,且还牵扯类型身份问题

最后一行是两类问题叠加:所有权不清 %2B 类型身份不等同,崩溃概率成倍上升。

五、同类问题:对象的生命周期

模块 A 创建对象交给模块 B 使用与销毁——除了内存归属,还多了一层:这个对象的方法在哪个模块里执行、它依赖哪一份运行时状态。

所以接口文档里必须写清三件事:谁创建、谁销毁、所有权是否可以转让。含糊的接口在单模块内测不出问题,一到拆分就暴露。

六、怎么排查这类崩溃

三个特征,凑齐两条就该怀疑:

  • 崩溃发生在释放或析构处,但调用方看起来毫无问题
  • 崩溃位置随机、难以复现(堆结构被破坏的典型表现)
  • 只在某个模块参与时才出现(例如只在插件启用时)

一个很有效的验证顺序:

  1. 先确认"分配与释放是否跨了模块"
  2. 检查各模块的运行时库链接方式与版本是否一致
  3. 统一运行时库、或改成谁分配谁释放,然后复测

如果第 3 步之后问题消失,基本可以确认是这一类。

顺带说明:这类问题在原地打转是没有出路的——它不是运行环境的问题,也不是文件问题,所以修补类的 DLL修复工具 在这儿派不上用场:模块全都在、版本也没错,出错的是内存被交给了不该释放它的那一方。

七、按现象定位

  • 释放时立刻崩溃(原因方向:堆不匹配;处理方向:改成谁分配谁释放)
  • 崩溃位置随机、延迟出现(原因方向:堆结构已被破坏;处理方向:同上,并优先恢复堆一致性检查)
  • 内存长期缓慢增长(原因方向:释放去了错误的堆;处理方向:检查所有权归属)
  • 只在启用某模块时出问题(原因方向:跨模块内存交接;处理方向:审查该模块的接口约定)
  • 传了结构体仍然崩(原因方向:结构体里含指针成员;处理方向:按"值 vs 资源"重新判断)
  • 调试版正常、发布版崩溃(原因方向:运行时库版本或方式不一致;处理方向:统一运行时库)
  • 单模块测试正常、拆分后异常(原因方向:所有权与类型身份问题叠加;处理方向:明确接口的所有权约定)

八、小结

关于跨模块的内存管理,记住四条:

  • "谁分配谁释放"是正确性要求,不是风格问题——静态链接时各模块各有一份堆,混用没有保证
  • 最麻烦的后果是随机崩溃:堆被破坏后报错延迟出现,位置与原因相距很远
  • 接口要成对提供创建与销毁,跨边界只传数据与句柄,不传需要对方释放的内存
  • 传结构体不等于安全:要按"跨边界的是值还是资源"来判断

这类问题有个共同的背景:一旦把代码拆成多个模块,就要同时管理"内存归属"和"类型身份"两件事。 它们都不是文件层面的问题——文件齐备、版本正确,照样会崩;所以排查时要往接口约定和运行时配置上看,而不是往"缺什么补什么"上想。

https://www.ijinshan.com/functions/repairdll.html?channel=4074

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

目录
  • 一、前提为什么容易不成立
  • 二、会造成什么后果
  • 三、正确做法:把所有权写进接口
  • 四、一个常见误解:传结构体就安全了吗
  • 五、同类问题:对象的生命周期
  • 六、怎么排查这类崩溃
  • 七、按现象定位
  • 八、小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档