第19章 模板和泛型编程

代码重用的概念

代码重用是指在不同的程序或项目中使用已有的代码,而不是重新编写相同的功能。C++提供了多种代码重用的机制,包括函数重载、模板、继承、组合等。

继承与组合

继承

继承是一种”is-a”关系,派生类是基类的一种特殊类型。

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
// 继承示例
class Animal {
protected:
std::string name;

public:
Animal(const std::string& n) : name(n) {}
void eat() {
std::cout << name << " is eating." << std::endl;
}
virtual void makeSound() {
std::cout << name << " makes a sound." << std::endl;
}
};

class Dog : public Animal {
public:
Dog(const std::string& n) : Animal(n) {}
void bark() {
std::cout << name << " is barking." << std::endl;
}
void makeSound() override {
bark();
}
};

组合

组合是一种”has-a”关系,一个类包含另一个类的对象作为成员。

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
// 组合示例
class Engine {
private:
int horsepower;

public:
Engine(int hp) : horsepower(hp) {
std::cout << "Engine created with " << hp << " HP" << std::endl;
}
void start() {
std::cout << "Engine started" << std::endl;
}
void stop() {
std::cout << "Engine stopped" << std::endl;
}
};

class Car {
private:
std::string model;
Engine engine; // 组合:Car包含Engine

public:
Car(const std::string& m, int hp) : model(m), engine(hp) {
std::cout << "Car created: " << model << std::endl;
}
void drive() {
engine.start();
std::cout << model << " is driving" << std::endl;
}
void park() {
std::cout << model << " is parking" << std::endl;
engine.stop();
}
};

继承与组合的比较

特性继承组合
关系is-ahas-a
代码重用继承基类的行为复用组件的功能
灵活性较低,强耦合较高,松耦合
可维护性较低,修改基类可能影响派生类较高,组件独立
扩展性较低,受限于继承层次较高,易于替换组件

私有继承

私有继承是一种实现代码重用的方式,它建立了一种”is-implemented-in-terms-of”(是根据…实现的)关系。

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
// 私有继承示例
class StringRep {
private:
char* data;
size_t length;

public:
StringRep(const char* str) {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}
~StringRep() {
delete[] data;
}
const char* getCString() const {
return data;
}
size_t getLength() const {
return length;
}
};

// 私有继承:String是根据StringRep实现的
class String {
private:
// 私有继承
class Impl : private StringRep {
public:
Impl(const char* str) : StringRep(str) {}
using StringRep::getCString;
using StringRep::getLength;
};

Impl* impl;

public:
String(const char* str) {
impl = new Impl(str);
}
~String() {
delete impl;
}
const char* c_str() const {
return impl->getCString();
}
size_t length() const {
return impl->getLength();
}
};

保护继承

保护继承是另一种实现代码重用的方式,它建立了一种”is-implemented-in-terms-of”关系,但允许派生类访问基类的保护成员。

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
// 保护继承示例
class Base {
protected:
int value;

public:
Base(int v) : value(v) {}
void print() {
std::cout << "Base::value = " << value << std::endl;
}
};

// 保护继承
class Derived : protected Base {
private:
int derivedValue;

public:
Derived(int v, int dv) : Base(v), derivedValue(dv) {}
void printDerived() {
std::cout << "Base::value = " << value << std::endl;
std::cout << "Derived::derivedValue = " << derivedValue << std::endl;
}
};

// 进一步派生
class FurtherDerived : public Derived {
public:
FurtherDerived(int v, int dv) : Derived(v, dv) {}
void printFurther() {
std::cout << "Base::value = " << value << std::endl; // 可以访问,因为是保护继承
}
};

C++20新特性:模板增强

C++20对模板系统进行了多项增强,包括模板lambda、requires表达式等:

模板Lambda

C++20允许在lambda表达式中使用模板参数:

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 <iostream>

