C++基础教程面向对象(学习笔记(53))

指针和对派生对象基类的引用

在上一章中,您学习了有关如何使用继承从现有类派生新类的所有知识。在本章中,我们将重点介绍继承的一个最重要和最强大的方面 - 虚函数。

但在我们讨论虚拟函数之前,让我们首先考虑一下,说明我们需要它们的原因。

在构造派生类的章节中,您了解到在创建派生类时,它由多个部分组成:每个继承类的一个部分和自身的一部分。

例如,这是一个简单的案例:

class Base
{
protected:
    int m_value;
 
public:
    Base(int value)
        : m_value(value)
    {
    }
 
    const char* getName() { return "Base"; }
    int getValue() { return m_value; }
};
 
class Derived: public Base
{
public:
    Derived(int value)
        : Base(value)
    {
    }
 
    const char* getName() { return "Derived"; }
    int getValueDoubled() { return m_value * 2; }
};

当我们创建Derived对象时,它包含一个Base部分(首先构造)和一个Derived部分(第二个构造)。请记住,继承意味着两个类之间的is-a关系。由于Derived is-a Base,Derived包含Base部分是合适的。

指针,引用和派生类

我们可以非常直观地设置派生指针和对Derived对象的引用:

#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
但是,由于Derived有一个Base部分,一个更有趣的问题是C ++是否允许我们设置Base指针或Derived对象的引用。事实证明,我们可以!

#include <iostream>
 
