调用构造函数中的虚函数
假设我有两个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; } };
如果我写下面的代码:
main() { B b; int n = b.getn(); }
人们可能会认为n
被设置为2。
事实certificate, n
被设置为1.为什么?
从构造函数或析构函数调用虚函数是危险的,只要有可能就应该避免。 所有的C ++实现都应该调用在当前构造函数中层次结构级别定义的函数版本,
C ++ FAQ Lite在第23.7节中详细介绍了这一点。 我build议阅读(和常见问题的其余部分)的后续。
编辑更正最重要的(谢谢litb)
从构造函数中调用多态函数是大多数面向对象语言的灾难处理方法。 遇到这种情况时,不同的语言会有不同的performance。
基本的问题是,在所有语言中,基types必须在派生types之前构造。 现在,问题是从构造函数中调用一个多态方法是什么意思。 你期望它的行为如何? 有两种方法:在基层调用方法(C ++风格)或在层次结构底部的未构造对象(Java方法)上调用多态方法。
在C ++中,Base类将在input自己的构造之前构build它的虚拟方法表的版本。 此时,对虚方法的调用将最终调用该方法的基本版本,或者生成一个纯虚方法 ,以防在该层次上没有实现。 Base完全构build完成后,编译器将开始构buildDerived类,并将覆盖方法指针指向层次结构下一级的实现。
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
在Java中,编译器将在进入基础构造函数或派生构造函数之前,在构build的第一步中构build虚拟表等效项。 其含义是不同的(对我的喜好更危险)。 如果基类构造函数调用在派生类中被覆盖的方法,调用实际上将在派生级别处理,调用未构造对象上的方法,从而产生意外的结果。 在构造函数块中初始化的派生类的所有属性尚未初始化,包括“final”属性。 具有在类级别定义的默认值的元素将具有该值。
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 ++中,至less你有保证它永远不会调用尚未构造的对象的方法…
原因是C ++对象是从内到外像洋葱一样构造的。 超级类是在派生类之前构造的。 所以,在制作B之前,必须制作一个A. 当A的构造函数被调用时,它不是B,所以虚函数表仍然有A的fn()副本的入口。
C ++ FAQ Lite涵盖了这一点:
本质上,在调用基类构造函数的过程中,对象还不是派生types,因此调用虚函数的基types实现,而不是派生types。
解决您的问题的一个办法是使用工厂方法来创build您的对象。
- 定义包含虚构方法afterConstruction()的类层次结构的公共基类:
类对象 { 上市: virtual void afterConstruction(){} // ... };
- 定义工厂方法:
模板<class C> C * factoryNew() { C * pObject = new C(); pObject-> afterConstruction(); 返回pObject; }
- 像这样使用它:
类MyClass:公共对象 { 上市: virtual void afterConstruction() { // 做一点事。 } // ... }; MyClass * pMyObject = factoryNew();
你知道从Windows资源pipe理器崩溃错误?! “纯虚函数调用 …”
同样的问题 …
class AbstractClass { public: AbstractClass( ){ //if you call pureVitualFunction I will crash... } virtual void pureVitualFunction() = 0; };
因为函数pureVitualFunction()没有实现,并且在构造函数中调用该函数,程序将会崩溃。
vtables是由编译器创build的。 一个类对象有一个指向它的vtable的指针。 当它开始生命时,该vtable指针指向基类的vtable。 在构造函数代码的末尾,编译器生成代码,将vtable指针重新指向该类的实际vtable。 这确保调用虚函数的构造函数代码调用这些函数的基类实现,而不是类中的重写。
C ++标准(ISO / IEC 14882-2014)说:
会员function,包括虚拟function(10.3),可以在施工或销毁期间调用(12.6.2)。 当构造函数或析构函数直接或间接地调用虚函数时,包括在构造或销毁类的非静态数据成员期间,以及调用所应用的对象是正在构build的对象(称为x)或销毁,被调用的函数是构造函数或析构函数类中的最终覆盖,而不是覆盖更多派生类中的函数。 如果虚函数调用使用显式的类成员访问(5.2.5),并且对象expression式引用x的完整对象或该对象的基类子对象之一,而不是x或其基类子对象之一,则行为是不确定的 。
所以,不要从构造函数或析构函数中调用正在构造或销毁的对象的virtual
函数,因为构造的顺序从基础到派生,并且析构函数的顺序从派生到基类开始 。
因此,试图在构造中调用基类的派生类函数是很危险的。类似地,一个对象以与构造相反的顺序销毁,所以试图从析构函数调用更多派生类中的函数可能会访问已经存在的资源被释放。
首先,创buildObject,然后将它的地址分配给指针。构造器在创build对象时被调用,用于初始化数据成员的值。 指向对象的指针在创build对象后进入scheme。 那就是为什么C ++不允许我们把构造函数变成虚拟的。 另一个原因是,没有什么像指向构造函数的指针,它可以指向虚拟构造函数,因为虚函数的一个属性是它只能被指针使用。
- 由于构造函数是静态的,因此虚函数用于dynamic赋值,所以我们不能使它们变成虚拟的。
这里我没有看到虚拟关键词的重要性。 b是一个静态types的variables,其types由编译器在编译时确定。 函数调用不会引用vtable。 当构造b时,它的父类的构造函数被调用,这就是为什么_n的值被设置为1的原因。
在对象的构造函数调用过程中,虚拟函数指针表没有完全build立。 这样做通常不会给你你期望的行为。 在这种情况下调用虚函数可能会有效,但不能保证,应该避免使用可移植的方式,并遵循C ++标准。