5

我和一个朋友就对象的构造进行了一次非常有趣的讨论,最终得到了这段代码:

#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 打印中测试相同的构造

做某事(0)

这是有道理的,因为从父构造函数调用时param未初始化。doSomething()

我希望在 C++ 中有类似的行为,不同之处在于param在调用函数时包含任何内容。

相反,编译上述代码会导致链接器错误,(c++ (Ubuntu 4.4.3-4ubuntu5.1) 4.4.3)并指出对的引用Parent::doSomething( )未定义。

所以,我的问题是:为什么这是一个链接器错误?如果这是一个错误,我希望编译器会抱怨,特别是因为有该函数的实现。任何有关链接器在这种情况下如何工作的见解或对进一步阅读的参考都将受到高度赞赏。

先感谢您!我希望这个问题不是重复的,但我找不到类似的问题..

4

3 回答 3

5

编译器知道,当您doSomething从构造函数内部调用时,该调用必须doSomething在此类中引用,即使它是虚拟的。所以它会优化掉虚拟调度,而只是做一个普通的函数调用。由于该函数未在任何地方定义,因此会导致链接时出错。

于 2012-07-11T16:37:20.133 回答
5

所以,我的问题是:为什么这是一个链接器错误?如果这是一个错误,我希望编译器会抱怨,特别是因为有该函数的实现。任何有关链接器在这种情况下如何工作的见解或对进一步阅读的参考都将受到高度赞赏。

让我们进一步扩展一下。

为什么是链接器错误?

因为编译器从构造函数注入了一个调用Parent::doSomething(),但是链接器发现没有定义函数。

我希望编译器会抱怨,特别是因为有一个函数的实现。

这是不正确的。该函数有一个可通过虚拟调度访问的覆盖Parent::doSomething(),但该函数未定义。那里有一个微妙但重要的区别,可以通过禁用动态调度以不同的方式进行测试。您可以通过使用类名限定函数来禁用特定调用的动态调度,例如,Child::doSomething()如果您添加Parent::doSomething(),这将生成一个调用,Parent::doSomething()而不使用动态调度来调用最终覆盖器。

为什么这很重要?

这很重要,因为即使函数是纯虚拟的(纯虚拟意味着动态调度永远不会调度到那个特定的重载),它也可以被定义调用

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 )。也就是说,pure 说明符的唯一目的不是将对该符号的依赖添加到虚拟表中。这反过来意味着链接器将不需要在Parent::doSomething程序中存在符号 ( ) 来实现动态调度 (vtable),但如果程序显式使用它,它仍然需要该符号。

于 2012-07-11T16:51:53.030 回答
0

在调用doSomething()的指针处,编译器可以确定*this的动态类型是Parent,所以不需要间接调用函数。这种行为很大程度上取决于您使用的编译器/链接器。

于 2012-07-11T16:37:50.713 回答