看zoom的win_sdk时,看到很多类在定义时,class和类名中间有一个DUILIB_API,形如: class DUILIB_API CWindowWnd
好奇之后查资料,发现DUILIB_API被展开为:
#ifdef UILIB_STATIC
# define DUILIB_API
#else
# if defined(UILIB_EXPORTS)
# if defined(_MSC_VER)
# define DUILIB_API __declspec(dllexport)
# else
# define DUILIB_API
# endif
# else
# if defined(_MSC_VER)
# define DUILIB_API __declspec(dllimport)
# else
# define DUILIB_API
# endif
# endif
#endif
————————————————————————————————————————————————————————
在 C++ 类中使用 dllimport 和 dllexport
可以使用 dllimport 或 dllexport
特性来声明 C++ 类。 这些形式表示已导入或导出整个类。 以这种方式导出的类称为可导出类。
dllexport 类
在声明类 dllexport
时,所有其成员函数和静态数据成员都将导出。 您必须在同一程序中提供所有此类成员的定义。 否则,将生成链接器错误。 此规则有一个例外情况,即对于纯虚函数,您无需为其提供显式定义。 但是,由于基类的析构函数始终在调用抽象类的析构函数,因此纯虚拟析构函数必须始终提供定义。 请注意,这些规则对不可导出的类是相同的。
如果导出类类型的数据或返回类的函数,请务必导出类。
dllimport 类
当声明类 dllimport 时,将导入所有其成员函数和静态数据成员。 与非类类型上的 dllimport 和 dllexport
的行为不同,静态数据成员无法在定义 dllimport 类的同一程序中指定定义。
继承和可导出类
可导出类的所有基类都必须是可导出的。 否则,会生成编译器警告。 此外,同样是类的所有可访问成员必须是可导出的。 此规则只允许 dllexport
类从 dllimport 类继承,dllimport 类从 dllexport
类继承(但不建议后一种方式)。 通常来说,对 DLL 客户端可访问的所有内容(根据 C++ 访问规则)都应该是可导出接口的一部分。 这包括在内联函数中引用的私有数据成员。
选择性成员导入/导出
由于类中的成员函数和静态数据隐式包含了外部链接,因此您可以使用 dllimport 或 dllexport
特性声明它们,除非整个类都已导出。 如果整个类都已导入或导出,则禁止将成员函数和数据显式声明为 dllimport 或 dllexport
。 如果在类定义中将静态数据成员声明为 dllexport
,定义一定会在同一程序中的某处出现(就像非类外部链接一样)。
同样,您可以使用 dllimport 或 dllexport
特性声明成员函数。 在这种情况下,您必须在同一程序中的某处提供 dllexport
定义。
有关选择性成员导入和导出的某些要点值得注意:
-
选择性成员导入/导出最适合用于提供具有更强限制的导出类接口版本;即,您可以为该版本设计一个 DLL,该 DLL 公开的公用和专用功能比本应允许的语言公开的更少。 这对于优化可导出接口也很有用:当通过定义知道客户端无法访问某些私有数据时,您不需要导出整个类。
-
如果导出了某个类中的一个虚函数,则必须导出其中的所有虚函数,或者至少必须提供客户端可直接使用的版本。
-
如果有在其中将选择性成员导入/导出用于虚函数的类,则这些函数必须在可导出接口或已定义内联中(对客户端可见)。
-
如果将某个成员定义为
dllexport
,但不在类定义中包含它,则会产生编译器错误。 必须在类头中定义成员。 -
尽管允许将类成员定义为 dllimport 或
dllexport
,但无法重写在类定义中指定的接口。 -
如果在声明成员函数的类定义的主体以外的地方定义成员函数,并且将函数定义为
dllexport
或 dllimport(如果此定义不同于类声明中指定的定义),则会生成警告。
————————————————————————————————————————————————————————
程序加载一个dll时,程序运行在两个独立空间的(dll的空间和程序空间),dll的对象模型其实相当严格,要访问dll空间的变量和函数,必须导出他们,否则这些对象是不可见的。这可以通过加入一个def文件,或者在声明中使用__declspec(dllimport)前缀,告诉编译器以下这些变量和函数是从dll导出的。同时定义这些变量的dll源文件必须加上__declspec(dllexport)前缀,告诉编译器这些函数需要被导出。
对类对象来说,静态成员和函数必须加上这个前缀,因为这些对象都是在dll空间内的。在类的前面加上这些前缀就对整个类的成员进行了声明。
下面对之前的代码做解析:
#ifdef UILIB_STATIC //如果是静态UI库
# define DUILIB_API //不需要动态链接,定义为空串
#else
# if defined(UILIB_EXPORTS)//需要导出UI库
# if defined(_MSC_VER)//如果是微软的编译器(定义了版本号)
# define DUILIB_API __declspec(dllexport)//定义为导出
# else
# define DUILIB_API //非微软编译器,空串
# endif
# else//不需要导出UI库
# if defined(_MSC_VER)//如果是微软的编译器(定义了版本号)
# define DUILIB_API __declspec(dllimport)//定义为导入
# else
# define DUILIB_API //非微软编译器,空串
# endif
# endif
#endif
对库作者而言,将类封装为动态库时,需要导出,会写在class _declspec(dllexport) A{};把这个类声明放在A.h文件中,库文件为A.dll。
对用户而言,为了在程序中使用A.dll动态库,需要导入,需要将A.h中关于类A的声明改为class _declspec(dllimport) A{};当库中有多个类时,替换非常麻烦,故用这种方式定义。
总结:定义动态库的时候是要考虑暴露接口(函数/类等)让外部用,所以用export,后续其他程序使用动态库时,是要导入接口,用import。