【问题标题】:Calling virtual functions in constructors在构造函数中调用虚函数
【发布时间】:2012-08-23 20:08:53
【问题描述】:

考虑以下程序:

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 或类型名类或第二个派生类等),在这方面我该如何继续行动? 我认为正确的方法是在实例化任何类对象时在构造函数中分配一个 id/name,以便立即知道类型。上述方法失败了,在这方面我还有哪些其他选择?

【问题讨论】:

  • 构造完成后,ClassID 返回你所需要的。
  • @BoPersson:实际上这个问题有很大不同。我不是在问为什么会这样,我是在问我如何通过其他方法使用这种方法来实现我想要实现的目标。
  • @karolyHarvath: 如果我使用这样的情况怎么办:void Foo(baseClass bar);在 Foo() 函数内部,根据对象类型,我有不同的操作。假设我有一个 Person 类和两个名为:Male 和 Female 的派生子类。有一个这样的函数:void Foo(Person person ){ if ( person.ClassID == 1 ) cout

标签: c++ constructor virtual


【解决方案1】:

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

cout << cDerived.ClassID();

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

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

virtual int ClassID() = 0;

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

【讨论】:

  • 谢谢。如果我使用这样的情况怎么办: void Foo(baseClass bar);在 Foo() 函数内部,根据对象类型,我有不同的操作。假设我有一个 Person 类和两个名为:Male 和 Female 的派生子类。有一个这样的函数:void Foo(Person person ){ if ( person.ClassID == 1 ) cout
【解决方案2】:

答案很简单:任何 C++ 对象在构造之前都不能使用。如果你还在构造基类子对象的过程中,那么你不能使用派生类对象因为它还没有被构造。(当前对象的 vptr只要你在基类构造函数中,underconstruction 仍然指向基类 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 您正在做的,那么您可以只拥有一个受保护的 Base 构造函数,它将 int nID 作为参数,完全忘记虚拟方法。

【讨论】:

  • 请注意,虽然它是该规则背后的动机,但 vtable/vptr 只是一个实现细节。
  • 正确,vtable/vptr 只是一个实现细节。但是(从我对第一句话的措辞可以看出)我不相信“你不能从 Base 的构造函数访问 Derived”的规则特定实现的影响:相反,它受制于“构造”一个​​对象意味着什么的抽象概念。从实现的 POV 来看,在 Derived::Derived 中只设置一次 vptr 会更简单,但出于严格的理论动机原因,标准要求实现首先将其设置为 Base 的 vtable,然后再重置它到 Derived 的。
【解决方案3】:

Derived 对象包含Base 类型的子对象,Base 对象存在于Derived 对象的“内部”。字面意思是里面。如果您获取对象的地址及其Base 子对象的地址,则Base 将位于Derived 对象占用的内存区域内。

当你构造一个Derived 时,它的构造函数运行,发生的第一件事就是构造它的每个基类,然后构造它的每个成员。所以当Derived::Derived() 开始执行时发生的第一件事就是Base::Base() 执行。在构造函数期间,对象的 动态类型 还不是 Derived,因为它还没有构造 Derived 部分。因此,当您在 Base 构造函数期间调用虚函数时,它会找到迄今为止已构造的唯一对象的 final overriderBase 部分。

在底层发生的事情是,当Base 构造函数启动时,它将对象的vptr 设置为指向Base 的vtable,因此它指向Base 的虚函数。完成之后,Derived 对象构造函数运行,它更新 vptr 以指向 Derived 的 vtable,因此它引用了 Derived 覆盖的函数。所以在Base构造函数完成之前,对象的vptr只会导致指向Base定义的虚函数的指针。

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

【讨论】:

    猜你喜欢
    • 2010-10-05
    • 2012-01-28
    • 2013-02-18
    • 2010-12-31
    • 2014-09-02
    • 2013-04-09
    • 1970-01-01
    • 2013-09-06
    相关资源
    最近更新 更多