int main() {
// 模板lambda
auto add = []<typename T>(T a, T b) {
return a + b;
};

// 使用不同类型
std::cout << "Add integers: " << add(1, 2) << std::endl;
std::cout << "Add doubles: " << add(1.5, 2.5) << std::endl;
std::cout << "Add strings: " << add(std::string("Hello"), std::string(" World")) << std::endl;

// 带约束的模板lambda
auto multiply = []<typename T>(T a, T b) requires std::is_arithmetic_v<T> {
return a * b;
};

std::cout << "Multiply integers: " << multiply(3, 4) << std::endl;
std::cout << "Multiply doubles: " << multiply(2.5, 4.0) << std::endl;

// 可变参数模板lambda
auto print = []<typename... Args>(Args&&... args) {
(std::cout << ... << args) << std::endl;
};

print("The answer is ", 42, ", and pi is ", 3.14159);

return 0;
}

带requires表达式的模板

C++20引入了requires表达式,用于在模板中添加约束:

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
#include <concepts>
#include <iostream>

// 使用requires表达式约束模板参数
template <typename T>
requires std::integral<T> || std::floating_point<T>
T absolute(T value) {
return value < 0 ? -value : value;
}

// 结合概念使用
template <std::integral T>
T square(T value) {
return value * value;
}

// 带多个约束的模板
template <typename T>
requires std::copyable<T> && requires(T a, T b) {
{ a + b } -> std::same_as<T>;
{ a * b } -> std::same_as<T>;
}
T sum_and_product(T a, T b) {
return a + b * a;
}

int main() {
std::cout << "Absolute value of -5: " << absolute(-5) << std::endl;
std::cout << "Absolute value of -3.14: " << absolute(-3.14) << std::endl;

std::cout << "Square of 5: " << square(5) << std::endl;

std::cout << "Sum and product of 3 and 4: " << sum_and_product(3, 4) << std::endl;

return 0;
}

C++20模板增强的优点

  1. 更简洁的语法:模板lambda简化了泛型lambda的写法
  2. 更强的表达能力:requires表达式提供了更灵活的模板约束方式
  3. 更好的错误信息:当模板参数不满足约束时,提供更清晰的错误信息
  4. 更高的代码可读性:使用概念和requires表达式使模板代码更易于理解
  5. 更好的编译性能:编译器可以更早地检查模板参数的有效性

模板

模板是C++中实现泛型编程的重要机制,它允许编写与类型无关的代码。

函数模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 函数模板示例
template<typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}

// 显式实例化
template int max<int>(int, int);
template double max<double>(double, double);

int main() {
// 隐式实例化
int i = max(10, 20);
double d = max(3.14, 2.71);
std::string s = max(std::string("hello"), std::string("world"));

std::cout << "Max int: " << i << std::endl;
std::cout << "Max double: " << d << std::endl;
std::cout << "Max string: " << s << std::endl;

return 0;
}

类模板

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
// 类模板示例
template<typename T>
class Stack {
private:
std::vector<T> elements;

public:
void push(const T& item) {
elements.push_back(item);
}
void pop() {
if (!empty()) {
elements.pop_back();
}
}
T& top() {
return elements.back();
}
bool empty() const {
return elements.empty();
}
size_t size() const {
return elements.size();
}
};

int main() {
// 使用int类型的Stack
Stack<int> intStack;
intStack.push(10);
intStack.push(20);
std::cout << "Top: " << intStack.top() << std::endl;
intStack.pop();
std::cout << "Top after pop: " << intStack.top() << std::endl;

// 使用string类型的Stack
Stack<std::string> stringStack;
stringStack.push("hello");
stringStack.push("world");
std::cout << "Top: " << stringStack.top() << std::endl;
stringStack.pop();
std::cout << "Top after pop: " << stringStack.top() << std::endl;

return 0;
}

模板特化

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
// 模板特化示例
template<typename T>
class MyClass {
public:
void print() {
std::cout << "General template" << std::endl;
}
};

// 特化版本
template<>
class MyClass<int> {
public:
void print() {
std::cout << "Specialized for int" << std::endl;
}
};

// 特化版本
template<>
class MyClass<std::string> {
public:
void print() {
std::cout << "Specialized for string" << std::endl;
}
};

