每当某个类包含另一个类的对象作为其成员变量之一时,就会出现类组合。组合在两个类之间建立了(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 的派生类。
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/22181.html