在构造函数中调用虚函数

假设我有两个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。为什么?

136074 次浏览

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

从构造函数或析构函数调用虚函数是危险的,应该尽可能避免。所有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()函数没有实现,并且在构造函数中调用该函数,因此程序将崩溃。

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

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

在c++中,基类将在进入自己的构造之前构建它的虚方法表版本。此时,对虚方法的调用将最终调用该方法的Base版本,或者在该层次结构级别没有实现的情况下产生纯虚方法调用。在Base完全构造之后,编译器将开始构建派生类,它将覆盖指向层次结构下一层中的实现的方法指针。

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中,编译器将在构造的第一步,即进入基本构造函数或派生构造函数之前,构建等价的虚拟表。其含义是不同的(在我看来更危险)。如果基类构造函数调用在派生类中被重写的方法,则调用实际上将在派生级别处理,在未构造的对象上调用方法,从而产生意想不到的结果。在构造函数块中初始化的派生类的所有属性都还没有初始化,包括“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++中,至少你可以保证它永远不会对一个尚未构造的对象调用方法……

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

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

  • 为你的类层次结构定义一个公共基类,其中包含一个虚拟方法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();


虚表是由编译器创建的。 类对象有一个指向虚表的指针。当它开始生命时,虚表指针指向虚表 基类的。在构造函数代码的末尾,编译器生成重新指向虚表指针的代码 到类的实际虚函数表。这样可以确保调用虚函数的构造函数代码调用 这些函数的基类实现,而不是类中的重写

我看不出这里虚拟关键词的重要性。B是一个静态类型变量,它的类型由编译器在编译时确定。函数调用不会引用虚表。当b被构造时,它的父类的构造函数被调用,这就是为什么_n的值被设置为1。

c++标准(ISO/IEC 14882-2014)表示:

可以调用成员函数,包括虚函数(10.3) 在建造或破坏期间(12.6.2)。当虚函数 从构造函数直接或间接调用 类的构造或销毁过程中 类的非静态数据成员,以及调用所指向的对象 apply是正在构建或销毁的对象(称为x), 被调用的函数是构造函数的或函数的最终覆盖 析构函数的类,而不是在派生类中重写它。 如果虚函数调用使用显式类成员访问 (5.2.5),对象表达式引用x的完整对象 或者该对象的基类子对象之一,而不是x或它的子对象之一 基类子对象,则行为为未定义的.

因此,不要从构造函数或析构函数中调用virtual函数,因为构造函数的顺序从派生的基开始,而析构函数的顺序从派生到基类开始。

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

首先,创建Object,然后将它的地址分配给指针。构造函数在创建对象时调用,用于初始化数据成员的值。对象指针在对象创建后进入场景。这就是为什么c++不允许我们将构造函数设为虚函数。 .另一个原因是,没有像指向构造函数的指针那样可以指向虚构造函数,因为虚函数的属性之一是它只能被指针使用。< / p >
  1. 虚函数用于动态赋值,因为构造函数是静态的,所以我们不能将它们设为虚函数。

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

然而,这可以通过使用静态多态性而不是虚函数的多态getter来解决,如果你的getter返回常量,或者可以在静态成员函数中表示。

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。

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

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

例如:

#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

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

显示this最初指向在堆栈0x7fffffffde80处构造的派生B对象b。下一步是进入base A() ctor, this变成相同地址的A * const,这是有意义的,因为base 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已经初始化,它的虚函数表指针包含virtual void A::fn()的地址:

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

因此,在给定活动this_vptr.A的情况下,下一步通过->fn()执行A::fn()是完全有意义的。再一步,我们回到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()将通过这个->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>

通过查看这个示例,以及一个具有3级继承的示例,似乎当编译器向下构造基子对象时,this*的类型和_vptr.A中相应的地址会发生变化,以反映正在构造的当前子对象,因此它向左指向派生最多的类型。因此,我们期望从ctors内部调用的虚函数选择该级别的函数,也就是说,如果它们是非虚函数,结果是一样的。医生也一样,但情况相反。在构造成员时,this成为成员的ptr,因此它们也正确地调用定义为为他们的任何虚函数。