C++实现has-a关系的另一个方式是:私有继承。
使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生对象公有接口的一部分,但可以在派生类成员函数中使用它们。总之,派生类不继承基类的接口,将继承实现。
<1> has-a关系之包含是将对象作为一个命名的成员对象添加到类中
<2> has-a私有继承将对象作为一个未被命名的继承对象添加到类中
<3> 一般使用术语子对象来表示通过继承或包含添加的对象
<1> 要进行私有继承,请使用关键字private,而不是public来定义类。
<2> 实际上派生类继承时,private是默认值,因此省略访问(继承)限定符也将导致私有继承
<3> 私有继承的语法如下列代码所示:
class Student : private std::string
{
// do something
...
};
has-a之包含与私有继承的区别:
<1> has-a之包含版本的Student类提供了两个被显式命名的对象成员,has-a之私有继承版本则是提供了两个无名称的子对象成员
<2> has-a之包含版本成员初始化列表使用成员对象名称初始化,而has-a之私有继承使用基类类型名称初始化(后面章节会展示私有继承成员初始化列表)
隐式的继承组件而不是成员对象将影响代码的编写,因为再也不能使用成员变量名称来描述对象,必须使用用于公有继承的技术,如下所示:
// has-a关系之包含版本初始化
// 使用成员变量名称来标识构造函数
Student(const char* str, const double* pd, int n)
: name(str)
, scores(pd, n)
{
}
// 1、has-a关系之私有继承版本初始化
// 2、使用类名来标识构造函数
// 3、ArrayDb是std::valarray的别名
Student(const char* str, const double* pd, int n)
: std::string(str)
, ArrayDb(pd, n)
{
}
下面是Student类的私有继承设计,与包含版本不同的是,省略了显式对象名称,并在内联构造函数中使用了类名,而不是成员名:
class StudentMI : private std::string, private std::valarray
{
private:
typedef std::valarray ArrayDb;
// 私有方法,分数输出
std::ostream& arr_out(std::ostream& os) const;
public:
StudentMI() : std::string("Null Student"), ArrayDb() {}
explicit StudentMI(const std::string& s) : std::string(s), ArrayDb() {}
explicit StudentMI(int n) : std::string("Nully"), ArrayDb(n) {}
StudentMI(const std::string& s, int n) : std::string(s), ArrayDb(n) {}
StudentMI(const std::string& s, const ArrayDb& a) : std::string(s), ArrayDb(a) {}
StudentMI(const char* str, const double* pd, int n) : std::string(str), ArrayDb(pd, n) {}
~StudentMI() {}
public:
double Average() const;
const std::string& Name() const;
double& operator[](int i);
double operator[](int i) const;
// 输入
friend std::istream& operator>>(std::istream& is, StudentMI& stu);
// 获取行数
friend std::istream& getline(std::istream& is, StudentMI& stu);
// 输出
friend std::ostream& operator<<(std::ostream& os, const StudentMI& stu);
};
使用私有继承时,只能在派生类的方法中使用基类的方法。
但有时可能希望基类工具是公有的。例如,在类函数定义中提出可以使用average()函数。要实习这样的目的,可以在公有StudentMI::Average()函数中使用私有StudentMI::average()函数。下面我们来看下包含与私有继承两种方式实现这样目的的设计:
// 包含方式通过对象名来调用其方法
double Student::Average() const
{
if (scores.size() > 0)
return scores.sum() / scores.size();
else
return 0;
}
// 私有继承方式类的公有函数实现来调用基类方法
// 私有继承使用类名和作用域解析运算符来调用基类的方法
double StudentMI::Average() const
{
if (ArrayDb::size() > 0)
return ArrayDb::sum() / ArrayDb::size();
else
return 0;
}
疑问1: 使用作用域解析运算符可以访问基类的方法,但如果要使用基类对象本身,该如何做呢?
疑问2: Student类包含版本实现了Name()函数,返回std::string对象成员name,但使用私有继承时没有对象成员,
StudentMI类的代码该如何访问内部的std::string对象呢?
答: 上述两个疑问的答案是使用强制类型转换。
解释: 由于StudentMI类是从std::string类派生而来的,因此可以通过强制类型转换,将StudentMI对象转换为std::string对象。结果为继承而来的std::string对象。可根据如下示例深入理解:
// 1、this指针指向用来调用方法的对象,因此*this为用来调用方法的对象,在此示例中*this是类型为StudentMI的对象
// 2、为避免调用构造函数创建新的对象,可使用强制类型转换来创建一个引用。
// 3、此示例返回一个引用,该引用指向调用该方法的StudentMI对象继承而来的std::string对象
const std::string& StudentMI::Name() const
{
return (const std::string&)*this;
}
用类名显式的限定函数名不适合于友元函数,这是因为友元函数不属于类成员函数。但是可以通过显式的转换为基类来调用正确的函数。如下面友元函数定义代码示例所示:
// 1、输出,使用string对象<<运算符函数
std::ostream& operator<<(std::ostream &os, const StudentMI &stu)
{
os << "Scores for " << (const std::string&)stu << ":\n";
return os;
}
// 2、举例,假如plato是一个StudentMI对象,则下面的语句将调用上述友元函数。
// 函数的stu形参成为指向plato的引用, os是指向std::cout的引用
std::cout << plato;
// 3、接第2条,调用的友元函数的下列语句显式的将stu(plato)转换为std::string对象引用,进而调用函数operator<<(ostream&, const string&).
os << "Scores for " << (const std::string&)stu << ":\n";
疑问1: 为什么stu引用不能自动转换为string引用?
答: 根本原因在于,在私有继承中未进行显式类型转换的派生类引用或指针,无法赋值给基类的引用或指针。
疑问2: 即使是公有继承,也必须使用显式类型转换,为什么呢?
答:
<1> 如果不使用显式类型转换,下述代码将与友元函数原型匹配,从而导致递归调用。
os << stu;
<2> 由于StudentMI这个类使用的是多重继承,编译器将无法确定应转换成哪个基类,且如果两个基类都提供了函数operator<<()。
疑问1: 既可以使用包含,也可以使用私有继承来建立has-a关系,那么应使用哪种方式呢?
答:
<1> 大多数C++程序员倾向于使用包含,具体原因有如下几点:
包含易于理解。类声明中包含表示被包含类的显式命名对象,代码可以通过名称引用这些对象,而使用继承将使关系更加抽象。
继承会引起很多问题,尤其是从多个基类继承时,可能必须处理很多问题,如包含同名方法的独立基类或共享祖先的独立基类。
包含能够包括多个同类的子对象。而继承则只能使用一个这样的对象(当对象没有名称时,将难以区分)
<2> 特定场景下私有继承的优势:
疑问2: 这两种不同的方式又该在在什么样的场景下优先使用呢?
答:
<1> 通常应使用包含来创建has-a关系
<2> 如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。
保护继承是私有继承的变体。示例如下:
class Student : protected std::string, protected std::valarray
{
// do something
......
};
保护继承的规则:
保护继承与私有继承的区别:
表1总结了公有继承、私有继承、保护继承的成员继承转换与隐式向上转换。
隐式向上转换:无需进行显示类型转换,就可以将基类指针或引用指向派生类对象。
特征 | 公有继承 | 保护继承 | 私有继承 |
---|---|---|---|
公有成员变成 | 派生类的公有成员 | 派生类的保护成员 | 派生类的私有成员 |
保护成员变成 | 派生类的保护成员 | 派生类的保护成员 | 派生类的私有成员 |
私有成员变成 | 只能通过基类接口访问 | 只能通过基类接口访问 | 只能通过基类接口访问 |
能否隐式向上转换 | 是 | 是(但只能在派生类中) | 否 |
使用保护继承或私有继承时,基类的公有成员将成为派生类的保护成员或私有成员。
基于上述两种情况下,如果想要让基类的方法在派生类外面可用,有哪些方法?
答:
方法一: 定义一个使用该基类方法的派生类方法。示例如下:
// 1、如果希望Student类能够使用valarray类的sum()方法
// 2、在Student类中声明一个sum()方法,然后像下面这样定义该方法
// 3、将方法声明为公有方法
class Student : private std::valarray
{
public:
double Student::sum() const
{
// 使用私有继承的方法
return std::valarray::sum();
}
};
方法二: 将函数调用包装在另一个函数调用中,即使用一个using声明(就像名称空间那样)来指出派生类可以使用特定的基类成员,即使使用的是私有继承。示例如下:
// 1、如果希望通过Student类能够使用valarray的方法min()和max()
// 2、可以在Student.h头文件的公有部分加入如下using声明
class Student : private std::valarray
{
public:
// 1、下述声明使得std::valarray::min和std::valarray::max可用
// 2、它们就像是Student类的公有方法一样使用
// 3、示例
// Student stu;
// stu.min();
// stu.max();
using std::valarray::min;
using std::valarray::max;
// do something
......
};
using声明使用 注意以下事项:
方法三: 有一种老式方法可用于在私有继承的派生类中重新声明基类方法,即将方法名放在派生类的公有部分,但是这种方式已被摒弃,即将停止使用。记录下来是为了做一个说明,说明曾经存在过此种方式(好像电影中的台词),示例如下:
class Student : private std::string, private std::valarray
{
public:
// 重新声明为公有方法,且只需要使用方法名称
std::valarray::operator[];
// do something
......
};
方法三注意: