【问题标题】:C++ Inheritance: Derived class pointer to a Base class invokes Derived class methodC++ 继承:派生类指向基类的指针调用派生类方法
【发布时间】:2016-07-03 23:59:14
【问题描述】:

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

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

我在谷歌上搜索了这个现象,但找不到一个清晰明了的解释来调用非创建的派生类对象的方法。如果它符合标准,那么请向我解释它是如何工作的。 我知道如果我使用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

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

【问题讨论】:

  • 行为未定义。任何事情都有可能发生。即使您将函数设为虚拟,这也是相同的未定义故事。
  • @P45Imminent - 行为未定义。您可以对观察到的行为提出合理的解释这一事实并不会改变这一点。 “未定义的行为”意味着语言定义没有告诉你发生了什么;仅此而已。
  • @jblixr 您无法通过观察行为来证明未定义行为的缺失。 “它似乎符合我的预期”是未定义行为的一个例子。它可能在不同的机器上执行其他操作,或者编译方式不同,或者在黄昏时潮低且附近有一只黑乌鸦叫。
  • 您可能会从stackoverflow.com/a/3826144/5987 获得一些见解。这根本不是同一个问题,但有类似的机制。
  • §9.3.1/2 非静态成员函数 / 2. 如果为非 X 类型或非类型的对象调用类 X 的非静态成员函数从 X 派生,行为未定义。

标签: c++ pointers inheritance subclass


【解决方案1】:

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

您是静态的downcasting 指向派生类指针的基类指针,而不是type-safe。 C++ 不会阻止你这样做;您可以确保您的指针确实指向派生类型的对象。如果指针不引用派生类型的对象,则取消引用指针可能是undefined behaviour。你很幸运,你的代码甚至可以打印任何东西。

需要确保您的基类函数whovirtual,否则函数调用将不会运行polymorphically。请记住,一旦将virtual 添加到您的 代码中,您肯定会调用未定义的行为,因为您非法向下转换为无效类型。您可以使用dynamic_cast 安全地向下转换,如果对象不是指定类型,它将返回nullptr

您的基类中通常还应该有一个virtual 析构函数,这样您的对象就可以是多态的deleted。说到这一点,您还需要确保在某个时候delete 您的动态分配的对象。通过手动调用newdelete,这样的内存泄露非常容易,即使知道需要调用deletestd::unique_ptrstd::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();
    }
}

【讨论】:

  • 我会听从你的建议,但一个子对象甚至没有创建,那么它如何调用子方法?
  • @jblixr 因为,正如我所提到的(我已编辑;如果您没有看到,请见谅),编译器不需要检查对象的实际类型。正如您所提到的,添加virtual 使其行为符合您的预期,但添加virtual 肯定会使您的代码非法,因为正如我还提到的,您不能通过对派生类型的引用来使用基类型的对象!但是,您可以做相反的事情(通过 base 派生的访问)。
  • @jblixr 为清楚起见再次编辑。
【解决方案2】:

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

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

child *c = (child*)new parent;

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

因此,当您调用c-&gt;who() 时,您是在专门调用child::who,因为指针是指向子对象的指针。

没有发生任何可怕的事情并且您看到“我是孩子”的原因是因为您没有尝试取消引用该指针或使用您指向的实例实际上没有的任何特定于孩子的字段.这样你就可以侥幸逃脱了。

【讨论】:

【解决方案3】:

首先,这不是有效的沮丧。 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();

然后,输出将是“我是孩子”,就像您期望的那样。

【讨论】:

  • 但是子方法是怎么调用的呢?因为没有为孩子创建任何对象
  • 之所以有效,是因为两个类都没有 vtable 并且只有一个函数;该函数的地址必须与类的地址相同。
  • 由于您的代码编写错误,因此无法保证任何事情。但是静态类型在这种情况下决定,最常见的是在有错误的代码中。有时甚至可以写 child* c = 0x12345678; c-&gt;who(); 并且您会得到输出,因为给出了静态类型(子)。当然,这种代码绝对不能用。
  • 不同意。代码针对这种特殊的边缘情况进行了明确定义。
  • @P45Imminent 不,它的定义不明确。它碰巧以您容易理解的方式失败,但不能保证在每个编译器或将来都会以这种方式失败。问题的 cmets 引用了标准的引文,使其非常明确。
【解决方案4】:

多态不是那样工作的。有趣的是,您从parent*child* 的C 风格转换是有效的,因为这些类没有v 表或除函数who 之外的任何其他东西。所以who的地址必须和class本身的地址一致。

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

会更有意义,但即便如此,如果您在 parent 类中标记函数 who virtualp-&gt;who() 只会调用子类函数,而您还没有这样做。

【讨论】:

  • 即使没有意义,但如何调用子方法,因为没有为子类创建对象
  • @jblixr 您可能想了解 v-tables 以及对象在内存中的布局方式,以了解为什么这会产生合理的结果。
  • @jvlixr 没关系,因为 sizeof(parent) 必须非零(标准坚持),但 sizeof(child) + sizeof(parent) = sizeof(parent)。
  • @P45Imminent 你说的很有道理,我理解你说的有点慢,所以延迟回复。这是标准还是你的假设?
  • 但是请不要把它在这种边缘情况下工作的事实作为你应该在实践中这样做的任何理由。这是一个糟糕的主意。
【解决方案5】:

正如@Mark Ransom 在 cmets 中所建议的那样,我查看了When does invoking a member function on a null instance result in undefined behavior? 的帖子。然后我想出了自己的解决方案。

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

但是由于没有为child 类创建对象,如何调用who() 方法?

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

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

即使c 指向NULL,它也会打印I am child。因为不需要取消引用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。这显然是一种未定义的行为。

【讨论】:

  • 实际上它们都是未定义的行为,只是在一种情况下“未定义”被证明是无害的。我不能再强调你应该始终避免未定义的行为,即使它看起来有效,因为它可能会在未来无缘无故停止工作。
【解决方案6】:

很简单,在您调用 c-&gt;who() 时,c 的静态类型(即编译器知道 c 的类型)是 child*,而方法 who 是非虚拟的。所以编译器发出指令来调用child::who的地址。

编译器不会(一般来说,它怎么可能?)在运行时跟踪 c 指向的对象的真实类型。

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-11
    • 1970-01-01
    • 2014-02-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多