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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <valarray> // 包含 valarray 头文件
#include <vector> // 用于初始化示例

int main() {
std::vector<double> data = {1.1, 2.2, 3.3, 4.4};
std::valarray<double> v1(data.data(), data.size()); // 从 double 数组创建 valarray
std::valarray<double> v2 = {10.0, 10.0, 10.0, 10.0}; // 使用初始化列表 (C++11)
std::valarray<double> v_sum(4); // 创建一个大小为 4 的 valarray

v_sum = v1 + v2; // 逐元素相加: v_sum = {11.1, 12.2, 13.3, 14.4}

std::cout << "Sum of elements in v1: " << v1.sum() << std::endl; // 输出 11
std::cout << "Size of v1: " << v1.size() << std::endl; // 输出 4
std::cout << "Element at index 1 in v_sum: " << v_sum[1] << std::endl; // 输出 12.2

v1 *= 2.0; // 所有元素乘以 2: v1 = {2.2, 4.4, 6.6, 8.8}
for(double x : v1) { // C++11 范围 for 循环
std::cout << x << " ";
}
std::cout << std::endl;

return 0;
}

我们将使用 valarray<double> 来存储 Student 的多门课成绩。

14.1.2 Student 类的设计

我们的 Student 类需要存储姓名和一组分数。

  • 姓名: 使用 std::string 类。
  • 分数: 使用 std::valarray<double> 类。

接口设计:

我们需要提供方法来:

  • 构造 Student 对象(提供姓名和分数)。
  • 获取学生的平均分。
  • 获取学生的姓名。
  • 获取某一门课的分数。
  • 输出学生的信息。

studentc.h (Student 类接口)

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
#ifndef STUDENTC_H_
#define STUDENTC_H_

#include <iostream>
#include <string>
#include <valarray> // 包含 valarray

class Student {
private:
// 使用包含(组合): Student "has-a" string and "has-a" valarray
std::string name; // 姓名
std::valarray<double> scores; // 分数 (使用 valarray)

// 私有方法,用于输出分数
std::ostream & arr_out(std::ostream & os) const;

public:
// 构造函数
Student() : name("Null Student"), scores() {} // 默认构造函数
// 使用 string 和 valarray 初始化
Student(const std::string & s, const std::valarray<double> & a)
: name(s), scores(a) {}
// 使用 string 和 C 风格数组初始化
Student(const std::string & s, const double * pd, int n)
: name(s), scores(pd, n) {} // valarray 构造函数接受 (指针, 数量)
// 使用 C 风格字符串和 C 风格数组初始化
Student(const char * str, const double * pd, int n)
: name(str), scores(pd, n) {} // string 和 valarray 构造函数处理转换

~Student() {} // 析构函数 (默认即可,因为 string 和 valarray 会自己管理资源)

// 访问器
double Average() const; // 计算平均分
const std::string & Name() const; // 获取姓名 (返回 const 引用避免复制)
double operator[](int i) const; // 获取第 i 门课分数 (const 版本)
double & operator[](int i); // 获取/设置第 i 门课分数 (非 const 版本)

// 友元函数 - 用于输入输出
// input
friend std::istream & operator>>(std::istream & is, Student & stu); // 读取姓名
friend std::istream & getline(std::istream & is, Student & stu); // 读取姓名 (整行)
// output
friend std::ostream & operator<<(std::ostream & os, const Student & stu);
};

#endif // STUDENTC_H_

关键点:

  • 成员初始化列表: Student 的构造函数使用成员初始化列表来初始化 namescores 成员。
    • name(s): 调用 std::string 的构造函数(或复制构造函数)来初始化 name
    • scores(a)scores(pd, n): 调用 std::valarray<double> 的相应构造函数来初始化 scores
  • 自动资源管理: 因为 std::stringstd::valarray 都是设计良好的类,它们会自动管理自己的内存(string 管理字符数据,valarray 管理 double 数据)。因此,Student不需要显式地编写析构函数、复制构造函数或赋值运算符来处理 namescores 的内存管理(遵循零法则)。编译器生成的默认版本会正确地调用 stringvalarray 的相应特殊成员函数。
  • 接口转发: Student 类的一些方法(如 operator[])将操作转发给其成员对象(scores[i])。

14.1.3 Student 类示例

studentc.cpp (Student 类实现)

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
#include "studentc.h"
using std::ostream;
using std::endl;
using std::istream;
using std::string;

// 公有方法实现
double Student::Average() const {
if (scores.size() > 0)
return scores.sum() / scores.size(); // 使用 valarray 的 sum() 和 size()
else
return 0;
}

const string & Student::Name() const {
return name;
}

// 使用 [] 访问分数
double Student::operator[](int i) const {
return scores[i]; // 调用 valarray 的 operator[]
}

double & Student::operator[](int i) {
return scores[i]; // 调用 valarray 的 operator[]
}

// 私有方法实现
// 输出分数数组
ostream & Student::arr_out(ostream & os) const {
int i;
int lim = scores.size();
if (lim > 0) {
for (i = 0; i < lim; i++) {
os << scores[i] << " ";
if (i % 5 == 4) // 每 5 个换行
os << endl;
}
if (i % 5 != 0)
os << endl;
} else
os << " empty array ";
return os;
}

