P33-c++中的代码重用-02私有继承、保护继承,继承特性总结

1. 私有继承

C++还有另一种实現has-a关系的途径一一私有继承。

使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。

这意味着基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们。

下面更深入地探讨接口问题。使用公有继承,基类的公有方法将成为派生类的公有方法。总之,派生类将继承基类的接口;这是is-a关系的一部分。

使用私有继承,基类的公有方法将成为派生类的私有方法。总之,派生类不继承基类的接口。正如从被包含对象中看到的,这种不完全继承是has-a关系的一部分。

使用私有继承,类将继承实现。例如,如果从 String类派生出 Student类,后者将有一个 String类组件可用于保存字符串。另外, Student方法可以使用 String方法来访问 String组件。

包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未被命名的继承对象添加到类中。

我们将使用术语子对象( subobject)来表示通过继承或包含添加的对象。

因此私有继承提供的特性与包含相同:获得实现,但不获得接口。所以,私有继承也可以用来实现has-a关系。
接下来介绍如何使用私有继承来重新设计 Student类

2. Student类示例(新版本)

要进行私有继承,请使用关键字 private而不是 public来定义类(实际上, private是默认值,因此省略访问限定符也将导致私有继承)。
Student类应从两个类派生而来,因此声明将列出这两个类:

class Student : private std::string, private std::valarray<double>
{
    
    
public:
}

使用多个基类的继承被称为多重继承( multiple inheritance,MI)。通常,MI尤其是公有MI将导致些问题,必须使用额外的语法规则来解决它们,这将在本章后面介绍。
但在这个示例中,MI不会导致问题, 新的 Student类不需要私有数据,因为两个基类已经提供了所需的所有数据成员。
包含版本提供了两被显式命名的对象成员,而私有继承提供了两个无名称的子对象成员。这是这两种方法的第一个主要区别.

1. 初始化基类组件

隐式地继承组件而不是成员对象将影响代码的编写,因为再也不能使用name和 scores来描述对象了, 而必须使用用于公有继承的技术。例如,对于构造函数,包含将使用这样的构造函数:

Student(const char *str, const double *pd, int n) : name(str), scores(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

在这里, Arraydb是std::valarray的别名。成员初始化列表使用std::string(str),而不是name(str), 这是包含和私有继承之间的第二个主要区别。
程序清单14.4列出了新的类定义。唯一不同的地方是,省略了显式对象名称,并在内联构造函数中使用了类名,而不是成员名。

程序清单14.4 studenti. h

/*
	author:梦悦foundation
	公众号:梦悦foundation
	可以在公众号获得源码和详细的图文笔记
*/

// studenti.h -- defining a Student class using private inheritance
#ifndef STUDENTC_H_
#define STUDENTC_H_

#include <iostream>
#include <valarray>
#include <<tring>   
class Student : private std::string, private std::valarray<double>
{
    
       
private:
    typedef std::valarray<double> ArrayDb;
    // private method for scores output
    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;
// friends
    // 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

2. 访问基类的方法

使用私有继承时,只能在派生类的方法中使用基类的方法。但有时候可能希望基类工具是公有的。例 如,在类声明中提出可以使用 average()函数。
和包含一样,要实现这样的目的,可以在公有 Student::average()函数中使用私有 Student::Average()函数(参见图14.2)。包含使用对象来调用方法:

double Student::Avarage() 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;
}

总之,使用包含时将使用对象名来调用方法,而使用私有继承时将使用类名和作用域解析运算符来调用方法。

3. 访问基类对象

使用作用域解析运算符可以访问基类的方法,但如果要使用基类对象本身,该如何做呢?
例如, Student类的包含版本实現了Name()方法,它返回 string对象成员name:但使用私有继承时,该 string对象没有名称。那么, Student类的代码如何访问内部的 string对象呢?

答案是使用强制类型转换。由于 Student 类是从 string类派生而来的,因此可以通过强制类型转换,将Student对象转换为 string对象:结果为继承而来的 string对象。
本书前面介绍过,指针this指向用来调用方法的对象,因此*this为用来调用方法的对象,在这个例子中,为类型为 Student 的对象。为避免调用构造函数创建新的对象,可使用强制类型转换来创建一个引用:

const string & Student::Name() const
{
    
    
	return(const string &) *this;
}

上述方法返回一个引用,该引用指向用于调用该方法的 Student对象中的继承而来的 string对象。

4.访问基类的友元函数

用类名显式地限定函数名不适合于友元函数,这是因为友元不属于类。然而,可以通过显式地转换为基类来调用正确的函数。例如,对于下面的友元函数定义:

ostream & operator<<(ostream &os, const Student & stu)
os << "Scores for " << (const string&)stu << ":\n";

如果 plato是一个 Student对象,则下面的语句将调用上述函数,stu将是指向 plato的引用,而os将是指向cout的引用:

cout < plato;

下面的代码:

os << "Scores for " << (const string&)stu << ":\n";

显式地将stu如转换为 string对象引用,进而调用函数 operator<<( ostream&, const String&)。

引用stu不会自动转换为 string引用。根本原因在于,在私有继承中,在不进行显式类型转换的情况下,不能将指向派生类的引用或指针赋给基类引用或指针。

然而,即使这个例子使用的是公有继承,也必须使用显式类型转换。原因之一是,如果不使用类型转换,下述代码将与友元函数原型匹配,从而导致递归调用

os << stu;

另一个原因是,由于这个类使用的是多重继承,编译器将无法确定应转换成哪个基类,如果两个基类都提供了函数 operator<<()。
程序清单14.5列出了除内联函数之外的所有 Student类方法。
程序清单14.5 studenti.cpp

/*
	author:梦悦foundation
	公众号:梦悦foundation
	可以在公众号获得源码和详细的图文笔记
*/

// studenti.cpp -- Student class using private inheritance
#include "studenti.h"
using std::ostream;
using std::endl;
using std::istream;
using std::string;

// public methods
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);         // use ArrayDb::operator[]()
}

