假设我有两个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++ FAQ Lite很好地涵盖了这一点:

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

其他回答

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

然而,如果你的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。

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

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

摘录:

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

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

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

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.

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

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

在对象的构造函数调用期间,虚函数指针表没有完全构建。这样做通常不会给你带来你期望的行为。在这种情况下调用虚函数可能有效,但不能保证,应该避免使用,以便便于移植并遵循c++标准。