本篇内容包括:
has-a
关系- 包含对象成员的类
- 模板类
valarray
- 私有和保护继承
- 多重继承
- 虚基类
- 创建类模板
- 使用类模板
- 模板的具体化
第14章 C++ 中的代码重用
14.1 包含对象成员的类
14.1.1 valarray
类简介
valarray
模板类是由头文件valarray
支持的。顾名思义,这个类用于处理数值(或具有类似特性的类),它支持诸如将数组中所有元素的值相加以及在数组中找出最大和最小的值等操作
14.2 私有继承
- 除了类成员,C++ 还有另一种实现
has-a
关系的途径——私有继承。使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员
14.2.1 Student 类示例(新版本)
- 省略限定符默认情况是私有继承。如下所示,Singer 是私有继承
1
class SingingWaiter : public Waiter, Singer { /* details omitted */ };
- 使用多个基类的继承被称为多重继承(
multiple inheritance
,MI
)。通常,MI
尤其是公有MI
将导致一些问题,必须使用额外的语法规则来解决它们,这将在本章后面介绍。但在这个示例中,MI
不会导致问题1
2
3
4
5
6
7
8
9
10
11
12
13class Student : private std::string, private std::valarray<double> {
private:
typedef std::valarray<double> ArrayDb;
public:
explicit Student(const std::string &s, const ArrayDb &a)
: std::string(s), ArrayDb(a) {}
double Average() const {
if (ArrayDb::size()) return ArrayDb::sum() / ArrayDb::size();
return 0;
}
const std::string &Name() const { return (const std::string &)*this; }
}; - 如上所示,使用作用域解析运算符可以访问基类的方法,但如果要使用基类对象本身,则要使用强制类型转换
14.2.2 使用包含还是私有继承
- 私有继承
- pros
- 假设类包含保护成员(可以是数据成员,也可以是成员函数),则这样的成员在派生类中是可用的,但在继承层次结构外是不可用的。如果使用组合将这样的类包含在另一个类中,则后者将不是派生类,而是位于继承层次结构之外,因此不能访问保护成员。但通过继承得到的将是派生类,因此它能够访问保护成员
- 派生类可以重新定义虚函数,但包含类不能。使用私有继承,重新定义的函数将只能在类中使用,而不是公有的
- cons
- 每个类型只能有一个对象
- pros
14.2.3 保护继承
- 使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。和私有私有继承一样,基类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的。当从派生类派生出另一个类时,私有继承和保护继承之间的主要区别便呈现出来了。使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变成私有方法;使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们
14.2.4 使用 using
重新定义访问权限
- 使用保护派生或私有派生时,基类的公有成员将成为保护成员或私有成员。假设要让基类的方法在派生类外面可用,可以在
public
部分使用using
声明指出派生类可以使用特定的基类成员。注意,using
声明只使用成员名——没有圆括号、函数特征标和返回类型,所以重载的多个版本都是可用的
14.3 多重继承
MI
描述的是有多个直接基类的类。与单继承一样,公有MI
表示的也是is-a
关系MI
会导致很多问题,下面以一个例子作为说明。Worker 为一个纯虚基类,Singer 和 Waiter 都公有继承自 Worker。 SingingWaiter 公有继承自 Singer 和 Waiter:
14.3.1 有多少基类对象
- 由于 Singer 和 Waiter 都继承自 Worker。所以在 SingingWaiter 中包含了两个 Worker 组件
- 将 SingingWaiter 的地址赋给 Worker* 时,应指明是哪个
1
2
3SingingWaiter ed;
Worker *pw1 = (Waiter *)&ed;
Worker *pw2 = (Singer *)&ed; - 虚基类
- 如果想要只有一个 Worker 组件,则需要引入虚基类
- 如在这个例子中,可将 Worker 当作 Singer 和 Waiter 的虚基类。(关键字顺序无关紧要)
1
2class Singer : virtual public Worker { /* details omitted */ };
class Waiter : public virtual Worker { /* details omitted */ }; - C++ 在基类是虚的时,禁止信息通过中间类自动传递给基类(防止两个中间类传递的信息有冲突)
- SingingWaiter 构造函数在被调用时,Singer 和 Waiter 的构造函数中对于 Worker 的构造函数的调用都会被无视
- 默认情况下 SingingWaiter 将调用 Worker 的默认构造函数
- 也可以手动显示调用 Worker 的构造函数
- 注意,这种操作对于非虚函数是非法的
- 如果混合使用虚基类和非虚基类,则派生类中会包含一个表示所有的虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象
14.3.2 调用的是哪个基类方法
- 如果两个基类没有重新定义祖先中的某个方法,则派生类调用是会出现二义性的问题。解决方案如下
- 作用域解析运算符
1
2SingingWaiter newhire;
newhire.Singer::Show(); - 在派生类中重定义方法
1
2
3void SingingWaiter::Show() const {
Singer::Show();
}
- 作用域解析运算符
- 但上述解决方案会出现如下问题。如果两个基类的方法是增量定义的,则在派生类中显式调用两个基类的方法会重复调用两个基类的共同基类的那个方法。解决方案为使用模块化方法
- 每个类只提供与自基类有关部分的接口(设为保护的而不是公有的),然后在派生类中将这些组件组合起来
1
2
3
4
5
6void SingingWaiter::Show() const {
Worker::Data();
Singer::Data();
Waiter::Data();
SingingWaiter::Data();
}
- 每个类只提供与自基类有关部分的接口(设为保护的而不是公有的),然后在派生类中将这些组件组合起来
- 二义性规则与访问规则无关,如一个私有方法的优先级比同名公有方法高(即是在对方的派生类中定义),则不加限定的调用这个方法会导致编译错误
14.4 类模板
14.4.1 定义类模板
- 定义和声明需要放在同一个头文件中
- 由于具体化的存在,方法定义时也得像下面这么写
1
2template <typename T>
void Foo<T>::Show() { /* details omitted */ }
14.4.3 深入探讨模板类
- 使用模板类时需考虑如果实例化成指针类型,会不会出问题(比如如果用 C 风格字符串实例化就容易出锅)
14.4.4 非类型参数(表达式参数)
- 例子(下图中的 n 就是非类型参数)
1
2
3
4
5template <typename T, int n>
class Array {
T value[n];
/* details omitted */
}; - 表达式参数可以是整型、枚举、引用或指针,不可以是浮点数等
- 模板代码不能修改参数的值,也不能使用参数的地址
- 实例化模板时,用作表达式参数的值必须是常量表达式
- 表达式参数方法的主要缺点是,每个不同的值都将生成自己的模板
14.4.5 模板多功能性
- 模板类可用作基类,也可用作组件类,还可用作其他模板的类型参数
- 在下面的语句中,C++98 要求使用至少一个空白字符将两个 > 符号分开,以免与运算符 >> 混淆。C++11 不要求这样做
1
std::set<std::set<int>> s;
- 模板可以递归使用
- 可以为类型参数提供默认值
1
template <typename T_1, typename T_2 = int> /* details omitted */
14.4.6 模板的具体化
- 隐式实例化
1
std::set<int> s;
- 显示实例化
1
template class std::set<int>;
- 显式具体化
- 为特点类型作特殊定义
- 格式
1
2
3
4template <>
class Classname<specialized - typename> {
/* details omitted */
};
- 部分具体化
- 格式
1
2
3
4
5
6
7
8template <typename T_1, typename T_2>
class Pair; // general template
template <typename T_1, typename T_2>
class Pair<T_1, T_2*>; // partial specialization
template <typename T_1>
class Pair<T_1, int>; // partial specialization - 如果有多个模板可供选择,编译器将使用具体化程度最高的模板
- 格式
14.4.7 成员模板
- 模板可以嵌套定义(如在模板类里面定义一个模板类)
14.4.8 将模板用作参数
- 例子
1
2
3
4
5
6
7
8
9
10template <typename T>
class Foo {};
template <template <typename T> class Stack, typename U, typename V>
class Bar {
Stack<U> s1;
Stack<V> s2;
};
Bar<Foo, int, double> b;
14.4.9 模板类和友元
- 模板类的非模板友元函数
1
2
3
4
5
6
7
8
9
10
11
12
13template <typename T>
class Foo {
public:
friend void Counts();
friend void Report(Foo<T> &);
};
void Counts() {}
void Report(Foo<int> &f) {}
void Report(Foo<double> &f) {}
template <typename T>
void Report(Foo<T> &f) {} // invalid - 模板类的约束模板友元函数
1
2
3
4
5
6
7
8
9
10
11template <typename T>
void Counts() {}
template <typename T>
void Report(T &) {}
template <typename T>
class Foo {
public:
friend void Counts<T>();
friend void Report<>(Foo<T> &);
// === friend void Report<Foo<T>>(Foo<T> &);
}; - 模板类的非约束模板友元函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14template <typename T>
class Foo {
T x;
public:
template <typename U, typename V>
friend void Show(U &, V &);
};
template <typename U, typename V>
void Show(U &u, V &v) {
std::cout << u.x << ' ' << v.x << std::endl;
}
Foo<int> i;
Foo<double> d;
Show(i, d);
14.4.10 模板别名(C++11)
- 例子
1
2
3template <typename T>
using pair_int = std::pair<T, int>;
pair_int<double> p; // === std::pair<double, int> p; - 这种
using
语法也可用于非模板1
2using pc = const char *; // === typedef const char *pc;
using pa = const int *(*)[10]; // === typedef const int *(*pa)[10];