__acrt_first_block == header’異常

c++:動態庫接口函數返回stl對象的設計原則塈‘__acrt_first_block == header’異常

 

 

版權聲明:本文爲博主原創文章,轉載請註明源地址。 https://blog.csdn.net/10km/article/details/80522287

問題描述

最近在寫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)釋放對方分配的內存操作要點》
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章