提问人:David Coufal 提问时间:6/7/2009 最后编辑:Jan SchultkeDavid Coufal 更新时间:9/25/2023 访问量:152239
为什么对构造函数中虚拟成员函数的调用是非虚拟调用?
Why is a call to a virtual member function in the constructor a non-virtual call?
问:
假设我有两个 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();
}
人们可能会期望将其设置为 2。n
事实证明,它设置为 1。为什么?n
答:
原因是C++对象像洋葱一样从内到外构造。基类是在派生类之前构造的。因此,在制作 B 之前,必须制作 A。当 A 的构造函数被调用时,它还不是 B,因此虚函数表仍然具有 A 的 fn() 副本的条目。
评论
从构造函数或析构函数调用虚函数是危险的,应尽可能避免。所有 C++ 实现都应调用在当前构造函数中的层次结构级别定义的函数版本,而不是进一步调用。
C++ FAQ Lite 在第 23.7 节中非常详细地介绍了这一点。我建议阅读该内容(以及常见问题解答的其余部分)以进行跟进。
摘录:
[...]在构造函数中,虚拟调用机制处于禁用状态,因为尚未从派生类重写。对象是从基础开始构建的,即“基础后派生”。
[...]
销毁是在“基类之前派生类”完成的,因此虚函数的行为与构造函数中的类似:仅使用本地定义 - 并且不调用重写函数以避免触及对象的(现已销毁的)派生类部分。
编辑最更正了所有(感谢 litb)
评论
C++ FAQ Lite 很好地涵盖了这一点:
实质上,在调用基类构造函数期间,对象还不是派生类型,因此调用基类型的虚函数实现,而不是派生类型的实现。
评论
你知道Windows资源管理器的崩溃错误吗?!“纯虚函数调用......”
同样的问题......
class AbstractClass
{
public:
AbstractClass( ){
//if you call pureVitualFunction I will crash...
}
virtual void pureVitualFunction() = 0;
};
因为函数 pureVitualFunction() 没有实现,并且该函数是在构造函数中调用的,所以程序将崩溃。
评论
在大多数面向对象语言中,从构造函数调用多态函数是灾难的根源。遇到这种情况时,不同的语言会有不同的表现。
基本问题是,在所有语言中,必须在派生类型之前构造 Base 类型。现在,问题是从构造函数调用多态方法意味着什么。你希望它表现如何?有两种方法:在基本级别调用方法(C++ 样式)或在层次结构底部的未构造对象上调用多态方法(Java 方式)。
在 C++ 中,Base 类将在输入自己的构造之前生成其虚拟方法表版本。此时,对虚拟方法的调用最终将调用该方法的基本版本,或者生成一个纯虚拟方法,以防它在层次结构的该级别没有实现。完全构造 Base 后,编译器将开始生成 Derived 类,并将重写方法指针以指向层次结构下一级的实现。
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 中,编译器将在构造的第一步,即输入 Base 构造函数或 Derived 构造函数之前,构建虚拟表等效项。含义是不同的(而且在我看来更危险)。如果基类构造函数调用在派生类中重写的方法,则该调用实际上将在派生级别处理,即在未构造对象上调用方法,从而产生意外结果。在构造函数块中初始化的派生类的所有属性都尚未初始化,包括“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();
vtables 由编译器创建。 类对象具有指向其 vtable 的指针。当它开始工作时,该 vtable 指针指向 vtable 的基类。在构造函数代码的末尾,编译器生成代码以重新指向 vtable 指针 到类的实际 vtable。这可确保调用虚拟函数的构造函数代码调用 这些函数的基类实现,而不是类中的重写。
评论
C::C
C
T::T(params)
T
我在这里没有看到虚拟关键字的重要性。b 是一个静态类型的变量,其类型由编译器在编译时确定。函数调用不会引用 vtable。构造 b 时,将调用其父类的构造函数,这就是将 _n 的值设置为 1 的原因。
评论
b
f()
b
B*
b.getN()
B
A
B
A
this
b.getn()
b
getn()
this
fn()
成员函数,包括虚函数 (10.3),可以调用 在建造或破坏期间(12.6.2)。当虚拟函数 直接或间接从构造函数或 析构函数,包括在建造或破坏 类的非静态数据成员,以及调用的对象 applies 是正在建造或破坏的对象(称为 X), 调用的函数是构造函数的 OR 中的最终覆盖者 析构函数的类,而不是在更派生的类中重写它。 如果虚拟函数调用使用显式类成员访问 (5.2.5) 对象表达式是指 x 的完整对象 或该对象的基类子对象之一,但不是 x 或其 基类子对象,则行为未定义。
因此,不要从构造函数或析构函数中调用函数,这些函数试图调用正在构造或销毁的对象,因为构造的顺序从基类开始到派生,析构函数的顺序从派生到基类开始。virtual
因此,尝试从正在构造的基类调用派生类函数是危险的。同样,对象的销毁顺序与构造相反,因此尝试从析构函数调用更派生的类中的函数可能会访问已释放的资源。
首先,创建对象,然后将其地址分配给指针。构造函数在创建对象时调用,用于初始化数据成员的值。指向对象的指针在创建对象后进入方案。这就是为什么C++不允许我们将构造函数设置为虚拟的。 另一个原因是,没有像指向构造函数的指针一样可以指向虚拟构造函数的东西,因为虚函数的一个特性就是它只能被指针使用。
- 虚函数是用来动态赋值的,因为构造函数是静态的,所以我们不能让它们虚拟。
如前所述,这些对象是在建造时自下而上创建的。在构造基本对象时,派生对象尚不存在,因此虚函数覆盖不起作用。
但是,如果 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。
评论
Base<T>
作为补充,调用尚未完成构造的对象的虚函数将面临同样的问题。
例如,在对象的构造函数中启动一个新线程,并将该对象传递给新线程,如果新线程在对象完成构造之前调用该对象的虚函数会导致意外结果。
例如:
#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
评论
sleep_for
this->Print()
this
getchar()
Sub
detach()
为了回答运行该代码时会发生什么/原因,我通过 编译了它,并使用 gdb 逐步完成。g++ -ggdb main.cc
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;
}
在 处设置一个断点,然后单步执行 B(),打印 ptr,单步执行 A() (基本构造函数):main
this
(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 obj 0x7fffffffde80。下一步是进入基 A() ctor 并进入相同的地址,这是有道理的,因为基 A 正好位于 B 对象的开头。但它仍然没有被构建:this
b
this
A * const
(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->fn() 执行 A::fn() 是完全有道理的,给定 active 和 .再往前走一步,我们又回到了 B() ctor:this
_vptr.A
(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 的 vtable。因此,对 fn() 的调用将通过 this->fn() 选择派生类覆盖 B::fn(),给定 B() 中的活动和 (取消注释调用 B::fn() 以查看此内容。再次检查存储在_vptr中的 1 个地址。A 显示它现在指向派生类重写:this
_vptr.A
(gdb) p fn
$36 = {void (B * const)} 0x401188 <B::fn()>
(gdb) x/1a 0x402020
0x402020 <_ZTV1B+16>: 0x401188 <_ZN1B2fnEv>
通过查看此示例,并查看具有 3 级继承的示例,似乎当编译器下降以构造基本子对象时,类型和相应的地址会发生变化以反映当前正在构造的子对象,因此它指向最派生的类型。因此,我们期望从 ctor 内部调用的虚函数选择该级别的函数,即与非虚拟相同的结果。dtors 也是如此,但相反。并在构造成员时成为成员的 ptr,因此它们也可以正确地调用为它们定义的任何虚函数。this*
_vptr.A
this
我刚刚在程序中遇到了这个错误。 我有这样的想法:如果该方法在构造函数中被标记为纯虚拟,会发生什么?
class Base {
public:
virtual int getInt() = 0;
Base(){
printf("int=%d\n", getInt());
}
};
class Derived : public Base {
public:
virtual int getInt() override {return 1;}
};
和。。。有趣的事情!你首先得到编译器的警告:
warning: pure virtual ‘virtual int Base::getInt() const’ called from constructor
还有来自 ld 的错误!
/usr/bin/ld: /tmp/ccsaJnuH.o: in function `Base::Base()':
main.cpp:(.text._ZN4BaseC2Ev[_ZN4BaseC5Ev]+0x26): undefined reference to `Base::getInt()'
collect2: error: ld returned 1 exit status
这完全不合逻辑,你只得到编译器的警告!
评论