假设我有两个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++标准(ISO/IEC 14882-2014)说:

Member functions, including virtual functions (10.3), can be called during construction or destruction (12.6.2). When a virtual function is called directly or indirectly from a constructor or from a destructor, including during the construction or destruction of the class’s non-static data members, and the object to which the call applies is the object (call it x) under construction or destruction, the function called is the final overrider in the constructor’s or destructor’s class and not one overriding it in a more-derived class. If the virtual function call uses an explicit class member access (5.2.5) and the object expression refers to the complete object of x or one of that object’s base class subobjects but not x or one of its base class subobjects, the behavior is undefined.

因此,不要从构造函数或析构函数中调用试图调用正在构造或销毁的对象的虚函数,因为构造函数的顺序从基类开始到派生类,而析构函数的顺序从派生类开始到基类。

因此,试图从正在构建的基类调用派生类函数是危险的。类似地,对象以与构造相反的顺序被销毁,因此试图从析构函数调用派生类中的函数可能会访问已经释放的资源。

其他回答

在大多数OO语言中,从构造函数调用多态函数是导致灾难的原因。遇到这种情况时,不同的语言会有不同的表现。

基本问题是,在所有语言中,基类型必须在派生类型之前构造。现在,问题是从构造函数调用多态方法意味着什么。你希望它表现得怎样?有两种方法:在基本层调用方法(c++风格)或在层次结构底部的未构造对象上调用多态方法(Java方式)。

In C++ the Base class will build its version of the virtual method table prior to entering its own construction. At this point a call to the virtual method will end up calling the Base version of the method or producing a pure virtual method called in case it has no implementation at that level of the hierarchy. After the Base has been fully constructed, the compiler will start building the Derived class, and it will override the method pointers to point to the implementations in the next level of the hierarchy.

class Base {
public:
   Base() { f(); }
   virtual void f() { std::cout << "Base" << std::endl; } 
};
class Derived : public Base
{
public:
   Derived() : Base() {}
   virtual void f() { std::cout << "Derived" << std::endl; }
};
int main() {
   Derived d;
}
// outputs: "Base" as the vtable still points to Base::f() when Base::Base() is run

In Java, the compiler will build the virtual table equivalent at the very first step of construction, prior to entering the Base constructor or Derived constructor. The implications are different (and to my likings more dangerous). If the base class constructor calls a method that is overriden in the derived class the call will actually be handled at the derived level calling a method on an unconstructed object, yielding unexpected results. All attributes of the derived class that are initialized inside the constructor block are yet uninitialized, including 'final' attributes. Elements that have a default value defined at the class level will have that value.

public class Base {
   public Base() { polymorphic(); }
   public void polymorphic() { 
      System.out.println( "Base" );
   }
}
public class Derived extends Base
{
   final int x;
   public Derived( int value ) {
      x = value;
      polymorphic();
   }
   public void polymorphic() {
      System.out.println( "Derived: " + x ); 
   }
   public static void main( String args[] ) {
      Derived d = new Derived( 5 );
   }
}
// outputs: Derived 0
//          Derived 5
// ... so much for final attributes never changing :P

如您所见,调用多态(c++术语为虚拟)方法是一个常见的错误来源。在c++中,至少你可以保证它永远不会对一个尚未构造的对象调用方法……

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

正如已经指出的那样,对象是在构造时创建的。在构造基对象时,派生对象还不存在,因此虚函数重写不能工作。

然而,如果你的getter返回常量,这可以用多态getter来解决,多态getter使用静态多态性而不是虚函数,或者可以在静态成员函数中表示。本例使用CRTP (https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern)。

template<typename DerivedClass>
class Base
{
public:
    inline Base() :
    foo(DerivedClass::getFoo())
    {}

    inline int fooSq() {
        return foo * foo;
    }

    const int foo;
};

class A : public Base<A>
{
public:
    inline static int getFoo() { return 1; }
};

class B : public Base<B>
{
public:
    inline static int getFoo() { return 2; }
};

class C : public Base<C>
{
public:
    inline static int getFoo() { return 3; }
};

int main()
{
    A a;
    B b;
    C c;

    std::cout << a.fooSq() << ", " << b.fooSq() << ", " << c.fooSq() << std::endl;

    return 0;
}

通过使用静态多态性,基类知道在编译时提供信息时调用哪个类的getter。

为了回答当你运行这段代码时会发生什么/为什么,我通过编译它 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.

作为补充,调用尚未完成构造的对象的虚函数也将面临同样的问题。

例如,在对象的构造函数中启动一个新线程,并将该对象传递给新线程,如果新线程在对象完成构造之前调用该对象的虚函数将导致意想不到的结果。

例如:

#include <thread>
#include <string>
#include <iostream>
#include <chrono>

class Base
{
public:
  Base()
  {
    std::thread worker([this] {
      // This will print "Base" rather than "Sub".
      this->Print();
    });
    worker.detach();
    // Try comment out this code to see different output.
    std::this_thread::sleep_for(std::chrono::seconds(1));
  }
  virtual void Print()
  {
    std::cout << "Base" << std::endl;
  }
};

class Sub : public Base
{
public:
  void Print() override
  {
    std::cout << "Sub" << std::endl;
  }
};

int main()
{
  Sub sub;
  sub.Print();
  getchar();
  return 0;
}

这将输出:

Base
Sub