C++的一个主要目标是促进代码重用。公有继承是实现这种目标的机制之一,其他还有:本身是另一个类的对象。这种方法称为包含、组合或层次化。另一种方法是使用私有或保护继承。通常,包含、私有继承和保护继承用于实现has-a关系,即新的类将包含另一种类的对象。多重继承使得能够使用两个或更多的基类派生出新的类,将基类的功能组合在一起。
14.1 包含对象成员的类
主要介绍了valarray类,该类是由头文件valarray支持的。valarray被定义为一个模板类,以便能够处理不同的数据类型。模板特性意味着声明对象时,必须指定具体的数据类型。这个类模板的一些方法:
1: operator[]; // 访问数组中的元素
2: size(); // 返回数组包含的数据
3: sum(); // 返回所有数据的总和
4: max(); // 最大值
5: min(); // 最小值
接下来讲了一个Student的范例,使用包含的关系。
要注意的两点:1)初始化被包含对象,使用初始化列表来初始化包含对象。对于继承对象,构造函数在成员初始化列表中使用类名来调用特定的基类构造函数。
1: hasDMA::hasDMA(const hasDMA & hs):baseDMA(hs) {...}
对于成员对象,构造函数则使用成员名。
1: Student(const char * str, const double * pd, int n) : name(str), scores(pd, n) {}
2)使用被包含对象接口:被包含对象的接口是公有的,可以在类方法中使用它。
14.2 私有继承
使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会称为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们。简而言之,派生类将继承基类的接口;这是is-a关系的一部分。使用私有继承,基类的公有方法将成为派生类的私有方法。简而言之,派生类不继承基类的接口。使用私有继承,类将继承实现。包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未被命名的继承对象添加到类中。
注意的问题:1)初始化基类组件,也是使用成员初始化列表的句法,它使用类名而不是成员名来标识构造函数:
1: Student(const char * str, const double * pd, int n):std::string(str), ArrayDb(pd, n) {}
2)访问基类的方法
使用私有继承时,只能在派生类的方法中使用基类方法。对比一下包含与私有继承调用基类方法的区别:
(1) 包含
1: double Student::Average() const
2: {
3: if(scores.size() > 0)
4: return scores.sum() / scores.size();
5: else
6: return 0;
7: }
(2) 私有继承
1: typedef std::valarray<double> ArrayDb;
2: double Student::Average() const
3: {
4: if(ArrayDb::size() > 0)
5: return ArrayDb::sum() / ArrayDb::size();
6: else
7: return 0;
8: }
3)访问基类对象
使用作用域解析操作符可以访问基类的方法,但如果要使用基类对象本身,该如何做呢?答案是使用强制类型转换。前面介绍过,指针this指向用来调用方法的对象,因此*this为用来调用方法的对象,在下例中,为类型为Student的对象。为避免调用构造函数创建新的对象,可以使用强制类型转换来创建一个引用:
1: const string & Student::Name() const
2: {
3: return (const string &) *this;
4: }
4)访问基类的友元函数
用类名显式地限定函数名不适合于友元函数,这是因为友元不属于类。不过可以通过显式地转换为基类调用正确的函数。
1: ostream & operator<<(ostream & os, const Student & stu)
2: {
3: os << "Scores for " << (const string &) stu << ":/n";
4: ...
5: }
引用stu不会自动转换为string引用。根本原因在于,在私有继承中,在不进行显式类型转换的情况下,不能将指向派生类的引用或指针赋给基类引用或指针。不过,即使这个例子使用的是公有继承,也必须使用显式类型转换。原因之一是,如果不使用类型转换,会导致递归调用。另一个原因是,由于这个类使用的是多重继承,编译器将无法确定应转换成那个基类,如果两个基类都提供了函数operator<<()。
5)使用包含还是私有继承
一般都选择包含。首先是易于理解。其次是继承会引起许多问题,尤其是从多个基类继承时。另外,包含能够包括多个同类的子对象。而继承只能使用一个这样的对象。不过私有继承也有很多优点:通过继承得到的是派生类,因此它可以访问保护成员。另一种需要使用私有继承的情况是需要重新定义虚函数。
6)保护继承
使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。与私有继承的主要区别在于:使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变为私有方法;使用保护继承时,基类的公有方法在第二代中将变成保护的,因此第三代派生类可以使用它们。
7)使用using重新定义访问权限
假设要让基类的方法在派生类外面可用,方法之一就是定义一个使用该基类方法的派生类方法。如下:
1: double Student::sum() const
2: {
3: return std::valarray<double>::sum();
4: }
另一种方法是。将函数调用包装在另一个函数调用中,即使用一个using声明来指出派生类可以使用特定的基类成员,即使采用的是私有派生。如下:
1: class Student:private std::string, private std::valarray<double>
2: {
3: ...
4: public:
5: using std::valarray<double>::min;
6: using std::valarray<double>::max;
7: ...
8: }
上述using声明使得valarray ::min()和valarray ::max()可用,就像它们是Student的公有方法一样:
1: cout << "high score: " << ada[i].max() << endl;
注意using声明只使用成员名--没有圆括号、函数特征标和返回类型。