当前位置:C++技术网 > 资讯 > 如何将C++对象导出到DLL,直接看DLL内部解析

如何将C++对象导出到DLL,直接看DLL内部解析

更新时间:2016-10-25 15:05:22浏览次数:1+次

如何将C++对象导出到DLL

摘要:文章将会说到,如何导出C++对象到DLL,有什么,需要注意的事项,以及面向接口编程。

 

1、  为什么

程序需要模块化,DLL很好解决这个分工,相互调用的问题。而且DLLwindows编程的重点,迟早都会接触到。用C函数封装成DLL接口,提供别人调用,有一个很大的优势:只需要函数名字,以及该函数的声明,仅此而已,有时候我们会将所有的函数声明都放到一个头文件提供调用,有时候却不能这么做,以免其他的函数声明调用暴露出来。

但是缺点很明显,全部都是函数。看上去并没有面向对象(即使里面是用了面向对象思想),这使得我们调用的接口都必须用不同的命名,如toPoint2DtoPoint3DMessageBoxA MessageBoxW。如果我们要把一个类的所有功能都用C接口重新封装,以便其他人使用里面的全部功能,那么我们就必须每一个公有成员函数都封装成C接口,通常是这样:toString,toArray那么如果我们两个类里面都有同名的成员函数,如Point2D里面有toStringPoint3D里面也有toString,我们通常会这么做 [ClassName]_[FunctionName] 也就是Point2D_toStringPoint3D_toString来区别。这么做会十分的麻烦

那么如果我们能把自己写的C++类能够通过DLL以供其他人使用,那不是很好吗?不用再次封装,也不用考虑重名问题。当然导出的条件也是有限制的,请看注意事项

2、  实例

class  _declspec(dllexport) DLLClass
{
public:
         typedef std::unordered_map<std::string,float> Attr;
         const std::string& getName()const { return _name; }
         const Attr& getAttr()const { return _keyValue; }
         const int& getIndex()const { return _index; }
private:
         std::string _name;
         int _index;
         Attr _keyValue;
};

当我们要导出一个类,其实很简单,加上_declspec(dllexport)就可以了,当然导出也很简单,换成就可以了class _declspec(dllimport) DLLClass。当然通常我们是用宏定义来控制的

#ifdef DLL_FILE
#define _declspec(dllexport) __DLL__//导出类
#else
#define _declspec(dllimport) __DLL__//导入类
#endif
class __DLL__ DLLClass{};

这样在工程加入头文件,在链接加入该DLL就可以直接使用了,我们可以直接新建,并调用该类的所有公有方法,十分方便。

 

3、  注意的事项

但是值得注意的是,C++类导入导出DLL只能用同一个平台的,同一个编译器。其实这个限制可以说是很苛刻。不能跨语言,跨编译器,其实是很大的限制。也就是这样的方法只能针对C++本身,而且如果导出DLL用的编译器是VS2010,那么在VS2015导出这个DLL的时候就有可能有错误提示:无法解析外部符号XXXXXXXXXXXXXXXX。为什么这样呢?

 

4、  里面的陷阱

 

看此代码段

typedef std::unordered_map<std::string,float> Attr;
const Attr& getAttr()const { return _keyValue; }   


大家都是std::unordered_map<std::string,float>为什么可能会出现这个错误提示呢?为什么找不到呢?难道是DLL里面真的没有吗?

答案是:DLL里真的没有找到对应的。

 

我们解析DLL,看里面为什么没有。

DLLVS2010


??0DLLClass@@QAE@ABV0@@Z

??0DLLClass@@QAE@XZ

??1DLLClass@@QAE@XZ

??4DLLClass@@QAEAAV0@ABV0@@Z

?getAttr@DLLClass@@QBEABV?$unordered_map@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@MV?$hash@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@2@U?$equal_to@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@2@V?$allocator@U?$pair@$$CBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@M@std@@@2@@tr1@std@@XZ

?getIndex@DLLClass@@QBEABHXZ

?getName@DLLClass@@QBEABV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@XZ

 

 

DLLVS2015

??0DLLClass@@QAE@$$QAV0@@Z

??0DLLClass@@QAE@ABV0@@Z

