评注程序均在Visual Studio.NET 2005 Professional中测试以得到结果,给出的代码均省略了大量语句,只保留了与主题最相关的部分,因此可能无法编译。
MSDN文档选自2007年4月更新的MSDN Library。
Using dllimport and dllexport in C++ Classes
在C++类中使用dllimport和dllexport
Microsoft Specific
You can declare C++ classes with the dllimport or dllexport attribute. These forms imply that the entire class is imported or exported. Classes exported this way are called exportable classes.
The following example defines an exportable class. All its member functions and static data are exported:
可以在声明C++类时使用dllimport和dllexport属性。这两个形式将隐含导入或导入整个类。通过这种方法导出的类称为可导出类。
下列范例定义了一个可导出类,其所有的成员函数和静态将被导出:
#define DllExport __declspec( dllexport )
class DllExport C {
int i;
virtual int func( void ) { return 1; }
};
Note that explicit use of the dllimport and dllexport attributes on members of an exportable class is prohibited.
注意,禁止在一个可导出类的成员上显式的使用dllimport和dllexport属性。
balon注:如你不能像下例这样写:
#define DllExport __declspec( dllexport )
class DllExport C {
DllExport int i; // 不可以在成员上使用dllexport
DllExport int func( void ) { return 1; } // 不可以在成员上使用dllexport
};
dllexport Classes
通过dllexport导出类
When you declare a class dllexport, all its member functions and static data members are exported. You must provide the definitions of all such members in the same program. Otherwise, a linker error is generated. The one exception to this rule applies to pure virtual functions, for which you need not provide explicit definitions. However, because a destructor for an abstract class is always called by the destructor for the base class, pure virtual destructors must always provide a definition. Note that these rules are the same for nonexportable classes.
If you export data of class type or functions that return classes, be sure to export the class.
当你声明一个类为dllexport,其所有的成员函数和静态数据成员将被导出。你必须在同一个程序中定义所有此类成员,否则会产生一个链接错误。
balon注:如在下面的导出类中,func必须在这个DLL的工程中或是在使用DLL的程序中被定义,否则在使用时会出现无法解析的外部符号的链接错误。注意,定义可以放在DLL中,也可放在DLL的使用者工程中,只要在使用前被定义就可以。
方法一:在Dll工程中定义,在使用者工程中直接使用。
// dll.h
#define DllExport __declspec( dllexport )
class DllExport C {
int func( void );
};
// dll.cpp
int C::func(void)
{
return 1;
}
方法二:在DLL工程中只有一个声明,没有定义。可以在使用者使用前给出该函数的定义。另外,如果你在使用者程序中根本就没有用到func,可以不提供其定义,不会出链接错误。
// dll.h
#define DllExport __declspec( dllexport )
class DllExport C {
int func( void );
}; // client.cpp
int C::func(void)
{
return 1;
}
int main()
{
C c;
c.func(); // ok
return 1;
}
这个规则的唯一例外是对纯虚函数你可以不用提供显示的定义。
balon注:如下面例子中的纯虚函数func,可以不必提供定义。因为这个导出的类可能本身就是一个抽象类,func就是一个没有实现的函数,当然可以不提供定义。
但是,由于抽象类的析构函数总是会被基类的析构函数调用的,因此纯虚析构函数必须提供一个定义。这条规则对于不可导出的类同样适用。
balon注:根据我的试验结果,将各种情况下的编译结果列了一个表:
DLL工程中没有对应函数定义的编译结果 客户程序使用情况
成员函数 正常链接。 可以定义此类的实例,不调用此成员函数就正常链接;调用此成员函数链接出错,提示此函数为未决的外部符号。
虚成员函数 链接出错,提示此函数为未决的外部符号。 -
纯虚成员函数 正常链接。 不能定义此类实例,编译出错,提示无法实例化一个抽象类。
析构函数 正常链接。 链接出错,提示析构函数为未决的外部符号
虚析构函数 链接出错,提示析构函数为未决的外部符号。 -
纯虚析构函数 链接出错,提示析构函数为未决的外部符号。 -
可见文档中说的规则属实,纯虚析构函数被当作虚析构函数对待。但另一个问题是,为什么虚函数(包括析构函数)就一定要在其声明所在的工程中被定义,而普通函数就不必呢?我分析原因估计如下:
我们知道,C++类如果有虚函数的话(包括其基类有虚函数的情况),C++编译器会在编译时为其生成一个虚表,并在构造函数中,将对应的虚表首地址填到类实例的虚表指针成员中。如果一个虚函数在派生体系中被多次实现,虚表中填入的是最底层(most-derived)实现。这个填表过程是,首先基类对象被构造,其构造函数先填入基类的虚表实现入口(这就是为什么,在构造函数中调用虚函数,只会调用到当前类层次的实现,无法调用到派生类的实现的原因),接着派生类被构造,其构造函数将派生类的虚表入口填入,覆盖掉刚才基类记录的入口地址。这个过程一直进行,直到整个对象构造完成。
说了这么多,其实其中最关键的一点就是:构造函数必须知道当前类的虚表入口地址,而这就需要知道虚表里填写的所有虚函数的入口地址!否则,编译器将无法生成构造函数的填虚表的功能。于是,编译器只好开始抱怨了。
因此,所有的虚函数必须在其声明所在的工程中有定义,在链接时能正确找到其入口地址以便编译器可以生成构造函数的填虚表功能。
如果你导出一个类类型的数据成员,或是一个返回类的函数,请确保导出那个类。
balon注:这个是一个指导原则,不是一个强制的编译链接要求。只要你不使用返回的类的成员函数,并且返回类没有虚表,可以不必导出类。但多数情况下是要导出的,所以作为一个好的原则,按MSDN的建议做就好了。在本文的下一篇中将对此有详细介绍。
dllimport Classes
通过dllimport导入类
When you declare a class dllimport, all its member functions and static data members are imported. Unlike the behavior of dllimport and dllexport on nonclass types, static data members cannot specify a definition in the same program in which a dllimport class is defined.
当你将一个类声明为dllimport,其所有的成员函数和静态数据成员将被导入。与在非类类型上使用dllimport和dllexport不同的是,不能在有dllimport类的定义的同一个程序中指给出静态数据成员的定义。
balon注:这句话译的有些拗口,原文也好不到哪里。其实看了通过dllexport导出类一节的注解,就好理解这里想要说的意思了。意思就是:静态数据成员不能像其它成员函数那样,可以在使用者工程中定义,而不在DLL本身工程中定义。
方法一、按要求在DLL中定义静态数据成员:
// dll.h
#define DllExport __declspec( dllexport )
class DllExport C {
static int x;
};
// dll.cpp
int C::x = 0;
// client.cpp
int main()
{
C c;
c.x = 10; // ok
return 1;
}
方法二、试图“在有dllimport类的定义的同一个程序中指给出静态数据成员的定义”,则在客户程序编译时 出现编译错误:
// dll.h
#define DllExport __declspec( dllexport )
class DllExport C {
static int x;
}; // client.cpp
int C::x = 0; // C4273
int main()
{
C c;
c.x = 10;
return 1;
}
Inheritance and Exportable Classes
继承与可导出类
All base classes of an exportable class must be exportable. If not, a compiler warning is generated. Moreover, all accessible members that are also classes must be exportable. This rule permits a dllexport class to inherit from a dllimport class, and a dllimport class to inherit from a dllexport class (though the latter is not recommended). As a rule, everything that is accessible to the DLL's client (according to C++ access rules) should be part of the exportable interface. This includes private data members referenced in inline functions.
一个可导出类的所有基类都必须可导出,否则会产生一个编译错误。
balon注:事实上,这条规则也不是一定的,在满足一定的条件情况下,基类不可导出程序也是正常的。当然,多数情况下还是要导出的,建议还是按MSDN的要求做好了。在本文的下一篇中将对此有详细介绍。
如下例中,A是一个可导出类,则A的基类Base也应当是一个可导出类。
#define DllExport __declspec( dllexport )
class DllExport A : public Base
{
// ...
};
此外,所有可访问的类类型成员也必须是可导出的。这条规则允许了一个dllexport类派生自一个dllimport类,或一个dllimport类派生自一个dllexport类(尽管后者是不被推荐的)。
balon注:如下例中,Base是一个dllimport类,
// BaseDll的头文件basedll.h
#define DllImport __declspec( dllimport )
class DllImport Base
{
}; // DerivedDll的头文件Deriveddll.h
#include “basedll.h” // 将一个dllimport类声明包含进来
#define DllExport __declspec( dllexport )
class DllExport A : public Base // A派生自dllimport类Base
{
// ...
};
结果就是,这个DLL的客户可访问的所有东西(依照C++的访问规则)都应当是导出接口的一部分。包括被inline函数引用的私有成员。
balon注:这句话其实是全文中最重要的一句话,其实这篇文档如果把这句话展开说清楚了,也不用我在这里写这篇文章了。在本文的下一篇中将有对于这句话的深入讨论
Selective Member Import/Export
选择性成员导入导出
Because member functions and static data within a class implicitly have external linkage, you can declare them with the dllimport or dllexport attribute, unless the entire class is exported. If the entire class is imported or exported, the explicit declaration of member functions and data as dllimport or dllexport is prohibited. If you declare a static data member within a class definition as dllexport, a definition must occur somewhere within the same program (as with nonclass external linkage).
因为类中的成员函数和静态数据隐含进行外部链接,你可以在没有将整个类导出的情况下,在他们声明中加上dllimport或是dllexport属性。如果整个类被导入或是导出,将不允许显式的以dllimport和dllexport对成员函数和数据进行声明。如果你将类中的一个静态数据成员声明为dllexport,在同一个程序中的某个地方应当有它的定义(如同非类外部链接那样)。
balon注:前面几句很好理解。最后一句实际上是在说,你可以把导出一个静态数据成员,当作与一个从DLL中导出一个非类成员的普通变量那样对待,要在DLL所在工程中有定义。导出一个普通变量方法就是在DLL中的某一个CPP文件中定义此变量,并加上dllexport声明:
// dll.cpp
__declspec( dllexport ) int x = 0;
那么,对比一下将一个类的静态数据成员导出的方法:
// dll.h
#define DllExport __declspec( dllexport )
class A // 注意,这里没有导出类A
{
public:
DllExport static int x; // 所以这里才可以导出个别成员
}; // dll.cpp
int A::x = 0;
Similarly, you can declare member functions with the dllimport or dllexport attributes. In this case, you must provide a dllexport definition somewhere within the same program.
类似的,你也可以为一个成员函数声明加上dllimport或dllexport属性。这种情况下,你必须在同一个程序中的某处提供dllexport定义。
It is worthwhile to note several important points regarding selective member import and export:
· Selective member import/export is best used for providing a version of the exported class interface that is more restrictive; that is, one for which you can design a DLL that exposes fewer public and private features than the language would otherwise allow. It is also useful for fine-tuning the exportable interface: when you know that the client, by definition, is unable to access some private data, you need not export the entire class.
· If you export one virtual function in a class, you must export all of them, or at least provide versions that the client can use directly.
· If you have a class in which you are using selective member import/export with virtual functions, the functions must be in the exportable interface or defined inline (visible to the client).
· If you define a member as dllexport but do not include it in the class definition, a compiler error is generated. You must define the member in the class header.
· Although the definition of class members as dllimport or dllexport is permitted, you cannot override the interface specified in the class definition.
· If you define a member function in a place other than the body of the class definition in which you declared it, a warning is generated if the function is defined as dllexport or dllimport (if this definition differs from that specified in the class declaration).
关于选择性成员导入导出的一些重点值得我们关注:
· 选择性成员导入导出最好用在为一个导出的类接口提供一个更具限制的版本;即是说允许你设计一个DLL导出比正常情况下语言允许的更少的公共或私有特性。这对于微调可导出的接口也很有用:如果根据定义,客户无法访问一些私有数据,你没必要导出整个类。
· 如果你导出一个类中的某一个虚函数,那你就必须把所有虚函数一并导出,或至少提供用户可以直接访问的版本。
· 如果你在一个类的虚函数上使用了选择性成员导入导出,那么这些函数必须是在可导出接口中,或是内联定义(对客户可见)。
· 如果你将一个成员定义为dllexport,但没有将定义包含在类的定义中,将产生一个编译器错误。你必须在类的头文件中定义这个成员。
· 尽管允许将类成员定义为dllimport和dllexport,但你无法覆写这个类的定义。
· 如果你没有在声明成员函数的类体定义处定义一个成员函数,并且此成员函数被定义为dllexport或dllimport,将产生一个警告(如果定义与在类中指定的声明不同时)。
END Microsoft Specific
那么,整个系统的底层机制是怎么样的?是通过什么途径,使得我们可以在另一个程序中使用一个DLL中导出的类的呢?
我们知道,要使用一个C++类,必要的条件是在编译期能得到这个类的头文件,并在链接期可以找到对应的符号的链接地址(比如成员函数、静态数据成员等)。如果这个C++类与你的使用者在同一个工程,那这个条件很好满足:
首先,C++类的头文件很好获得。直接在使用者那里将类的头文件include即可
其次,C++类往往被编译器作为一个编译单元,生成一个obj文件。在最后进行链接的过程中,链接器会把工程中所有的obj链接以生成最终的二进制目标文件。所以链接器在遇到一处对类成员函数(或其它形式的符号引用)时,会在这个类生成的obj文件中找到符号的链接地址。
那么,在代码中使用一个C++类,编译期和链接期需要的到底是些什么东西呢?换句话说,满足了什么样的条件,编译器和链接器就不会抱怨了呢?
根据C++语言的定义,一个C++类实际上是声明或定义了如下几类内容:
1. 声明了一个数据结构,类中的非静态数据成员、代码中看不到但如果有虚函数就会生成的虚表入口地址指针等。
2. 声明并定义了一堆函数,它们第一个参数都是一个指向这个数据结构的指针。这些实际上就是类中那些非静态成员函数(包括虚函数),它们虽然在类声明中是写在类的一对大括号内部,但实际上没有任何东西被加到前面第1条中所说的内部数据结构中。实际上,这样的声明只是为这些函数增加了两个属性:函数名标识符的作用域被限制在类中;函数第一个参数是this,被省略不写了。
3. 声明并定义了另一堆函数,它们看上去就是一些普通函数,与这个类几乎没有关系。这些实际上就是类中那些静态函数,它们也是一样,不会在第1条中所说的内部数据结构中增加什么东西,只是函数名标识符的作用域被限制在类中。
4. 声明并定义了一堆全局变量。这些实际上就是类中那些静态数据成员。
5. 声明并定义了一个全局变量,此全局变量是一个函数指针数组,用来保存此类中所有的虚函数的入口地址。当然,这个全局变量生成的前提是这个类有虚函数。
下面是一个例子。
class MyClass
{
public:
int x;
int y;
void Foo();
void Bar(int newX, int newY);
virtual void VFoo();
virtual void VBar(int newX, int newY) = 0;
static void SFoo();
static void SBar(int newX, int newY);
static int sx;
static int sy;
};
对于上面列出的这个类MyClass,C++编译器多数会以如下的方式进行编译:
现在我们再来看一下为什么编译器需要头文件和符号地址就可以编译链接一个使用MyClass的程序了。
首先,由于编译器需要在编译期就知道类的内存布局,以保证可以生成正确的开辟内存的代码,及那些sizeof(MyClass)的值。有了头文件,编译器就知道,一个MyClass占用12字节的内存空间(见上图,两个整数和一个指针)。
其次,在调用MyClass的成员函数、静态函数时,链接器需要知道这些函数的入口地址,如果无法提供入口地址,链接器就会报错。
最后,在引用MyClass的静态数据成员时,实际上与引用一个外部全局对象一样,链接器需要知道这些变量的地址。如果无法提供这些变量的地址,链接器也会报错。
可以看出:
1. 编译期:必须要提供的是类的头文件,以使编译器可以得知类实例的尺寸和内存布局。
2. 链接期:必须要提供的是程序中引用过的,类的成员函数、静态函数、静态数据成员的地址,以使链接器可以正确的生成最终程序。
到这里,我们可以猜到,实际上,导出一个类,编译器实际上只需要将这个类中的:成员函数、静态函数、静态数据成员当成普通的函数、全局变量导出即可。也就是说,我们实际上没有“导出一个类”,而是把这个类中需要被引用的“有定义的实体”的入口地址像普通函数和变量那样正常导出即可。
最后我们来看一下,实际上生成的一个导出前面列的的那个MyClass类的DLL。用Dependence来查看,可以看到下面的结果:
可以看到,除了VBar函数是一个纯虚函数外,其它函数、静态数据成员的入口地址都被导出。另外可以看到,vtable也被导出,以便操作虚函数时引用。
Balon白话MSDN:从普通DLL中导出C++类(1) – dllexport和dllimport的使用方法(中英对照、附注解)
我写这些内容时偷了个懒,避开了虚表的一大堆复杂内容。谢谢houdy的提示,他的文章对于虚表,以及从DLL导出虚表的底层机制进行了详细的剖析,想对此刨根问底的同学一定要看下:
虚函数表放在哪里?
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/hxb20082008/archive/2009/09/02/4510494.aspx