调用构造函数中的虚拟函数

Calling virtual functions in constructors

本文关键字:虚拟 函数 构造函数 调用      更新时间:2023-10-16

考虑以下程序:

class Base
{
private:
    int m_nID;
public:
    Base()
    {
        m_nID = ClassID();
    }
    // ClassID returns a class-specific ID number
    virtual int ClassID() { return 1; }
    int GetID() { return m_nID; }
};
class Derived: public Base
{
public:
    Derived()
    {
    }
    virtual int ClassID() { return 2; }
};
int main()
{
    Derived cDerived;
    cout << cDerived.GetID(); 
    return 0;
}

在上面的例子中,派生的id出人意料地是1而不是2。我已经在这里发现了关于同一个问题的类似问题。但我不明白的是,如果这是错误的,那么我们应该如何识别(派生的)类成员并使用它?我的意思是,假设我想为每个类(基类、第一个派生类、第二个派生类,它要么是基类的派生,要么是第二个衍生类,等等)指定一个唯一的id或typename,在这方面,我该如何继续行动?我认为正确的方法是,在实例化任何类对象时,在构造函数中分配一个id/name,以便立即知道类型。上述方法失败了,在这方面我还有什么其他选择?

要详细说明Karoly所说的内容,也许你可以避免在构造函数中使用类ID,但在构造之后,你可以执行以下操作:

cout << cDerived.ClassID();

没有理由让两个函数返回相同的东西,也没有理由在每个对象中存储int m_nID;来浪费内存。

此外,您应该更改基类,使其显示:

virtual int ClassID() = 0;

如果你试图在Base构造函数中调用ClassID,尽管我没有尝试过,但这个应该会使它成为编译器错误。此外,它会使Base成为一个抽象类,所以你不能创建它的新实例(这很好)。

答案很简单:在构建任何C++对象之前,都不能使用它。如果您仍在构造基类子对象的过程中,则不能使用派生类对象,因为它尚未构造(当前正在构建的对象的vptr仍然指向基类vtable,只要你在基类构造函数内;它不会更新,直到你到达派生类构造函数。)

但是,基类构造函数是如何确定它是为常规对象还是子对象调用的呢?好吧,就像它告诉任何其他关于世界的随机信息一样:你必须明确地告诉它。例如:

struct Base {
    Base() { puts("I am a whole object!"); }
  protected:
    Base(bool is_subobject) { assert(is_subobject); puts("I'm only part of an object."); }
};
struct Derived : Base {
    Derived(): Base(/*is_subobject=*/true) { }
};

如果你想做一个真正聪明的人,你可以使用模板元编程:

struct Base {
    int m_nID;
    template<typename T>
    Base(T *)
    {
#ifdef UNSAFE
        // This breaks a lot of rules, but it will work in your case.
        // "this" is a Base, not a Derived, so the cast is undefined;
        // and if ClassID tried to use any non-static data members,
        // you'd be hosed anyway.
        m_nID = reinterpret_cast<T*>(this)->T::ClassID();
#else
        // This version is guaranteed to work in standard C++,
        // but you lose that nice virtual-ness that you worked
        // so hard for.
        m_nID = T::staticClassID();
#endif
    }
    virtual int ClassID() { return 1; }
    static int staticClassID() { return 1; }
    int GetID() { return m_nID; }
};
struct Derived : Base {
    Derived(): Base(this) { }
    virtual int ClassID() { return 2; }
    static int staticClassID() { return 2; }
};
int main() {
    Derived cDerived;
    std::cout << cDerived.GetID() << std::endl; // prints "2"
}

但正如Dave S在我写这个答案时所说的。。。如果您的示例是all,那么您可以只使用一个受保护的以int nID为参数的Base构造函数,而完全忘记虚拟方法。

Derived对象包含类型为Base的子对象,Base对象存在于Derived对象的"内部"。如果你取对象的地址和它的Base子对象的地址,Base将在Derived对象占用的内存区域内。

当你构造一个Derived时,它的构造函数会运行,首先要做的是构造它的每个基类,然后构造它的每一个成员。因此,当Derived::Derived()开始执行时,发生的第一件事就是Base::Base()执行。在该构造函数中,对象的动态类型还不是Derived,因为它还没有构造Derived部分。因此,当您在Base构造函数期间调用虚拟函数时,它会找到迄今为止唯一构造的对象的最终覆盖者Base部分。

在后台,当Base构造函数启动时,它将对象的vptr设置为指向Base的vtable,因此它指向Base的虚拟函数。完成并运行Derived对象构造函数之后,它将更新vptr以指向Derived的vtable,因此它引用Derived覆盖的函数。因此,在Base构造函数完成之前,对象的vptr只指向Base定义的虚拟函数的指针。

一旦Derived构造函数更新了vptr,调用虚拟函数将调用Derived重写,因此问题的答案是在派生构造函数中重新分配m_nId,此时它将调用重写函数并为您提供派生类ID。