C++的一个主要目标是促进代码重用。公有继承是实现这种目标的机制之一,但并不是唯一的机制,还有其他的方法。其中之一是使用这样的类成员:本身是另一个类的对象。这种方法称为包含(containment)、组合(composition)或层次化(layering)。另一种方法是使用私有或保护继承。通常,包含、私有继承和保护继承用于实现has-a关系,即新的类将包含另一个类的对象。多重继承使得能够使用两个或更多的基类派生出新的类,将基类的功能组合在一起。
包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未命名的继承对象添加到类中。
包含对象成员的类
class Student
{
private:
typedef std::valarray<double>ArrayDb;
std::string name;
ArrayDb scores;
std::ostream & arr_out(std::ostream & os)const;
public:
Student() :name("Null Student"), scores() {}
Student(const char * str, const double * pd, int n) :name(str), scores(pd, n) {}
~Student() {}
double Average()const;
const std::string & Name()const;
friend std::ostream & operator<<(std::ostream & os, const Student & stu); //const确保方法不修改参数
};
上面的Student类包含string类的对象name以及valarray<double>类的scores对象。上述类将数据成员声明为私有的,这意味着Student类的成员函数可以使用string和valarray<double>类的公有接口来访问和修改name和scores对象。但在类的外面不能这样做,而只能通过Student类的公有接口访问name和scores。对于这种情况,通常被描述为Student类获得了其对象成员的实现,但没有继承接口。
接口和实现
使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现)。获得接口关系式is-a关系的组成部分。而使用组合,类可以获得实现,但不能获得接口。不继承接口是has-a关系的组成部分。
私有继承
另一种实现has-a关系的途径——私有继承。使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们。
使用公有继承,基类的公有方法将成为派生类的公有方法。总之,派生类将继承基类的接口;这是is-a关系的一部分。使用私有继承,类将继承实现,基类的公有方法将成为派生类的私有方法。总之,派生类不继承基类的接口,这种不完全继承是has-a关系的一部分。因此私有继承提供的特性与包含相同:获得实现,但不获得接口。
class Student :private std::string, private std::valarray<double>
{
private:
typedef std::valarray<double>ArrayDb;
std::ostream & arr_out(std::ostream & os)const; //private method for scores output
public:
Student() :std::string("Null Student"), ArrayDb() {}
Student(const char * str, const double * pd, int n) :std::string(str), ArrayDb(pd, n) {}
~Student() {}
double Average()const;
const std::string & Name()const;
friend std::ostream & operator<<(std::ostream & os, const Student & stu); //const确保方法不修改参数
};
这个版本的Student类不需要私有数据,因为两个基类已经提供了所需的所有数据成员。包含版本提供了两个被显式命名的对象成员,而私有继承提供了两个无名称的子对象成员。另外,在构造函数中的成员初始化列表里,私有继承使用类名而不是成员名来标识构造函数,这是私有继承与包含之间的第二个主要区别。
使用私有继承时,只能在派生类的方法中使用基类的方法。但有时候希望基类工具是公有的。例如下面包含中的double Student::Average() const函数使用对象来调用方法。然而,私有继承使得能够使用类名和作用域解析运算符来调用基类方法,如上面的Average()函数使用ArrayDd::sum()和ArrayDb::size()方法。
//use containment
double Student::Average()const
{
if (scores.size() > 0)
return scores.sum() / scores.size();
else
return 0;
}
//use private inheritance
double Student::Average()const
{
if (ArrayDb::size() > 0)
return ArrayDb::sum() / ArrayDb::size();
else
return 0;
}
访问基类对象时,包含版本中的Name()方法返回string成员name。使用私有继承时,Student类的代码将使用强制类型转换来访问内部的string对象。由于Student类是从string类派生而来的,因此可以通过强制类型转换将Student对象转换为string对象,结果为继承而来的string对象。此方法返回一个引用,该引用指向调用该方法的Student对象继承而来的string对象。
//use containment
const string & Student::Name()const //第一个const确保引用或指针返回的值不能用于修改对象中的数据
{ //第二个const确保方法不修改调用它的对象
return name;
}
//use private inheritance
const string & Student::Name()const
{
return (const string &) *this;
}
访问基类的友元函数时,私有继承用类名显式地限定函数名不适合于友元函数,因为友元不属于类。然而可以通过显式地转换为基类来调用正确的函数。而包含直接使用成员名。
另外,私有继承中,os << "Scores for " << (const string &)stu << ":\n";显式地将stu转换为string对象引用,进而调用函数operator<<(ostream&, const string &)。引用stu,即const string & stu不会自动转换为string引用。因为在私有继承中,未进行显式类型转换的派生类引用或指针,无法赋给基类引用或指针。而且,由于使用的是多重继承,编译器将无法确定应转换为哪个基类,如果两个基类都提供了函数operator<<()。这个例子中,即使使用公有继承,也必须进行显式类型转换。如果不使用类型转换,os<<stu将与友元函数原型匹配,从而导致递归调用。
//use containment
ostream & operator<<(ostream & os, const Student & stu)
{
os << "Scores for " << stu.name << ":\n";
stu.arr_out(os);
return os;
}
//use private inhertance
ostream & operator<<(ostream & os, const Student & stu)
{
os << "Scores for " << (const string &)stu << ":\n";
stu.arr_out(os);
return os;
}
使用包含还是私有继承
通常应使用包含来建立has-a关系;如果新嘞需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。
保护继承
保护继承是私有继承的辩题。保护继承在列出基类时使用关键字protected:
class Student :protected std::string, protected std::valarray<double>
{
...
};
使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。和私有继承一样,基类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的。当从派生类派生出另一个类时,私有继承和保护继承之间的主要区别就出来了。使用私有继承时,第三代类将不能使用基类的接口,因为基类的公有方法在派生类中将变成私有方法;使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们。
使用using重新定义访问权限
使用保护派生或私有派生时,基类的公有成员将成为保护成员或私有成员。假设要让基类的方法在派生类外面可用,方法之一是定义一个使用该基类的派生类方法。如下,这样Student对象便能够调用Student::sum(),sum()进而将valarray<double>::sum()方法应用于被包含的valarray对象。
double Student::sum()const
{
return std::valarray<double>sum(); //use privately-inherited method
}
另一种方法就是将函数调用包装在另一个函数调用中,即使用一个using声明来指出派生类可以使用特定的基类成员,即使使用的是私有派生。注意using声明只适用于继承,不适用于包含。下述using声明使得valarray<double>::min可用,就像它们是Student的公有方法一样。其中ada[i]是Student类的对象。
class Student :private std::string, private std::valarray<double>
{
public:
using std::valarray<double>::min;
using std::valarray<double>::max;
...
};
cout << "high score: " << ada[i].max() << end;