为什么此虚拟析构函数会触发未解析的外部析构函数?

Why does this virtual destructor trigger an unresolved external?

提问人:Greg D 提问时间:8/25/2010 最后编辑:RakibGreg D 更新时间:8/4/2014 访问量:15603

问:

请考虑以下几点:

X.h 中:

class X
{
    X();
    virtual ~X();
};

X.cpp:

#include "X.h"

X::X()
{}

尝试构建这个(我正在使用 .dll 目标以避免在缺少主节点上出错,并且我正在使用 Visual Studio 2010):

错误 1 错误 LNK2001:未解析的外部符号“private: virtual __thiscall X::~X(void)”(??1X@@EAE@XZ)

但是,小的修改会导致成功的构建:

X.h:

class X
{
    inline X(); // Now inlined, and everything builds
    virtual ~X();
};

X.h:

class X
{
    X();
    ~X(); // No longer virtual, and everything builds
};

当 .dtor 是虚拟的或 .ctor 不是内联的时,什么原因导致链接器中出现未解析的外部?

编辑:

或者,也许更有趣的是,如果我使析构函数成为非虚拟的,或者如果我内联构造函数,为什么我不会得到未解析的外部函数?

C++ 析构函数 未解析的外部 虚函数

评论

0赞 GManNickG 8/25/2010
我确定我理解问题/变体,但底线是:如果要调用它,它需要一个定义。在第一个变体中,您从未定义析构函数,这正是错误所说的。我看不出变体是如何构建的,因为我仍然没有看到构造函数或析构函数的定义。
0赞 Greg D 8/25/2010
然而,他们做到了。因此我的困惑。这是一个非常简单的代码示例,我的同事都没有针对此行为的明确答案。最好的猜测是某些东西是未定义的,我们最终会进入特定于供应商的领域,但我很好奇究竟是什么未定义会触发不同的行为。
0赞 Martin York 8/25/2010
除非您向我们展示您正在构建的一切,否则无法分辨!
0赞 Greg D 8/25/2010
@Martin York:这就是我正在构建的一切!按照字面!好吧,我可能在其中一个文件前面省略了回车,但说真的,这就是项目中的一切。您可以在 20 秒内将其复制到您自己的临时库中。就是这么简单。
3赞 Martin York 8/25/2010
在第一个中,它需要析构函数的地址来构建虚拟表。在第二种情况下:由于构造函数从未实际构建过,因此它从不需要构建虚拟表,因此不需要析构函数的地址。在第三个中,我们在构建构造函数时不需要析构函数的地址(因为它不再在虚拟表中);。

答:

9赞 Dima 8/25/2010 #1

您需要为虚拟析构函数提供一个主体:


class X
{
    X();
    virtual ~X() {}
};

评论

3赞 Greg D 8/25/2010
谢谢你的建议,但这不是一个请发送代码如何摆脱错误的问题。相反,这是一个关于更密切地理解我们都以此为生的工具的问题。
1赞 Lou Franco 8/25/2010 #2

这些还不是一个完整的程序(甚至不是一个完整的DLL)。当你得到错误时,你实际上是在得到帮助,因为如果没有 ~X() 的定义,X 是无法使用的

这意味着在某些情况下,这个特定的编译器实例需要一个定义。即使它编译了,它也不会做任何事情。

评论

0赞 Greg D 8/25/2010
这是一个提炼样本,询问观察到的特定行为。我对让它成为一个完整的程序不感兴趣,我感兴趣的是理解为什么它在观察到的情况下构建和不构建。
0赞 Lou Franco 8/25/2010
由于编译器特定的特性,它不会构建。它可能应该有,因为你永远不会破坏一个 X,而且它也可能只是修剪掉所有的代码,因为它从未被调用过。但是,事实并非如此。编写的类没有意义,因此可以不编译。
0赞 Steve Townsend 8/25/2010 #3

你可能会侥幸逃脱,因为 constr 和 destr 都是私有的 - 如果你的构建中没有其他对类 X 的引用,那么编译器可能会推断出 destr 不是必需的,所以缺乏定义没什么大不了的。

这并不能向我解释为什么案例 1 失败,而案例 2 和 3 构建正常。想知道如果两者都公开会发生什么?

评论

0赞 Steve Townsend 8/25/2010
我看到马丁·约克(Martin York)在上面完美地解释了这一点。
24赞 Martin York 8/25/2010 #4

