在构造函数中调用虚函数

时间:2012-08-23 20:08:54

标签: c++ constructor virtual

考虑以下计划:

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.我已经找到了关于同一个问题here的类似问题。但我不明白的是,如果这是错误的,那么我们应该如何识别(派生)类成员并使用它?我的意思是假设我想为每个类专用一个唯一的id或typename(基类,第一个派生类,第二个派生类,它是一个派生的基类或第二个派生类等等),在这方面我怎样才能继续行动? 我认为正确的方法是在构造函数中在任何类对象的实例化时分配一个id / name,以便立即知道类型。上面的方法很复杂,我在这方面有什么其他选择?

3 个答案:

答案 0 :(得分:3)

详细说明Karoly所说的,也许你可以避免在构造函数中使用类ID,但是在构造之后你可以这样做:

cout << cDerived.ClassID();

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

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

virtual int ClassID() = 0;

如果你尝试在Base构造函数中调用ClassID,那么应该使它成为编译器错误,尽管我还没有尝试过。此外,它将使Base成为一个抽象类,因此您无法创建它的新实例(这很好)。

答案 1 :(得分:3)

答案很简单:在构造之前不能使用任何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在撰写此答案时所说的那样......如果您的示例是所有,那么您可以使用受保护的Base构造函数来获取int nID作为参数,完全忘记虚拟方法。

答案 2 :(得分:2)

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时。