假设我有两个c++类:

class A
{
public:
  A() { fn(); }

  virtual void fn() { _n = 1; }
  int getn() { return _n; }

protected:
  int _n;
};

class B : public A
{
public:
  B() : A() {}

  virtual void fn() { _n = 2; }
};

如果我写下面的代码:

int main()
{
  B b;
  int n = b.getn();
}

有人可能认为n被设为2。

结果是n被设为1。为什么?


当前回答

原因是c++对象的构造就像洋葱,由内而外。基类在派生类之前构造。所以,在生成B之前,必须先生成a。当调用A的构造函数时,它还不是B,因此虚函数表中仍然有A的fn()副本的条目。

其他回答

解决这个问题的一个方法是使用工厂方法来创建对象。

为你的类层次结构定义一个公共基类,其中包含一个虚拟方法afterConstruction():

class Object
{
public:
  virtual void afterConstruction() {}
  // ...
};

定义一个工厂方法:

template< class C >
C* factoryNew()
{
  C* pObject = new C();
  pObject->afterConstruction();

  return pObject;
}

像这样使用它:

class MyClass : public Object 
{
public:
  virtual void afterConstruction()
  {
    // do something.
  }
  // ...
};

MyClass* pMyObject = factoryNew();

为了回答当你运行这段代码时会发生什么/为什么,我通过编译它 g++ -ggdb main。Cc,并逐步使用gdb。

main.cc:

class A { 
  public:
    A() {
      fn();
    }
    virtual void fn() { _n=1; }
    int getn() { return _n; }

  protected:
    int _n;
};


class B: public A {
  public:
    B() {
      // fn();
    }
    void fn() override {
      _n = 2;
    }
};


int main() {
  B b;
}

在main处设置断点,然后进入B(),打印this ptr,进入a()(基构造函数):

(gdb) step
B::B (this=0x7fffffffde80) at main2.cc:16
16    B() {
(gdb) p this
$27 = (B * const) 0x7fffffffde80
(gdb) p *this
$28 = {<A> = {_vptr.A = 0x7fffffffdf80, _n = 0}, <No data fields>}
(gdb) s
A::A (this=0x7fffffffde80) at main2.cc:3
3     A() {
(gdb) p this
$29 = (A * const) 0x7fffffffde80

显示它最初指向派生的B对象B,该对象B被构造在0x7fffffffde80的堆栈上。下一步是进入以A()为基数的ctor,这变成了A * const到相同的地址,这是有意义的,因为以A为基数的对象正好在B对象的开头。但它仍然没有被构建:

(gdb) p *this
$30 = {_vptr.A = 0x7fffffffdf80, _n = 0}

还有一步:

(gdb) s
4       fn();
(gdb) p *this
$31 = {_vptr.A = 0x402038 <vtable for A+16>, _n = 0}

_n已经初始化,它的虚函数表指针包含虚void A::fn()的地址:

(gdb) p fn
$32 = {void (A * const)} 0x40114a <A::fn()>
(gdb) x/1a 0x402038
0x402038 <_ZTV1A+16>:   0x40114a <_ZN1A2fnEv>

因此,下一步通过this->fn()执行A::fn()是完全有意义的,前提是活动this和_vptr.A。再一步,我们回到B() ctor:

(gdb) s
B::B (this=0x7fffffffde80) at main2.cc:18
18    }
(gdb) p this
$34 = (B * const) 0x7fffffffde80
(gdb) p *this
$35 = {<A> = {_vptr.A = 0x402020 <vtable for B+16>, _n = 1}, <No data     fields>}

碱基A已经构造好了。注意,存储在虚函数表指针中的地址已经更改为派生类B的虚表,因此调用fn()将通过this->fn()选择派生类重写B::fn(),给定活动this和_vptr。A(在B()中调用B::fn()来查看这个。)再次检查存储在_vptr中的1个地址。A显示它现在指向派生类重写:

(gdb) p fn
$36 = {void (B * const)} 0x401188 <B::fn()>
(gdb) x/1a 0x402020
0x402020 <_ZTV1B+16>:   0x401188 <_ZN1B2fnEv>

By looking at this example, and by looking at one with a 3 level inheritance, it appears that as the compiler descends to construct the base sub-objects, the type of this* and the corresponding address in _vptr.A change to reflect the current sub-object being constructed, - so it gets left pointing to the most derived type's. So we would expect virtual functions called from within ctors to choose the function for that level, i.e., same result as if they were non-virtual.. Likewise for dtors but in reverse. And this becomes a ptr to member while members are being constructed so they also properly call any virtual functions that are defined for them.

从构造函数或析构函数调用虚函数是危险的,应该尽可能避免。所有c++实现都应该在当前构造函数中调用在层次结构级别定义的函数的版本,而不是更进一步。

c++ FAQ Lite在第23.7节中详细介绍了这一点。我建议你阅读这篇文章(以及FAQ的其余部分)。

摘录:

[…在构造函数中,虚调用机制被禁用,因为从派生类重写还没有发生。对象是从基础开始构造的,即“先基础后派生”。 […] 销毁是“在基类之前执行派生类”,因此虚函数的行为与构造函数一样:只使用局部定义—并且不调用覆盖函数以避免触及对象的(现在已销毁的)派生类部分。

编辑修正大部分到全部(谢谢litb)

c++ FAQ Lite很好地涵盖了这一点:

本质上,在调用基类构造函数期间,对象还不是派生类型,因此调用的是基类型的虚函数实现,而不是派生类型的实现。

你知道Windows资源管理器的崩溃错误吗?“纯虚函数调用…” 同样的问题…

class AbstractClass 
{
public:
    AbstractClass( ){
        //if you call pureVitualFunction I will crash...
    }
    virtual void pureVitualFunction() = 0;
};

由于pureVitualFunction()函数没有实现,并且在构造函数中调用该函数,因此程序将崩溃。