Skip to content

18.1 基类指针和引用指向派生类

By Alex on January 29th, 2008 | last modified by nascardriver on December 5th, 2020 翻译 by dashjay 2020-12-17

在前些章节,我们学习了所有关于如何使用继承从已经存在的类来派生出新类。在这个章中,我们将专注学习继承中最重要和最强大的方面 -- 虚函数。

但是之前我们讨论过虚函数是什么,让我们先来说说为什么我们需要它。

在这个派生类的构造这个章节中,你了解了当你创建一个派生类的时候,它是由多个部分组成的:每一个都是一个继承类,和它本身的一个部分。

例如,这里有一个简单的例子:

#include <string_view>

class Base
{
protected:
    int m_value;

public:
    Base(int value)
        : m_value{ value }
    {
    }

    std::string_view getName() const { return "Base"; }
    int getValue() const { return m_value; }
};

class Derived: public Base
{
public:
    Derived(int value)
        : Base{ value }
    {
    }

    std::string_view getName() const { return "Derived"; }
    int getValueDoubled() const { return m_value * 2; }
};

当我们创建了一个派生的对象,它包含了一个基类部分(先被构造出来),然后包含了一个派生(第二个被构造出来的)。记住继承意味着在两个类间产生了”是一个 (is a)“的关系。因为一个继承类是一个基类,也可以认为继承类包含了一个基类部分。

指针,引用和继承类

非常直观的是,我们可以将派生指针和引用指向派生对象:

#include <iostream>

int main()
{
    Derived derived{ 5 };
    std::cout << "derived is a " << derived.getName() << " and has value " << derived.getValue() << '\n';

    Derived &rDerived{ derived };
    std::cout << "rDerived is a " << rDerived.getName() << " and has value " << rDerived.getValue() << '\n';

    Derived *pDerived{ &derived };
    std::cout << "pDerived is a " << pDerived->getName() << " and has value " << pDerived->getValue() << '\n';

    return 0;
}

下面就是输出:

derived is a Derived and has value 5
rDerived is a Derived and has value 5
pDerived is a Derived and has value 5

然而,自从派生类有一个基类部分,一个更有趣的问题就是 C++ 将会我们让我们将一个基类指针或者引用指向一个派生类对象。我们确实可以这样做!

# include <iostream>

int main()
{
    Derived derived{ 5 };

    // These are both legal!
    Base &rBase{ derived };
    Base *pBase{ &derived };

    std::cout << "derived is a " << derived.getName() << " and has value " << derived.getValue() << '\n';
    std::cout << "rBase is a " << rBase.getName() << " and has value " << rBase.getValue() << '\n';
    std::cout << "pBase is a " << pBase->getName() << " and has value " << pBase->getValue() << '\n';

    return 0;
}

这个产生了结果:

derived is a Derived and has value 5
rBase is a Base and has value 5
pBase is a Base and has value 5

这个结果可能一开始不那么符合你的期望!

事实证明 rBasepBase 是一个基类引用和指针,他们可以仅仅看到基类成员(或者任何从基类继承的类)。因此尽管 Derived::getName() 隐藏了派生对象的Base::getName(),基类指针/引用不可以看到 Derived::getName()。因此,他们调用 Base::getName(),这就是为什么 rBasepBase 显示它们是一个基类而不是一个派生类。

(上面这句话太难翻译)It turns out that because rBase and pBase are a Base reference and pointer, they can only see members of Base (or any classes that Base inherited). So even though Derived::getName() shadows (hides) Base::getName() for Derived objects, the Base pointer/reference can not see Derived::getName(). Consequently, they call Base::getName(), which is why rBase and pBase report that they are a Base rather than a Derived.

注意这也意味着不可能从 rBasepBase 来调用 Derived::getValueDoubled()。它们是不可能在派生类中看见的。

这有一个更加复杂的例子,我们将会在下节课中详细说明:

# include <iostream>
# include <string_view>
# include <string>

class Animal
{
protected:
    std::string m_name;

    // We're making this constructor protected because
    // we don't want people creating Animal objects directly,
    // but we still want derived classes to be able to use it.
    Animal(std::string_view name)
        : m_name{ name }
    {
    }

    // To prevent slicing (covered later)
    Animal(const Animal&) = delete;
    Animal& operator=(const Animal&) = delete;

public:
    const std::string& getName() const { return m_name; }
    std::string_view speak() const { return "???"; }
};

class Cat: public Animal
{
public:
    Cat(std::string_view name)
        : Animal{ name }
    {
    }

    std::string_view speak() const { return "Meow"; }
};

class Dog: public Animal
{
public:
    Dog(std::string_view name)
        : Animal{ name }
    {
    }

    std::string_view speak() const { return "Woof"; }
};

