缘起
前一阵子,同事遇到了一个奇怪的编译问题,大概情况如下:
classA
是模块 A
中的一个类, classA
没有定义构造函数,其它函数都是导出的。B
模块依赖了 A
模块,并且会调用 classA
的接口。当在 B
模块中添加了实例化 classA
对象的代码的时候,报链接错误,提示找不到 classA
类的某个虚函数。
我帮忙看过之后发现又是虚函数相关的编译问题(正好最近在总结虚函数相关的问题),这是送上门的素材啊!
说明: 项目代码不方便对外发布,本文所有的代码是我基于实际项目模拟的
示例程序简介
示例程序由两个工程组成:主模块和接口模块。
接口模块
接口模块只提供了头文件
interface.h
和对应的interface.lib
文件及接口实现文件interface.dll
。头文件内容如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// interface.h
class InterfaceBase
{
public:
virtual ~InterfaceBase() {}
};
class Interface1 : public InterfaceBase
{
public:
DLL_EXPORT_INTERFACE virtual void Test1(int);
DLL_EXPORT_INTERFACE virtual void Test2(int);
DLL_EXPORT_INTERFACE virtual void Test3(int);
DLL_EXPORT_INTERFACE virtual void Test4(int);
};
DLL_EXPORT_INTERFACE InterfaceBase* GetInterface();
主模块
主模块会隐式依赖
interface.lib
,并通过GetInterface()
接口获取Interface1
指针,然后调用其接口。1
2
3
4
5
6
7
8
9
10
11// InterfaceExe.cpp
int main()
{
Interface1* if1 = dynamic_cast<Interface1*>(GetInterface());
if1->Test1(0);
if1->Test4(0);
auto if2 = Interface1();
return 0;
}
当在 main()
函数中通过 auto if2 = Interface1();
创建 Interface1
类型的对象时,编译器会报链接错误,提示无法解析的外部符号,如下图:
初步排查
通过之前总结的几篇文章
《基础知识 | c++ 有趣的动态转换之 delete 崩溃探究兼谈基类虚析构的重要性》
《基础知识 | 函数基础 1 —— 基本概念 & 如何调用外部模块的函数》
《基础知识 | 函数基础 2 —— 如何不依赖外部模块却能调用它的函数?》
《基础知识 | 函数基础 3 —— 跨模块调用未导出虚函数的各种姿势 》
《基础知识 | 函数基础 4 —— 又崩溃了,原来是虚函数声明顺序不一致捣的鬼 》
我对虚函数编译相关的问题有了比较全面的认识。
头文件中没声明 Interface1
的构造函数,并且 Interface1
包含虚函数,那么编译器会自动生成构造函数代码,与我们手动在头文件中定义一个空造函数是一样的。
在这种情况下,如果想在外部模块实例化 Interface1
的对象,那么 Interface1
所有的虚函数都需要是导出的或者需要在头文件中定义。
快速扫了一遍头文件,所有虚函数确实都是导出的。这就奇怪了,难道 interface.lib
文件又出问题了?
查看 lib 文件
使用 vs
自带的 dumpbin
查看 interface.lib
的导出符号,在命令行中分别输入以下两行命令,即可把 Interface.lib
中的导出符号信息导出到 interface.txt
中。
1 | set PATH=%PATH%;"C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\amd64\" |
在 interface.txt
中搜索 Test4
,结果什么也没所搜到,如下图:
原来又是头文件与 lib
文件不匹配导致的问题,头文件中包含 Test4
,但是对应的 lib
文件中却没有。
亲自动手
示例工程已经上传到这里,感兴趣的小伙伴儿可以自行下载验证。
说明: 我已经上传了对应的
Interface.lib
及Interface.dll
,如果想重新编译Interface.vcxproj
,需要先注释掉Interface.h
中包含Test4
的那一行。编译成功后,如果想像我一样重现链接错误,那么需要在Interface.h
中把删掉的Test4
哪一行添加回来并且只重新编译InterfaceExe.vcxproj
。
总结
- 如果类构造函数不是导出的,如果想在外部模块实例化类对象,那么类中所有虚函数都需要是导出的
dumpbin
是查看.lib
或.dll
文件中导出符号的神兵利器