情况 1:

您有构造函数的代码。
因此,它将构造函数构建到对象文件中。构造函数需要将析构函数的地址放到虚表中,因为它找不到它,构造函数无法构建。

情况 2:(内联构造函数)

编译器决定它不需要构建构造函数(因为它将被内联)。
因此,它不会植入任何代码,因此不需要析构函数的地址。

如果实例化 X 类型的对象,它将再次报错。

情况 3:(非虚拟析构函数)

不需要析构函数的地址来生成构造函数。
所以它不抱怨。

如果实例化 X 类型的对象,它将报错。

2赞 John Dibling 8/25/2010 #5

第一个问题的答案,

是什么原因导致未解决的外部 .dtor 为虚拟时的链接器 或者当 .ctor 没有内联时?

...很简单,就是你没有析构函数的定义。

现在你的第二个问题有点有趣:

为什么我没有得到解决 外部,如果我制作析构函数 非虚拟,或者如果我内联 构造 函数?

原因是因为你的编译器不需要析构函数,因为你从未实例化过,所以它把你的整个类都扔掉了。如果你尝试编译这个程序,你会得到一个未解析的外部:XX

class X
{
public:
    X();
     ~X();
};

X::X() {};

int main()
{
    X x;
    return 0;
}

但是,如果您注释掉它,它将编译得很好,正如您所观察到的。X x;

现在让我们回过头来谈谈为什么它不会编译 if 析构函数 if .我在这里推测,但我相信原因是因为,因为你有一个虚拟析构函数,现在是一个多态类。为了在内存中布局多态类,使用 vtable 实现多态的编译器需要每个虚拟函数的地址。您尚未实现,因此未解决的外部结果。virtualXX::~X

为什么编译器不像不是多态类时那样扔掉呢?更多猜测在这里。但我想原因是因为即使你没有直接实例化,也不能确定你的代码中没有任何地方会像其他东西一样进行实时、混淆。例如,考虑一个抽象基类。在这种情况下,您永远不会直接实例化,并且代码可能位于完全独立的翻译单元中。因此,当编译器到达这个多态类时,即使它不知道你实例化了它,它也无法丢弃它。XXXXBaseDerived

5赞 AnT stands with Russia 8/25/2010 #6

在 C++ 中,当且仅当函数在程序中使用时,必须定义函数(参见 3.2/2 中的 ODR)。通常,如果从可能计算的表达式调用非虚函数,则使用非虚函数。任何非纯虚函数都被视为无条件使用当使用[非虚拟]特殊成员时,在语言标准的专用位置定义函数。等等。

  • 第一个示例中,您将析构函数声明为非纯虚函数。这立即意味着您的析构函数已在程序中使用。这反过来又意味着需要该析构函数的定义。您未能提供定义,因此编译器报告了错误。

  • 第三个示例中,析构函数是非虚拟的。由于您没有在程序中使用析构函数,因此不需要定义,代码会编译(有关使用析构函数详细说明,请参见 12.4)。

  • 第二个示例中,您正在处理实现的一个怪癖,该怪癖是由构造函数内联的事实触发的。由于析构函数是非纯虚拟的,因此定义是必需的。但是,编译器未能检测到错误,这就是代码似乎编译成功的原因。您可以在实现的细节中挖掘此行为的原因,但从 C++ 的角度来看,由于完全相同的原因,此示例与第一个示例一样损坏。

1赞 Chubsdad 8/25/2010 #7

我对此表示怀疑,这是实现定义的行为。原因如下

$10.3/8- “声明了一个虚拟函数 在类中应定义,或 在该类中声明为 pure (10.4),或 双;但不需要诊断 (3.2)."

GCC 给出了如下错误,这再次高度暗示了(至少对我而言)关于实现虚函数的非标准实现细节

/home/OyXDcE/ccS7g3Vl.o:在函数 vtable for X' /home/OyXDcE/ccS7g3Vl.o:在函数 vtable for X' collect2:ld 返回 1 退出状态X::X()': prog.cpp:(.text+0x6): undefined reference toX::X()': prog.cpp:(.text+0x16): undefined reference to

我很困惑是否真的需要编译器对 OP 代码进行诊断,所以想到发布这个,即使我冒着投反对票的风险:)。当然,我应该猜一个好的编译器。