-1

考虑以下程序:

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/name,以便立即知道类型。上述方法失败了,在这方面我还有哪些其他选择?

4

3 回答 3

3

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

cout << cDerived.ClassID();

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

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

virtual int ClassID() = 0;

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

于 2012-08-23T20:20:33.633 回答
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参数,而完全忘记虚拟方法。

于 2012-08-23T20:26:16.870 回答
2

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

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

在幕后发生的是,当Base构造函数启动时,它将对象的 vptr 设置为指向 vtable for Base,因此它指向Base的虚函数。在完成并且Derived对象构造函数运行之后,它会更新 vptr 以指向的 vtable,因此它引用了覆盖Derived的函数。Derived因此,在Base构造函数完成之前,对象的 vptr 只会导致指向由Base.

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

于 2012-08-23T20:31:20.963 回答