Chapter 14 C++中的代码重用
本章内容包括:
- has-a关系
- 包含对象成员的类
- 模板类 valarray
- 私有和保护继承
- 多重继承
- 虚基类
- 创建类模板
- 使用类模板
- 模板的具体化
C++的主要目标是促进代码重用。公有继承是实现这种目标的机制之一,但不是唯一的机制。使用类成员称为包含、组合或层次化。另一种方法是使用私有或保护继承。
通常包含、私有继承和保护继承用于实现has-a关系。
14.1 包含对象成员的类
14.1.1 valarray类简介
该类是一个模板类,模板类可以处理不同的数据类型。
该类使用尖括号里指名创造数组的类型。
下面是这个类的方法:
14.1.2 Student类的设计
Student类获得了包含了一个string对象和一个valarray对象,获得其成员对象的实现,但没有继承接口。
接口和实现
使用公有继承,类可以继承接口,可能还有实现(虚函数)。获得接口时is-a关系的组成部分。使用组合,类可以获得实现,但不能获得接口。
C++和约束
C++包含让程序员能够限制程序结构的特性——使用explicit防止单参数构造函数的隐式转换,用const防止修改数据等等。在编译阶段出现错误优于在运行阶段出现错误。
1.初始化被包含的对象
构造函数通常使用初始化列表方法来初始化成员对象。
如果不使用初始化列表语法,C++要求构建对象的其他部分之前,先构建对象的所有成员对象,因此会使用成员对象所属类的默认构造函数。
初始化顺序
当初始化列表包含多个项目时,这些项目初始化的顺序为它们被声明的顺序,而不是它们初始化列表中的顺序。对于这个例子来说,初始化顺序并不重要,但如果代码使用一个成员的值作为另一个成员的初始化表达式的一部分时,初始化顺序就非常重要。
2.使用被包含对象的接口
被包含对象的接口不是公有的,但是可以在类方法中使用它。总之,Student对象调用Student对象调用Student的方法,而后者使用被包含的valarray对象来调用valarray类的方法。
14.2 私有继承
C++还有另一种实现has-a关系的途径,私有继承。
14.2.1 新版本Student类示例
私有继承请使用关键字private而不是public来定义类。(private是默认值)
使用多个基类的继承称为多重继承(multiple inheritance,MI)。
1.初始化基类组件
继承类的构造函数使用成员列表初始化语法,使用类名而不是成员名来识别构造函数。
2.访问基类的方法
私有继承,只能在派生类的方法中使用基类的方法。使用作用域解析符来调用string类的公有方法。包含方法使用对象来调用方法;私有继承使用类名和作用域解析符来调用基类的方法。
3.访问基类对象
使用私有继承时,对象没有名称,通过强制类型转换来使用基类对象。因为派生类是从基类派生出来,因此可以通过强制类型转换将派生类对象转换成基类对象。为避免调用构造函数创建新的对象,可使用强制类型转换来创建一个引用&
4.访问基类的友元函数
用类名显式地限定函数名不适合于友元函数,友元函数不属于类,可以通过显式转换位基类来调用正确的函数。
引用不会自动转换为string引用,在私有继承中,不进行显式类型转换地情况下,不能将指向派生的引用或指针赋给基类引用或指针。
14.2.2 使用包含还是私有继承
包含:
- 易于理解,而且引起的问题较少
- 包含可以包括多个同类的对象
私有继承:
- 继承会引起很多问题
- 继承只能创建一个这样的对象
- 私有继承提供的特性比包含多,基类的保护成员私有继承可以使用
- 需要重新定义虚函数时,应该是使用继承
14.2.3 保护继承
使用保护继承时,基类的公有成员和保护成员都会成为派生类的保护成员。当从派生类派生出另一个类时,私有继承和保护继承的区别便显示出来了。
各种继承方式
特征 | 公有继承 | 保护继承 | 私有继承 |
---|---|---|---|
公有成员变成 | 派生类的公有成员 | 派生类的保护成员 | 派生类的私有成员 |
保护成员变成 | 派生类的保护成员 | 派生类的保护成员 | 派生类的私有成员 |
私有成员 | 只能通过基类接口访问 | 只能通过基类接口访问 | 只能通过基类接口访问 |
能否隐式向上转换 | 是 | 是(但只在派生类中) | 否 |
14.2.4 使用using重新定义访问权限
保护派生或私有派生时,要想让基类的方法在派生类外部可用:
- 定义一个使用该基类方法的派生类方法
- 将函数调用包装在另一个函数调用中,使用using声明
using声明只是用成员名——没有圆括号、函数特征标和返回类型using声明只适用于继承,不适用包含
14.3 多重继承
公有MI也是表示is-a关系。
MI会带来很多问题,其中两个主要的问题:
- 从两个不同的基类继承同名方法——限定符解决
- 从两个或更多相关基类继承同一个类的多个实例——虚基类解决,同时也需要运算符
enum{}方法来表示一种选择
14.3.1 有多少基类实例
非虚基类继承时,将会产生多个基类实例,将引起使用基类指针来引用不同对象的复杂化。
使用关键字virtual,可以使基类称为派生类的虚基类。
从本质上说,派生类对象共享一个基类对象。
1.新的构造函数规则
使用虚基类时,需要对类构造函数采用一种新的方法。
非虚基类继承中,派生类的构造函数只能调用其直接继承的基类的构造函数。
虚基类继承时,禁止信息通过中间类自动传递给基类。
如果类有间接虚基类,除非只需使用该虚基类的默认构造函数,否则必须显式地调用该虚基类地某个构造函数。
14.3.2 哪个方法
多重继承会导致二义性,需要使用作用域解析运算符来指定使用哪个方法。更好地解决方法是重新定义Show(),
重新定义时可能调用基类方法会导致基类地方法调用两次,解决该问题地办法是使用模块化方式。添加保护的组件,通常是保护方法。
祖先相同时,使用MI必须引用虚基类,并引用构造函数初始化列表的规则。
strchr("wstq",choice),函数返回字符串choice指定的字符在"wtsq"第一次出现的地址,没有这样的字符。则返回NULL。
MI的其他问题:
1.混合使用虚基类和非虚基类
会产生多少个基类子对象?虚基类1+非虚基类个数
2.虚基类和支配
虚基类会改变C++解析二义性的方式。如果一个名称优先于其他说有名称时,使用它不适用限定符,也不会导致二义性。
14.3.3 MI小结
不适用虚基类时,需要使用限定符,否则会导致二义性。
派生类使用virtual来指示派生时,基类就成为虚基类。
从虚基类的一个或多个实例派生而来的类将只继承一个基类对象。
- 有间接虚基类的派生类包含直接调用基类构造函数的构造函数。
- 通过优先规则解决二义性
14.4 类模板
14.4.1 定义类模板
模板类以下列代码开头:
template<class Type> // class can repalce by typename and typename is better
经常使用的泛型名称是T和Type。
使用模板成员函数替换原有类的类方法,每个函数头以相同的模板声明打头。
在类声明中定义的方法可以省略模板前缀和类限定符。
模板类并不生成类定义,说明了如何生成类定义,模板的具体实现称为实例化或具体化。模板必须与特定的实例化请求一起使用。
14.4.2 使用模板类
泛型标识符称为类型参数,模板类必须显式的提供类所需的类型,这与常规的函数模板不同。
14.4.3 深入讨论模板类
1.不正确的使用指针栈
指针栈需要重新定义栈类。
2.正确的使用指针栈
指针栈的任务是管理指针,而不是创建指针。
14.4.4 数组模板示例和非类型参数
可以指定数组大小的类模板使用的是自动变化维护的内存栈,执行速度更快。表达式参数方法的主要缺点是每种数组大小都将生成自己的模板。
14.4.5 模板多功能性
模板类可用作基类,也可用作组件类。
1.递归使用模板
递归使用数组模板,可以创造二维数组。
2.使用多个类型参数
模板可以包含多个类型参数
3.默认类型模板参数
类模板可以为类型参数提供默认值。
14.4.6 模板的具体化
1.隐式实例化
声明一个或多个对象,指出所需的类型,编译器在需要对象之前,不会生成类的隐式实例化,创造指向类的指针,在分配内存时才会创建类模板实例。
2.显式实例化
使用关键字template并指出所需类型来声明类时,生成的时显式实例化。在该情况下,即使没有创建或提及类对象,编译器也将生成类声明。
3.显式具体化
显式具体化时特定类型(用于替换模板中的泛型)的定义。例如为const char *字符串定义具体类型的模板。具体化模板定义的格式如下:
tempalte <> class Classname<specialized-type-name>{ ... };
4.部分具体化
部分限制模板的通用性,用于含有多个类型参数的类模板。
14.4.7 成员模板
模板嵌套,当模板是嵌套的时,成员函数定义需要两个模板都加上,例如:
template <typename T>
class beta
{
private:
template <typename V>
class hold;
hold<T> q;
hold<int> n;
public:
beta(T t, int i) : q(t), n(i) {}
template <typename U>
U blab(U u, T t);
void Show() const { q.show(); n.show(); }
}
// member definition
template <typename T>
template <typename V>
beta<T>::hold
{
private:
V val;
public:
hold(V v=0) : val(v) {}
void show() const { std::cout << val << std::endl; }
V Value() const { return val; }
}
// member definition
template <typename T>
template <typename U>
U beta<T>::blab(U u, T t)
{
return (n.Value() + q.Value()) * u / t;
}
14.4.8 将模板用作参数
模板可以包含类型参数(typename T)和非类型参数(int n),模板还可以包含本身就是模板的参数。
可以混合使用模板参数和常规参数。
14.4.9 模板类和友元
模板的友元分3类:
- 非模板友元;
- 约束(bound)模板友元,友元的类型取决于类被实例化时的类型;
- 非约束(unbound)模板友元,即友元的所有具体化都是类的每一个具体化的友元。
1.模板类的非模板友元函数
非模板友元容易引起警告。
2.模板类的约束模板友元函数
友元函数本身称为模板,分三步实现:
- 首先,在类定义前声明友元模板函数
- 然后在类中将模板函数声明为友元
- 提供模板友元函数的定义
3.模板类的非约束模板友元函数
通过再类内部声明模板友元函数,创建非约束友元函数,每个函数具体化都是每个类具体化的友元。
14.5 复习题
1.以A栏的类为基类时,B栏的类采用公有派生还是私有派生更合适。
A | B | 派生方式 |
---|---|---|
class Bear | class PolarBear | 公有派生 |
class Kitchen | class Home | 私有派生 |
class Person | class Programer | 公有派生 |
class Person | class HorseAndJockey | 私有派生 |
class Person, class Automobile | class Driver | 私有派生 |
最后一个错误,司机是人,公有,司机有车私有,我理解成了必须一样。 |
2.假设有下面的定义
class Frabjous {
private:
char fab[20];
public:
Frabjous(const char * s = "C++") : fab(s) { }
virtual void tell() { cout << fab; }
};
class Gloam{
private:
int glip;
Frabjous fb;
public:
Gloam(int g = 0, const char * s = "C++");
Gloam(int g, const Frabjous & f);
void tell();
};
假设Gloam版本的tell()应显式glip和fb的值,请为这3个Gloam方法提供定义。定义如下:
Gloam::Gloam(int g, const char * s) : glip(g), fb(s){ }
Gloam::Gloam(int g, const Frabjous & f) : glip(g), fb(f) { }
void Gloam::tell()
{
cout << glip << ", ";
fb.tell();
}
3.假设有下面的定义:
class Frabjous {
private:
char fab[20];
public:
Frabjous(const char * s = "C++") : fab(s) { }
virtual void tell() { cout << fab; }
};
class Gloam : private Frabjous{
private:
int glip;
public:
Gloam(int g = 0, const char * s = "C++");
Gloam(int g, const Frabjous & f);
void tell();
};
假设Gloam版本的tell()应显式glip和fb的值,请为这3个Gloam方法提供定义。定义如下:
Gloam::Gloam(int g, const char * s) : glip(g), Frabjous(s){ }
Gloam::Gloam(int g, const Frabjous & f) : glip(g), Frabjous(f) { }
void Gloam::tell()
{
cout << glip << ", ";
Frabjous::tell();
}
4.假设有下面的定义,它是基于程序清单14.13中Stack模板和程序清单14.10的Work类的:
Stack <Worker *> sw;
请写出将生成的类声明。只实现类声明,不实现非内联类方法。
class Stack <Worker *>
{
private:
enum {SIZE = 10};
int stacksize;
Type * items;
int top;
public:
Stack(int ss = SIZE);
Stack(const Stack & st);
~ Stack() { delete [] items; }
bool isempty() const { return top == 0; }
bool isfull() const { return top == stacksize; }
bool push(const Type & item);
bool pop(Type & item);
Stack & operator=(const Stack & st);
};
5.使用本章的模板定义对下面的内容进行定义:
- string 对象数组;
- double 数组栈;
- 指向Work对象的指针的栈数组
程序清单14.18生成了多少个模板类定义。
4 个。
Array<string,10> arst;
Stack<double> sdb;
Stack<Worker *> sw;
6.指出虚基类于非虚基类的区别。
非虚基类与虚基类最大的区别在多重派生时,虚基类,派生类共享一个实例,非虚基类派生出的派生类产生多个实例。