第14章 C++中的代码重用

本章内容包括:

  • has-a关系
  • 包含对象成员的类
  • 模板类valarray
  • 私有和保护继承
  • 多重继承
  • 虚基类
  • 创建类模板
  • 使用类模板
  • 模板的具体化

通常,包含,私有继承和保护继承用于实现has-a关系,即新的类将包含另一个类的对象. 
类模板使我们能够使用通用术语定义类,然后使用模板来创建针对特定类型定义的特殊类.

14.1 包含对象成员的类 
14.1.1 valarray类简介

  • valarray类是由头文件valarray支持的.valarray被定义为一个模板类,以便能够处理不同的数据类型.

14.1.2Student类的设计

  • 通常,用于建立has-a关系的C++技术是组合(包含),即创建一个包含其他类对象的类.
  • 接口和实现:使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现).获得接口是is-a关系的组成部分.而使用组合,类可以获得实现,但不能获得接口.不继承接口是has-a关系的组成部分.
  • 对于has-a关系来说,类对象不能自动获得被包含对象的接口是一件好事.

14.1.3 Student类示例

  • 程序清单14.1 studentc.h
  • 可以用一个参数调用的构造函数将用作从参数类型到类类型的隐式转换函数;但这通常不是好主意.
  • 使用explicit关闭隐式转换.
  • C++和约束:C++包含让程序员能够限制程序结构的特性—使用explicit防止单参数构造函数的隐式转换,使用const限制方法修改数据,等等.这样做的根本原因是:在编译阶段出现错误优于在运行阶段出现错误.
  • 1.初始化被包含的对象. 
    • 对于继承的对象,构造函数在成员初始化列表汇总使用类名来调用特定的基类构造函数.对于成员对象,构造函数则使用成员名.
    • C++要求在构建对象的其他部分之前,先构建继承对象的所有成员对象.因此,如果省略初始化列表,C++将使用成员对象所属类的默认构造函数.
    • 初始化顺序:当初始化列表包含多个项目时,这些项目被初始化的顺序为他们被声明的顺序,而不是它们在初始化列表中的顺序.例如,假设Student构造函数如下:
Student(const char * str,const double * pd,int n)
    : scores(pd,n), name(str){}
  • 1
  • 2
  • 则name成员仍将首先被初始化,因为在类定义中它首先被声明.对于这个例子来说,初始化顺序并不重要,但如果代码使用一个成员的值作为另一个成员的初始化表达式的一部分时,初始化顺序就非常重要了.
  • 2.使用被包含对象的接口 
    • 程序清单14.2 studentc.cpp
  • 3.使用新的Student类 
    • 程序清单14.3 use_stuc.cpp

14.2 私有继承

  • 私有继承提供的特性与包含相同:获得实现,但不获得接口.所以,私有继承也可以用来实现has-a关系.

14.2.1 Student类示例(新版本)

  • 实际上,private是默认值,因此省略访问限定符也将导致私有继承.
  • 使用多个基类的继承被称为多重继承multiple inheritance,MI.通常,MI尤其是公有MI将导致一些问题,必须使用额外的语法规则来解决它们.
  • 1.初始化基类组件 
    • 对于继承类,新版本的构造函数将使用成员初始化列表语法,它使用类名而不是成员名来标识构造函数.
    • 程序清单14.4 studenti.h
  • 2.访问基类的方法 
    • 使用包含时将使用对象名来调用方法,而使用私有继承时将使用类名和作用域解析运算符来调用方法.
  • 3.访问基类对象 
    • 使用作用域解析运算符可以访问基类的方法,但如果要使用基类对象本身,该如何做呢?答案是使用强制类型转换.
  • 4.访问基类的友元函数 
    • 用类名显式的限定函数名不适合于友元函数,这是因为友元不属于类.然而,可以通过显示地转换为基类来调用正确的函数.
    • 在私有继承中,在不进行显式类型转换的情况下,不能将指向派生类的引用或指针赋给基类引用或指针.
    • 程序清单14.5 studenti.cpp
  • 5.使用修改后的Student类 
    • 程序清单14.6 use_stui.cpp