int main()
{
    const Cat cat{ "Fred" };
    std::cout << "cat is named " << cat.getName() << ", and it says " << cat.speak() << '\n';

    const Dog dog{ "Garbo" };
    std::cout << "dog is named " << dog.getName() << ", and it says " << dog.speak() << '\n';

    const Animal *pAnimal{ &cat };
    std::cout << "pAnimal is named " << pAnimal->getName() << ", and it says " << pAnimal->speak() << '\n';

    pAnimal = &dog;
    std::cout << "pAnimal is named " << pAnimal->getName() << ", and it says " << pAnimal->speak() << '\n';

    return 0;
}

这生成结果:

cat is named Fred, and it says Meow
dog is named Garbo, and it says Woof
pAnimal is named Fred, and it says ???
pAnimal is named Garbo, and it says ???

我们在这里看到同样的问题。因为 pAnimal 是一个 Animal 指针,它只能看见类中 Animal 部分的。因此, pAnimal->speak() 调用了 Animal::speak() 而不是 Dog::Speak 或者 Cat::speak() 函数。

指向基类的指针和引用的使用方法

现在你可能会说,“上面的例子看起来有点蠢,为什么我们不设置一个指针或者引用到一个派生类中的基类部分,这样我们就能使用派生类的对象?”事实证明,有充分的理由。

首先,我们知道你想要写出一个函数可以打印一个动物的名字和声音。不适用基类指针,你必须像这样写重载函数:

void report(const Cat &cat)
{
    std::cout << cat.getName() << " says " << cat.speak() << '\n';
}

void report(const Dog &dog)
{
    std::cout << dog.getName() << " says " << dog.speak() << '\n';
}

不是很复杂,但是想象如果你有30种不同的动物,而不是两种。你就需要写三十种不同的函数!而且,如果你每添加一种新的动物,你就要为这个动物写一个新的函数。这是一个巨大的时间浪费,因为我们可以发现只有类型的参数不同。

然而,因为 CatDog 是派生于 AnimalCatDog 都有 Animal 的基类部分。因此,我们就可以像这样做啦:

void report(const Animal &rAnimal)
{
    std::cout << rAnimal.getName() << " says " << rAnimal.speak() << '\n';
}

我们就可以传入任何的 Animal 派生类,甚至是我们在写这个函数以后创建新的派生类!而无需给每个派生类都写一个函数,我们就得到了一个可以为所有 Animal 派生类服务的函数。

问题是,当然,因为 rAnimal 是一个 Animal 引用,rAnimal.speak() 将会调用 Animal::speak() 而不是派生类版本的 speak()

其次,我们现在有三个 cats 和 三个 dogs 你想要把他们放在数组里,方便你去访问。因为数组只能持有一个对象的类型,如果不使用基类指针或者引用,你就不得不创建两个不同的数组分别给两个不同的类型,像这样:

# include <array>
# include <iostream>

// Cat and Dog from the example above

int main()
{
    const auto cats{ std::to_array<Cat>({{ "Fred" }, { "Misty" }, { "Zeke" }}) };
    const auto dogs{ std::to_array<Dog>({{ "Garbo" }, { "Pooky" }, { "Truffle" }}) };

    // Before C++20
    // const std::array<Cat, 3> cats{{ { "Fred" }, { "Misty" }, { "Zeke" } }};
    // const std::array<Dog, 3> dogs{{ { "Garbo" }, { "Pooky" }, { "Truffle" } }};

    for (const auto& cat : cats)
    {
        std::cout << cat.getName() << " says " << cat.speak() << '\n';
    }

    for (const auto& dog : dogs)
    {
        std::cout << dog.getName() << " says " << dog.speak() << '\n';
    }

    return 0;
}

现在,想象如果有三十种不同的动物,你需要30个不同的数组,每种动物都需要一个!

然而,因为 CatDog 是从 Animal 派生的,我们也可以去做一些事,像这样:

# include <iostream>

int main()
{
    const Cat fred{ "Fred" };
    const Cat misty{ "Misty" };
    const Cat zeke{ "Zeke" };

    const Dog garbo{ "Garbo" };
    const Dog pooky{ "Pooky" };
    const Dog truffle{ "Truffle" };

    // Set up an array of pointers to animals, and set those pointers to our Cat and Dog objects
    const auto animals{ std::to_array<const Animal*>({&fred, &garbo, &misty, &pooky, &truffle, &zeke }) };

    // Before C++20, with the array size being explicitly specified
    // const std::array<const Animal*, 6> animals{ &fred, &garbo, &misty, &pooky, &truffle, &zeke };

    for (const auto animal : animals)
    {
        std::cout << animal->getName() << " says " << animal->speak() << '\n';
    }

    return 0;
}

当这个编译执行时,不幸的是每个 animals 是一个指针指向一个 Animal 这意味着 animal->speak() 将会调用 Animal::speak() 而不是派生类版本的我们想要的派生类版本的 speak(),这会得到输出:

Fred says ???
Garbo says ???
Misty says ???
Pooky says ???
Truffle says ???
Zeke says ???

尽管这些技巧可以节省我们大量的时间和能量,他们也有同样的问题。指向基类的指针或者引用调用基类版本的函数而不是派生类版本的。假设我们有一些方式使得那些基类指针调用派生类版本的函数而不是基类版本的……

请猜猜虚函数是干啥用的?:)

Quiz time

1)我们的 Animal/Cat/Dog 解释了上方的代码不能像我们想象的那样去工作,因为一个引用或者指针指向 Animal 不能访问派生类的版本的 speak() 需要返回一个 Cat 或者 Dog 的正确值。唯一的方式就是使得 speak() 返回的数据成为 Animal 基类的一部分(就像是 Animal 的 name 是通过 m_name 这个成员来访问的)

(题目翻译也许有问题)1) Our Animal/Cat/Dog example above doesn’t work like we want because a reference or pointer to an Animal can’t access the derived version of speak() needed to return the right value for the Cat or Dog. One way to work around this issue would be to make the data returned by the speak() function accessible as part of the Animal base class (much like the Animal’s name is accessible via member m_name).

Update the Animal, Cat, and Dog classes in the lesson above by adding a new member to Animal named m_speak. Initialize it appropriately. The following program should work properly:

更新之前课程中编写的 Animal, Cat, Dog 三个类,通过添加一个新的成员 m_speak 到 Animal 基类。以合适的方式初始化他们,下面的程序能够很好的工作。

# include <array>
# include <iostream>

int main()
{
    const Cat fred{ "Fred" };
    const Cat misty{ "Misty" };
    const Cat zeke{ "Zeke" };

    const Dog garbo{ "Garbo" };
    const Dog pooky{ "Pooky" };
    const Dog truffle{ "Truffle" };

    // Set up an array of pointers to animals, and set those pointers to our Cat and Dog objects
    const auto animals{ std::to_array<const Animal*>({ &fred, &garbo, &misty, &pooky, &truffle, &zeke }) };

    // Before C++20, with the array size being explicitly specified
    // const std::array<const Animal*, 6> animals{ &fred, &garbo, &misty, &pooky, &truffle, &zeke };

    for (const auto animal : animals)
    {
        std::cout << animal->getName() << " says " << animal->speak() << '\n';
    }

    return 0;
}

答案:

# include <array>
# include <string>
# include <string_view>
# include <iostream>

class Animal
{
protected:
    std::string m_name{};
    std::string m_speak{};

    // We're making this constructor protected because
    // we don't want people creating Animal objects directly,
    // but we still want derived classes to be able to use it.
    Animal(std::string_view name, std::string_view speak)
        : m_name{ name }, m_speak{ speak }
    {
    }

    // To prevent slicing (covered later)
    Animal(const Animal&) = delete;
    Animal& operator=(const Animal&) = delete;

public:
    const std::string& getName() const { return m_name; }
    const std::string& speak() const { return m_speak; }
};

class Cat: public Animal
{
public:
    Cat(std::string_view name)
        : Animal{ name, "Meow" }
    {
    }
};

class Dog: public Animal
{
public:
    Dog(std::string_view name)
        : Animal{ name, "Woof" }
    {
    }
};

int main()
{
    const Cat fred{ "Fred" };
    const Cat misty{ "Misty" };
    const Cat zeke{ "Zeke" };

    const Dog garbo{ "Garbo" };
    const Dog pooky{ "Pooky" };
    const Dog truffle{ "Truffle" };

    // Set up an array of pointers to animals, and set those pointers to our Cat and Dog objects
    const auto animals{ std::to_array<const Animal*>({ &fred, &garbo, &misty, &pooky, &truffle, &zeke }) };

    // Before C++20, with the array size being explicitly specified
    // const std::array<const Animal*, 6> animals{ &fred, &garbo, &misty, &pooky, &truffle, &zeke };

    // animal is not a reference, because we're looping over pointers
    for (const auto animal : animals)
    {
        std::cout << animal->getName() << " says " << animal->speak() << '\n';
    }

    return 0;
}

2)为什么上面的解决方案不是最优的?

提示:想想猫和狗的未来状态,我们想用更多的方式来区分猫和狗

提示:考虑一下在初始化时需要设置的成员对您的限制。

当前的解决方案并不是最优的,原因是我们必须给每个不同的类添加一个成员来分辨 CatDog,一段时间之后,我们的 Animal 类可能变得非常消耗内存并且非常复杂!

而且,这个方法只能在初始化的时候决定才能正常工作。举个例子,如果 speak() 返回了一个随机的结果,为每个 Animal 类(例如调用 Dog::speak() 可能返回 woofarf,或者 yip),这类的解决方案开始变得尴尬、崩溃。