我用虚函数编写了以下程序:
struct A
{
virtual void foo() = 0;
A(){ init(); }
void init(){ foo(); }
};
struct B : A
{
virtual void foo(){ }
};
B a;
int main(){
return 0;
}
我认为应该确定一些链接器错误,因为找不到foo
的实现。我们得到了运行时错误。为什么?为什么链接器没有错误?
答案 0 :(得分:2)
这里你必须要理解的第一件事是,当foo()
类的构造函数处于活动状态时调用A
被调度到A::foo()
,即使正在构建的完整对象也是如此类型B
和B
会覆盖foo()
。 B::foo()
的存在被忽略了。
这意味着您的代码会尝试调用A::foo()
。由于A::foo()
是一个纯虚函数,因此代码的行为是未定义的。
C ++语言不保证在这种情况下应该出现什么样的“错误”。这意味着您对“链接器错误”的期望是完全没有根据的。如果程序尝试对纯虚函数执行虚拟调用,则行为只是 undefined 。从C ++语言的角度来看,这是唯一可以说的。
这种未定义的行为如何在实际实现中表现出来取决于实现。例如,允许未定义的行为通过编译时错误表现出来。
在您的情况下,您的程序会尝试对纯虚函数A::foo()
进行虚拟调用。在一般情况下,编译器通过实现多态的运行时机制来调度虚拟调用动态(所谓的虚方法表是最常用的)。在某些情况下,当编译器可以确定调用中使用的对象的确切类型时,它会优化代码并对虚函数进行普通的直接(非动态)调用。
实际上,如果函数是纯虚函数,则其虚方法表条目包含空指针。对此类函数的动态调用通常会导致运行时错误。同时,对此类函数的直接(优化)调用通常会导致编译器或链接器错误。
在您的示例中,编译器未优化调用。它通过虚拟方法表对A::foo()
进行了全面的动态调用。该表中的空指针触发了运行时错误。
如果直接从构造函数
调用纯虚函数 A() { foo(); }
典型的编译器通常会对foo()
进行直接(优化)调用,这通常会导致链接器错误。
答案 1 :(得分:1)
B
确实有foo
的实现,因此链接器没有问题。
据我所知,A
在错误的时间调用foo
的事实是编译器/链接器不需要弄清楚的。 (虽然在这种情况下进行这样的检查可能很简单,但我确信我们可以提出更复杂的案例,这些案例会更难或可能无法捕捉。)
答案 2 :(得分:0)
您的错误是从构造函数中调用虚函数的结果。调用的函数是A中的函数,而不是更多的派生函数。 C ++标准,第12.7.4节说明,
可以调用成员函数,包括虚函数(10.3) 在施工或毁坏期间(12.6.2)。 当虚拟功能 从构造函数或从构造函数直接或间接调用 析构函数,包括在构造或破坏期间 类非静态数据成员,以及调用的对象 适用于建造或销毁的对象(称之为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()'