我和一位朋友就最终使用这段代码构建对象进行了非常有趣的讨论:
#include <iostream>
class Parent {
public:
Parent( ) {
this->doSomething( );
}
virtual void doSomething( ) = 0;
};
class Child : public Parent {
int param;
public:
Child( ) {
param = 1000;
}
virtual void doSomething( ) {
std::cout << "doSomething( " << param << " )" << std::endl;
}
};
int main( void ) {
Child c;
return 0;
}
我知道标准没有定义从构造函数或析构函数调用纯虚函数时的行为,这也不是我如何在生产中编写代码的实际示例,它只是一个测试来检查什么编译器。
在Java中测试相同的构造
doSomething(0)
这是有道理的,因为param
未在doSomething()
处初始化,而是从父构造函数调用。
我希望在C ++中有类似的行为,区别在于param
在调用函数时包含任何内容。
相反,编译上面的代码导致链接器错误,(c++ (Ubuntu 4.4.3-4ubuntu5.1) 4.4.3)
表示对Parent::doSomething( )
的引用未定义。
所以,我的问题是:为什么这是一个链接器错误?如果这是一个错误,我希望编译器会抱怨,特别是因为有一个函数的实现。 任何关于链接器如何在这种情况下工作或对进一步阅读的参考的任何见解都将受到高度赞赏。
提前谢谢!我希望这个问题不重复,但我找不到类似的问题..
答案 0 :(得分:5)
编译器知道当你从构造函数中调用doSomething
时,该调用必须引用此类中的doSomething
,即使它是虚拟的。因此,它将优化远程虚拟调度,而只是进行正常的函数调用。由于函数未在任何地方定义,因此会在链接时导致错误。
答案 1 :(得分:5)
所以,我的问题是:为什么这是一个链接器错误?如果这是一个错误,我希望编译器会抱怨,特别是因为有一个函数的实现。任何关于链接器如何在这种情况下工作或对进一步阅读的参考的任何见解都将受到高度赞赏。
让我们再扩展一下。
为什么它是链接器错误?
因为编译器从构造函数中注入了对Parent::doSomething()
的调用,但是链接器找不到该函数的定义。
我希望编译器会抱怨,特别是因为有一个函数的实现。
这不正确。该函数有一个覆盖,可通过虚拟调度访问,但未定义函数Parent::doSomething()
。那里有一个微妙但重要的区别,可以通过禁用动态调度以不同的方式进行测试。您可以通过使用类名限定函数来禁用特定调用的动态调度,例如,如果添加Child::doSomething()
,则在Parent::doSomething()
中,这将生成对Parent::doSomething()
的调用而不使用动态派遣调用最终的覆盖者。
为什么这很重要?
这很重要,因为即使该函数是 pure-virtual (纯虚拟意味着动态调度永远不会调度到该特定的重载),它也可以定义并且称为:
struct base {
virtual void f() = 0;
};
inline void base::f() { std::cout << "base\n"; }
struct derived : base {
virtual void f() {
base::f();
std::cout << "derived\n";
}
};
int main() {
derived d;
d.f(); // outputs: base derived
}
现在,C ++有一个单独的编译模型,这意味着不需要在这个特定的翻译单元中定义函数。即Parent::doSomething()
可以在链接到同一程序的不同翻译单元中定义。编译器不可能知道是否有任何其他TU将定义该函数,只有链接器知道,因此链接器是抱怨的链接器。
对于链接器在这种情况下如何工作或对进一步阅读的参考的任何见解都将受到高度赞赏。
如前所述,编译器(此特定编译器)正在Parent
级别添加对特定覆盖的调用。所有其他函数调用中的链接器试图找到在任何转换单元中定义的符号并且失败,从而触发错误。
pure-virtual 说明符的唯一目的是避免在创建虚拟表时隐式使用函数(标准中的 odr-use )。也就是说,纯说明符的唯一目的不是将该符号的依赖项添加到虚拟表中。这反过来意味着链接器不会要求程序中存在符号(Parent::doSomething
)以用于动态分派(vtable),但如果程序明确使用它,它仍然需要该符号
答案 2 :(得分:0)
在调用doSomething()的指针处,编译器可以确定* this的动态类型是Parent,因此不需要调用函数indirect。此行为在很大程度上取决于您使用的编译器/链接器。