【问题标题】:Pointer to a Superclass object may serve as pointers to subclass objects. But can't call memeber functions of the subclass. why?指向超类对象的指针可以用作指向子类对象的指针。但不能调用子类的成员函数。为什么?
【发布时间】:2021-11-11 17:38:24
【问题描述】:

我参加了一门 C++ 课程,其中我有以下代码 sn-p:

class Pet {
protected:
    string name;
public:
    Pet(string n)
    {
        name = n;
    }
    void run()
    { 
        cout << name << ": I'm running" << endl; 
    }
};

class Dog : public Pet {
public:
    Dog(string n) : Pet(n) {};
    void make_sound()
    { 
        cout << name << ": Woof! Woof!" << endl; 
    }
};

class Cat : public Pet {
public:
    Cat(string n) : Pet(n) {};
    void make_sound()
    { 
        cout << name << ": Meow! Meow!" << endl; 
    }
};

int main()
{
    Pet *a_pet1 = new Cat("Tom");
    Pet *a_pet2 = new Dog("Spike");

    a_pet1 -> run(); 
    // 'a_pet1 -> make_sound();' is not allowed here!
    a_pet2 -> run();
    // 'a_pet2 -> make_sound();' is not allowed here!
}

我无法弄清楚为什么这是无效的。请为此建议合适的参考资料,以充分解释为什么会发生这种情况。

【问题讨论】:

标签: c++ inheritance


【解决方案1】:

在 C++ 中,任何时候变量的类型和名称都是编译器允许自己知道的。

每一行代码都会根据当前范围内的变量类型和名称进行检查。

当你有一个指向基类的指针时,变量的类型仍然是指向基类的指针。它指向的实际对象可能是派生类,但变量仍然是指向基类的指针。

Pet *a_pet1 = new Cat("Tom");
a_pet1 -> run(); 
// 'a_pet1 -> make_sound();' is not allowed here!

a_pet1 的类型是Pet*。它可能指向一个实际的Cat 对象,但这不是a_pet1 的类型所具有的信息。

在下一行,您使用的是a_pet1。您可以以对该行上的Pet 指针有效的方式使用它。 a_pet1-&gt;make_sound() 不是对 Pet 指针的有效操作,因为 Pet 类型没有 make_sound 方法。

你可以这样做:

Cat *a_pet1 = new Cat("Tom");
a_pet1 -> run(); 
a_pet1 -> make_sound(); // it now works!

因为我们将a_pet1 的类型从Pet* 更改为Cat*。现在编译器允许自己知道a_pet1Cat,因此允许调用Cat 方法。

如果您不想更改a_pet1 的类型(这是一个合理的要求),这意味着您希望在Pet 上支持make_sound,您必须将其添加到@987654342 类型中@:

class Pet {
protected:
    string name;
public:
    Pet(string n)
    {
        name = n;
    }
    void make_sound();
    void run()
    { 
        cout << name << ": I'm running" << endl; 
    }
};

现在,a_pet1-&gt;make_sound() 将被允许。它将尝试调用Pet::make_sound,它不是 Dog::make_sound,并且由于我们没有为Pet::make_sound 提供定义,这将导致错误在链接时。

如果你想让Pet::make_sound 分派到它的派生方法,你必须告诉编译器这是你想要的。如果你正确使用 virtual 关键字,C++ 将为你编写调度代码,如下所示:

class Pet {
protected:
    string name;
public:
    Pet(string n)
    {
        name = n;
    }
    virtual void make_sound() = 0;
    void run()
    { 
        cout << name << ": I'm running" << endl; 
    }
};

在这里我都制作了make_soundvirtual,并把它变成了纯虚拟的。使其成为虚拟意味着编译器将信息添加到每个PetPet 派生对象,因此,当它实际上指向派生对象类型而不是Pet 时,调用者可以找到正确的派生方法。

纯虚拟(=0)只是告诉编译器基类方法Pet::make_sound 故意没有实现,这也意味着不允许任何人创建Pet,甚至是Pet 派生对象实例,但没有为其实际类型提供 make_sound 实现。

最后,请注意我提到了“允许自己知道”。编译器在编译的某些阶段限制了它所知道的内容。您关于a_pet1Pet* 的声明告诉编译器“我不希望您假设这是Cat,即使我在其中放了Cat”。在编译的后期阶段,编译器可以记住这个事实。即使在运行时,有时也可以确定对象的实际类型(使用 RTTI)。忘记对象的类型既是有意的,也是有限的。

事实证明,“强制遗忘”在许多软件工程问题中非常有用。

在其他语言中,对所有对象的所有方法调用都经过一个动态调度系统,你永远不知道一个对象是否可以接受方法调用,除非在运行时尝试它。在这样的语言中,在任何对象上调用 make_sound 都会编译,并且在运行时它会失败或不失败,具体取决于对象是否实际上具有 make_sound 方法。 C++ 故意不这样做。有很多方法可以获得这种能力,但它们相对深奥。

【讨论】:

  • 感谢先生您的时间和努力!
【解决方案2】:

在您的示例中,a_pet1 和 a_pet2 是指向“Pet”类对象的指针,因此您的编译器仅允许您访问该类中实际可用的函数。在这种情况下,“Pet”类本身不包含“make_sound”函数。要解决此问题,您可以在基类中定义一个“make_sound”函数并将其标记为“虚拟”。这将使对基指针的函数调用始终调用继承类中相应函数的执行。

class Pet {
protected:
    string name;
public:
    Pet(string n)
    {
        name = n;
    }
    void run()
    {
        cout << name << ": I'm running" << endl;
    }

    virtual void make_sound() {}
};

class Dog : public Pet {
public:
    Dog(string n) : Pet(n) {};
    void make_sound() override
    {
        cout << name << ": Woof! Woof!" << endl;
    }
};

class Cat : public Pet {
public:
    Cat(string n) : Pet(n) {};
    void make_sound() override
    {
        cout << name << ": Meow! Meow!" << endl;
    }
};

int main()
{
    Pet* a_pet1 = new Cat("Tom");
    Pet* a_pet2 = new Dog("Spike");

    a_pet1->run();
    a_pet1->make_sound();
    a_pet2->run();
    a_pet2->make_sound();
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-06-06
    • 2023-02-09
    • 1970-01-01
    • 2017-03-09
    • 1970-01-01
    • 1970-01-01
    • 2018-11-05
    • 1970-01-01
    相关资源
    最近更新 更多