double Student::operator[](int i) const
{
    
    
    return ArrayDb::operator[](i);
}

// private method
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; 
}

// friends
// use String version of operator>>()
istream & operator>>(istream & is, Student & stu)
{
    
    
    is >> (string &)stu;
    return is; 
}

// use string friend getline(ostream &, const string &)
istream & getline(istream & is, Student & stu)
{
    
    
    getline(is, (string &)stu);
    return is;
}

// use string version of operator<<()
ostream & operator<<(ostream & os, const Student & stu)
{
    
    
    os << "Scores for " << (const string &) stu  << ":\n";
    stu.arr_out(os);  // use private method for scores
    return os;
}

5. 使用修改后的 Student类

接下来也需要测试这个新类。注意到两个版本的 Student类的公有接口完全相同,因此可以使用同程序测试它们。
程序清单14.6列出列该程序,请将其与 studenti. cpp一起编译。
程序清单14.6 use_stui.cpp

/*
	author:梦悦foundation
	公众号:梦悦foundation
	可以在公众号获得源码和详细的图文笔记
*/

// use_stui.cpp -- using a class with private inheritance
// compile with studenti.cpp
#include <iostream>
#include "studenti.h"
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 << ada[i].Name() << endl;
    cout << "\nResults:";
    for (i = 0; i < pupils; i++)
    {
    
    
        cout << endl << ada[i];
        cout << "average: " << ada[i].Average() << endl;
    }
    cout << "Done.\n";
    // cin.get();
    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; 
}

P32-c++中的代码重用-01valarray类简介,初始化顺序
这个运行结果和之前的结果是相同的。

3. 使用包含还是私有继承

由于既可以使用包含,也可以使用私有继承来建立hasa关系,那么应使用种方式呢?
大多数C++程序员倾向于使用包含。首先,它易于理解。类声明中包含表示被包含类的显式命名对象,代码可以通过名称引用这些对象,而使用继承将使关系更抽象。其次,继承会引起很多问题,尤其从多个基类继承时,可能必须处理很多问题,如包含同名方法的独立的基类或共享祖先的独立基类。

总之,使用包含不太可能遇到这样的麻烦。另外,包含能够包括多个同类的子对象。如果某个类需要3个 string对象,可以使用包含声明3个独立的 string成员。而继承则只能使用一个这样的对象(当对象都没有名称时,将难以区分)。

然而,私有继承所提供的特性确实比包含多。例如,假设类包含保护成员(可是数据成员,也可以是成员函数),则这样的成员在派生类中是可用的,但在继承层次结构外是不可用的。

如果使用组合将这样的类包含在另一个类中,则后者将不是派生类,而是位于继承层次结构之外,因此不能访问保护成员。但通过继承得到的将是派生类,因此它能够访问保护成员。

另一种需要使用私有继承的情况是需要重新定义虚函数。派生类可以重新定义虚函数,但包含类不能使用私有继承,重新定义的函数将只能在类中使用,而不是公有的。

提示:通常,应使用色含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。

4. 保护继承

保护继承是私有继承的变体。保护继承在列出基类时使用关键字 protected:

class Student : protected std::string, protected std::valarray<double>
{
    
    
}

使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。和私有私有继承一样,类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的。

当从派生类派生出另一个类时,私有继承和保护继承之间的主要区别便呈现出来了。使用私有继承时,第三代类将不能使用基类的接口, 这是因为基类的公有方法在派生类中将变成私有方法;使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们

表14.1总结了公有、私有和保护继承。隐式向上转换( implicit upcasting)意味着无需进行显式类型转 换,就可以将基类指针或引用指向派生类对象。

在这里插入图片描述

5. 使用 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[];

这将使两个版本( const和非 const)都可用。这样,便可以删除 Student: operator()的原型和定义。

using声明只适用于继承,而不适用于包含。

有一种老式方式可用于在私有派生类中重新声明基类方法,即将方法名放在派生类的公有部分,如下所示:

class Student : private std::string, private std::valarray<double>
{
    
    
public:
	std::valarray<double>::operator[]; // redeclare as public, just use name
}

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

猜你喜欢

转载自blog.csdn.net/sgy1993/article/details/113932315