C++ 继承:Derived class 指向 Base class 的指针调用 Derived class 方法

C++ Inheritance: Derived class pointer to a Base class invokes Derived class method

我正在学习 C++ 继承,所以我通过动态创建一个 Base class 并对它的 Derived class 进行向下转换(显然向下转换无效)来尝试这段代码,以便使这个动态创建的 Base 对象由 Derived class 指针指向。但是当我通过这个 Derived 指针调用方法 who() 时,它会调用 Derived class 方法而不是 Base class 方法。

根据我的假设,没有为 Derived class 创建对象,那么怎么可能调用未创建的 Derived class 对象的方法而不是实际创建的 Base class 对象?

我用谷歌搜索了这种现象,但找不到关于调用非创建的 Derived class 对象方法的清晰明了的解释。如果它符合标准,请向我解释它是如何工作的。 我知道如果我使用方法 virtual 故事会有所不同,但这里使用的方法不是 virtual.

class parent{
    public:
        void who(){
            cout << "I am parent";
        }
};

class child : public parent{
    public:
        void who(){
            cout << "I am child";
        }
};

int main(){
    child *c = (child *)new parent();
    c->who();
    return 0;
}

输出是 I am child 但我期望 I am parent

编辑:: 我没有在上面的代码中释放内存并进行无效的向下转换,因为我只是想看看会发生什么。所以只解释这种只调用方法的行为。

多态性不是那样工作的。有趣的是,您从 parent*child* 的 C 风格转换有效,因为 classes 没有 v-table 或函数 [=12= 以外的任何其他内容].所以 who 的地址必须与 class 本身的地址相同。

parent *p = (parent*)new child();

会更有意义,但即便如此,如果您在 [=19] 中标记 who virtual 函数,p->who() 只会调用子 class 函数=] class,你还没有完成。

首先,这不是有效的沮丧。 new parent() 的真实类型确实是 parent 而不是 child。仅当真实(也称为动态)类型为 child 但指向对象目前为 parent 时才允许向下转型。

反过来会更有意义。如果您创建一个 child 并将其分配给一个 parent 指针,那就没问题了。但即便如此:除非 whovirtual 静态类型而不是动态类型决定调用哪个函数。

示例:

class parent{
    public:
        void who(){
            cout << "I am parent";
        }

    ~virtual parent() {}; // important to have that in class hierarchies where you might use a child that is assigned to a parent pointer!
};

class child : public parent{
    public:
        void who(){
            cout << "I am child";
        }
};

int main(){
    parent *c = (parent *)new child();
    c->who(); // output would still be parent because who() is not virtual
    delete c; // important! never forget delete!
    return 0;
}

另一方面,如果您使用

virtual void who();

那么,输出将是 "I am child" 正如您所期望的那样。

您的代码之所以如此,可能是因为编译器不检查对象的实际类型(除非您的函数是 virtual,否则不需要检查);它只是调用 child::who 因为你告诉它。也就是说,您的代码肯定是可疑的。

你是静态的downcasting the base class pointer to a derived class pointer, which not type-safe. C++ will not prevent you from doing this; it is up to you to ensure that your pointer really does point to an object of the derived type. Dereferencing the pointer if it does not refer to an object of the derived type is potentially undefined behaviour。你很幸运,你的代码甚至可以打印任何东西。

需要确保您的基础class函数whovirtual,否则函数调用将无法运行polymorphically. Keep in mind, once you add virtual to your code, you will certainly be invoking undefined behaviour, because you are illegally downcasting to an invalid type. You can safely downcast using dynamic_cast,如果对象不是指定类型,它将 return 一个 nullptr

您通常还应该在基 class 中有一个 virtual 析构函数,这样您的对象就可以 deleted 多态。说到这一点,您还需要确保 delete 在某些时候动态分配的对象。通过手动调用newdelete,很容易像这样泄漏内存,即使你知道需要调用deletestd::unique_ptr and std::shared_ptr 被添加到 C++11 的标准库中以解决这些问题。在除最低级代码之外的所有代码中使用这些而不是 newdelete

总而言之,我建议您的代码应该如下所示:

#include <iostream>
#include <memory>

class parent {
    public:
        virtual ~parent() {}

        virtual void who() {
            std::cout << "I am parent";
        }
};

class child : public parent {
    public:
        void who() override {
            std::cout << "I am child";
        }
};

int main() {
    auto p = std::make_unique<parent>();
    auto c = dynamic_cast<child*>(p.get());

    if (c) // will obviously test false in this case
    {
        c->who();
    }
}

@Mark Ransom has suggested in the comments, I looked the post When does invoking a member function on a null instance result in undefined behavior?。然后我想出了自己的解决方案。

这种调用非创建对象的方法的行为与 parent class 或 child class 或向下转换或继承无关。我已经静态地告诉通过向下转型调用 child::who(),编译器调用 child::who() 而不关心对象类型。

But how the who() method is called since no object is created for child class?

我尝试调用的方法没有任何成员变量,因此不需要取消引用 this 指针。它只是调用 who(),如果 child class 指针也指向 NULL,同样的行为也是如此。

child *c = NULL;
c->who(); 

它打印 I am child 即使 c 指向 NULL。因为不需要取消引用 this 指针。假设 who() 方法包含一个成员变量 x,如下所示。

class child : public parent{
    private:
        int x;
    public:
        void who(){
            x = 10;
            cout << "I am child " << x;
        }
};

child *c = NULL;
c->who()

现在它导致 Segmentation fault,因为为了使用 x,编译器必须将 x 的 this 指针取消引用为 (*this).x。由于 this 指向 NULL,取消引用将导致 Segmentation fault。这显然是未定义的行为。

您看到的是工作中的未定义行为。

您的函数是非虚函数,它们只是您告诉编译器指针指向的类型的成员函数。

child *c = (child*)new parent;

这是一个 c 风格的强制转换,使编译器相信指针 c 肯定 指向某个子对象。

因此,当您调用 c->who() 时,您实际上是在调用 child::who,因为指针是指向子节点的指针。

没有什么可怕的事情发生并且您看到 "I am child" 的原因是因为您没有尝试取消引用该指针或使用您的指向实例不使用的任何特定于子的字段其实有。所以你逃脱了。

很简单,在调用 c->who() 时,c 的静态类型(即编译器知道的 c 的类型)是 child* 并且方法 who 是非虚。因此,编译器会发出调用地址 child::who 的指令。

编译器不(通常,它怎么可能?)跟踪运行时 c 指向的对象的真实类型。

如果 child 中有任何不在 parent 中的成员,访问 child::who 中的这些成员会产生越界访问,这可能会导致SIGSEGV,或其他不愉快。

最后,关于你观察到的行为是否由标准保证,我倾向于同意@P45Imminent:parentchild都满足POD和child的要求没有非静态数据成员。因此,根据标准,parentchild 对象的运行时布局必须是不可区分的(至少就 parentchild 的方法而言——也许 childparent 末尾的填充量可能不同?)。因此,其中一条评论中引用的 9.3.1/2 中的行不适用于恕我直言。如果标准不支持这种布局假设,我很乐意听取更多知识渊博的人的意见。