14.2.2 使用包含还是私有继承

  • 由于既可以使用包含,也可以使用私有继承来建立has-a关系,那么应使用哪种方式呢?大多数C++程序员倾向于使用包含. 
    • 首先,它易于理解.
    • 其次,继承会引起很多问题,尤其从多个基类继承时.
    • 另外,包含能够包括多个同类的子对象.
  • 然而,私有继承所提供的特性确实比包含多.
  • 另一种需要使用私有继承的情况是需要重新定义虚函数.派生类可以重新定义虚函数,但包含类不能.
  • 提示:通常,应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承.

14.2.3 保护继承

  • 保护继承是私有继承的辩题.保护继承在列出基类时使用关键字protected.

表14.1 各种继承方式

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

14.2.4 使用using重新定义访问权限

  • 注意,using声明只使用成员名—没有圆括号,函数特征标和返回类型.
  • using声明只适用于继承,而不适用于包含.
  • 有一种老式方式可用于在私有派生类中重新声明基类方法,极简啊ing防发明放在派生类的公有部分.这看起来像不包含关键字using的using声明.这种方法已被摒弃,即将停止使用.

14.3 多重继承

  • MI带来的新问题: 
    • 从两个不同的基类继承同名方法;
    • 从两个或更多相关基类那里继承同一个类的多个实例.
  • 程序清单14.7 worker0.h
  • 程序清单14.8 worker0.cpp
  • 程序清单14.9 worktest.cpp

14.3.1 有多少worker

  • C++引入多重继承的同时引入了一种新技术—虚基类virtual base class,使MI成为可能.
  • 1.虚基类 
    • 虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象.
    • C++对这种新特性也使用关键字virtual—有点像关键字重载.
  • 2.新的构造函数规则 
    • C++在基类是虚的时,禁止信息通过中间类自动传递给基类.
    • 如果不希望默认构造函数来构造虚基类对象,则需要显式地调用所需的基类构造函数.
    • 警告:如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显式的调用该虚基类的某个构造函数.

14.3.2 哪个方法?

  • 对于单继承,如果没有重新定义Show(),则将使用最近祖先中的定义.而在多重继承中,每个直接祖先都有一个Show()函数,这使得上述调用是二义性的.
  • 警告:多重继承可能导致函数调用的二义性.
  • 总之,在祖先相同时,使用MI必须引入虚基类,并修改构造函数初始化列表的规则.另外,如果在编写这些类时没有考虑到MI,则还可能需要重新编写它们.
  • 程序清单14.10 workermi.h
  • 程序清单14.11 workermi.cpp
  • 程序清单14.12 workmi.cpp
  • 1.混合使用虚基类和非虚基类 
    • 当类通过多条虚途径和非虚途径继承某个特定的基类时,该类包含一个表示所有的虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象.
  • 2.虚基类和支配 
    • 一个成员名如何优先于另一个成员名呢?派生类中的名称优先于直接或间接祖先类中的相同名称.
    • 虚二义性规则与访问规则无关.

14.3.3 MI小结

  • 从虚基类的一个或多个实例派生而来的类将只继承了一个基类对象.为实现这种特性,必须满足其他要求: 
    • 有间接虚基类的派生类包含直接调用间接调用间接基类构造函数的构造函数,这对于间接非虚基类来说是非法的.
    • 通过优先规则解决名称二义性.
  • MI会增加编程的复杂程度.然而,这种复杂性主要是由于派生类通过多条途径继承同一个基类引起的.避免这种情况后,唯一需要注意的是,在必须时对继承的名称进行限定.

14.4 类模板 
14.4.1 定义类模板

  • 模板类以下面这样的代码开头:template ,较新的C++实现允许在这种情况下使用不太容易混淆的关键字typename代替class:template //newer choice.
  • 不能将模板成员函数放在独立的实现文件汇总,但支持该关键字的编译器不多:C++11不再这样使用关键字export,而将其保留用于其他用途.
  • 由于模板不是函数,他们不能单独编译.模板必须与特定的模板示例化请求一起使用.
  • 程序清单14.13 stacktp.h

14.4.2 使用模板类

  • 仅在程序包含模板并不能生成模板类,而必须请求示例化.
  • 在kernel声明中,类型参数Type的值为int
  • 注意:必须显式地提供所需的类型,这与常规的函数模板是不同的,因为编译器可以根据函数的参数来信来确定要生成哪种函数.
  • 程序清单14.14 stacktem.cpp

