C++ Primer Plus Chapter 14
Chapter14 C++的代码重用
包含对象成员的类
-
is-a模型,用公有方式派生类
-
has-a关系,使用组合(包含),创建一个包含其他类对象的类
-
使用公有继承时,类可以继承接口,可能还有实现,获得的接口是is-a关系的组成部分;使用组合,类可以获得实现,但不能获得接口。不继承接口是has-a关系的组成部分
-
模板类意味着声明对象,必须指定具体的数据类型。即使用valarray类来声明一个对象时,需要在标识符valarray后面加上<>,在其中包含所需的数据类型
valarray<int> q_values;
-
使用explicit防止单参数构造函数的隐式转换
-
初始化被包含对象:对继承的对象,构造函数在成员初始化列表中,使用类名来调用特定的基类构造函数;对于成员对象,构造函数则使用成员名
Student(const char* str, const double *pd, int n): name(str), scores(pd, n) {}
因为该构造函数初始化的是成员对象,而不是继承对象,所以在初始化列表中使用的是成员名,而不是类名。
-
初始化顺序:成员被初始化顺序为被声明的顺序
-
使用被包含对象的接口:被包含对象的接口不是公有的,但是可以在类函数中使用。Student对象调用Student函数,而后者通过被包含的valarray对象来调用valarray类的函数
私有继承
-
另一种实现has-a关系的方法:私有继承。使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。即基类函数将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用。
-
公有继承:基类的公有函数将成为派生类的公有函数,派生类将继承基类的接口,即is-a关系的一部分;私有继承:基类的公有函数将成为派生类的私有函数,即派生类不继承基类的接口,只获得实现,不完全继承是has-a关系的一部分
-
私有继承将对象作为一个未被命名的继承对象添加到类中,即使用类名而不是成员名来标识
class Student: private std::string, private std::valarray<double> { public: }
-
多个基类的继承被称为多重继承
-
包含提供了两个被显式命名的对象成员,而私有继承提供了两个无名称的子对象成员;初始化使用类名,而不是成员对象名
Student( const char* str, const double *pd, int n ): std::string(str), ArrayDb(pd, n){}
-
访问基类函数:私有继承通过类名和作用域解析运算符来调用基类的函数
-
使用私有继承将使用类名和作用域解析运算符来调用函数,使用包含将使用对象名调用函数
-
访问基类函数:私有继承要使用基类对象本身,使用强制类型转换
-
访问基类的友元函数:通过显式地转换为基类来调用正确的函数;在私有继承中,不进行显式类型转换的情况下,不能将指向派生类的引用或指针赋给基类引用或指针;否则,如果有多重继承,编译器无法确定转换为哪个基类
使用包含还是私有继承
-
has-a关系,即派生属性只是对象的一部分。大多数情况下,倾向于使用包含:
- 易于理解,类声明中包含表示被包含类的显式命名对象,代码可以通过名称引用这些对象,而使用继承将使关系更抽象
- 继承会引起很多问题,例如多个同类的子对象,继承智能使用一个这样的对象(难以区分)
但是,私有继承所提供的特性比包含多,保护成员在派生类中是可以用的,在继承层次结构外是不可用的。使用组合将类对象包含在另一个类中,最外层的类不是派生类,而是位于继承层次结构之外,因此不能访问保护成员;但是通过私有继承得到的将是派生类,可以访问保护成员。
另外,需要重新定义虚函数,则要使用私有继承。派生类可以重新定义虚函数,但包含类不能。使用私有继承,重新定义的函数将只能在类中使用,而不是公有的。
-
总之,分为以下三种情况:
- 通常情况,应使用包含来建立has-a关系
- 新类需要访问原有类的保护成员,使用私有继承
- 重新定义虚函数,使用私有继承
保护继承
-
保护继承是私有继承的变体
class Student: protected std::string, protected std::valarray<double>
-
使用保护继承时,基类的公有成员和保护成员都将称为派生类的保护成员。基类的接口(函数)在派生类中也可用,但是在继承结构之外不可用。
-
私有继承和保护继承区别,体现在从派生类中派生出另一个类
-
使用using重新定义访问权限:使用保护派生或私有派生时,基类的公有成员将称为保护成员或私有成员。要让基类的函数在派生类外可用,可将函数调用包装在另一个函数调用中,即使用using声明。 using指出派生类可以使用特定的基类成员,即使采用的是私有派生。
public: using std::valarray<double>::min; using std:valarray<double>::max;
-
using声明知识用成员名,没有圆括号、函数特征标和返回类型。using只适用于继承,而不适用于包含。
多重继承
-
多重继承MI( multiple inheritance),描述有多个直接的基类的类,表示的是is-a关系。
-
编译器默认派生为private
-
虚基类:虚基类使得从多个类(其基类相同)派生出的对象只继承一个基类对象,在类声明中使用关键字virtual(virtual与public顺序无关紧要)
class Singer: virtual public Worker {}; class Waiter: public virtual Worker{}; class SingingWaiter: public Singer, public Waiter {..};
现在SingingWaiter对象将只包含Worker基类对象的一个副本,即继承对象Singer和Waiter对象共享一个Worker对象,而不是各自引入自己的Worker对象副本,即多态
-
虚基类的构造函数规则:显式地调用所需的基类构造函数,不能通过中间类自动传递给基类
SingingWaiter(const Worker &wk, int p=0, int v=Sing::other):Worker(wk), Waiter(wk, p), Singer(wk, v) {}
上述代码将显式调用构造函数,对于虚基类,必须这样做
-
多重继承的对象使用函数:一种办法是使用模块化方式,对不同对象使用不同模块化管理,最后进行模块的组装;另一种方法是将所有数据组件都设置为保护的,而不是私有的,使用保护函数更严格地控制对数据的访问
-
在祖先相同时,使用MI必须引入虚基类,并修改构造函数初始化列表的规则
-
使用虚基类:从虚基类的一个或多个实例派生而来的类将只继承一个基类对下个,满足:
- 有间接虚基类的派生类包含直接调用间接基类构造函数的构造函数,对于间接非虚基类是非法的
- 通过优先规则解决名称二义性
类模板
-
容器类,设计用来存储其他对象或数据类型;模板提供参数化类型,即能够将类型名作为参数传递给接收方建立类或函数
-
定义类模板:采用模板时,将使用模板定义声明
template <class Type>
使用模板成员函数
template <class Type> bool Stack<Type>::push(const Type &item){...}
-
模板的具体实现——实例化或具体化
-
由于模板不是函数,不能单独编译,模板必须与特定的模板实例化请求一起使用。将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。
-
泛型标识符(Type),称为类型参数,意味着其类似于变量,但赋给其的不能是数字,只能是类型
-
将类模板实例化为具体类型,则形成模板类
-
对于模板成员函数的返回类型为泛型Type时:在类中的模板声明或者模板函数定义可使用Stack
Stack & operator=(const Stack<Type> & st)
在类外面,即指定返回类型或使用作用域解析运算符时,必须使用完整的Stack
Stack<Type> & Stack<Type>::operator=( const Stack<Type> & st)
-
指定数组大小的简单数组模板:一种方法是在类中使用动态数组和构造函数来提供元素数目;另一种方法是使用模板参数来提供常规数组的大小
-
非类型参数或表达式参数:
template <class T, int n>
表达式参数可以是整型,枚举,引用或指针。模板代码不能修改参数的值,也不能使用参数的地址
- 表达式参数使用的是自动变量维护的内存栈,执行速度更快 - 表达式参数方法缺点是,每种数组大小都将生成自己的模板,以下语句将生成两种类声明 ArrayTP<double, 12> eggweights; ArrayTP<double, 13> donuts;
-
模板的多功能性:
-
用作基类
template <typename T> class Array { T entry; }
-
用作继承
template <typename Type> class GrowArray: public Array<Type>
-
用作组件类
template <typename Tp> class Stack { Array<Tp> ar; };
- 用作其他模板的类型 Array< Stack<int> > asi;
-
-
递归使用模板:
ArrayTP< ArrayTP<int, 5> 10> twodee;
-
使用多个类型参数:模板可包含多个类型参数
template <class T1, class T2>
-
默认类型模板参数:可以为类型参数提供默认值
template <class T1, class T2 = int > clsaa Topo { };
模板的具体化
-
隐式实例化:声明一个或多个对象,指出所需的类型
Array<int, 100> stuff;
-
显式实例化:使用关键字template并支出所需类型来声明类时,编译器将使用显式实例化。声明必须位于模板定义所在的名称空间中
template class ArrayTP<string, 100>;
虽然没有创建或提及类对象,但是编译器生成类声明
-
显示具体化:定义特定类型(用于替换模板中的泛型)。例如int排序与char *排序方式不同,需要特定方法来比较。当具体化模板和通用模板都与实例化请求匹配时,则使用具体化版本
template <> class SortedArray< const char *>{ }; SortedArray<int> scores; //使用通用模板定义 SortedArray<const char*> //使用具体化定义
-
部分具体化:部分限制模板的通用性。部分具体化可给类型参数之一指定具体的类型:
template <class T1, class T2> class Pair {...}; //通用模板 template <class T1> class Pair <T1, int> {...}; //将T2设置为int具体化
也可以通过为指针提供特殊版本,部分具体化现有的模板:
template<class T> class Feeb {}; //通用版本 template<class T*> class Feeb {}; //部分具体化
-
成员模板:模板可用作结构、类或模板类的成员。
-
模板用作参数:模板包含本身就是模板的参数。
template <template <typename T> class Thing> class Crab
其中Thing为参数;假设有声明:Crab
legs; 则King必须是模板类,与模板参数Thing对应 template <typename T> class King
-
模板类和友元:
-
非模板友元
template <class T> class HasFriend { friend void report (HasFriend<T> &); }
- 约束模板友元,即友元的类型取决于类被实例化的类型
- 非约束模板类
-