基础知识 | 函数基础 5 —— 实战修复虚函数导致的编译错误

缘起

前一阵子,同事遇到了一个奇怪的编译问题,大概情况如下:

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
    #pragma once

    #ifdef DLL_EXPORT_INTERFACE
    #define DLL_EXPORT_INTERFACE __declspec(dllexport)
    #else
    #define DLL_EXPORT_INTERFACE __declspec(dllimport)
    #endif

    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
    #include "../Interface/Interface.h"
    int main()
    {
    Interface1* if1 = dynamic_cast<Interface1*>(GetInterface());
    if1->Test1(0);
    if1->Test4(0);

    auto if2 = Interface1();
    return 0;
    }

当在 main() 函数中通过 auto if2 = Interface1(); 创建 Interface1 类型的对象时,编译器会报链接错误,提示无法解析的外部符号,如下图:

LNK2001

初步排查

通过之前总结的几篇文章

《基础知识 | 有趣的动态转换》

《基础知识 | C++ 虚函数简介》

《基础知识 | c++ 有趣的动态转换之 delete 崩溃探究兼谈基类虚析构的重要性》

《基础知识 | 函数基础 1 —— 基本概念 & 如何调用外部模块的函数》

《基础知识 | 函数基础 2 —— 如何不依赖外部模块却能调用它的函数?》

《基础知识 | 函数基础 3 —— 跨模块调用未导出虚函数的各种姿势 》

《基础知识 | 函数基础 4 —— 又崩溃了,原来是虚函数声明顺序不一致捣的鬼 》

我对虚函数编译相关的问题有了比较全面的认识。

头文件中没声明 Interface1 的构造函数,并且 Interface1 包含虚函数,那么编译器会自动生成构造函数代码,与我们手动在头文件中定义一个空造函数是一样的。

在这种情况下,如果想在外部模块实例化 Interface1 的对象,那么 Interface1 所有的虚函数都需要是导出的或者需要在头文件中定义。

快速扫了一遍头文件,所有虚函数确实都是导出的。这就奇怪了,难道 interface.lib 文件又出问题了?

查看 lib 文件

使用 vs 自带的 dumpbin 查看 interface.lib 的导出符号,在命令行中分别输入以下两行命令,即可把 Interface.lib 中的导出符号信息导出到 interface.txt 中。

1
2
3
set PATH=%PATH%;"C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\amd64\"

dumpbin /EXPORTS Interface.lib > interface.txt

interface.txt 中搜索 Test4,结果什么也没所搜到,如下图:

cannot-search-Test4-from-interface-lib

原来又是头文件与 lib 文件不匹配导致的问题,头文件中包含 Test4,但是对应的 lib 文件中却没有。

亲自动手

示例工程已经上传到这里,感兴趣的小伙伴儿可以自行下载验证。

说明: 我已经上传了对应的 Interface.libInterface.dll,如果想重新编译 Interface.vcxproj,需要先注释掉 Interface.h 中包含 Test4 的那一行。编译成功后,如果想像我一样重现链接错误,那么需要在 Interface.h 中把删掉的 Test4 哪一行添加回来并且只重新编译 InterfaceExe.vcxproj

总结

  • 如果类构造函数不是导出的,如果想在外部模块实例化类对象,那么类中所有虚函数都需要是导出的
  • dumpbin 是查看 .lib.dll 文件中导出符号的神兵利器
BianChengNan wechat
扫描左侧二维码关注公众号,扫描右侧二维码加我个人微信:)
0%