??0DLLClass@@QAE@XZ

??1DLLClass@@QAE@XZ

??4DLLClass@@QAEAAV0@$$QAV0@@Z

??4DLLClass@@QAEAAV0@ABV0@@Z

?__autoclassinit2@DLLClass@@QAEXI@Z

?getAttr@DLLClass@@QBEABV?$unordered_map@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@MU?$hash@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@2@U?$equal_to@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@2@V?$allocator@U?$pair@$$CBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@M@std@@@2@@std@@XZ

?getIndex@DLLClass@@QBEABHXZ

?getName@DLLClass@@QBEABV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@XZ

  

很明显,同样的代码VS2015导出的DLL里面多了几个函数。想知道他们到底是什么,可以比啊度查一下,函数名跟着的标记含义。

 

函数多了,那么函数的声明是否会有变化? 

 


这是我用比较器(BCompare)比较出来的结果,很明显两者的getAttr的声明是不同的。这是因为stl的更新导致的,stl20102015之间有更新,添加或删除一些特征等等。目前我测试20102015std::stringstd::map<std::string,float>是没有变化的。至少类声明是没有变化了,如果将来有一天,这些标准库的模板类作出了更新,那么我们的DLL就无法正常导出使用了。

这些都和我们的初衷相违背,我们需要DLL是为了分工,为什么不管那么多,调用就能得到结果。但是C++类导出却限制了平台,编译器及版本。又爱又恨的。

 

5、  如何优化

对于上面所属的dll我们应该怎么优化?能否避免这些模板块更新所带来的变动?我们知道基础类型是不变的,那么我们只返回基础类型,是不是能够避免这样情况,而且还能愉快的和标准库玩耍?享受他们代码的安全和便利?

改进:

改进前

改进后

class  _declspec(dllexport) DLLClass
{
public:
         typedef std::unordered_map<std::string,float> Attr;
         const std::string& getName()const { return _name; }
         const Attr& getAttr()const { return _keyValue; }
         const int& getIndex()const { return _index; }
private:
         std::string _name;
         int _index;
         Attr _keyValue;
}; 

 

class  _declspec(dllexport) DLLClass
{
public:
         typedef std::unordered_map<std::string,float> Attr;
         const char* getName()const { return _name.c_str(); }
         const float& getAttr(const char* str)const {
                   auto it = _keyValue.find(str);
                   if( it!= _keyValue.end())
                            return it->second;
                   else
                            return 0;
         }
         const int& getIndex()const { return _index; }
private:
         std::string _name;
         int _index;
         Attr _keyValue;
}; 

 

 

我们把所有的参数及返回都用基础类型就成了这样:


对比来说,除了构造函数和析构函数,其他的成员函数都已经是通用了,不会因为stl标准库的更新而无法解析外部符号XXXXXXXXXXXX。如果说你的函数必须返回一个集合,那个集合应该怎么写呢?。(请大神作答,其实我暂时也不知道,等以后我遇到研究到了,就再写文章)

然而,我一直回避了一个问题,就是构造函数和析构函数在VS2010VS2015,也是有所不同的,也就是说,当你使用不同的版本的stl来构造一个类,或者用2015版本构造用2010析构函数来释放。这就出问题了。很可能会少做或者多做释放行为。

那么我们应该怎么做呢?

侯捷的《effective  C++》告诉我们,让类提供一个构造函数和它自己的析构函数如:

DLLClass*  DLLClass::Create(){new  DLLClass ()}     //构建
Void DLLClass::Destory(){delete this;}                   //析构 

 

6、  面向接口编程

面向接口编程,其实就是通过一个接口来控制真正的实体类,当然你不知道那个实体类里面到底有什么东西,成员有哪些。你只知道,你可以调用某些函数来获得。

如这样的纯虚基类

class  DLLClass
{
public:
         const char* getName()const =0;
         const float& getAttr(const char*)const=0;
         const int& getIndex()const = 0;
}; 

通过接口,我们不需要知道里面是怎么执行的,只需要知道我们要什么,调用就好了。这样的程序耦合度很低。而且扩展性是一流的。关于面向接口编程,我也是入门级别,等我学到了,再告诉你们,谢谢。。。