// 友元函数实现
// 使用 >> 读取姓名
istream & operator>>(istream & is, Student & stu) {
is >> stu.name; // 调用 string 的 operator>>
return is;
}

// 使用 getline 读取姓名 (整行)
istream & getline(istream & is, Student & stu) {
getline(is, stu.name); // 调用 string 的 getline
return is;
}

// 使用 << 输出学生信息
ostream & operator<<(ostream & os, const Student & stu) {
os << "Scores for " << stu.name << ":\n";
stu.arr_out(os); // 使用私有方法输出分数
return os;
}

use_stuc.cpp (使用 Student 类)

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
#include <iostream>
#include "studentc.h" // 包含 Student 类定义
using std::cin;
using std::cout;
using std::endl;

void set(Student & sa, int n); // 函数原型

const int pupils = 3;
const int quizzes = 5;

int main() {
Student ada[pupils] = { // 创建 Student 对象数组
Student(quizzes), Student(quizzes), Student(quizzes) // 假设有 Student(int n) 构造函数
};

// 假设 studentc.h/cpp 已修改,添加构造函数 Student(int n)
// Student::Student(int n) : name("Nully"), scores(n) {} // 示例构造函数

// 如果没有 Student(int n),则需要这样创建:
// double sample_scores[quizzes] = {0.0}; // 示例分数
// Student ada[pupils] = {
// Student("Default1", sample_scores, quizzes),
// Student("Default2", sample_scores, quizzes),
// Student("Default3", sample_scores, quizzes)
// };
// 为了编译通过,我们假设 Student(int n) 存在

int i;
for (i = 0; i < pupils; ++i)
set(ada[i], quizzes); // 设置学生数据

cout << "\nStudent List:\n";
for (i = 0; i < pupils; ++i)
cout << ada[i].Name() << endl; // 输出姓名

cout << "\nResults:";
for (i = 0; i < pupils; ++i) {
cout << endl << ada[i]; // 输出完整信息 (调用 operator<<)
cout << "average: " << ada[i].Average() << endl; // 输出平均分
}

cout << "Done.\n";
return 0;
}

// 函数:设置学生姓名和分数
void set(Student & sa, int n) {
cout << "Please enter the student's name: ";
getline(cin, sa); // 使用友元 getline 读取姓名
cout << "Please enter " << n << " quiz scores:\n";
for (int i = 0; i < n; i++)
cin >> sa[i]; // 使用 operator[] 设置分数
while (cin.get() != '\n') // 清除输入缓冲区
continue;
}

// --- 需要修改 studentc.h/cpp 添加 Student(int n) 构造函数 ---
/*
// studentc.h 添加:
explicit Student(int n) : name("Nully"), scores(n) {} // explicit 防止意外转换

// studentc.cpp 不需要额外添加,因为初始化列表已完成工作
*/

编译和运行:

你需要将 studentc.cppuse_stuc.cpp 一起编译链接。

1
2
3
## 假设 studentc.h/cpp 已添加 Student(int n) 构造函数
g++ use_stuc.cpp studentc.cpp -o use_stuc
./use_stuc

程序会提示输入每个学生的名字和分数,然后显示学生列表和每个学生的详细信息及平均分。

总结:

  • 包含 (组合) 是通过将一个类的对象作为另一个类的成员来实现的,模拟 “has-a” 关系。
  • 这是代码重用的重要方式,允许利用现有类的功能。
  • 包含类的构造函数通常使用成员初始化列表来调用成员对象的构造函数。
  • 如果成员对象能正确管理自己的资源(如 std::string, std::valarray, 智能指针),包含类通常不需要自定义析构函数、复制/移动操作(零法则)。
  • 包含类可以通过其成员对象的公有接口来使用它们的功能。

14.2 私有继承

除了公有继承 (public) 模拟 “is-a” 关系外,C++ 还提供了私有继承 (private)**。私有继承模拟的是 **”has-a” 或更准确地说是 “is-implemented-in-terms-of” (根据…来实现)的关系。

语法:

1
2
3
class DerivedClassName : private BaseClassName {
// ... 派生类成员 ...
};

默认的继承方式(如果省略访问说明符)也是 private (对于 class)。

访问规则:

当一个类私有继承自基类时:

  • 基类的 public 成员在派生类中变为 **private**。
  • 基类的 protected 成员在派生类中变为 **private**。
  • 基类的 private 成员在派生类中仍然是不可直接访问的。

核心思想:

私有继承意味着派生类继承了基类的实现,但不继承其接口。基类的公有方法不会成为派生类对象的公有接口的一部分。外部代码不能通过派生类对象直接调用基类的公有方法。派生类内部的成员函数(以及友元)仍然可以访问基类的 publicprotected 成员(因为它们在派生类内部是 private 的)。

