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 A
,test()
返回類型改爲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;
}
總結
通過這次跳坑填坑的經歷,針對動態的接口設計可以總結幾點設計原則,以避免上述的問題,就可以傳遞複雜類型:
- 動態庫設計接口時,應該避免直接返回stl類型,如果不可避免(比如本例),就封裝將其成一個類返回(可以照搬本文的方法)
- 動態庫接口函數的輸入/出參數如果是class,應儘量設計爲常量引用(
const &
),不允許被修改。 如本例,如果允許raii_dll
中的_resource
被exe重新賦值,程序立即就崩了。