int main() {
MyClass<double> d;
MyClass<int> i;
MyClass<std::string> s;

d.print(); // 输出 General template
i.print(); // 输出 Specialized for int
s.print(); // 输出 Specialized for string

return 0;
}

标准模板库(STL)

STL是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
// 容器示例
#include <vector>
#include <list>
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>

int main() {
// 向量
std::vector<int> vec = {1, 2, 3, 4, 5};
vec.push_back(6);

// 列表
std::list<int> lst = {1, 2, 3, 4, 5};
lst.push_back(6);
lst.push_front(0);

// 映射
std::map<std::string, int> map;
map["one"] = 1;
map["two"] = 2;

// 集合
std::set<int> set = {1, 2, 3, 4, 5};
set.insert(6);

// 无序映射
std::unordered_map<std::string, int> umap;
umap["one"] = 1;
umap["two"] = 2;

// 无序集合
std::unordered_set<int> uset = {1, 2, 3, 4, 5};
uset.insert(6);

return 0;
}

算法

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 <algorithm>
#include <vector>

int main() {
std::vector<int> vec = {5, 2, 8, 1, 9, 3};

// 排序
std::sort(vec.begin(), vec.end());

// 查找
auto it = std::find(vec.begin(), vec.end(), 5);
if (it != vec.end()) {
std::cout << "Found 5 at position: " << std::distance(vec.begin(), it) << std::endl;
}

// 计数
int count = std::count(vec.begin(), vec.end(), 5);
std::cout << "Count of 5: " << count << std::endl;

// 最大值
auto maxIt = std::max_element(vec.begin(), vec.end());
std::cout << "Max element: " << *maxIt << std::endl;

// 最小值
auto minIt = std::min_element(vec.begin(), vec.end());
std::cout << "Min element: " << *minIt << std::endl;

// 反转
std::reverse(vec.begin(), vec.end());

return 0;
}

迭代器

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
// 迭代器示例
#include <vector>

int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};

// 正向迭代器
std::cout << "Forward iteration: " << std::endl;
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;