与公有继承的区别:

  • 关系: 公有继承是 “is-a”,私有继承是 “is-implemented-in-terms-of”。
  • 接口: 公有继承继承接口和实现;私有继承只继承实现。
  • 指针/引用转换: 公有继承下,基类指针/引用可以指向派生类对象;私有继承下,这种隐式转换不允许(除非在派生类内部或其友元中)。

14.2.1 Student 类示例(新版本)

让我们重新实现 Student 类,这次使用私有继承而不是包含(组合)。Student 类将私有继承自 std::string(用于姓名)和 std::valarray<double>(用于分数)。

studenti.h (私有继承版本)

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
#ifndef STUDENTI_H_
#define STUDENTI_H_

#include <iostream>
#include <valarray>
#include <string>

// 使用私有继承的 Student 类
class Student : private std::string, private std::valarray<double> {
private:
// 私有辅助函数,用于输出分数
// 注意:现在可以直接访问 valarray 的方法,但它们是私有的
std::ostream & arr_out(std::ostream & os) const;

public:
// 构造函数
// 需要显式调用基类的构造函数
Student() : std::string("Null Student"), std::valarray<double>() {}
explicit Student(const std::string & s)
: std::string(s), std::valarray<double>() {}
explicit Student(int n) : std::string("Nully"), std::valarray<double>(n) {}
Student(const std::string & s, int n)
: std::string(s), std::valarray<double>(n) {}
Student(const std::string & s, const std::valarray<double> & a)
: std::string(s), std::valarray<double>(a) {}
Student(const char * str, const double * pd, int n)
: std::string(str), std::valarray<double>(pd, n) {}

~Student() {} // 析构函数 (默认即可)

// 访问器 - 需要自己提供接口,不能直接用基类的
double Average() const;
double & operator[](int i);
double operator[](int i) const;
const std::string & Name() const; // 提供访问姓名的接口

// 友元函数 - 用于输入输出
friend std::istream & operator>>(std::istream & is, Student & stu);
friend std::istream & getline(std::istream & is, Student & stu);
friend std::ostream & operator<<(std::ostream & os, const Student & stu);
};

#endif // STUDENTI_H_

studenti.cpp (实现)

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
#include "studenti.h"
using std::ostream;
using std::endl;
using std::istream;
using std::string;
using std::valarray;

// 公有方法实现
double Student::Average() const {
// 可以访问 valarray 的 public 方法 (在 Student 内部是 private)
if (valarray<double>::size() > 0)
return valarray<double>::sum() / valarray<double>::size();
else
return 0;
}

const string & Student::Name() const {
// 可以访问 string 的 public 方法 (在 Student 内部是 private)
// 需要强制类型转换回基类类型来调用
return (const string &) *this; // 将 *this 转换为对基类 string 的引用
}

// 使用 [] 访问分数
double & Student::operator[](int i) {
// 可以访问 valarray 的 public 方法 (在 Student 内部是 private)
// 需要强制类型转换回基类类型来调用
return valarray<double>::operator[](i); // 或者 ((valarray<double> &)*this)[i];
}

double Student::operator[](int i) const {
return valarray<double>::operator[](i); // 或者 ((const valarray<double> &)*this)[i];
}

// 私有方法实现
ostream & Student::arr_out(ostream & os) const {
int i;
int lim = valarray<double>::size();
if (lim > 0) {
for (i = 0; i < lim; i++) {
os << operator[](i) << " "; // 使用 Student::operator[]
if (i % 5 == 4)
os << endl;
}
if (i % 5 != 0)
os << endl;
} else
os << " empty array ";
return os;
}

// 友元函数实现
// 友元可以访问派生类的私有成员,包括继承来的私有成员(原基类的公有/保护成员)
istream & operator>>(istream & is, Student & stu) {
// 直接访问继承来的 string 部分 (现在是 stu 的私有部分)
is >> (string &)stu; // 需要类型转换
return is;
}

istream & getline(istream & is, Student & stu) {
getline(is, (string &)stu); // 需要类型转换
return is;
}

ostream & operator<<(ostream & os, const Student & stu) {
os << "Scores for " << (const string &) stu << ":\n"; // 需要类型转换
stu.arr_out(os); // 调用私有辅助函数
return os;
}

使用示例 (use_stui.cpp)

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
#include "studenti.h" // 使用私有继承版本
#include <iostream>
using std::cin;
using std::cout;
using std::endl;

void set(Student & sa, int n); // 函数原型 (同之前)

const int pupils = 3;
const int quizzes = 5;

int main() {
Student ada[pupils] = {Student(quizzes), Student(quizzes), Student(quizzes)};
int i;
for (i = 0; i < pupils; ++i)
set(ada[i], quizzes);

cout << "\nStudent List:\n";
for (i = 0; i < pupils; ++i)
cout << ada[i].Name() << endl; // 调用 Student 提供的 Name() 接口

cout << "\nResults:";
for (i = 0; i < pupils; ++i) {
cout << endl << ada[i]; // 调用 Student 的 operator<<
cout << "average: " << ada[i].Average() << endl; // 调用 Student 的 Average()
}

// 无法直接访问基类方法
// cout << ada[0].size(); // 错误!valarray::size() 在 Student 中是 private
// cout << ada[0].length(); // 错误!string::length() 在 Student 中是 private

// 无法将派生类指针隐式转换为基类指针
// string * pstr = &ada[0]; // 错误!
// valarray<double> * pva = &ada[0]; // 错误!

cout << "Done.\n";
return 0;
}

// set 函数定义 (同之前)
void set(Student & sa, int n) {
cout << "Please enter the student's name: ";
getline(cin, sa); // 使用友元 getline
cout << "Please enter " << n << " quiz scores:\n";
for (int i = 0; i < n; i++)
cin >> sa[i]; // 使用 Student::operator[]
while (cin.get() != '\n')
continue;
}

代码说明:

  • Student 类通过私有继承获得了 stringvalarray<double> 的所有实现。
  • 构造函数通过初始化列表调用基类的构造函数。
  • 由于基类的公有方法在 Student 中变为私有,Student 类必须提供自己的公有接口(如 Average(), Name(), operator[])来暴露所需的功能。
  • Student 的成员函数或友元函数内部,需要通过显式类型转换(如 (const string &) *thisvalarray<double>::sum())来调用继承来的基类方法(因为它们现在是 Student 的私有成员)。
  • 外部代码不能直接访问继承来的基类方法,也不能将 Student 对象隐式转换为 stringvalarray<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 保护继承

除了 publicprivate 继承,还有**保护继承 (protected)**。

语法:

1
2
3
class DerivedClassName : protected BaseClassName {
// ...
};

访问规则:

当一个类保护继承自基类时:

  • 基类的 public 成员在派生类中变为 **protected**。
  • 基类的 protected 成员在派生类中变为 **protected**。
  • 基类的 private 成员在派生类中仍然是不可直接访问的。

特点:

  • 与私有继承类似,它也是一种实现继承,不继承接口。外部代码不能访问继承来的成员。
  • 与私有继承不同的是,基类的公有和保护成员在派生类中是 protected 的,这意味着后续从该派生类继承的类仍然可以访问这些成员。而在私有继承下,这些成员在派生类中变为 private,后续的派生类就无法访问了。

用途: 保护继承非常少见。它主要用于一种特殊情况:你想让基类的实现对外部隐藏,但又希望后续的派生类能够访问这些实现。

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

有时,在使用私有或保护继承时,我们可能希望将基类的某个特定成员恢复其原始的访问权限(或使其变为 public),而不是让它在派生类中保持 privateprotected。可以使用 using 声明来实现这一点。

语法:

在派生类的定义中(通常放在 publicprotected 部分),使用:

1
using BaseClassName::MemberName;

这会将 BaseClassName 中的 MemberName(可以是数据成员、成员函数、甚至重载的一组函数)引入到派生类的作用域,并使其具有 using 声明所在区域的访问权限(如果在 public 下,就变为 public;如果在 protected 下,就变为 protected)。

示例 (修改 Student 类):

假设我们希望 Student 类(私有继承版本)能够直接使用 valarraysize()sum() 方法,就像它们是 Student 的公有方法一样。

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
// studenti.h (修改后)
class Student : private std::string, private std::valarray<double> {
private:
std::ostream & arr_out(std::ostream & os) const;
public:
// ... 构造函数 ...
~Student() {}

// 使用 using 声明将基类的部分成员暴露为 public
using std::valarray<double>::size; // 使 valarray::size() 成为 Student 的 public 成员
using std::valarray<double>::operator[]; // 使 valarray::operator[] 成为 Student 的 public 成员

// 仍然需要提供自己的接口,或者暴露更多基类成员
double Average() const;
const std::string & Name() const;
// operator[] 现在通过 using 声明暴露了,可以移除 Student 自己的版本 (如果签名匹配)

// ... 友元 ...
};

// studenti.cpp (修改后)
double Student::Average() const {
// 现在可以直接调用 size() 和 sum() (如果 sum 也 using 了)
if (size() > 0) // 直接调用继承来的 size()
// return sum() / size(); // 假设 sum() 也 using 了
return std::valarray<double>::sum() / size(); // 或者仍然显式调用
else
return 0;
}
// operator[] 的实现可以移除了,因为 using 声明提供了它

// ... 其他实现可能也需要调整 ...

注意: using 声明只改变成员的可访问性,不改变其继承属性(例如,虚函数仍然是虚函数)。它不能用来降低访问权限(例如,不能在 private 部分 using 一个基类的 public 成员来使其变为 private)。

using 声明提供了一种在私有/保护继承下选择性地暴露基类接口的方法,但过度使用可能会破坏原本隐藏实现的意图。

14.3 多重继承

多重继承 (Multiple Inheritance, MI) 允许一个派生类从多个基类继承。这意味着派生类可以同时拥有多个基类的特性和接口。

语法:

1
2
3
class DerivedClassName : accessSpecifier1 BaseClass1, accessSpecifier2 BaseClass2, ... {
// ... 派生类成员 ...
};
  • 在类头中列出所有要继承的基类,用逗号分隔。
  • 可以为每个基类指定不同的访问说明符(public, protected, private)。

示例场景:

假设我们有一个通用的 Worker 类,表示工作人员的基本信息。我们还有 Singer 类表示歌手,Waiter 类表示服务员。现在我们想创建一个 SingingWaiter 类,表示一个既会唱歌又能提供服务的服务员。SingingWaiter 可以同时继承 SingerWaiter

1
2
3
4
5
6
7
8
class Worker { /* ... id, name ... */ };
class Singer { /* ... voice range, sing() ... */ };
class Waiter { /* ... panache, serve() ... */ };

// SingingWaiter 继承自 Singer 和 Waiter
class SingingWaiter : public Singer, public Waiter {
// ... SingingWaiter 特有的成员 ...
};

潜在问题:

多重继承虽然强大,但也引入了一些复杂性和潜在的问题:

14.3.1 有多少 Worker (钻石问题 - Diamond Problem)

如果多个基类是从同一个更远的基类派生而来的,那么派生类将包含该共同基类的多个副本(每个继承路径一个)。这被称为钻石问题菱形继承

示例: 假设 SingerWaiter 都继承自 Worker

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
class Worker {
public:
long id;
std::string fullname;
virtual ~Worker() {}
virtual void Show() const { /* ... show id, name ... */ }
// ... other methods ...
};

class Waiter : public Worker {
public:
int panache;
virtual void Show() const override { /* ... show worker info + panache ... */ }
// ... other methods ...
};

class Singer : public Worker {
public:
enum {other, alto, contralto, soprano, bass, baritone, tenor};
static const int Vtypes = 7;
int voice;
virtual void Show() const override { /* ... show worker info + voice ... */ }
// ... other methods ...
};

// SingingWaiter 继承自 Waiter 和 Singer,两者都继承自 Worker
class SingingWaiter : public Waiter, public Singer {
public:
virtual void Show() const override { /* ... show all info ... */ }
// ... other methods ...
};

在这种情况下,一个 SingingWaiter 对象内部会包含两个 Worker 子对象:一个来自 Waiter 的继承路径,另一个来自 Singer 的继承路径。

这会导致几个问题:

  1. 成员访问歧义: 如果你想访问 SingingWaiter 对象的 idfullname(来自 Worker),编译器不知道你指的是哪个 Worker 子对象的成员。

    1
    2
    SingingWaiter sw;
    // sw.id = 123; // 错误!歧义:Waiter::Worker::id 还是 Singer::Worker::id?
  2. 资源冗余: 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 子对象。访问 idfullname 不再有歧义。

    1
    2
    SingingWaiter 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 哪个方法 (成员名冲突)

即使没有钻石问题,如果不同的基类提供了同名的方法或数据成员,派生类在调用该成员时也会产生歧义

示例: 假设 WaiterSinger 都有一个名为 Talent() 的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Waiter {
public:
void Talent() const { std::cout << "Serves with style.\n"; }
// ...
};
class Singer {
public:
void Talent() const { std::cout << "Sings beautifully.\n"; }
// ...
};
class SingingWaiter : public Waiter, public Singer {
// ...
};

SingingWaiter sw;
// sw.Talent(); // 错误!歧义:Waiter::Talent() 还是 Singer::Talent()?

解决方案:使用作用域解析运算符 (::)

为了解决这种歧义,需要使用作用域解析运算符明确指定要调用哪个基类的版本:

1
2
sw.Waiter::Talent(); // 调用 Waiter 版本的 Talent()
sw.Singer::Talent(); // 调用 Singer 版本的 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
    #ifndef WORKER0_H_
    #define WORKER0_H_
    #include <string>
    // ... (Worker 定义,包含虚函数 Show() 和 Set()) ...
    #endif

    // workermi.h - MI 相关类
    #ifndef WORKERMI_H_
    #define WORKERMI_H_
    #include "worker0.h" // 包含基类
    #include <iostream>

    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;
    };
    #endif

    // workermi.cpp - 实现 (部分)
    #include "workermi.h"
    // ... 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 - 使用示例
    #include <iostream>
    #include "workermi.h"
    // ... (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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template <typename T1, typename T2, ...> // 或者 template <class T1, class T2, ...>
class ClassName {
// 类定义,可以使用模板参数 T1, T2 等作为类型
private:
T1 member1;
T2 member2;
// ...
public:
ClassName(const T1 & val1, const T2 & val2);
T1 getMember1() const;
// ...
};

// 成员函数的定义也需要模板前缀
template <typename T1, typename T2, ...>
ClassName<T1, T2, ...>::ClassName(const T1 & val1, const T2 & val2)
: member1(val1), member2(val2) {
// ...
}

template <typename T1, typename T2, ...>
T1 ClassName<T1, T2, ...>::getMember1() const {
return member1;
}
  • template <typename T>template <class T>: 声明一个模板,T 是一个类型参数的占位符。typenameclass 在这里是等价的。你可以使用任何有效的标识符作为类型参数名(通常用 T, U, V 等)。
  • 在类定义内部,可以使用模板参数 T 就像使用普通类型一样(例如,定义成员变量类型、函数参数类型、返回值类型)。
  • 成员函数定义: 当在类外部定义模板类的成员函数时,必须再次使用 template <...> 前缀,并且在类名后面跟上模板参数列表 <T1, T2, ...>

