前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >c++:动态库接口函数返回stl对象的设计原则塈‘__acrt_first_block == header’异常

c++:动态库接口函数返回stl对象的设计原则塈‘__acrt_first_block == header’异常

作者头像
10km
发布2019-05-26 01:14:08
4K0
发布2019-05-26 01:14:08
举报
文章被收录于专栏:10km的专栏10km的专栏10km的专栏

版权声明:本文为博主原创文章,转载请注明源地址。 https://cloud.tencent.com/developer/article/1434344

问题描述

最近在写dll动态库时,动态库函数返回的std::string对象在析构时抛出了异常:

为简化描述问题,测试代码如下(MSVC /MT 编译),就是返回一个简单的std::string

tools.h

#  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

#include "tools.h"
std::string test()
{
	return std::string("hello!!!!!");
}

原因分析

关于__acrt_first_block == header异常,google上查了一下,根本的原因是对象在析构时不正确的释放内存导致的。stackoverflow上这篇文章的回复写得比较清晰:Debug Assertion Failed! Expression: __acrt_first_block == header

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

#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 Atest()返回类型改为class A,这样以来exe就不再直接析构std::string,而是析构dll中的class A,class A在析构成员时就能正确释放在当前dll中heap分配的内存了。

如果为每个需要封装的类型都定义一个class A也够烦的,所以可以把这个class A设计成一个模板类raii_dll,它不干别的,只是为了正确释放dll或exe中的对象。代码如下:

	/* 用于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

#  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

#include "tools.h"
raii_dll<std::string> test()
{
	return raii_dll<std::string>("hello!!!!!");
}

调用测试代码也同步修改

main.cpp

#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》
  2. 《跨DLL的内存分配释放问题 Heap corruption》
  3. 《Windows 下主程序与动态库(*.dll)释放对方分配的内存操作要点》
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018年05月31日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问题描述
  • 原因分析
  • 解决方案
  • 总结
  • 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档