假设我有两个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.为什么?
答案 0 :(得分:193)
从构造函数或析构函数调用虚函数是危险的,应尽可能避免使用。所有C ++实现都应调用当前构造函数中层次结构级别定义的函数版本,不再进一步调用。
C++ FAQ Lite在第23.7节中详细介绍了这一点。我建议您阅读(以及常见问题解答的其余部分)以进行跟进。
摘录:
[...]在构造函数中,虚拟调用机制被禁用,因为尚未发生从派生类的覆盖。对象是从基础构建的,“基础在派生之前”。
[...]
销毁是在“基类之前的派生类”完成的,因此虚函数的行为与构造函数一样:只使用本地定义 - 并且不会调用重写函数以避免触及(现在销毁的)派生类的部分对象
编辑最多修正(感谢litb)
答案 1 :(得分:78)
在大多数OO语言中,从构造函数调用多态函数是一种灾难。遇到这种情况时,不同语言的表现会有所不同。
基本问题是,在所有语言中,必须在派生类型之前构建基本类型。现在,问题是从构造函数中调用多态方法意味着什么。你期望它的表现如何?有两种方法:在Base级别调用方法(C ++样式)或在层次结构底部的未构造对象上调用多态方法(Java方式)。
在C ++中,Base类将在进入自己的构造之前构建其虚拟方法表的版本。此时,对virtual方法的调用将最终调用方法的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 ++中,至少你可以保证它永远不会在一个尚未构造的对象上调用方法......
答案 2 :(得分:54)
原因是C ++对象从内到外构造成洋葱。在派生类之前构造超类。因此,在制作B之前,必须制作A.当调用A的构造函数时,它还不是B,因此虚函数表仍然具有A的fn()副本的条目。
答案 3 :(得分:23)
C++ FAQ Lite很好地涵盖了这一点:
本质上,在调用基类构造函数期间,该对象还不是派生类型,因此调用基类型的虚函数实现而不是派生类型。
答案 4 :(得分:13)
您的问题的一个解决方案是使用工厂方法来创建对象。
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();
答案 5 :(得分:1)
您是否知道Windows资源管理器出现崩溃错误?! “Pure virtual function call ...”
同样的问题......
class AbstractClass
{
public:
AbstractClass( ){
//if you call pureVitualFunction I will crash...
}
virtual void pureVitualFunction() = 0;
};
因为函数pureVitualFunction()没有实现,并且在构造函数中调用了函数,程序将崩溃。
答案 6 :(得分:1)
vtable由编译器创建。 类对象具有指向其vtable的指针。当它开始生命时,该vtable指针指向vtable 基类。在构造函数代码的末尾,编译器生成代码以重新指向vtable指针 到班级的实际vtable。这可以确保调用虚函数的构造函数代码调用 这些函数的基类实现,而不是类中的覆盖。
答案 7 :(得分:1)
C++ Standard (ISO/IEC 14882-2014)说:
可以调用成员函数,包括虚函数(10.3) 在施工或毁坏期间(12.6.2)。当一个虚函数 从构造函数或从构造函数直接或间接调用 析构函数,包括在构造或破坏期间 class的非静态数据成员,以及调用的对象 适用于建造或销毁的对象(称之为x), 被调用的函数是构造函数中的最终覆盖 析构函数的类,而不是在更派生的类中重写它。 如果虚函数调用使用显式类成员访问 (5.2.5)和对象表达式是指x的完整对象 或该对象的基类子对象之一,但不是x或其中一个 基类子对象,行为未定义。
因此,不要从构造函数或析构函数中调用virtual
函数来尝试调用正在构建或销毁的对象,因为构造顺序从 base开始到派生析构函数的顺序从派生到基类开始。
因此,尝试从构造中的基类调用派生类函数是危险的。同样,对象在构造中以相反的顺序销毁,因此尝试从析构函数调用更多派生类中的函数可能会访问资源已经被释放。
答案 8 :(得分:1)
其他答案已经解释了为什么virtual
函数调用从构造函数中调用时无法按预期方式工作。相反,我想提出另一种可能的解决方法,以便从基本类型的构造函数中获取类似多态的行为。
通过将模板构造函数添加到基本类型,以便始终将模板参数推导出为派生类型,可以知道派生类型的具体类型。从那里,您可以为该派生类型调用static
成员函数。
此解决方案不允许调用非static
成员函数。尽管执行是在基本类型的构造函数中进行的,但派生类型的构造函数甚至没有时间浏览其成员初始化列表。正在创建的实例的派生类型部分尚未开始对其进行初始化。而且由于非static
成员函数几乎可以肯定地与数据成员进行交互,因此想要从基类型的构造函数中调用派生类型的非static
成员函数是不寻常的。
这是一个示例实现:
#include <iostream>
#include <string>
struct Base {
protected:
template<class T>
explicit Base(const T*) : class_name(T::Name())
{
std::cout << class_name << " created\n";
}
public:
Base() : class_name(Name())
{
std::cout << class_name << " created\n";
}
virtual ~Base() {
std::cout << class_name << " destroyed\n";
}
static std::string Name() {
return "Base";
}
private:
std::string class_name;
};
struct Derived : public Base
{
Derived() : Base(this) {} // `this` is used to allow Base::Base<T> to deduce T
static std::string Name() {
return "Derived";
}
};
int main(int argc, const char *argv[]) {
Derived{}; // Create and destroy a Derived
Base{}; // Create and destroy a Base
return 0;
}
此示例应打印
Derived created
Derived destroyed
Base created
Base destroyed
构造Derived
时,Base
构造函数的行为取决于所构造对象的实际动态类型。
答案 9 :(得分:0)
首先,创建Object,然后将其地址分配给指针。在创建对象时调用构造函数,并用于初始化数据成员的值。对象指针在对象创建后进入场景。这就是为什么,C ++不允许我们将构造函数设置为虚拟。 另一个原因是,没有像指向构造函数的指针,它可以指向虚构造函数,因为虚函数的一个属性是它只能被指针使用。
答案 10 :(得分:0)
正如已经指出的那样,这些物体是在建造时基础建造的。构造基础对象时,派生对象尚不存在,因此虚函数覆盖不起作用。
但是,这可以通过使用静态多态而不是虚函数的多态getter来解决,如果你的getters返回常量,或者可以用静态成员函数表示,这个例子使用CRTP({ {3}})。
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 to call,因为信息是在编译时提供的。
答案 11 :(得分:0)
作为补充,调用一个尚未完成构造的对象的虚函数也会面临同样的问题。
比如在对象的构造函数中启动一个新线程,并将对象传递给新线程,如果新线程在对象构造完成前调用了该对象的虚函数,会导致意外结果。
例如:
Traceback (most recent call last):
File "C:\Users\hp\.spyder-py3\temp.py", line 14, in <module>
ls_component('D:\SDP\speaker-identification-master\data\eval')
File "C:\Users\hp\.spyder-py3\temp.py", line 10, in ls_component
ls = os.path.sep(path)
TypeError: 'str' object is not callable
这将输出:
#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;
}
答案 12 :(得分:-3)
我没有看到虚拟关键词的重要性。 b是静态类型变量,其类型由编译器在编译时确定。函数调用不会引用vtable。构造b时,会调用其父类的构造函数,这就是_n的值设置为1的原因。
答案 13 :(得分:-4)
在对象的构造函数调用期间,虚拟函数指针表未完全构建。这样做通常不会给你你期望的行为。在这种情况下调用虚函数可能有效,但不能保证,应该避免可移植并遵循C ++标准。