C++(除了成员变量之外)还有另一种实现has-a关系的途径——私有继承。
使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。
(如果使用保护继承,基类的公有成员和保护成员都将称为派生类的保护成员。)
这意味着基类方法将不会称为派生类对象共有接口的一部分,但可以在派生类的成员函数中使用它们。
14.2.1 Student类示例(新版本)
Student类应从两个类派生而来,因此声明将列出这两个类:
class Student : private std::string, private std::valarray<double>
{
public:
...
};
使用多个基类的继承被称为多重继承(multiple inheritance,MI)。
新的Student类不需要私有数据,因为两个基类已经提供了所需的所有数据成员。包含版本(14.1节中的)提供了两个被显式命名的对象成员,而私有继承提供了两个无名称的子对象成员。这是两个方法的第一个主要区别。
1.初始化基类组件
隐式地继承组件而不是成员对象将影响代码的编写,因为再也不能使用name和scores来描述对象了,而必须使用用于公有继承的技术。例如,对于构造函数,包含将使用这样的构造函数:
Student(cosnt char * str, const double * pd, int n)
: name(str), scored(pd, n) {} // use object names for containment
对于继承类,新版本的构造函数将使用成员初始化列表语法,它使用类名而不是成员名来标识构造函数:
Student(const char * str, const double * pd, int n)
: std::string(str), ArrayDb(pd, n) {} // use class names for inheritance
成员初始化列表使用std::string(str),而不是name(str)。这是包含和私有继承之间的第二个主要区别。
2.访问基类的方法
使用私有继承时,只能在派生类的方法中使用基类的方法。但有时候可能希望基类工具是公有的。例如,在类声明中提出可以使用average()函数。和包含一样,要实现这样的目的,可以在公有Student::average()函数中使用私有Student::Average()函数,包含使用对象来调用方法:
double Student::Average() const
{
if (scores.size() > 0)
return scores.sum() / scoresh.size();
else
return 0;
}
然而,私有继承使得能够使用类名和作用于解析运算符来调用基类的方法:
double Student::Average() const
{
if (ArrayDb::size() > 0)
return ArrayDb::sum() / ArrayDb::size();
else
return 0;
}
总之,使用包含是将使用对象名来调用方法,而使用私有继承时将使用类名和作用域解析运算符来调用方法。
3.访问基类对象
当派生类要使用基类对象本身,例如,Student类的包含版本实现了Name()方法,它返回string对象成员name;但使用私有继承时,该string对象没有名称。那么,Student类的代码如何访问内部的string对象呢?
答案是使用强制类型转换。由於Student类是从string类派生而来的,因此可以通过强制类型转换,将Student对象转换为string对象:结果为继承而来的string对象。本书前面介绍过,指针this指向用来调用方法的对象,因此*this为用来调用方法的对象,在这个例子中,为类型为Student的对象。为避免调用构造函数创建新的对象,可使用强制类型转换来创建一个引用。
const string & Student::Name() cosnt
{
return (cosnt string &) *this;
}
上述方法返回一个引用,该引用指向用于调用该方法的Student对象中的继承而来的string对象。
4.访问基类的友元函数
用类名显式地限定函数名不适合于友元函数,这时因为友元不属于类。然而,可以通过显式地转换为基类来调用正确的函数。例如,对于下面的友元函数定义:
ostream & operator<<(ostream & os, const Student & stu)
{
os << "Scores for " << (const string &) str << ":\n";
...
}
如果plato是一个Student对象,则下面的语句将调用上述函数,stu将是指向plato的引用,而
将是指向cout的引用:
cout << plato;
下面的代码:
os << "Scores for " << (const string &) stu << ":\n";
显式地将stu转换为string对象引用,进而调用函数operator<<(oshtream & const string &)。
引用stu不会自动转换为string对象引用。根本原因在于,在私有继承中,在不进行显式类型转换的情况下,不能将指向派生类的引用或指针赋给基类引用或指针。
然而,即使这个例子使用的是公有继承,也必须使用显式类型转换,原因之一是,如果不使用类型转换,下述代码将与友元函数原型匹配,从而导致递归调用:
os << stu;
(注:我觉得这里的意思是:当我os << stu时,他发现是一个Student类型,但是Student类型没有operator<<()方法,所以函数回去找他的基类的方法,但是他的基类会发现它实际上是一个Student类型,于是又会调用Student的<<方法……)
5.使用修改后的Student类
……
14.2.2 使用包含还是私有继承
通常,应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。
14.2.3 保护继承
保护继承是私有继承的变体。保护及成在列出基类时使用关键字protected:
class Student : protected std::string, protected std::valarray<double>
{ ... };
使用保护继承时,积累的共有成员和保护成员都将成为派生类的保护成员。和私有继承一样,积累的接口在派生类中也是可用的,但在继承层次结构之外是不可用的。当从派生类派生出另一个类时,私有继承和保护继承之间的主要区别便呈现出来了。使用私有继承时,第三代类将不能使用积累的接口,这时因为基类的公有方法在类中将变成私有方法;使用保护继承时,基类的公有方法在第二代中将编程受保护的,因此第三代派生类可以使用它们。
14.2.4 使用using重新定义访问权限
使用保护派生或私有派生时,基类的公有成员将称为保护成员或私有成员。假设要让基类的方法在派生类外面可用,方法之一是定义一个使用该基类方法的派生类方法。例如,假设希望Student能够使用valarray类的sum()方法,可以在Student类的声明中生命一个sum()方法,然后像下面这样定义该方法:
double Student::sum() const // public Student method
{
return std::valarray<double>::sum(); // use privately-inherited method
}
这样Student类便能够调用Student::sum(),后者进而将valarray<double>::sum()方法应用于被包含的valarray对象(如果ArrayDb typedef在作用于中,也可以使用ArrayDb而不是std::valarray<double>)。
另一种方法是,将函数调用包装在另一个函数调用中,即使用一个using声明(就像名称空间一样)来指出派生类可以使用特定的基类成员,即使采用的两是私有派生。例如,假设希望通过Student类能够使用valarray的方法min()和max(),可以在studenti.h的共有部分加入如下using声明:
class Student : private std::string, private std::valarray<double>
{
...
public:
using std::valarray<double>::min;
using std::valarray<double>::max;
};
上述using声明使得valarray<double>::min()和valarray<double>::max()可用,就像他们是Student的共有方法一样:
cout << "high score: " << ada[i].max << endl;
注意,using声明只使用成员名——没有圆括号、函数特征标和返回类型。例如,为使Student类可以使用valarray的operator[]()方法拇指虚在Student类声明的公有部分包含下面的using声明:
using std::valarray>double<::operator[];
这将使两个版本(xonst和非const)都可用。这样,便可以删除Student::operator[]()的原型和定义。using声明只适用于继承,而不适用于包含。
有一种老式方式可用于在私有派生类中重新声明基类方法,即将方法名放在派生类的公有部分,如下所示:
class Student : private std:stirng, private std:valarray<double>
{
public:
std::valarray<double>::operator[]; // redeclare as public, just use name
}
这看起来像不包含关键字using的using声明。这种方式已被废弃,即将停止使用。因此,如果编译器支持using声明,应使用它来使派生类可以使用私有基类中的方法。