示例:简单的 Stack 模板类

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
// stacktp.h -- 一个简单的栈模板类定义
#ifndef STACKTP_H_
#define STACKTP_H_

template <typename T> // 或者 template <class T>
class Stack {
private:
enum { MAX = 10 }; // 栈的最大容量 (非类型参数可以改进)
T items[MAX]; // 存储栈元素的数组,类型为 T
int top; // 栈顶索引
public:
Stack();
bool isempty() const;
bool isfull() const;
// push() returns false if stack already is full, true otherwise
bool push(const T & item); // 参数类型为 T
// pop() returns false if stack already is empty, true otherwise
bool pop(T & item); // 参数类型为 T (通过引用返回弹出的元素)
};

// 成员函数实现 (通常也放在头文件中)
template <typename T>
Stack<T>::Stack() : top(0) { // 初始化栈顶
}

template <typename T>
bool Stack<T>::isempty() const {
return top == 0;
}

template <typename T>
bool Stack<T>::isfull() const {
return top == MAX;
}

template <typename T>
bool Stack<T>::push(const T & item) {
if (top < MAX) {
items[top++] = item; // 使用类型 T 的赋值操作
return true;
} else {
return false;
}
}

template <typename T>
bool Stack<T>::pop(T & item) {
if (top > 0) {
item = items[--top]; // 使用类型 T 的赋值操作
return true;
} else {
return false;
}
}

#endif // STACKTP_H_

14.4.2 使用模板类

要使用类模板,你需要实例化 (Instantiate) 它。实例化是指为模板参数提供具体的类型(或值),从而创建一个具体的类。

语法:

1
ClassName<ConcreteType1, ConcreteType2, ...> objectName(constructor_arguments);
  • 在类模板名称后面跟上尖括号 <>,并在其中指定用于替换模板参数的具体类型。
  • 然后像创建普通类的对象一样声明对象并调用构造函数。

示例:使用 Stack 模板

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
// stack_user.cpp -- 使用 Stack 模板类
#include <iostream>
#include <string>
#include <cctype> // for isalpha, etc. (假设用于处理输入)
#include "stacktp.h" // 包含模板定义

int main() {
// 实例化一个处理 int 的栈
Stack<int> intStack; // intStack 是一个 Stack<int> 类型的对象
int i = 0;
std::cout << "Pushing integers onto intStack:\n";
while (!intStack.isfull() && i < 5) {
intStack.push(i++);
std::cout << i-1 << " pushed. ";
}
std::cout << std::endl;

int tempInt;
std::cout << "Popping integers from intStack:\n";
while (intStack.pop(tempInt)) {
std::cout << tempInt << " popped. ";
}
std::cout << std::endl;

// 实例化一个处理 string 的栈
Stack<std::string> stringStack; // stringStack 是一个 Stack<std::string> 对象
std::string items[] = {"apple", "banana", "cherry"};
std::cout << "Pushing strings onto stringStack:\n";
for (const auto& s : items) {
if (stringStack.push(s)) {
std::cout << "\"" << s << "\" pushed. ";
} else {
std::cout << "Stack full, cannot push \"" << s << "\".\n";
break;
}
}
std::cout << std::endl;

std::string tempString;
std::cout << "Popping strings from stringStack:\n";
while (stringStack.pop(tempString)) {
std::cout << "\"" << tempString << "\" popped. ";
}
std::cout << std::endl;

return 0;
}

编译和运行:

由于模板的实现通常放在头文件中,你只需要编译使用模板的源文件。

1
2
g++ stack_user.cpp -o stack_user
./stack_user

输出:

1
2
3
4
5
6
7
8
Pushing integers onto intStack:
0 pushed. 1 pushed. 2 pushed. 3 pushed. 4 pushed.
Popping integers from intStack:
4 popped. 3 popped. 2 popped. 1 popped. 0 popped.
Pushing strings onto stringStack:
"apple" pushed. "banana" pushed. "cherry" pushed.
Popping strings from stringStack:
"cherry" popped. "banana" popped. "apple" popped.

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=),因为 pushpop 方法中使用了赋值。如果尝试用不满足这些要求的类型(如没有赋值运算符的类)实例化模板,编译器会报错。

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

类模板不仅可以有类型参数 (typename Tclass T),还可以有**非类型参数 (Non-type Parameters)**。非类型参数是具有固定类型的值,通常是整型(int, size_t 等)、指针、引用或枚举。

语法:

1
2
3
4
5
6
7
8
9
template <typename T, int N> // T 是类型参数,N 是非类型参数 (int 类型)
class ArrayTP {
private:
T ar[N]; // 数组大小由模板参数 N 决定
// ...
public:
T & operator[](int i);
// ...
};
  • 非类型参数 N 成为模板定义的一部分。
  • 在实例化时,必须为非类型参数提供一个常量表达式

示例:固定大小的数组模板 ArrayTP

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
// arraytp.h -- 模板类 ArrayTP
#ifndef ARRAYTP_H_
#define ARRAYTP_H_