14.4.3 深入探讨模板类

  • 可以使用指针栈,但如果不对程序做重大修改,将无法很好地工作.编译器可以创建类,但使用效果如何就因人而异了.
  • 1.不正确地使用指针栈
  • 2.正确使用指针栈 
    • 使用指针栈的方法之一是,让调用程序提供一个指针数据,其中每个指针都指向不同的字符串.注意,创建不同指针是调用程序的职责,而不是栈的职责.栈的任务是管理指针,而不是穿件指针.
    • 程序清单14.15 stcktp1.h
    • 程序清单14.16 stckoptr1.cpp

14.4.4 数组模板示例和非类型参数

  • 程序清单14.17 arraytp.h 
    • 注意模板头emplate < class T,int n > ,关键字class(或在这种上下文中等价的关键字typename)指出T为类型参数,int指出n的类型为int.这种参数(制定特殊的类型而不是用作泛型名)称为非类型non-type或表达式expression参数.
    • 表达式参数有一些限制.表达式参数可以是整型,枚举,引用或指针.
    • 模板代码不能修改参数的值,也不能使用参数的地址.

14.4.5 模板多功能性

  • 可以将用于常规类的技术用于模板类.模板类可用作基类,也可用做组件类,还可用作其他模板的类型参数.
  • 1.递归使用模板 
    • 请注意,在模板语法中,维的顺序与等价的二维数组相反.
    • 程序清单14.18 twod.cpp
  • 2.使用多个类型参数 
    • 程序清单14.19 pairs.cpp
  • 3.默认类型模板参数 
    • 类模板可以为类型参数提供默认值.虽然可以为类模板类型参数提供默认值,但不能为函数模板参数提供默认值.然而,可以为非类型参数提供默认值,这对于类模板和函数模板都是适用的.

14.4.6 模板的具体化

  • 1.隐式实例化
  • 2.显式实例化 
    • 当使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化.声明必须位于模板定义所在的名称空间中.
  • 3.显式具体化 
    • 其是特定类型(用于替换模板众的泛型)的定义.有时候,可能需要在为特殊类型实例化时,对模板进行修改,使其行为不同.在这种情况下,可以创建显式具体化.
  • 4.部分具体化 
    • C++还允许部分具体化,即部分限制模板的通用性.

14.4.7 成员模板

  • 模板可用作结构,类或模板类的成员.要完全实现STL的设计,必须使用这项特性.
  • 程序清单14.20 tempmemb.cpp

14.4.8 将模板用作参数

  • 程序清单14.21 tempparm.cpp

14.4.9 模板类和友元

  • 模板类声明也可以有友元.模板的友元分3类: 
    • 非模板友元;
    • 约束bound模板友元,即友元的类型取决于类被实例化时的类型.
    • 非约束unbound模板友元,即友元的所有具体化都是类的每一个具体化的友元.
  • 1.模板类的非模板友元函数 
    • 程序清单14.22 frnd2tmp.cpp
  • 2.模板类的约束模板友元函数 
    • 要约束模板友元作准备,要使类的每一个具体化都获得与友元匹配的具体化.
    • 程序清单14.23 tmp2tmp.cpp
  • 3.模板类的非约束模板友元函数 
    • 通过在类内部声明模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元.
    • 对于非约束友元,友元模板类型参数与模板类类型参数是不同的.
    • 程序清单14.24 manyfrnd.cpp

14.4.10 模板别名(C++11)

  • 可以使用typedef为模板具体化制定别名.C++11新增了一项功能—使用模板提供一系列别名.
  • C++11允许将语法using=用于非模板.用于非模板时,这种语法与常规typedef等价.
  • 习惯这种语法后,您可能发现其可读性更强,因为它让类型名和类型信息更清晰.
  • C++11新增的另一项模板功能是可变参数模板,让您能够定义这样的模板类和模板函数,即可接受可变数量的参数.

14.5 总结 
14.6 复习题 
14.7 编程练习

附件:本章源代码下载地址

猜你喜欢

转载自blog.csdn.net/weixin_39345003/article/details/82110671