int main()
{
    Derived derived(5);
 
    // 这些都是合法的!
    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
这个结果可能不是你最初期望的结果!

事实证明,因为rBase和pBase是Base引用和指针,它们只能看到Base的成员(或Base继承的任何类)。因此,即使Derived :: getName()为Derived对象阴影(隐藏)Base :: getName(),Base指针/引用也看不到Derived :: getName()。因此,他们调用Base :: getName(),这就是rBase和pBase返回它们是Base而不是Derived的原因。

请注意,这也意味着无法使用rBase或pBase调用Derived :: getValueDoubled()。他们无法在Derived中看到任何内容。

这是我们将在下一课中构建的另一个稍微复杂的示例:

#include <string>
#include <iostream>
 
class Animal
{
protected:
    std::string m_name;
 
    // 我们正在使这个构造函数受到保护 ,因为
    // 我们不希望人们直接创建Animal对象,
    // 但是我们仍然希望派生类能够使用它。
    Animal(std::string name)
        : m_name(name)
    {
    }
 
public:
    std::string getName() { return m_name; }
    const char* speak() { return "???"; }
};
 
class Cat: public Animal
{
public:
    Cat(std::string name)
        : Animal(name)
    {
    }
 
    const char* speak() { return "Meow"; }
};
 
class Dog: public Animal
{
public:
    Dog(std::string name)
        : Animal(name)
    {
    }
 
    const char* speak() { return "Woof"; }
};
 
int main()
{
    Cat cat("Fred");
    std::cout << "cat is named " << cat.getName() << ", and it says " << cat.speak() << '\n';
 
    Dog dog("Garbo");
    std::cout << "dog is named " << dog.getName() << ", and it says " << dog.speak() << '\n';
 
    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(Cat &cat)
{
    std::cout << cat.getName() << " says " << cat.speak() << '\n';
}
 
void report(Dog &dog)
{
    std::cout << dog.getName() << " says " << dog.speak() << '\n';
}

不是太难,但考虑如果我们有30种不同的动物类型而不是2种情况会发生什么。你必须编写30种几乎相同的功能!另外,如果您添加了一种新型动物,那么您也必须为该动物编写一个新功能。考虑到唯一真正的区别是参数的类型,这是浪费大量时间。

然而,因为猫和狗来自动物,猫和狗有动物部分。因此,我们应该能够做到这样的事情是有道理的:

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

这将让我们传入任何派生自Animal的类,甚至是我们在编写函数后创建的类!而不是每个派生类的一个函数,我们得到一个函数,适用于从Animal派生的所有类!

问题是,因为rAnimal是Animal引用,所以rAnimal.speak()将调用Animal :: speak()而不是speak()的派生版本。

其次,假设您有3只猫和3只狗,您希望将它们保存在数组中以便于访问。因为数组只能保存一种类型的对象,没有指针或对基类的引用,所以必须为每个派生类型创建一个不同的数组,如下所示:

#include <iostream>
 
int main()
{
    Cat cats[] = { Cat("Fred"), Cat("Misty"), Cat("Zeke") };
    Dog dogs[] = { Dog("Garbo"), Dog("Pooky"), Dog("Truffle") };
 
    for (int iii=0; iii < 3; ++iii)
        std::cout << cats[iii].getName() << " says " << cats[iii].speak() << '\n';
 
    for (int iii=0; iii < 3; ++iii)
        std::cout << dogs[iii].getName() << " says " << dogs[iii].speak() << '\n';
 
    return 0;
}

现在,考虑如果您有30种不同类型的动物会发生什么。你需要30个阵列,每种类型的动物一个!

但是,因为Cat和Dog都来自Animal,所以我们应该能够做到这样的事情是有意义的:

#include <iostream>
 
int main()
{
    Cat fred("Fred"), misty("Misty"), zeke("Zeke");
    Dog garbo("Garbo"), pooky("Pooky"), truffle("Truffle");
 
    // 设置一个指向动物的指针数组,并将这些指针设置为我们的Cat和Dog对象
    Animal *animals[] = { &fred, &garbo, &misty, &pooky, &truffle, &zeke };
    for (int iii=0; iii < 6; ++iii)
        std::cout << animals[iii]->getName() << " says " << animals[iii]->speak() << '\n';
 
    return 0;
}

虽然这会编译并执行,但不幸的是,数组“animals”的每个元素都是一个指向Animal的指针,这意味着animals[iii]->speak()将调用Animal :: speak()而不是我们想要的speak()的派生类版本。

虽然这两种技术都可以为我们节省大量的时间和精力,但它们也存在同样的问题。对基类的指针或引用调用函数的基本版本而不是派生版本。如果只有某种方法使这些基本指针调用函数的派生版本而不是基本版本…

想猜猜虚拟功能的用途是什么??

Quiz Time:

1)上面的动物/猫/狗示例不能像我们想要的那样工作,因为对Animal的引用或指针无法访问为cat或Dog返回正确值所需的speak()派生版本。解决此问题的一种方法是使speak()函数返回的数据作为Animal基类的一部分可访问(很像Animal的名称可以通过成员m_name访问)。

通过向名为m_speak的Animal添加新成员,更新上述课程中的Animal,Cat和Dog类。适当地初始化它。以下程序应该正常工作:

#include <iostream>
 
int main()
{
    Cat fred("Fred"), misty("Misty"), zeke("Zeke");
    Dog garbo("Garbo"), pooky("Pooky"), truffle("Truffle");
 
    // 设置一个指向动物的指针数组,并将这些指针设置为我们的Cat和Dog对象
    Animal *animals[] = { &fred, &garbo, &misty, &pooky, &truffle, &zeke };
    for (int iii=0; iii < 6; iii++)
        std::cout << animals[iii]->getName() << " says " << animals[iii]->speak() << '\n';
 
    return 0;
}

解决方案

#include <string>
#include <iostream>
 
class Animal
{
protected:
    std::string m_name;
    const char* m_speak;
 
    // 我们正在使这个构造函数受到保护 ,因为
    // 我们不希望人们直接创建Animal对象,
    // 但是我们仍然希望派生类能够使用它。
    Animal(std::string name, const char* speak)
        : m_name(name), m_speak(speak)
    {
    }
 
public:
    std::string getName() { return m_name; }
    const char* speak() { return m_speak; }
};
 
class Cat: public Animal
{
public:
    Cat(std::string name)
        : Animal(name, "Meow")
    {
    }
};
 
class Dog: public Animal
{
public:
    Dog(std::string name)
        : Animal(name, "Woof")
    {
    }
};
 
int main()
{
    Cat fred("Fred"), misty("Misty"), zeke("Zeke");
    Dog garbo("Garbo"), pooky("Pooky"), truffle("Truffle");
 
    // 设置一个指向动物的指针数组,并将这些指针设置为我们的Cat和Dog对象
    Animal *animals[] = { &fred, &garbo, &misty, &pooky, &truffle, &zeke };
    for (int iii=0; iii < 6; iii++)
        std::cout << animals[iii]->getName() << " says " << animals[iii]->speak() << '\n';
 
    return 0;
}

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

提示:想想Cat和Dog的未来状态,我们希望以更多方式区分Cat和Dog。
提示:考虑一下在初始化时需要设置成员的方式限制了你。

解决方案

目前的解决方案不是最优的,因为我们需要为我们想要区分Cat和Dog的每种方式添加一个成员。随着时间的推移,我们的Animal类可能会变得非常大,而且很复杂!
此外,只有在初始化时可以确定基类成员时,此解决方案才有效。例如,如果speak()返回每个Animal的随机化结果(例如,调用Dog :: speak()可以返回“woof”,“arf”或“yip”),这种解决方案开始变得笨拙而且麻烦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值