- 内容提要
- 前言
- 第 1 章 预备知识
- 第 2 章 开始学习 C++
- 第 3 章 处理数据
- 第 4 章 复合类型
- 第 5 章 循环和关系表达式
- 第 6 章 分支语句和逻辑运算符
- 第 7 章 函数——C++的编程模块
- 第 8 章 函数探幽
- 第 9 章 内存模型和名称空间
- 第 10 章 对象和类
- 第 11 章 使用类
- 第 12 章 类和动态内存分配
- 第 13 章 类继承
- 第 14 章 C++中的代码重用
- 第 15 章 友元、异常和其他
- 第 16 章 string 类和标准模板库
- 第 17 章 输入、输出和文件
- 第 18 章 探讨 C++新标准
- 附录 A 计数系统
- 附录 B C++保留字
- 附录 C ASCII 字符集
- 附录 D 运算符优先级
- 附录 E 其他运算符
- 附录 F 模板类 string
- 附录 G 标准模板库方法和函数
- 附录 H 精选读物和网上资源
- 附录 I 转换为 ISO 标准 C++
- 附录 J 复习题答案
14.2 私有继承
C++还有另一种实现 has-a 关系的途径——私有继承。使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们。
下面更深入地探讨接口问题。使用公有继承,基类的公有方法将成为派生类的公有方法。总之,派生类将继承基类的接口;这是 is-a 关系的一部分。使用私有继承,基类的公有方法将成为派生类的私有方法。总之,派生类不继承基类的接口。正如从被包含对象中看到的,这种不完全继承是 has-a 关系的一部分。
使用私有继承,类将继承实现。例如,如果从 String 类派生出 Student 类,后者将有一个 String 类组件,可用于保存字符串。另外,Student 方法可以使用 String 方法来访问 String 组件。
包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未被命名的继承对象添加到类中。我们将使用术语子对象(subobject)来表示通过继承或包含添加的对象。
因此私有继承提供的特性与包含相同:获得实现,但不获得接口。所以,私有继承也可以用来实现 has-a 关系。接下来介绍如何使用私有继承来重新设计 Student 类。
14.2.1 Student 类示例(新版本)
要进行私有继承,请使用关键字 private 而不是 public 来定义类(实际上,private 是默认值,因此省略访问限定符也将导致私有继承)。Student 类应从两个类派生而来,因此声明将列出这两个类:
使用多个基类的继承被称为多重继承(multiple inheritance,MI)。通常,MI 尤其是公有 MI 将导致一些问题,必须使用额外的语法规则来解决它们,这将在本章后面介绍。但在这个示例中,MI 不会导致问题。
新的 Student 类不需要私有数据,因为两个基类已经提供了所需的所有数据成员。包含版本提供了两个被显式命名的对象成员,而私有继承提供了两个无名称的子对象成员。这是这两种方法的第一个主要区别。
1.初始化基类组件
隐式地继承组件而不是成员对象将影响代码的编写,因为再也不能使用 name 和 scores 来描述对象了,而必须使用用于公有继承的技术。例如,对于构造函数,包含将使这样的构造函数:
对于继承类,新版本的构造函数将使用成员初始化列表语法,它使用类名而不是成员名来标识构造函数:
在这里,ArrayDb 是 std::valarray<double>的别名。成员初始化列表使用 std::string(str),而不是 name(str)。这是包含和私有继承之间的第二个主要区别。
程序清单 14.4 列出了新的类定义。唯一不同的地方是,省略了显式对象名称,并在内联构造函数中使用了类名,而不是成员名。
程序清单 14.4 studenti.h
2.访问基类的方法
使用私有继承时,只能在派生类的方法中使用基类的方法。但有时候可能希望基类工具是公有的。例如,在类声明中提出可以使用 average( ) 函数。和包含一样,要实现这样的目的,可以在公有 Student::average( ) 函数中使用私有 Student::Average( ) 函数(参见图 14.2)。包含使用对象来调用方法:
图 14.2 对象中的对象:私有继承
然而,私有继承使得能够使用类名和作用域解析运算符来调用基类的方法:
总之,使用包含时将使用对象名来调用方法,而使用私有继承时将使用类名和作用域解析运算符来调用方法。
3.访问基类对象
使用作用域解析运算符可以访问基类的方法,但如果要使用基类对象本身,该如何做呢?例如,Student 类的包含版本实现了 Name( ) 方法,它返回 string 对象成员 name;但使用私有继承时,该 string 对象没有名称。那么,Student 类的代码如何访问内部的 string 对象呢?
答案是使用强制类型转换。由于 Student 类是从 string 类派生而来的,因此可以通过强制类型转换,将 Student 对象转换为 string 对象;结果为继承而来的 string 对象。本书前面介绍过,指针 this 指向用来调用方法的对象,因此*this 为用来调用方法的对象,在这个例子中,为类型为 Student 的对象。为避免调用构造函数创建新的对象,可使用强制类型转换来创建一个引用:
上述方法返回一个引用,该引用指向用于调用该方法的 Student 对象中的继承而来的 string 对象。
4.访问基类的友元函数
用类名显式地限定函数名不适合于友元函数,这是因为友元不属于类。然而,可以通过显式地转换为基类来调用正确的函数。例如,对于下面的友元函数定义:
如果 plato 是一个 Student 对象,则下面的语句将调用上述函数,stu 将是指向 plato 的引用,而 os 将是指向 cout 的引用:
下面的代码:
显式地将 stu 转换为 string 对象引用,进而调用函数 operator<<(ostream &, const String &)。
引用 stu 不会自动转换为 string 引用。根本原因在于,在私有继承中,在不进行显式类型转换的情况下,不能将指向派生类的引用或指针赋给基类引用或指针。
然而,即使这个例子使用的是公有继承,也必须使用显式类型转换。原因之一是,如果不使用类型转换,下述代码将与友元函数原型匹配,从而导致递归调用:
另一个原因是,由于这个类使用的是多重继承,编译器将无法确定应转换成哪个基类,如果两个基类都提供了函数 operator<<( )。程序清单 14.5 列出了除内联函数之外的所有 Student 类方法。
程序清单 14.5 student.cpp
同样,由于这个示例也重用了 string 和 valarray 类的代码,因此除私有辅助方法外,它包含的新代码很少。
5.使用修改后的 Student 类
接下来也需要测试这个新类。注意到两个版本的 Student 类的公有接口完全相同,因此可以使用同一个程序测试它们。唯一不同的是,应包含 studenti.h 而不是 studentc.h,应使用 studenti.cpp 而不是 studentc.cpp 来链接程序。程序清单 14.6 列出列该程序,请将其与 studenti.cpp 一起编译。
程序清单 14.6 use_stui.cpp
下面是该程序的运行情况:
输入与前一个测试程序相同,输出也相同。
14.2.2 使用包含还是私有继承
由于既可以使用包含,也可以使用私有继承来建立 has-a 关系,那么应使用种方式呢?大多数 C++程序员倾向于使用包含。首先,它易于理解。类声明中包含表示被包含类的显式命名对象,代码可以通过名称引用这些对象,而使用继承将使关系更抽象。其次,继承会引起很多问题,尤其从多个基类继承时,可能必须处理很多问题,如包含同名方法的独立的基类或共享祖先的独立基类。总之,使用包含不太可能遇到这样的麻烦。另外,包含能够包括多个同类的子对象。如果某个类需要 3 个 string 对象,可以使用包含声明 3 个独立的 string 成员。而继承则只能使用一个这样的对象(当对象都没有名称时,将难以区分)。
然而,私有继承所提供的特性确实比包含多。例如,假设类包含保护成员(可以是数据成员,也可以是成员函数),则这样的成员在派生类中是可用的,但在继承层次结构外是不可用的。如果使用组合将这样的类包含在另一个类中,则后者将不是派生类,而是位于继承层次结构之外,因此不能访问保护成员。但通过继承得到的将是派生类,因此它能够访问保护成员。
另一种需要使用私有继承的情况是需要重新定义虚函数。派生类可以重新定义虚函数,但包含类不能。使用私有继承,重新定义的函数将只能在类中使用,而不是公有的。
提示:
通常,应使用包含来建立 has-a 关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。
14.2.3 保护继承
保护继承是私有继承的变体。保护继承在列出基类时使用关键字 protected:
使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。和私有私有继承一样,基类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的。当从派生类派生出另一个类时,私有继承和保护继承之间的主要区别便呈现出来了。使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变成私有方法;使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们。
表 14.1 总结了公有、私有和保护继承。隐式向上转换(implicit upcasting)意味着无需进行显式类型转换,就可以将基类指针或引用指向派生类对象。
表 14.1 各种继承方式
特征 | 公有继承 | 保护继承 | 私有继承 |
---|---|---|---|
公有成员变成 | 派生类的公有成员 | 派生类的保护成员 | 派生类的私有成员 |
保护成员变成 | 派生类的保护成员 | 派生类的保护成员 | 派生类的私有成员 |
私有成员变成 | 只能通过基类接口访问 | 只能通过基类接口访问 | 只能通过基类接口访问 |
能否隐式向上转换 | 是 | 是(但只能在派生类中) | 否 |
14.2.4 使用 using 重新定义访问权限
使用保护派生或私有派生时,基类的公有成员将成为保护成员或私有成员。假设要让基类的方法在派生类外面可用,方法之一是定义一个使用该基类方法的派生类方法。例如,假设希望 Student 类能够使用 valarray 类的 sum( ) 方法,可以在 Student 类的声明中声明一个 sum( ) 方法,然后像下面这样定义该方法:
这样 Student 对象便能够调用 Student::sum( ),后者进而将 valarray<double>::sum( ) 方法应用于被包含的 valarray 对象(如果 ArrayDb typedef 在作用域中,也可以使用 ArrayDb 而不是 std::valarray<double>)。
另一种方法是,将函数调用包装在另一个函数调用中,即使用一个 using 声明(就像名称空间那样)来指出派生类可以使用特定的基类成员,即使采用的是私有派生。例如,假设希望通过 Student 类能够使用 valarray 的方法 min( ) 和 max( ),可以在 studenti.h 的公有部分加入如下 using 声明:
上述 using 声明使得 valarray<double>::min( ) 和 valarray<double>::max( ) 可用,就像它们是 Student 的公有方法一样:
注意,using 声明只使用成员名——没有圆括号、函数特征标和返回类型。例如,为使 Student 类可以使用 valarray 的 operator 方法,只需在 Student 类声明的公有部分包含下面的 using 声明:
这将使两个版本(const 和非 const)都可用。这样,便可以删除 Student::operator[] ( ) 的原型和定义。using 声明只适用于继承,而不适用于包含。
有一种老式方式可用于在私有派生类中重新声明基类方法,即将方法名放在派生类的公有部分,如下所示:
这看起来像不包含关键字 using 的 using 声明。这种方法已被摒弃,即将停止使用。因此,如果编译器支持 using 声明,应使用它来使派生类可以使用私有基类中的方法。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论