14.1 包含对象成员的类
除了继承 (“is-a” 关系) 之外,C++ 提供了另一种重要的代码重用机制:包含 (Containment) 或 组合 (Composition)**。这种方式模拟的是 **”has-a” (有一个)关系。
当一个类(称为包含类或容器类)将另一个类的对象作为其成员变量时,就使用了包含或组合。例如:
- 一辆
Car
有一个Engine
。 - 一个
Person
有一个Name
(可能是string
对象)。 - 一个
Order
有一组OrderItem
对象。
这种方式允许我们通过组合已有的、功能完善的类来构建更复杂的新类。
本节我们将设计一个 Student
类,它将包含一个 std::string
对象(表示学生姓名)和一个 std::valarray<double>
对象(表示学生的考试分数)。
14.1.1 valarray 类简介
valarray
是 C++ 标准库中的一个模板类(定义在 <valarray>
头文件中),专门设计用于简化和优化数值数组的操作。它提供了许多方便的功能,例如:
- 逐元素运算: 可以直接对整个
valarray
对象执行算术运算(如+
,-
,*
,/
),运算会自动应用于每个对应的元素。 - 数学函数: 可以将许多标准数学函数(如
sqrt()
,abs()
,sin()
)应用于valarray
的所有元素。 - 切片和索引: 提供灵活的方式来访问和操作数组的子集。
- 聚合操作: 内置了计算总和 (
sum()
)、平均值 (sum()/size()
)、最大值 (max()
)、最小值 (min()
) 等方法。
简单示例:
1 |
|
我们将使用 valarray<double>
来存储 Student
的多门课成绩。
14.1.2 Student 类的设计
我们的 Student
类需要存储姓名和一组分数。
- 姓名: 使用
std::string
类。 - 分数: 使用
std::valarray<double>
类。
接口设计:
我们需要提供方法来:
- 构造
Student
对象(提供姓名和分数)。 - 获取学生的平均分。
- 获取学生的姓名。
- 获取某一门课的分数。
- 输出学生的信息。
studentc.h (Student 类接口)
1 |
|
关键点:
- 成员初始化列表:
Student
的构造函数使用成员初始化列表来初始化name
和scores
成员。-
name(s)
: 调用std::string
的构造函数(或复制构造函数)来初始化name
。 -
scores(a)
或scores(pd, n)
: 调用std::valarray<double>
的相应构造函数来初始化scores
。
-
- 自动资源管理: 因为
std::string
和std::valarray
都是设计良好的类,它们会自动管理自己的内存(string
管理字符数据,valarray
管理double
数据)。因此,Student
类不需要显式地编写析构函数、复制构造函数或赋值运算符来处理name
和scores
的内存管理(遵循零法则)。编译器生成的默认版本会正确地调用string
和valarray
的相应特殊成员函数。 - 接口转发:
Student
类的一些方法(如operator[]
)将操作转发给其成员对象(scores[i]
)。
14.1.3 Student 类示例
studentc.cpp (Student 类实现)
1 |
|
use_stuc.cpp (使用 Student 类)
1 |
|
编译和运行:
你需要将 studentc.cpp
和 use_stuc.cpp
一起编译链接。
1 | ## 假设 studentc.h/cpp 已添加 Student(int n) 构造函数 |
程序会提示输入每个学生的名字和分数,然后显示学生列表和每个学生的详细信息及平均分。
总结:
- 包含 (组合) 是通过将一个类的对象作为另一个类的成员来实现的,模拟 “has-a” 关系。
- 这是代码重用的重要方式,允许利用现有类的功能。
- 包含类的构造函数通常使用成员初始化列表来调用成员对象的构造函数。
- 如果成员对象能正确管理自己的资源(如
std::string
,std::valarray
, 智能指针),包含类通常不需要自定义析构函数、复制/移动操作(零法则)。 - 包含类可以通过其成员对象的公有接口来使用它们的功能。
14.2 私有继承
除了公有继承 (public
) 模拟 “is-a” 关系外,C++ 还提供了私有继承 (private
)**。私有继承模拟的是 **”has-a” 或更准确地说是 “is-implemented-in-terms-of” (根据…来实现)的关系。
语法:
1 | class DerivedClassName : private BaseClassName { |
默认的继承方式(如果省略访问说明符)也是 private
(对于 class
)。
访问规则:
当一个类私有继承自基类时:
- 基类的
public
成员在派生类中变为 **private
**。 - 基类的
protected
成员在派生类中变为 **private
**。 - 基类的
private
成员在派生类中仍然是不可直接访问的。
核心思想:
私有继承意味着派生类继承了基类的实现,但不继承其接口。基类的公有方法不会成为派生类对象的公有接口的一部分。外部代码不能通过派生类对象直接调用基类的公有方法。派生类内部的成员函数(以及友元)仍然可以访问基类的 public
和 protected
成员(因为它们在派生类内部是 private
的)。
与公有继承的区别:
- 关系: 公有继承是 “is-a”,私有继承是 “is-implemented-in-terms-of”。
- 接口: 公有继承继承接口和实现;私有继承只继承实现。
- 指针/引用转换: 公有继承下,基类指针/引用可以指向派生类对象;私有继承下,这种隐式转换不允许(除非在派生类内部或其友元中)。
14.2.1 Student 类示例(新版本)
让我们重新实现 Student
类,这次使用私有继承而不是包含(组合)。Student
类将私有继承自 std::string
(用于姓名)和 std::valarray<double>
(用于分数)。
studenti.h (私有继承版本)
1 |
|
studenti.cpp (实现)
1 |
|
使用示例 (use_stui.cpp)
1 |
|
代码说明:
-
Student
类通过私有继承获得了string
和valarray<double>
的所有实现。 - 构造函数通过初始化列表调用基类的构造函数。
- 由于基类的公有方法在
Student
中变为私有,Student
类必须提供自己的公有接口(如Average()
,Name()
,operator[]
)来暴露所需的功能。 - 在
Student
的成员函数或友元函数内部,需要通过显式类型转换(如(const string &) *this
或valarray<double>::sum()
)来调用继承来的基类方法(因为它们现在是Student
的私有成员)。 - 外部代码不能直接访问继承来的基类方法,也不能将
Student
对象隐式转换为string
或valarray<double>
。
14.2.2 使用包含还是私有继承
对于 “has-a” 或 “is-implemented-in-terms-of” 的关系,我们既可以使用包含 (组合)**,也可以使用私有继承**。那么如何选择呢?
通常推荐使用包含 (组合):
- 更清晰: 明确表示 “has-a” 关系,代码更易于理解。
Student
类包含一个name
成员和一个scores
成员,这很直观。 - 更简单: 不需要处理继承带来的复杂性(如访问控制变化、构造函数调用链、名称冲突等)。
- 更灵活:
- 可以轻松包含多个同类型的成员对象(例如,一个学生可以有家庭住址和学校住址两个
string
成员)。私有继承通常只能继承一个基类一次(虽然多重继承是可能的,但更复杂)。 - 可以包含指向对象的指针或引用,实现更松散的耦合。
- 可以轻松包含多个同类型的成员对象(例如,一个学生可以有家庭住址和学校住址两个
- 封装性更好: 包含类只能通过成员对象的公有接口与其交互,不会意外地依赖其实现细节(除非成员对象本身封装不佳)。
私有继承的可能优势 (相对少见):
- 访问
protected
成员: 如果派生类需要访问基类的protected
成员(数据或函数),私有继承提供了这种能力,而包含则不行(除非通过友元)。 - 覆盖虚函数: 如果需要覆盖基类的虚函数(即使是私有继承,基类的虚函数在派生类中仍然是虚函数,只是访问权限变为
private
),私有继承是必要的。这在使用策略模式或模板方法模式的某些变体时可能有用,但较为高级。 - 空基类优化 (EBO - Empty Base Optimization): 在某些情况下,如果基类是空的(没有非静态数据成员),编译器可能优化掉基类子对象占用的空间,使得私有继承的对象比包含同样空类成员的对象更小。但这通常是微优化,不应作为主要选择依据。
结论: 对于大多数 “has-a” 关系,**优先选择包含 (组合)**。只有在需要访问基类的 protected
成员或覆盖其虚函数等特殊情况下,才考虑使用私有继承。
14.2.3 保护继承
除了 public
和 private
继承,还有**保护继承 (protected
)**。
语法:
1 | class DerivedClassName : protected BaseClassName { |
访问规则:
当一个类保护继承自基类时:
- 基类的
public
成员在派生类中变为 **protected
**。 - 基类的
protected
成员在派生类中变为 **protected
**。 - 基类的
private
成员在派生类中仍然是不可直接访问的。
特点:
- 与私有继承类似,它也是一种实现继承,不继承接口。外部代码不能访问继承来的成员。
- 与私有继承不同的是,基类的公有和保护成员在派生类中是
protected
的,这意味着后续从该派生类继承的类仍然可以访问这些成员。而在私有继承下,这些成员在派生类中变为private
,后续的派生类就无法访问了。
用途: 保护继承非常少见。它主要用于一种特殊情况:你想让基类的实现对外部隐藏,但又希望后续的派生类能够访问这些实现。
14.2.4 使用 using
重新定义访问权限
有时,在使用私有或保护继承时,我们可能希望将基类的某个特定成员恢复其原始的访问权限(或使其变为 public
),而不是让它在派生类中保持 private
或 protected
。可以使用 using
声明来实现这一点。
语法:
在派生类的定义中(通常放在 public
或 protected
部分),使用:
1 | using BaseClassName::MemberName; |
这会将 BaseClassName
中的 MemberName
(可以是数据成员、成员函数、甚至重载的一组函数)引入到派生类的作用域,并使其具有 using
声明所在区域的访问权限(如果在 public
下,就变为 public
;如果在 protected
下,就变为 protected
)。
示例 (修改 Student
类):
假设我们希望 Student
类(私有继承版本)能够直接使用 valarray
的 size()
和 sum()
方法,就像它们是 Student
的公有方法一样。
1 | // studenti.h (修改后) |
注意: using
声明只改变成员的可访问性,不改变其继承属性(例如,虚函数仍然是虚函数)。它不能用来降低访问权限(例如,不能在 private
部分 using
一个基类的 public
成员来使其变为 private
)。
using
声明提供了一种在私有/保护继承下选择性地暴露基类接口的方法,但过度使用可能会破坏原本隐藏实现的意图。
14.3 多重继承
多重继承 (Multiple Inheritance, MI) 允许一个派生类从多个基类继承。这意味着派生类可以同时拥有多个基类的特性和接口。
语法:
1 | class DerivedClassName : accessSpecifier1 BaseClass1, accessSpecifier2 BaseClass2, ... { |
- 在类头中列出所有要继承的基类,用逗号分隔。
- 可以为每个基类指定不同的访问说明符(
public
,protected
,private
)。
示例场景:
假设我们有一个通用的 Worker
类,表示工作人员的基本信息。我们还有 Singer
类表示歌手,Waiter
类表示服务员。现在我们想创建一个 SingingWaiter
类,表示一个既会唱歌又能提供服务的服务员。SingingWaiter
可以同时继承 Singer
和 Waiter
。
1 | class Worker { /* ... id, name ... */ }; |
潜在问题:
多重继承虽然强大,但也引入了一些复杂性和潜在的问题:
14.3.1 有多少 Worker (钻石问题 - Diamond Problem)
如果多个基类是从同一个更远的基类派生而来的,那么派生类将包含该共同基类的多个副本(每个继承路径一个)。这被称为钻石问题或菱形继承。
示例: 假设 Singer
和 Waiter
都继承自 Worker
。
1 | class Worker { |
在这种情况下,一个 SingingWaiter
对象内部会包含两个 Worker
子对象:一个来自 Waiter
的继承路径,另一个来自 Singer
的继承路径。
这会导致几个问题:
成员访问歧义: 如果你想访问
SingingWaiter
对象的id
或fullname
(来自Worker
),编译器不知道你指的是哪个Worker
子对象的成员。1
2SingingWaiter sw;
// sw.id = 123; // 错误!歧义:Waiter::Worker::id 还是 Singer::Worker::id?资源冗余:
Worker
的数据成员被存储了两次,造成浪费。
解决方案:虚基类 (Virtual Base Classes)
为了解决钻石问题,C++ 引入了虚基类的概念。当一个类被声明为虚基类时,派生类在通过多条路径继承该基类时,只会包含该虚基类的一个共享子对象副本。
语法: 在派生类继承虚基类时,使用
virtual
关键字。1
2
3
4
5
6// Waiter 和 Singer 将 Worker 作为虚基类继承
class Waiter : virtual public Worker { /* ... */ };
class Singer : virtual public Worker { /* ... */ };
// SingingWaiter 正常继承 Waiter 和 Singer
class SingingWaiter : public Waiter, public Singer { /* ... */ };效果: 现在,
SingingWaiter
对象只包含一个共享的Worker
子对象。访问id
或fullname
不再有歧义。1
2SingingWaiter sw;
sw.id = 123; // OK!只有一个 Worker 子对象,没有歧义。构造函数责任: 当使用虚基类时,最底层的派生类(在这个例子中是
SingingWaiter
)的构造函数负责调用虚基类(Worker
)的构造函数。中间的基类(Waiter
,Singer
)在其初始化列表中对虚基类的构造函数调用会被忽略(除非该中间类是直接创建对象)。1
2
3
4
5
6
7// SingingWaiter 构造函数需要初始化 Worker
SingingWaiter::SingingWaiter(const Worker & wk, int p, int v)
: Worker(wk), // 显式调用虚基类构造函数
Waiter(wk, p),
Singer(wk, v) {
// Waiter(wk, p) 和 Singer(wk, v) 内部对 Worker 的构造调用会被忽略
}
14.3.2 哪个方法 (成员名冲突)
即使没有钻石问题,如果不同的基类提供了同名的方法或数据成员,派生类在调用该成员时也会产生歧义。
示例: 假设 Waiter
和 Singer
都有一个名为 Talent()
的方法。
1 | class Waiter { |
解决方案:使用作用域解析运算符 (::
)
为了解决这种歧义,需要使用作用域解析运算符明确指定要调用哪个基类的版本:
1 | sw.Waiter::Talent(); // 调用 Waiter 版本的 Talent() |
虚函数和歧义:
如果冲突的方法是虚函数,情况会更复杂一些。
如果只有一个基类提供了该虚函数,或者所有提供该虚函数的基类都继承自同一个(可能是虚)基类的同一个虚函数,那么通过派生类对象调用通常没有歧义(动态联编会起作用)。
但是,如果不同的基类提供了签名相同但无关的虚函数,或者派生类想提供一个覆盖所有基类版本的新版本,情况会变得复杂。通常建议在派生类中提供一个新的虚函数,并显式调用所需的基类版本。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113// workermi.h, workermi.cpp, worktest.cpp 示例 (模拟 Worker 层次结构)
// worker0.h - 基类 Worker
// ... (Worker 定义,包含虚函数 Show() 和 Set()) ...
// workermi.h - MI 相关类
class Waiter : virtual public Worker { // 虚继承
private:
int panache;
protected:
void Data() const;
void Get();
public:
Waiter() : Worker(), panache(0) {}
Waiter(const std::string & s, long n, int p = 0)
: Worker(s, n), panache(p) {}
Waiter(const Worker & wk, int p = 0)
: Worker(wk), panache(p) {}
void Set();
void Show() const;
// ...
};
class Singer : virtual public Worker { // 虚继承
protected:
enum {other, alto, contralto, soprano, bass, baritone, tenor};
enum {Vtypes = 7};
void Data() const;
void Get();
private:
static const char *pv[Vtypes]; // string equivs of voice types
int voice;
public:
Singer() : Worker(), voice(other) {}
Singer(const std::string & s, long n, int v = other)
: Worker(s, n), voice(v) {}
Singer(const Worker & wk, int v = other)
: Worker(wk), voice(v) {}
void Set();
void Show() const;
// ...
};
// 多重继承
class SingingWaiter : public Singer, public Waiter {
protected:
void Data() const;
void Get();
public:
SingingWaiter() {}
// 显式调用虚基类 Worker 的构造函数
SingingWaiter(const std::string & s, long n, int p = 0, int v = other)
: Worker(s, n), Waiter(s, n, p), Singer(s, n, v) {}
SingingWaiter(const Worker & wk, int p = 0, int v = other)
: Worker(wk), Waiter(wk, p), Singer(wk, v) {}
SingingWaiter(const Waiter & wt, int v = other)
: Worker(wt), Waiter(wt), Singer(wt, v) {} // 从 Waiter 构造
SingingWaiter(const Singer & sg, int p = 0)
: Worker(sg), Waiter(sg, p), Singer(sg) {} // 从 Singer 构造
void Set();
void Show() const;
};
// workermi.cpp - 实现 (部分)
// ... Worker 实现 ...
// --- Waiter methods ---
void Waiter::Set() {
Worker::Get(); // 调用基类方法获取基本信息
Get(); // 调用自己的 Get 获取 panache
}
void Waiter::Show() const {
Worker::Show(); // 调用基类方法显示基本信息
Data(); // 调用自己的 Data 显示 panache
}
void Waiter::Data() const { /* show panache */ }
void Waiter::Get() { /* get panache */ }
// --- Singer methods ---
// ... (类似实现 Set, Show, Data, Get) ...
// --- SingingWaiter methods ---
void SingingWaiter::Data() const {
Singer::Data(); // 显示 Singer 的数据
Waiter::Data(); // 显示 Waiter 的数据
}
void SingingWaiter::Get() {
Waiter::Get(); // 获取 Waiter 的数据
Singer::Get(); // 获取 Singer 的数据
}
void SingingWaiter::Set() {
Worker::Get(); // 获取 Worker 的数据 (只需要一次,因为是虚基类)
Get(); // 调用 SingingWaiter::Get() 获取 Waiter 和 Singer 的数据
}
void SingingWaiter::Show() const {
Worker::Show(); // 显示 Worker 的数据 (只需要一次)
Data(); // 调用 SingingWaiter::Data() 显示 Waiter 和 Singer 的数据
}
// worktest.cpp - 使用示例
// ... (main 函数创建 Worker*, Waiter*, Singer*, SingingWaiter* 数组,并通过基类指针调用 Set 和 Show) ...
14.3.3 MI 小结
优点:
- 可以组合多个不同类的功能。
- 允许更灵活的类层次结构设计。
缺点:
- 复杂性增加: 可能导致名称冲突和歧义。
- 钻石问题: 需要使用虚基类来解决,这会增加构造函数实现的复杂性,并可能引入额外的运行时开销。
- 难以理解和维护: 复杂的 MI 层次结构可能难以理解和调试。
设计建议:
- 谨慎使用: 多重继承是一个强大的工具,但也容易被滥用。在决定使用 MI 之前,仔细考虑是否有更简单的替代方案(如包含/组合,或者单继承加接口)。
- 优先考虑组合: 对于 “has-a” 关系,组合通常是更简单、更安全的选择。
- 虚基类: 如果确实需要 MI 并且遇到了钻石问题,务必使用虚基类。
- 明确解决歧义: 使用作用域解析运算符 (
::
) 来解决成员名称冲突。
许多现代 C++ 实践倾向于避免复杂的多重继承,尤其是在应用层面。然而,在某些库设计(如 IO 流库)或需要混合不同抽象接口的场景中,MI 仍然是一个有用的工具。
14.4 类模板
就像函数模板允许我们编写通用的函数,可以处理不同类型的数据一样,类模板 (Class Templates) 允许我们定义通用的类蓝图,可以用来创建处理不同数据类型的类。
类模板是参数化的类定义,其中的某些类型(或值)在定义时是未指定的,而在创建类的实例(对象)时才被指定。这极大地提高了代码的重用性,是泛型编程的核心。STL 中的容器(如 vector
, list
, map
)和智能指针等都是类模板的典型应用。
14.4.1 定义类模板
定义类模板的语法与函数模板类似,使用 template
关键字,后跟尖括号 <>
中的模板参数列表。
语法:
1 | template <typename T1, typename T2, ...> // 或者 template <class T1, class T2, ...> |
-
template <typename T>
或template <class T>
: 声明一个模板,T
是一个类型参数的占位符。typename
和class
在这里是等价的。你可以使用任何有效的标识符作为类型参数名(通常用T
,U
,V
等)。 - 在类定义内部,可以使用模板参数
T
就像使用普通类型一样(例如,定义成员变量类型、函数参数类型、返回值类型)。 - 成员函数定义: 当在类外部定义模板类的成员函数时,必须再次使用
template <...>
前缀,并且在类名后面跟上模板参数列表<T1, T2, ...>
。
示例:简单的 Stack
模板类
1 | // stacktp.h -- 一个简单的栈模板类定义 |
14.4.2 使用模板类
要使用类模板,你需要实例化 (Instantiate) 它。实例化是指为模板参数提供具体的类型(或值),从而创建一个具体的类。
语法:
1 | ClassName<ConcreteType1, ConcreteType2, ...> objectName(constructor_arguments); |
- 在类模板名称后面跟上尖括号
<>
,并在其中指定用于替换模板参数的具体类型。 - 然后像创建普通类的对象一样声明对象并调用构造函数。
示例:使用 Stack
模板
1 | // stack_user.cpp -- 使用 Stack 模板类 |
编译和运行:
由于模板的实现通常放在头文件中,你只需要编译使用模板的源文件。
1 | g++ stack_user.cpp -o stack_user |
输出:
1 | Pushing integers onto intStack: |
14.4.3 深入探讨模板类
模板编译模型: 类模板本身并不会被编译成代码。只有当你用具体类型实例化模板时,编译器才会根据提供的类型生成对应的具体类(例如
Stack<int>
类和Stack<string>
类)的代码。这个过程称为**模板实例化 (Template Instantiation)**。实现放在头文件: 由于编译器在实例化模板时需要看到模板的完整定义(包括成员函数的实现),所以类模板的成员函数实现通常也放在头文件中 (
.h
或.hpp
),而不是单独的.cpp
文件中。如果放在.cpp
文件中,链接器在链接其他使用该模板的.cpp
文件时,可能找不到所需实例化的代码。隐式实例化 (Implicit Instantiation): 当你声明一个特定类型的模板类对象时(如
Stack<int> s;
),编译器会自动进行隐式实例化。显式实例化 (Explicit Instantiation): 你可以显式地指示编译器创建一个特定类型的实例,即使代码中没有直接使用该类型的对象。这在某些库设计或构建过程中可能有用。
1
template class Stack<double>; // 在某个 .cpp 文件中显式实例化 Stack<double>
类型要求: 模板代码对用作模板参数的类型通常有一些隐式要求。例如,
Stack
模板要求类型T
具有可用的赋值运算符 (operator=
),因为push
和pop
方法中使用了赋值。如果尝试用不满足这些要求的类型(如没有赋值运算符的类)实例化模板,编译器会报错。
14.4.4 数组模板示例和非类型参数
类模板不仅可以有类型参数 (typename T
或 class T
),还可以有**非类型参数 (Non-type Parameters)**。非类型参数是具有固定类型的值,通常是整型(int
, size_t
等)、指针、引用或枚举。
语法:
1 | template <typename T, int N> // T 是类型参数,N 是非类型参数 (int 类型) |
- 非类型参数
N
成为模板定义的一部分。 - 在实例化时,必须为非类型参数提供一个常量表达式。
示例:固定大小的数组模板 ArrayTP
1 | // arraytp.h -- 模板类 ArrayTP |
-
ArrayTP<int, 10>
和ArrayTP<double, 10>
是不同的类型。 -
ArrayTP<int, 10>
和ArrayTP<int, 12>
也是不同的类型。 - 非类型参数允许我们在编译时确定数组大小等属性,这比动态分配更高效,并且可以进行更严格的类型检查。
std::array
就是使用非类型参数来指定大小的。
14.4.5 模板多功能性
类模板可以与 C++ 的其他特性结合使用,提供强大的功能:
- 递归使用: 模板可以递归地使用自身,如
ArrayTP< ArrayTP<int, 5>, 10 >
创建二维数组。 - 指针类型参数: 可以用指针类型实例化模板,例如
Stack<int*>
创建一个存储int
指针的栈。 - 包含模板成员: 类模板可以包含其他模板类的对象作为成员。
- 继承: 类模板可以参与继承,可以从模板类派生,也可以从普通类派生,或者模板类本身从其他类派生。
14.4.6 模板的具体化
有时,通用的模板定义对于某些特定类型可能不是最优的,或者根本无法工作。这时,我们需要为特定类型提供一个专门化的模板定义,这称为**模板具体化 (Template Specialization)**。
显式具体化 (Explicit Specialization): 为某个特定的类型(或一组特定类型)提供一个完全不同的类定义。
语法:
1
2
3
4template <> // 空的模板参数列表
class ClassName<SpecificType> {
// 针对 SpecificType 的特殊实现
};示例: 假设我们想为
Stack<const char*>
提供一个特殊版本,它能正确处理 C 风格字符串的复制。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 显式具体化 Stack<const char*>
template <>
class Stack<const char *> {
private:
enum { MAX = 5 }; // 假设容量不同
char * items[MAX]; // 存储指向动态分配字符串的指针
int top;
public:
Stack();
~Stack(); // 需要析构函数释放内存
// ... isempty, isfull ...
bool push(const char * const & item); // 注意参数类型
bool pop(const char * & item); // 注意参数类型
// 需要禁用或实现复制构造和赋值
Stack(const Stack &) = delete;
Stack & operator=(const Stack &) = delete;
};
// 实现 (部分)
Stack<const char *>::Stack() : top(0) {
for(int i=0; i<MAX; ++i) items[i] = nullptr;
}
Stack<const char *>::~Stack() {
for (int i = 0; i < top; ++i) {
delete [] items[i]; // 释放 push 时分配的内存
}
}
bool Stack<const char *>::push(const char * const & item) {
if (top < MAX) {
items[top] = new char[std::strlen(item) + 1]; // 分配新内存
std::strcpy(items[top], item); // 复制字符串内容
top++;
return true;
} else {
return false;
}
}
bool Stack<const char *>::pop(const char * & item) {
if (top > 0) {
top--;
item = items[top]; // 返回指针 (注意:调用者不应 delete 这个指针)
// 或者更好的做法是复制一份返回
// delete [] items[top]; // 如果 pop 后不再需要,则删除
// items[top] = nullptr;
return true;
} else {
return false;
}
}
// ... 其他实现 ...当编译器遇到
Stack<const char *>
时,它会使用这个显式具体化版本,而不是通用的Stack<T>
模板。部分具体化 (Partial Specialization): 只限制模板参数的一部分,而不是全部。例如,为所有指针类型提供一个特殊版本,或者为一个有两个类型参数的模板固定其中一个参数。
语法 (示例):
1
2
3
4
5
6
7
8
9
10
11// 原始模板
template <typename T1, typename T2> class Pair { /* ... */ };
// 部分具体化:T2 固定为 int
template <typename T1> class Pair<T1, int> { /* ... 特殊实现 ... */ };
// 部分具体化:T1 和 T2 都是指针类型
template <typename T1, typename T2> class Pair<T1*, T2*> { /* ... 特殊实现 ... */ };
// 部分具体化:T1 和 T2 是相同类型
template <typename T> class Pair<T, T> { /* ... 特殊实现 ... */ };编译器会选择最匹配的具体化版本。如果多个部分具体化都能匹配,或者一个部分具体化和一个显式具体化都能匹配,编译器会选择“更具体”的那个。
14.4.7 成员模板
类(无论是普通类还是模板类)可以包含本身是模板的成员函数或成员类。这称为**成员模板 (Member Templates)**。
示例:模板构造函数和模板赋值运算符
智能指针类(如 unique_ptr
, shared_ptr
)经常使用模板构造函数和模板赋值运算符,以允许从指向派生类的智能指针构造或赋值给指向基类的智能指针。
1 | template<typename T> |
成员模板增加了类的灵活性,允许成员函数或嵌套类处理更广泛的类型。
14.4.8 将模板用作参数
模板本身也可以作为模板的参数,这称为**模板模板参数 (Template Template Parameters)**。
语法:
1 | template <typename T, template <typename U> class Container> // Container 是一个模板模板参数 |
-
template <typename U> class Container
: 声明Container
是一个接受一个类型参数的类模板。 - 在
Manager
内部,可以用具体的类型T
来实例化Container
,如Container<T>
。
示例: 创建一个可以使用不同容器(如 std::vector
, std::list
, 或我们自己的 Stack
)来存储元素的 Manager
。
1 |
|
模板模板参数使得代码更加通用,可以适配不同的模板结构。
14.4.9 模板类和友元
友元关系可以与类模板结合,有几种不同的形式:
非模板友元函数/类: 一个普通的(非模板)函数或类可以是模板类的友元。这意味着这个函数/类可以访问所有该模板类实例化的私有成员。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15template <typename T>
class HasFriend {
friend void report(const HasFriend<T> &); // 非模板函数 report 是所有实例的友元
private:
T item;
public:
HasFriend(const T & i) : item(i) {}
};
// report 函数需要为每个实例单独定义或使用模板
// void report(const HasFriend<int> & hf) { std::cout << hf.item; } // 针对 int 实例
// void report(const HasFriend<double> & hf) { std::cout << hf.item; } // 针对 double 实例
// 或者将 report 也定义为模板函数
template <typename T>
void report(const HasFriend<T> & hf) { std::cout << hf.item; } // 模板友元函数约束模板友元函数/类 (Bound Template Friend): 模板函数/类的特定实例化是模板类的特定实例化的友元。即
Friend<T>
是Target<T>
的友元。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 前向声明
template <typename T> class Target;
template <typename T> void friend_func(const Target<T> &);
template <typename T>
class Target {
// friend_func<T> 是 Target<T> 的友元
friend void friend_func<T>(const Target<T> &);
private:
T data;
};
template <typename T>
void friend_func(const Target<T> & t) {
std::cout << t.data; // 可以访问 Target<T> 的私有成员
}这里,
friend_func<int>
是Target<int>
的友元,friend_func<double>
是Target<double>
的友元,但friend_func<int>
不是Target<double>
的友元。非约束模板友元函数/类 (Unbound Template Friend): 模板函数/类的所有实例化都是模板类的所有实例化的友元。即任何
Friend<U>
都是任何Target<T>
的友元。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20template <typename T>
class AnotherTarget {
// 声明模板函数 show_data 为友元
template <typename U> // 模板参数 U 可以与 T 不同
friend void show_data(const AnotherTarget<U> &);
private:
T data;
};
template <typename U>
void show_data(const AnotherTarget<U> & at) {
std::cout << at.data; // 可以访问任何 AnotherTarget<U> 实例的私有成员
}
// 使用
AnotherTarget<int> ati(10);
AnotherTarget<double> atd(3.14);
// show_data<int>(ati); // OK
// show_data<double>(atd); // OK
// show_data<int>(atd); // 也能访问 atd.data,因为 show_data<int> 是所有 AnotherTarget 的友元非约束友元提供了最大的访问权限,但也可能破坏封装,需要谨慎使用。
14.4.10 模板别名(C++11)
C++11 引入了 using
关键字(之前主要用于 using
声明和 using
指令)来创建**模板别名 (Template Aliases)**,使得使用复杂的模板类型更加方便。
语法:
1 | // 为特定实例化创建别名 |
示例:
1 |
|
模板别名比传统的 typedef
更强大,因为 typedef
不能直接为模板创建别名(只能为完全实例化的类型创建别名)。模板别名提高了代码的可读性和易用性。
14.5 总结
本章探讨了 C++ 中除了公有继承之外的其他代码重用技术,包括包含(组合)、私有继承、保护继承、多重继承以及强大的类模板。这些技术提供了不同的方式来建立类之间的关系和创建可重用的通用代码。
主要内容回顾:
包含/组合 (Composition):
- 通过将一个类的对象作为另一个类的成员来实现,模拟 “has-a” 关系。
- 是代码重用的常用且推荐的方式,特别是当成员对象能自我管理资源时(遵循零法则)。
- 包含类的构造函数使用成员初始化列表来初始化成员对象。
- 例如,
Student
类包含std::string
和std::valarray<double>
成员。
私有继承 (
private
):- 模拟 “is-implemented-in-terms-of” 关系,继承实现但不继承接口。
- 基类的公有和保护成员在派生类中变为私有。
- 通常不如包含直观和灵活,但可用于访问基类的保护成员或覆盖虚函数(虽然访问权限变为私有)。
- 派生类需要提供自己的接口来暴露所需功能,内部访问基类成员通常需要类型转换。
保护继承 (
protected
):- 基类的公有和保护成员在派生类中变为保护。
- 与私有继承类似,继承实现但不继承接口。
- 允许后续的派生类访问继承来的(现在是保护的)成员。
- 使用场景非常有限。
包含 vs. 私有继承: 对于 “has-a” 关系,**优先选择包含 (组合)**,因为它更简单、清晰、灵活。
using
声明与继承: 在私有或保护继承下,可以使用using Base::member;
在派生类中恢复基类某个成员的可访问性(通常提升到public
或protected
)。多重继承 (MI):
- 允许一个类从多个基类继承。
- 可能导致歧义(成员名冲突)和钻石问题(共同基类的多副本)。
- 名称冲突通过作用域解析运算符 (
::
) 解决。 - 钻石问题通过虚基类 (
virtual public Base
) 解决,确保共享基类的单一副本,但构造函数实现更复杂。 - 应谨慎使用,优先考虑组合或单继承。
类模板:
- 允许创建参数化的类蓝图,用于生成处理不同类型的类。
- 使用
template <typename T, ...>
定义。 - 通过提供具体类型来实例化模板类,如
Stack<int>
。 - 模板实现通常放在头文件中。
- 可以有非类型参数(如
template <typename T, int N>
),用于在编译时确定常量值(如数组大小)。 - 模板具体化(显式和部分)允许为特定类型提供专门的实现。
- 类可以包含成员模板(模板化的成员函数或嵌套类)。
- 模板可以作为其他模板的参数(模板模板参数)。
- 模板可以与友元结合,有约束和非约束两种形式。
- C++11 引入了**模板别名 (
using Alias = ...
)**,简化复杂模板类型的使用。
本章介绍的技术极大地扩展了 C++ 代码重用的可能性,从简单的对象组合到复杂的继承层次结构和强大的泛型编程工具——类模板。理解这些技术及其适用场景对于设计灵活、可维护和可重用的 C++ 代码至关重要。