C++ 类定义中class+宏+类名的意义

看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。

版权声明:本文为CSDN博主「旧衣新雪」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/LinearF/article/details/81981031


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!