私有继承
C++还有另一种实现 has-a关系的途径一私有继承。 使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味若基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们。
公有继承,基类的公有方法将成为派生类的公有方法,派生类将将继承基类的接口。
私有继承,基类的公有方法将成为派生类的私有方法,派生类不继承基类的接口。
私有继承提供的特性与包含相同:获得实现,但不获得接口。
Student类范例
要执行私有继承,请使用关键字private而不是public来定义类(实际上,private 是默认值,因此省略访问限定符也将导致私有继承)。Student 类应从两个类派生而来,因此声明将列出这两个类:
class Student : private std::string, private std::valarray<double>
{
public:
...
};
使用多个基类的继承被称为多重继承(MI)。 通常,MI 尤其是公有MI将导致一些问题,必须使用额外的句法规则来解决它们,这将在后面介绍。但在这个范例中,MI不会导致问题。
新的Student 类不需要私有数据,因为两个基类已经提供了所需的所有数据成员。包含版本提供了两个被显式命名的对象成员,而私有继承提供了两个无名称的子对象成员。这是这两种方法的第一个主要区别。
初始化基类组件
隐式地继承组件而不是成员对象将影响代码的编写,因为再也不能使用name和scores来描述对象了,而必须使用用于公有继承的技术。例如,对于构造函数,包含将使用这样的构造函数:
Student(const char * str, const double * pd, int n)
: name(str), scores(pd, n) {} // 使用对象名称进行包含
对于继承类,新版本的构造函数将使用成员初始化列表句法,它使用类名而不是成员名来标识构造函数:
Student(const char * str, const double * pd, int n)
: std::string(str), ArrayDb(pd, n) {} // 使用对象名称进行包含
ArrayDb是std::valarray<double>
的别名。成员初始化列表使用std::string(str),而不是name(str)。这是包含和私有继承之间的第二个主要区别。
studenti.h
省略了显式对象名称,并在内联构造函数中使用类名,而不是成员名。
#ifndef STUDENTC_H_
#define STUDENTC_H_
#include <iostream>
#include <valarray>
#include <string>
class Student : private std::string, private std::valarray<double>
{
private:
typedef std::valarray<double> ArrayDb;
//私有方法 scores输出
std::ostream & arr_out(std::ostream & os) const;
public:
Student() : std::string("Null Student"), ArrayDb() {}
explicit Student(const std::string & s)
: std::string(s), ArrayDb() {}
explicit Student(int n) : std::string("Nully"), ArrayDb(n) {}
Student(const std::string & s, int n)
: std::string(s), ArrayDb(n) {}
Student(const std::string &s, const ArrayDb & a)
: std::string(s), ArrayDb(a) {}
Student(const char * str, const double * pd, int n)
: std::string(str), ArrayDb(pd, n) {}
~Student() {}
double Average() const;
double & operator[](int i);
double operator[](int i) const;
const std::string & Name() const;
//friend
//input
friend std::istream & operator>>(std::istream & is, Student & stu); //1 word
friend std::istream & getline(std::istream & is, Student & stu); //1 line
//output
friend std::ostream & operator<<(std::ostream & os, const Student & stu);
};
#endif // !STUDENTC_H_
访问基类的方法
使用私有继承时,只能在派生类的方法中使用基类的方法。但有时候可能希望基类工具是公有的。例如,在类声明中提出可以使用average()函数。和包含一样,要实现这样的目的,可以在公有Student::average()函数中使用私有Student::Average()函数。包含使用对象来调用方法:
double Student::Average() const
{
if (scores.size() > 0)
return scores.sum() / scores.size();
else
return 0;
}
而私有继承使得能够使用类名和作用域解析操作符来调用基类的方法:
double Student::Average() const
{
if (ArrayDb::size() > 0)
return ArrayDb::sum() / ArrayDb::size();
else
return 0;
}
总之,使用包含时将使用对象名来调用方法,而使用私有继承时将使用类名和作用城解析操作符来调用方法。
访问基类对象
使用作用域解析操作符可以访问基类的方法,但如果要使用基类对象本身该如何做呢?例如,Student类的包含版本实现了Name() 方法,它返回string 对象成员name;但使用私有继承时,该string 对象没有名称。那么,Student 类的代码如何访间内部的string对象呢?
答案是使用强制类型转换。由于Student类是从string 类派生而来的,因此可以通过强制类型转换,将Student对象转换为string对象;结果为继承而来的string对象。前面介绍过,指针this指向用来调用方法的对象,因此*this 为用来调用方法的对象。在这个例子中,为类型为Sudent的对象。为避免调用构造函数创建新的对象,可使用强制类型转换来创建一个引用:
const string & Student::Name() const
{
return (const string &)*this;
}
上述方法返回一个引用,该引用指向用于调用该方法的Student对象中的继承而来的string对象。
访问基类的友元函数
用类名显式地限定函数名不适合于友元函数,这是因为友元不属于类。不过,可以通过显式地转换为基类来调用正确的函数。例如,对于下面的友元函数定义:
ostream & operator<<(ostream & os, const Student & stu)
{
os << "Scores for " << (const string &)stu << ":\n";
...
}
如果plato是一个Student对象,则下面语句:
cout << plato;
将调用上述函数,stu 将是指向plato 的引用,而os将是指向cout的引用。下面的代码:
os << "Scores for " << (const string &)stu << ":\n";
显示地将stu转换为string对象引用,这与operator<<(ostream &, const String &) 函数匹配。
引用stu不会自动转换为string引用。根本原因在于,在私有继承中,在不进行显式类型转换的情况下,不能将指向派生类的引用或指针赋给基类引用或指针。
不过,即使这个例子使用的是公有继承,也必须使用显式类型转换。原因之一是, 如果不使用类型转换,下述代码将与友元函数原型匹配,从而导致递归调用:
os << stu;
另个原因是,由于这个类使用的是多重继承,编译器将无法确定应转换成哪个基类,如果两个基类都提供了函数operator<<()。
student.cpp
#include "studenti.h"
using std::ostream;
using std::endl;
using std::istream;
using std::string;
//public
double Student::Average() const
{
if (ArrayDb::size() > 0)
return ArrayDb::sum() / ArrayDb::size();
else
return 0;
}
const string & Student::Name() const
{
return (const string &)*this;
}
double & Student::operator[](int i)
{
return ArrayDb::operator[](i);
}
double Student::operator[](int i) const
{
return ArrayDb::operator[](i);
}
//private
ostream & Student::arr_out(ostream & os) const
{
int i;
int lim = ArrayDb::size();
if (lim > 0)
{
for (i = 0; i < lim; i++)
{
os << ArrayDb::operator[](i) << " ";
if (i % 5 == 4)
{
os << endl;
}
}
if (i % 5 != 0)
{
os << endl;
}
}
else
os << " empty array ";
return os;
}
//friend
istream & operator>>(istream & is, Student & stu)
{
is >> (string &)stu;
return is;
}
istream & getline(istream & is, Student & stu)
{
getline(is, (string &)stu);
return is;
}
ostream & operator<<(ostream & os, const Student & stu)
{
os << "Scores for " << (const string &)stu << ":\n";
stu.arr_out(os);
return os;
}
同样,由于这个范例也重用了string 和valarray类的代码,因此除私有辅助方法外,它包含的新代码很少。
main.cpp
#include "studenti.h"
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
void set(Student & sa, int n);
const int pupils = 3;
const int quizzes = 5;
int main()
{
Student ada[pupils] = { Student(quizzes), Student(quizzes), Student(quizzes) };
int i;
for (i = 0; i < pupils; i++)
{
set(ada[i], quizzes);
}
cout << "\nStudent List:\n";
for (i = 0; i < pupils; i++)
{
cout << endl << ada[i];
cout << "average: " << ada[i].Average() << endl;
}
cout << "Done.\n";
return 0;
}
void set(Student & sa, int n)
{
cout << "Please enter the student's name: ";
getline(cin, sa);
cout << "Please enter " << n << "quiz scores:\n";
for (int i = 0; i < n; i++)
{
cin >> sa[i];
}
while (cin.get() != '\n')
{
continue;
}
}
使用包含还是私有继承
由于既可以使用包含,也可以使用私有继承来建立has-a关系,那么应使用种方式呢?大多数C++程序员倾向于使用包含。
首先,它易于理解。类声明中包含表示被包含类的显式命名对象,代码可以通过名称引用这些对象,而使用继承将使关系更抽象。其次,继承会引起许多问题,尤其从多个基类继承时,可能必须处理许多问题,例如包含同名方法的独立的基类,或共享祖先的独立基类。总之,使用包含不太可能遇到这样的麻烦。另外,包含能够包括多个同类的子对象。如果某个类需要3个string对象,可以使用包含声明3个独立的strng成员。而继承则只能使用一个这样的对象(当对象都没有名称时,将难以区分)。
不过,私有继承所提供的特性确实比包含多。例如,假设类包含保护成员( 可以是数据成员,也可以是成员函数),则这样的成员在派生类中是可用的,但在继承层次结构外是不可用的。如果使用组合将这样的类包含在另一个类中,则后者将不是派生类,而是位于继承层次结构之外,因此不能访问保护成员。但通过继承得到的将是派生类,因此它能够访问保护成员。
另一种需要使用私有继承的情况是需要重新定义虚函数。派生类可以重新定义虛函数,但包含类不能。使用私有继承,重新定义的函数将只能在类中使用,而不是公有的。
注意:通常,应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。
保护继承
保护继承是私有继承的变体。保护继承在列出基类时使用关键字protected:
class Student : protected std::string, protected std::valarray<double>
{...};
使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。和私有继承一样,基类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的。当从派生类派生出另一个类时,私有继承和保护继承之间的主要区别便呈现出来了。使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变成私有方法:使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们。
表14.1总结了公有、私有和保护继承。隐式向上转换( implicit upcastin)意味者无须进行显式类型转换,就可以将基类指针或引用指向派生类对象。
使用using重新定义访问权限
使用保护派生或私有派生时,基类的公有成员将成为保护成员或私有成员。假设要让基类的方法在派生类外面可用,方法之一是定义一个使用该基类方法的派生类方法。例如,假设希望Student 类能够使用valarray类的sum()方法,可以在Student类的声明中声明一个sum()方法,然后想下面这样定义:
double Student::sum() const
{
return std::valarray<double>::sum();
}
这样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 scores: " << ada[i].max() << endl;
注意,using 声明只使用成员名一没有圆括号、 函数特征标和返回类型。例如,为使Student类可以使用valarray的operator方法,只需在Student类声明的公有部分包含下面的using声明:
using student::valarray<double>::operator[];
这将使两个版本(const 和非const)都可用。这样,便可以删除Stuent::operator 的原型和定义。using声明只适用于继承,而不适用于包含。
有一种老式方式可用于在私有派生类中重新声明基类方法,即将方法名放在派生类的公有部分,如下所示:
class Student : private std::string, private std::valarray<double>
{
public:
std::valarray<double>::operator[];
...
};
这看起来像不包含关键字using的using声明。这种方法已被摒弃,即将停止使用。因此,如果编译器支持using声明,应使用它来使派生类可以使用私有基类中的方法。