// 反向迭代器
std::cout << "Reverse iteration: " << std::endl;
for (auto it = vec.rbegin(); it != vec.rend(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;

// 常量迭代器
std::cout << "Constant iteration: " << std::endl;
for (auto it = vec.cbegin(); it != vec.cend(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;

return 0;
}

函数对象

函数对象是重载了函数调用运算符()的类的实例,可以像函数一样被调用。

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
// 函数对象示例
class Add {
private:
int value;

public:
Add(int v) : value(v) {}

int operator()(int x) const {
return x + value;
}
};

class Multiply {
private:
int factor;

public:
Multiply(int f) : factor(f) {}

int operator()(int x) const {
return x * factor;
}
};

int main() {
Add add5(5);
int result1 = add5(10); // 调用operator()
std::cout << "10 + 5 = " << result1 << std::endl;

Multiply multiply3(3);
int result2 = multiply3(10); // 调用operator()
std::cout << "10 * 3 = " << result2 << std::endl;

// 函数对象作为参数
std::vector<int> vec = {1, 2, 3, 4, 5};
std::transform(vec.begin(), vec.end(), vec.begin(), add5);

std::cout << "After adding 5: " << std::endl;
for (int num : vec) {
std::cout << num << " ";
}
std::cout << std::endl;

return 0;
}

Lambda 表达式(C++11+)

Lambda表达式是C++11引入的匿名函数对象,提供了一种简洁的方式来定义函数。

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
// Lambda表达式示例
int main() {
// 基本lambda
auto add = [](int a, int b) { return a + b; };
std::cout << "5 + 3 = " << add(5, 3) << std::endl;

// 带捕获的lambda
int x = 10;
auto addX = [x](int a) { return a + x; };
std::cout << "5 + x = " << addX(5) << std::endl;

// 引用捕获
auto addXRef = [&x](int a) { return a + x; };

// 捕获所有变量
auto addAll = [=](int a) { return a + x; };

// 可变lambda
auto increment = [x]() mutable { return ++x; };

// lambda作为参数
std::vector<int> vec = {5, 2, 8, 1, 9, 3};
std::sort(vec.begin(), vec.end(), [](int a, int b) { return a > b; });

std::cout << "Sorted in descending order: " << std::endl;
for (int num : vec) {
std::cout << num << " ";
}
std::cout << std::endl;

return 0;
}

代码重用的最佳实践

1. 选择合适的代码重用机制

  • 继承:适用于”is-a”关系,需要多态的场景
  • 组合:适用于”has-a”关系,需要松耦合的场景
  • 模板:适用于泛型编程,需要与类型无关的代码
  • 函数对象:适用于需要状态的函数
  • Lambda表达式:适用于简短的、一次性的函数

2. 遵循设计原则

  • 单一职责原则:每个类只负责一项功能
  • 开放-封闭原则:对扩展开放,对修改封闭
  • 里氏替换原则:子类应该能够替换父类
  • 接口隔离原则:客户端不应该依赖它不使用的接口
  • 依赖倒置原则:依赖于抽象,而不是具体实现

3. 合理使用STL

  • 选择合适的容器:根据访问模式和性能需求选择容器
  • 使用算法:优先使用STL算法而不是手写循环
  • 使用迭代器:通过迭代器访问容器元素
  • 使用函数对象和lambda:简化代码,提高可读性

4. 代码组织

  • 模块化:将相关功能组织到模块中
  • 命名空间:使用命名空间避免名称冲突
  • 头文件和源文件分离:声明和实现分离
  • 文档:为代码添加注释和文档

常见错误和陷阱

1. 继承滥用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 错误:滥用继承
class Person {
public:
void eat() {}
void sleep() {}
};

// 错误:Student不是一种特殊的Person,而是有额外的功能
class Student : public Person {
public:
void study() {}
};

// 正确:使用组合
class Student {
private:
Person person;

public:
void eat() { person.eat(); }
void sleep() { person.sleep(); }
void study() {}
};

2. 模板错误

1
2
3
4
5
6
7
8
9
10
11
12
// 错误:模板特化顺序
// 一般模板
template<typename T>
class MyClass {
};

// 错误:特化模板在使用后定义
template<>
class MyClass<int> {
};

// 正确:先定义特化模板,再使用

3. STL使用错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 错误:在遍历过程中修改容器
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (*it == 3) {
vec.erase(it); // 错误:迭代器失效
}
}

// 正确:使用erase的返回值
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end();) {
if (*it == 3) {
it = vec.erase(it); // 正确:更新迭代器
} else {
++it;
}
}

4. 函数对象错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 错误:函数对象没有正确实现
class Comparator {
public:
// 错误:缺少const
bool operator()(int a, int b) {
return a < b;
}
};

// 正确:添加const
class Comparator {
public:
bool operator()(int a, int b) const {
return a < b;
}
};

小结

本章介绍了C++中的代码重用机制,包括:

  1. 继承与组合:两种基本的代码重用方式,分别对应”is-a”和”has-a”关系
  2. 私有继承和保护继承:实现”is-implemented-in-terms-of”关系
  3. 模板:实现泛型编程,编写与类型无关的代码
  4. 标准模板库(STL):提供容器、算法、迭代器等工具
  5. 函数对象:重载函数调用运算符的类的实例
  6. Lambda表达式:C++11引入的匿名函数对象
  7. 代码重用的最佳实践:选择合适的机制、遵循设计原则、合理使用STL等
  8. 常见错误和陷阱:继承滥用、模板错误、STL使用错误等

代码重用是提高开发效率、减少代码冗余、提高代码质量的重要手段。通过合理使用C++提供的代码重用机制,可以编写出更简洁、更高效、更可维护的代码。在实际编程中,应根据具体情况选择合适的代码重用方式,遵循最佳实践,避免常见错误。

在后续章节中,我们将学习C++的其他高级特性,如异常处理、命名空间、模板特化等,进一步提高C++编程能力。