C++继承和组合(无师自通)

在面向对象语言中,类继承可用于描述派生类的类型是基类的一种特殊情况的事实,并建立相应的模型。实际上,类应该被视为可从中创建的所有对象的集合。因为派生类是基类的特例,所以对应于派生类的对象集合将是对应于基类的对象集合的子集。因此,派生类的每个对象也是基类的一个对象。换句话说,每个派生类对象都是一个(Is-a)基类对象。

每当某个类包含另一个类的对象作为其成员变量之一时,就会出现类组合。组合在两个类之间建立了(Has-a)的关系。

由于派生类继承了其基类的所有成员,派生类实际上包含其基类的一个对象。正因为如此,可能在某些需要组合的地方也可以使用继承。

现在来看一个示例。假设有一个程序需要能代表某个人的数据,比如说这个人的姓名和街道地址。街道地址可能由两行组成:

123 Main Street
Hometown, 12345

现在假设有一个代表街道地址的类:
class StreetAddress
{
    private:
        string line1, line2;
    public:
        void setLine1(string);
        void setLine2(string);
        string getLine1();
        string getLine2();
};
因为一个人的数据包含一个姓名和一个街道地址,所以正确表示一个人的数据的类将使用以下方式的组合:
class PersonData
{
    private:
        string name;
        StreetAddress address;
    public:
        ...
};

PemmData 的类声明中忽略了其余部分,因为那些与我们要阐述的主旨无关。

在这里可以使用继承而不是组合来定义这个类。例如,可以定义一个类 PersonData1,如下所示:
class PersonData1:public StreetAddress
{
    private:
        string name;
    public:
};
虽然这个新的定义能够正确编译,但从概念上讲这其实是错误的,因为它将一个人的数据视为一种特殊的街道地址,而事实并非如此。这种类型的设计概念错误可能会导致程序理解困扰并难以维护。

所以,更好的设计做法是,尽可能优先选择组合而不是继承。这样做还有一个原因是,继承打破了基类的封装,因为它将基类的受保护成员暴露给了派生类的方法。

现在来看一个使用继承比组合更有意义的示例。假设有一个 dog 类,代表了所有狗的集合。每个 Dog 对象都有一个类型为 double 的成员变量 weight 和一个成员函数 voidbark(),该类的示例如下:
class Dog
{
    protected:
        double weight;
    public:
        Dog(double w)
        { weight = w; }
        virtual void bark() const
        {
            cout << "I am dog weighing " << weight << " pounds." << endl;
        }
};
这个类还有一个构造函数,允许 Dog 对象被初始化。请注意,以上示例中已经声明了一个虚成员函数 bark(),以允许它在派生类中被覆盖。

假设需要一个代表所有牧羊犬集合的类。由于每只牧羊犬都是狗,因此从 Dog 类中派生出新的 SheepDog 类是有意义的。这样,SheepDog 对象将继承 Dog 类的每个成员。除了具有狗所具有的各种特征之外,每只牧羊犬还应该具有区别于其他犬种的特殊特征,例如,有一个整数成员 numberSheep 指示牧羊犬被训练看护的绵羊的最大数量。另外,牧羊犬的吠叫方式可能不同于普通犬种,也许要适应看护羊群的需要。这可以通过覆盖 Dog 类的 bark() 成员函数来解决。
class SheepDog:public Dog
{
    private:
        int numberSheep;
    public:
        SheepDog(double w, int nSheep) : Dog(w)
        {
            numberSheep = nSheep;
        }
        virtual void bark() const override
        {
            cout << "I am a sheepdog weighing " << weight << " pounds \nand guarding " << numberSheep << " sheep." << endl;
        }
};
为了演示该类,可以建立一个狗的矢量,矢量中的一些狗就是牧羊犬。为了规避矢量不能拥有两种不同类型的事实,可以使用指向 Dog 的指针的矢量。前面讲过,一个指向基类(在本示例中即 Dog 类)的指针也可以指向任何派生类对象(在本示例中为 SheepDog)。因此,可以创建一个指向 Dog 的指针矢量,并且其中一些指针指向 Dog 对象,而另一些指针则指向 SheepDog 对象。
vector<shared_ptr<Dog>> kenne1
{
    make_shared<Dog>(40.5),
    make_shared<SheepDog>(45.3, 50),
    make_shared<Dog>(24.7)
};
最后,可以使用一个循环来调用矢量中每个 Dog 对象的 bark() 成员函数:
for (int k = 0; k < 3; k++)
{
    cout << k+1 << ": ";
    kennel[k]->bark();
}
由于多态性,并且因为 bark() 函数被声明为虚函数,所以循环内的同一行代码对普通狗将调用原始的 bark() 函数,而对牧羊犬则会调用派生类 SheepDog 中的特殊 bark() 函数。完整的程序为:
// This program demonstrates the Is-A relation in inheritance.
#include <iostream>
#include <memory>
#include <vector>

using namespace std;
// Base class
class Dog
{
    protected:
        double weight;
    public:
        Dog(double w)
        {
            weight = w;
        }
        virtual void bark() const
        {
            cout << "I am a dog weighing " << weight << " pounds." << endl;
        }
};

// A SheepDog is a special type of Dog
class SheepDog :public Dog
{
        int numberSheep;
    public:
        SheepDog(double w, int nSheep) : Dog(w)
        {
            numberSheep = nSheep;
        }
        void bark() const override
        {
            cout << "I am a sheepdog weighing " << weight << " pounds and guarding " << numberSheep << " sheep." << endl;
        }
};

int main()
{
    // Create a vector of dogs
    vector<shared_ptr<Dog>> kennel
    {
        make_shared<Dog>(40.5),
        make_shared<SheepDog>(45.3, 50),
        make_shared<Dog>(24.7)
    };
    // Walk by each kennel and make the dog bark
    for (int k = 0; k < kennel.size(); k++)
    {
        cout << k + 1 << ": ";
        kennel[k]->bark();
    }
    return 0;
}
程序输出结果:

1: I am a dog weighing 40.5 pounds.
2: I am a sheepdog weighing 45.3 pounds and guarding 50 sheep.
3: I am a dog weighing 24.7 pounds.

在本示例中,继承是一个比组合更好的选择,因为使用组合就等于说一只牧羊犬有一只(Has-a)狗,而不是说一只牧羊犬是一只(Is-a)狗。

有些作者认为在类之间存在第三种关系,即:使用实现关系。基本上,一个类如果调用第二个类的对象的成员函数,则称它使用了第二个类的实现。

如何才能知道何时该使用继承,何时该使用组合呢?

假设有一个现有的类 C1,并且需要为另一个类 C2 编写一个定义,而 C2 需要一个关联的 C1 对象的服务。那么,究竟是需要从 C1 派生 C2,还是应该给 C2 —个 C1 类型的成员变量?一般来说,应该优选组合而不是继承。

为了帮助确定继承是否合适,可以提出以下问题:
  • 将 C2 对象设想成 C1 对象的特殊类型是否自然?如果是,那么应该使用继承。
  • C2 类对象是否需要在 C1 类对象使用的地方使用?例如,C2 对象是否需要被传递给函数,而该函数釆用的引用形参为 C1 类型或指向 C1 的指针?如果是,那么应该使 C2 成为 C1 的派生类。