Simon 's Blog
» 做笔记做笔记
Toggle navigation
Simon 's Blog
HOME
总裁介绍
coper
zongcai
what
ARCH
TAGS
navigation
!!! c++:动态库接口函数返回stl对象的设计原则塈‘__acrt_first_block == header’异常
? C/C++ ?
2022-02-10 14:14:32
34
0
0
simon88
? C/C++ ?
版权声明:本文为转载文章,转载请注明源地址,原文地址: [https://cloud.tencent.com/developer/article/1434344](https://cloud.tencent.com/developer/article/1434344) # 问题描述 最近在写dll动态库时,动态库函数返回的`std::string`对象在析构时抛出了异常:  为简化描述问题,测试代码如下(MSVC /MT 编译),就是返回一个简单的`std::string` tools.h ```javascript # if defined(_WIN32) && !defined(__CYGWIN__) # ifdef GFAUX_EXPORTS # define GAX_API __declspec(dllexport) # else # define GAX_API __declspec(dllimport) # endif # else # define GAX_API # endif #include <string> // 返回一个std::string GAX_API std::string test(); ``` tools.cpp ```javascript #include "tools.h" std::string test() { return std::string("hello!!!!!"); } ``` # 原因分析 关于`__acrt_first_block == header`异常,google上查了一下,根本的原因是对象在析构时不正确的释放内存导致的。stackoverflow上这篇文章的回复写得比较清晰:[Debug Assertion Failed! Expression: __acrt_first_block == header](https://stackoverflow.com/questions/35310117/debug-assertion-failed-expression-acrt-first-block-header?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa) `std::string`是STL中定义的模板类,所以编译器在编译动态库时会将`std::string`实例化,在编译exe时也会将其实例化,也就是说有两套`std::string`实例代码分别在exe和dll中. 因为我的dll是/MT编译的所以连接的是static crt,所以动态库有自己独立的heap,参见参考资料3. 那么问题来了: 如下面的exe调用代码,当`test()`返回一个`std::string`对象给exe时,这个对象的内存是由dll分配的。但在exe中并不能区分这个`std::string`对象的内存是不是自己的的heap中分配的。在main结束时要析构result,会调用exe中实例化的`std::string`析构函数代码来释放内存,然后就会抛出`__acrt_first_block == header`异常。 调用测试代码 main.cpp ```javascript #include <iostream> #include "tools.h" int main(int argc, char *argv[]) { std::string result = test();// 从dll返回std::string,result的内存是由dll分配的 std::cout << result << std::endl; } // 析构result时抛出异常 ``` 如果和exe和动态库都是`/MD`编译,不会存在上述问题,因为大家使用同一个heap,内存在哪里释放都是一样的。 但我的项目需要必须用静态链接(`/MT`)所以不能通过修改动态库的编译方式的方法解决问题。 # 解决方案 知道了原因,就可以推导出解决问题的关键在于不能让exe去析构dll返回的`std::string`,简单的办法就是在dll中定义一个只包含一个`std::string`类型成员的`class A`,`test()`返回类型改为`class A`,这样以来exe就不再直接析构`std::string`,而是析构dll中的`class A`,`class A`在析构成员时就能正确释放在当前dll中heap分配的内存了。 如果为每个需要封装的类型都定义一个`class A`也够烦的,所以可以把这个`class A`设计成一个模板类`raii_dll`,它不干别的,只是为了正确释放dll或exe中的对象。代码如下: ```javascript /* 用于dll分配的资源T的raii管理类,析构时自动正确释放资源 * T为资源类型,外部不可修改 */ template<typename T> class raii_dll { public: typedef raii_dll<T> _Self; typedef T resource_type; /* 默认构造函数 */ raii_dll() :_resource() {} /** res 资源对象 */ explicit raii_dll(const T& res) : _resource(res) { } /* 获取资源引用 */ const T& get() const noexcept { return _resource; } const T& operator*() const noexcept { return get(); } const T& operator()() const noexcept { return get(); } /* 成员指针引用运算符 */ const T* operator->()const noexcept { return &get(); } private: /* 封装的资源对象,外部不可修改 */ T _resource; }; /* raii_dll */ ``` 请注意为了确保dll返回的对象不会被赋值为exe的内存对象,这里`get()`返回的是常量引用(`const &`) 有了`raii_dll`这个模板类,我们可以重新设计一下`test()`的接口定义 tools.h ```javascript # if defined(_WIN32) && !defined(__CYGWIN__) # ifdef GFAUX_EXPORTS # define GAX_API __declspec(dllexport) # else # define GAX_API __declspec(dllimport) # endif # else # define GAX_API # endif #include <string> #include "raii_dll.h" // 实例化并导出模板raii_dll,确保只在dll中有一份raii_dll<std::string>实例代码 template class GAX_API raii_dll<std::string>; // 返回raii_dll<std::string> GAX_API raii_dll<std::string> test(); ``` tools.cpp ```javascript #include "tools.h" raii_dll<std::string> test() { return raii_dll<std::string>("hello!!!!!"); } ``` 调用测试代码也同步修改 main.cpp ```javascript #include <iostream> #include "tools.h" int main(int argc, char *argv[]) { raii_dll<std::string> result = test(); // 调用operator()返回对象引用 std::cout << result() << std::endl; } ``` # 总结 通过这次跳坑填坑的经历,针对动态的接口设计可以总结几点设计原则,以避免上述的问题,就可以传递复杂类型: 1. 动态库设计接口时,应该避免直接返回stl类型,如果不可避免(比如本例),就封装将其成一个类返回(可以照搬本文的方法) 2. 动态库接口函数的输入/出参数如果是class,应尽量设计为常量引用(`const &`),不允许被修改。 如本例,如果允许`raii_dll`中的`_resource`被exe重新赋值,程序立即就崩了。 # 参考资料 1. [《Debug Assertion Failed! Expression: __acrt_first_block == header》](https://stackoverflow.com/questions/35310117/debug-assertion-failed-expression-acrt-first-block-header?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa) 2. [《跨DLL的内存分配释放问题 Heap corruption》](https://blog.csdn.net/zj510/article/details/35290505) 3. [《Windows 下主程序与动态库(*.dll)释放对方分配的内存操作要点》](http://blog.sina.com.cn/s/blog_4c451e0e0100u9gu.html) 本文参与[腾讯云自媒体分享计划](https://cloud.tencent.com/developer/support-plan),欢迎正在阅读的你也加入,一起分享。
上一篇:
cmd递归删除指定文件
下一篇:
git操作
0
赞
34 人读过
新浪微博
微信
腾讯微博
QQ空间
人人网
提交评论
0
条评论
More...
<>