#include <iostream>
#include <cstdlib> // for exit()

template <typename T, int n> // n 是非类型参数
class ArrayTP {
private:
T ar[n]; // 数组大小为 n
public:
ArrayTP() {};
explicit ArrayTP(const T & v);
virtual T & operator[](int i);
virtual T operator[](int i) const;
};

template <typename T, int n>
ArrayTP<T, n>::ArrayTP(const T & v) {
for (int i = 0; i < n; i++)
ar[i] = v;
}

template <typename T, int n>
T & ArrayTP<T, n>::operator[](int i) {
if (i < 0 || i >= n) {
std::cerr << "Error in array limits: " << i
<< " is out of range\n";
std::exit(EXIT_FAILURE);
}
return ar[i];
}

template <typename T, int n>
T ArrayTP<T, n>::operator[](int i) const {
if (i < 0 || i >= n) {
std::cerr << "Error in array limits: " << i
<< " is out of range\n";
std::exit(EXIT_FAILURE);
}
return ar[i];
}

#endif // ARRAYTP_H_

// 使用 ArrayTP
#include "arraytp.h" // 包含模板定义

int main() {
// 实例化一个包含 10 个 int 的数组
ArrayTP<int, 10> sums;
// 实例化一个包含 10 个 double 的数组
ArrayTP<double, 10> aves;
// 实例化一个包含 5 个指向 double 的指针的数组
ArrayTP< ArrayTP<int, 5>, 10 > twodee; // 模板嵌套:10x5 的 int 数组

int i, j;
for (i = 0; i < 10; i++) {
sums[i] = 0;
for (j = 0; j < 5; j++) {
twodee[i][j] = (i + 1) * (j + 1);
sums[i] += twodee[i][j];
}
aves[i] = (double) sums[i] / 5;
}

for (i = 0; i < 10; i++) {
std::cout.width(2);
std::cout << i << ": ";
std::cout.width(3);
std::cout << sums[i] << " = average " << aves[i] << std::endl;
}

// ArrayTP<double, 0> zero_size; // 错误或无意义,取决于编译器和实现

return 0;
}
  • 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)**。

  1. 显式具体化 (Explicit Specialization): 为某个特定的类型(或一组特定类型)提供一个完全不同的类定义。

    语法:

    1
    2
    3
    4
    template <> // 空的模板参数列表
    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
    #include <cstring> // for strcpy, strlen

    // 显式具体化 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> 模板。

  2. 部分具体化 (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
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
template<typename T>
class SmartPtr {
private:
T* ptr;
public:
explicit SmartPtr(T* p = nullptr) : ptr(p) {}
~SmartPtr() { delete ptr; }

// 模板成员:模板复制构造函数
template<typename U>
SmartPtr(const SmartPtr<U>& other) {
// 允许从 SmartPtr<U> 构造 SmartPtr<T>
// 通常需要 U* 能够隐式转换为 T* (例如 U 是 T 的派生类)
ptr = other.get(); // 简化示例,实际智能指针复制更复杂
// ... 可能需要增加引用计数等 ...
}

// 模板成员:模板赋值运算符
template<typename U>
SmartPtr& operator=(const SmartPtr<U>& other) {
// ... 类似逻辑 ...
return *this;
}

T* get() const { return ptr; }
// ... 其他成员 ...
};

// 使用
class Base {};
class Derived : public Base {};

SmartPtr<Derived> pDerived(new Derived);
SmartPtr<Base> pBase = pDerived; // OK: 调用模板复制构造函数 SmartPtr<Base>(const SmartPtr<Derived>&)

成员模板增加了类的灵活性,允许成员函数或嵌套类处理更广泛的类型。

14.4.8 将模板用作参数

模板本身也可以作为模板的参数,这称为**模板模板参数 (Template Template Parameters)**。

语法:

1
2
3
4
5
6
template <typename T, template <typename U> class Container> // Container 是一个模板模板参数
class Manager {
private:
Container<T> items; // 使用模板参数 Container 来实例化
// ...
};
  • template <typename U> class Container: 声明 Container 是一个接受一个类型参数的类模板。
  • Manager 内部,可以用具体的类型 T 来实例化 Container,如 Container<T>

示例: 创建一个可以使用不同容器(如 std::vector, std::list, 或我们自己的 Stack)来存储元素的 Manager

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
#include <vector>
#include <list>
#include "stacktp.h" // 假设 Stack<T> 在这里

template <typename T, template <typename Elem> class Container> // Container 接受一个类型参数 Elem
class Manager {
private:
Container<T> items; // 使用 Container<T> 来存储 T 类型的元素
public:
void add(const T& item) {
// 假设 Container 有 push_back 或 push 方法
// items.push_back(item); // 如果是 vector/list
// items.push(item); // 如果是 Stack
// 需要更通用的方式或对 Container 有要求
}
// ...
};

int main() {
// 使用 std::vector 作为容器
Manager<int, std::vector> vectorManager;
// vectorManager.add(10);

// 使用 std::list 作为容器
Manager<double, std::list> listManager;
// listManager.add(3.14);

// 使用我们自己的 Stack 作为容器
Manager<std::string, Stack> stackManager;
// stackManager.add("hello");

return 0;
}

模板模板参数使得代码更加通用,可以适配不同的模板结构。

14.4.9 模板类和友元

友元关系可以与类模板结合,有几种不同的形式:

  1. 非模板友元函数/类: 一个普通的(非模板)函数或类可以是模板类的友元。这意味着这个函数/类可以访问所有该模板类实例化的私有成员。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    template <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; } // 模板友元函数
  2. 约束模板友元函数/类 (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> 的友元。

  3. 非约束模板友元函数/类 (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
    20
    template <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
2
3
4
5
6
// 为特定实例化创建别名
using AliasName = ExistingTemplate<ConcreteType>;

// 为模板本身创建别名 (参数化的别名)
template <typename T>
using AliasTemplateName = ExistingTemplate<T, SomeFixedType, ...>;

示例:

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
#include <vector>
#include <string>
#include <array>

// 为特定实例化创建别名
using IntVec = std::vector<int>;
using StringArray10 = std::array<std::string, 10>;

// 为模板创建别名
template<typename T>
using Vec = std::vector<T>; // Vec<T> 等价于 std::vector<T>

template<typename T>
using Array12 = std::array<T, 12>; // Array12<T> 等价于 std::array<T, 12>

int main() {
IntVec numbers; // numbers 是 std::vector<int>
numbers.push_back(1);

StringArray10 names; // names 是 std::array<std::string, 10>
names[0] = "Alice";

Vec<double> doubles; // doubles 是 std::vector<double>
doubles.push_back(3.14);

Array12<char> chars; // chars 是 std::array<char, 12>
chars[0] = 'a';

return 0;
}

模板别名比传统的 typedef 更强大,因为 typedef 不能直接为模板创建别名(只能为完全实例化的类型创建别名)。模板别名提高了代码的可读性和易用性。

14.5 总结

本章探讨了 C++ 中除了公有继承之外的其他代码重用技术,包括包含(组合)、私有继承、保护继承、多重继承以及强大的类模板。这些技术提供了不同的方式来建立类之间的关系和创建可重用的通用代码。

主要内容回顾:

  1. 包含/组合 (Composition):

    • 通过将一个类的对象作为另一个类的成员来实现,模拟 “has-a” 关系。
    • 是代码重用的常用且推荐的方式,特别是当成员对象能自我管理资源时(遵循零法则)。
    • 包含类的构造函数使用成员初始化列表来初始化成员对象。
    • 例如,Student 类包含 std::stringstd::valarray<double> 成员。
  2. 私有继承 (private):

    • 模拟 “is-implemented-in-terms-of” 关系,继承实现但不继承接口。
    • 基类的公有和保护成员在派生类中变为私有。
    • 通常不如包含直观和灵活,但可用于访问基类的保护成员或覆盖虚函数(虽然访问权限变为私有)。
    • 派生类需要提供自己的接口来暴露所需功能,内部访问基类成员通常需要类型转换。
  3. 保护继承 (protected):

    • 基类的公有和保护成员在派生类中变为保护。
    • 与私有继承类似,继承实现但不继承接口。
    • 允许后续的派生类访问继承来的(现在是保护的)成员。
    • 使用场景非常有限。
  4. 包含 vs. 私有继承: 对于 “has-a” 关系,**优先选择包含 (组合)**,因为它更简单、清晰、灵活。

  5. using 声明与继承: 在私有或保护继承下,可以使用 using Base::member; 在派生类中恢复基类某个成员的可访问性(通常提升到 publicprotected)。

  6. 多重继承 (MI):

    • 允许一个类从多个基类继承。
    • 可能导致歧义(成员名冲突)和钻石问题(共同基类的多副本)。
    • 名称冲突通过作用域解析运算符 (::) 解决。
    • 钻石问题通过虚基类 (virtual public Base) 解决,确保共享基类的单一副本,但构造函数实现更复杂。
    • 应谨慎使用,优先考虑组合或单继承。
  7. 类模板:

    • 允许创建参数化的类蓝图,用于生成处理不同类型的类。
    • 使用 template <typename T, ...> 定义。
    • 通过提供具体类型来实例化模板类,如 Stack<int>
    • 模板实现通常放在头文件中。
    • 可以有非类型参数(如 template <typename T, int N>),用于在编译时确定常量值(如数组大小)。
    • 模板具体化(显式和部分)允许为特定类型提供专门的实现。
    • 类可以包含成员模板(模板化的成员函数或嵌套类)。
    • 模板可以作为其他模板的参数(模板模板参数)。
    • 模板可以与友元结合,有约束和非约束两种形式。
    • C++11 引入了**模板别名 (using Alias = ...)**,简化复杂模板类型的使用。

本章介绍的技术极大地扩展了 C++ 代码重用的可能性,从简单的对象组合到复杂的继承层次结构和强大的泛型编程工具——类模板。理解这些技术及其适用场景对于设计灵活、可维护和可重用的 C++ 代码至关重要。

评论