使用gcc进行纯虚函数调用时链接器错误

her*_*mos 5 c++ linker

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

#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;
}
Run Code Online (Sandbox Code Playgroud)

我知道标准没有定义从构造函数或析构函数调用纯虚函数时的行为,这也不是我如何在生产中编写代码的实际示例,它只是检查编译器执行什么的测试.

在Java打印中测试相同的构造

做某事(0)

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

我期望在C++中有类似的行为,其差异param在调用函数时包含任何内容.

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

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

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

Ham*_*son 5

编译器知道当你doSomething从构造函数内部调用时,该调用必须doSomething在此类中引用,即使它是虚拟的.因此,它将优化远程虚拟调度,而只是进行正常的函数调用.由于函数未在任何地方定义,因此会在链接时导致错误.


Dav*_*eas 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
}
Run Code Online (Sandbox Code Playgroud)

现在,C++有一个单独的编译模型,这意味着不需要在这个特定的翻译单元中定义函数.这Parent::doSomething()可以在链接到同一程序的不同翻译单元中定义.编译器不可能知道是否有任何其他TU将定义该函数,只有链接器知道,因此它是链接器,它是抱怨的链接器.

任何关于链接器如何在这种情况下工作或对进一步读取的参考的任何见解都将受到高度赞赏.

如前所述,编译器(此特定编译器)正在添加对该Parent级别的特定覆盖的调用.所有其他函数调用中的链接器试图找到在任何转换单元中定义的符号并且失败,从而触发错误.

纯虚拟符具有作为唯一的目的避免隐式使用(ODR使用的功能的标准)创建虚拟表时.也就是说,纯说明符的唯一目的不是将该符号的依赖项添加到虚拟表中.这反过来意味着链接器不需要在程序中存在符号(Parent::doSomething)以用于动态分派(vtable),但如果程序明确使用它,它仍然需要该符号.