C++has-a关系之私有继承

一、简述

C++实现has-a关系的另一个方式是:私有继承

使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生对象公有接口的一部分,但可以在派生类成员函数中使用它们。总之,派生类不继承基类的接口,将继承实现

<1> has-a关系之包含是将对象作为一个命名的成员对象添加到类中

<2> has-a私有继承将对象作为一个未被命名的继承对象添加到类中

<3> 一般使用术语子对象来表示通过继承或包含添加的对象

二、私有继承方式重新设计Student类

2.1 私有继承语法

<1> 要进行私有继承,请使用关键字private,而不是public来定义类。

<2> 实际上派生类继承时,private是默认值,因此省略访问(继承)限定符也将导致私有继承

<3> 私有继承的语法如下列代码所示:

class Student : private std::string
{
    // do something
    ...
};
2.2 Student类设计

has-a之包含与私有继承的区别

<1> has-a之包含版本的Student类提供了两个被显式命名的对象成员,has-a之私有继承版本则是提供了两个无名称的子对象成员

<2> has-a之包含版本成员初始化列表使用成员对象名称初始化,而has-a之私有继承使用基类类型名称初始化(后面章节会展示私有继承成员初始化列表)

2.3 初始化基类组件

隐式的继承组件而不是成员对象将影响代码的编写,因为再也不能使用成员变量名称来描述对象,必须使用用于公有继承的技术,如下所示:

// 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)
{
        
}
2.4 使用私有继承设计类

下面是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);
};

三、私有继承方式下的基类访问

3.1 访问基类的方法

使用私有继承时,只能在派生类的方法中使用基类的方法

但有时可能希望基类工具是公有的。例如,在类函数定义中提出可以使用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;
}
3.2 访问基类对象

疑问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;
}
3.3 访问基类的友元函数

用类名显式的限定函数名不适合于友元函数,这是因为友元函数不属于类成员函数。但是可以通过显式的转换为基类来调用正确的函数。如下面友元函数定义代码示例所示:

// 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> 如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。

五、保护继承

5.1 简述

保护继承是私有继承的变体。示例如下:

class Student : protected std::string, protected std::valarray
{
	// do something
    ......
};

保护继承的规则:

  • 保护继承在列出基类时使用关键字protected
  • 使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员
  • 与私有继承一样,基类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的
5.2 区别

保护继承与私有继承的区别:

  • 使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变成私有方法
  • 使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们
5.3 三种继承方式的成员继承

表1总结了公有继承、私有继承、保护继承的成员继承转换与隐式向上转换

隐式向上转换:无需进行显示类型转换,就可以将基类指针或引用指向派生类对象。

特征 公有继承 保护继承 私有继承
公有成员变成 派生类的公有成员 派生类的保护成员 派生类的私有成员
保护成员变成 派生类的保护成员 派生类的保护成员 派生类的私有成员
私有成员变成 只能通过基类接口访问 只能通过基类接口访问 只能通过基类接口访问
能否隐式向上转换 是(但只能在派生类中)
表1 三种继承方式特征

六、使用using声明重新定义访问权限

6.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声明使用 注意以下事项:

  • using声明只使用成员名,没有圆括号、函数特征标和返回类型
  • using声明只适用于继承,而不适用于包含

方法三: 有一种老式方法可用于在私有继承的派生类中重新声明基类方法,即将方法名放在派生类的公有部分,但是这种方式已被摒弃,即将停止使用。记录下来是为了做一个说明,说明曾经存在过此种方式(好像电影中的台词),示例如下:

class Student : private std::string, private std::valarray
{
public:
    // 重新声明为公有方法,且只需要使用方法名称
    std::valarray::operator[];
    // do something
    ......
};

方法三注意:

  • 方法三的声明看起来像是不包含关键字using的using声明,但是这种方式已被摒弃,即将停止使用。
  • 如果编译器支持using声明,应使用using声明来使派生类可以使用私有继承的基类中的方法

你可能感兴趣的:(#,C++,Primer,Plus,c++,开发语言)