指向超类对象的指针可以用作指向子类对象的指针。但是不能调用子类的成员函数。为什么?

Pointer to a Superclass object may serve as pointers to subclass objects. But can't call memeber functions of the subclass. why?

我注册了一个 C++ 课程,我有以下代码片段:

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++ 中,任何时候变量的类型和名称都是编译器允许自己知道的。

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

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

的指针
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->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_pet1 是一个 Cat,因此允许调用 Cat 方法。

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

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

现在,a_pet1->make_sound() 将被允许。它将尝试调用 Pet::make_sound,这是 而不是 Dog::make_sound,因为我们没有提供 定义 Pet::make_sound,这将在 link 时导致错误。

如果您希望 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)只是告诉编译器基本class方法Pet::make_sound有意没有实现,这也意味着不允许任何人创建Pet,甚至是 Pet 派生对象实例,而没有为其实际类型提供 make_sound 实现。

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

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

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

在您的示例中,a_pet1 和 a_pet2 是指向 'Pet' class 对象的指针,因此您的编译器只允许您访问其中实际可用的函数class。在这种情况下,'Pet' class 本身不包含 'make_sound' 函数。要解决此问题,您可以在基 class 中定义一个 'make_sound' 函数并将其标记为 'virtual'。这将使基指针上的函数调用始终调用继承 class.

中相应函数的执行
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();
}