了解链接器错误

Understanding linker errors

本文关键字:错误 链接 了解      更新时间:2023-10-16

我用虚拟函数编写了以下程序:

struct A
{
    virtual void foo() = 0;
    A(){ init(); }
    void init(){ foo(); }
};
struct B : A
{
    virtual void foo(){ }
};
B a;
int main(){
    return 0;
}

演示

我认为应该出现一些链接器错误,因为没有找到foo的实现。我们得到了运行时错误。为什么?为什么不出现链接器错误?

这里首先要理解的是,在类A的构造函数处于活动状态时对foo()的调用会被调度到A::foo(),即使正在构建的完整对象的类型为B,而B会覆盖foo()B::foo()的存在被简单地忽略。

这意味着您的代码尝试调用A::foo()。由于A::foo()是一个纯虚拟函数,因此代码的行为是未定义的。

C++语言不能保证在这种情况下会发生什么样的"错误"。这意味着你对"链接器错误"的期望是完全没有根据的。如果程序尝试对纯虚拟函数执行虚拟调用,则行为只是未定义。从C++语言的角度来看,这是唯一可以说的。

这种未定义的行为将如何在实际实现中表现出来取决于实现。例如,允许未定义的行为通过编译时错误来表现自己。

在您的情况下,您的程序尝试对纯虚拟函数A::foo()进行虚拟调用。在一般情况下,编译器通过实现多态性的运行时机制动态调度虚拟调用(所谓的虚拟方法表是最流行的)。在某些情况下,当编译器可以确定调用中使用的对象的确切类型时,它会优化代码,并对虚拟函数进行普通的直接(非动态)调用。

在实践中,如果一个函数是纯虚拟的,那么它的虚拟方法表条目包含一个空指针。对此类函数的动态调用通常会导致运行时错误。同时,对此类函数的直接(优化)调用通常会导致编译器或链接器错误。

在您的示例中,编译器没有优化调用。它通过虚拟方法表对A::foo()进行了全面的动态调用。该表中的空指针触发了运行时错误。

如果您直接从构造函数调用纯虚拟函数

 A() { foo(); } 

典型的编译器通常会直接(优化)调用foo(),这通常会导致链接器错误。

B确实有foo的实现,所以链接器没有问题。

据我所知,A在错误的时间调用foo这一事实并不需要编译器/链接器来解决。(尽管在这种情况下进行这样的检查可能很简单,但我相信我们可能会遇到更复杂的情况,这些情况可能更难或不可能被发现。)

您的错误是从构造函数中调用虚拟函数的结果。被调用的函数是A中的函数,而不是更多的派生函数。C++标准,第12.7.4节规定,

成员函数,包括虚拟函数(10.3),可以调用在建造或破坏期间(12.6.2)。当虚拟功能直接或间接从构造函数或从销毁器,包括在建造或销毁类非静态数据成员,以及调用的对象applies是正在构建或销毁的对象(称之为x),调用的函数是构造函数或中的最后一个重写器析构函数的类,而不是在更派生的类中重写它如果虚拟函数调用使用显式类成员访问(5.2.5),对象表达式是指x的完整对象或该对象的基类子对象之一,但不是x或其基类子对象,行为是未定义的。

现在,你在作弊。您正在从构造函数调用一个普通函数,然后从普通函数调用一个虚拟函数。将您的代码更改为,

struct A
{
    virtual void foo() = 0;
    A(){ foo(); }
};

你会得到你的错误,

warning: pure virtual ‘virtual void A::foo()’ called from constructor [enabled by default]
_ZN1AC2Ev[_ZN1AC5Ev]+0x1f): undefined reference to `A::foo()'
相关文章: