C++ 从基础到进阶之七

大纲

C++ 模板与泛型

  • 泛型编程

    • 泛型编程是一种以独立于任何特定类型的方式编写代码的编程方式。在使用泛型编程时,需要提供具体程序实例所需的类型、习惯或值。
    • 在 C++ 中,泛型编程(基于模板)可以用于实现静态多态(编译期多态),即在编译阶段根据具体类型生成对应的代码;与之对应,虚函数机制用于实现动态多态(运行时多态),通过基类指针或引用在运行时决定调用哪个派生类的实现。
  • 模板的作用

    • 模板是泛型编程的基础。它是一种用于创建类或函数的蓝图或公式,开发者只需为这些蓝图提供足够的信息,模板便能真正执行并生成具体的代码。
  • 模板的类型支持

    • 模板支持将类型作为参数进行程序设计,从而直接支持泛型编程过程。也就是说,C++ 的模板机制允许在定义类或函数时,将类型作为参数传递,实现代码的通用性。
  • 模板的两种分类

    • 函数模板
      • 函数模板是用于生成特定类型函数的蓝图。它允许在定义函数时,将参数或返回值的类型作为模板参数,从而实现通用操作(如比较、交换等)。
      • 比如,基于函数模板编写一个比较两个值大小的函数,可以适用于 intdoublestring 等多种类型。
    • 类模板
      • 类模板是用于生成特定类型类的蓝图。它允许在定义类时,将成员变量或成员函数的类型作为模板参数,从而实现通用数据结构或容器(如栈、队列、数组等)。
      • 比如,基于类模板实现一个可存储任意类型元素的栈类,使用时再指定具体类型(如 Stack<int>Stack<string>)。

模板基础

函数模板

函数模板的概述
  • 模的板定义以 template 关键字开头

    • 后面紧跟 <>,其中称为模板参数列表(或模板实参列表)。
  • 模板参数列表规则

    • 至少包含一个模板参数。
    • 多个参数用逗号分隔。
    • 每个模板参数前需使用 typenameclass 关键字(此处 class 非类定义用途)。
  • 常见写法示例

    • 单个模板参数
      • template<class T>
      • 或者 template<typename T>
    • 多个模板参数:
      • template<class T, class Q>
      • 或者 template<typename T, typename Q>
  • 模板参数的含义

    • 表示函数定义中使用的类型或值,类似于函数参数列表的作用。
  • 模板实参的指定方式

    • 显式指定:使用 <> 包裹模板实参,比如 max<int>(3, 5);
    • 隐式推断:编译器根据函数调用上下文自动推断模板实参,无需显式指定。
  • 类型参数 T 的作用

    • T 是模板的类型参数,本身只是一个占位符。
    • 在模板被使用时,编译器会进行模板实例化,根据调用上下文推导出 T 的具体类型。这个过程通常发生在编译期,而不是运行时
函数模板的使用

关键点

函数模板的调用与普通函数的调用区别不大。在调用函数模板时,编译器会根据调用时提供的实参,去推断模板参数列表中的参数(即模板形参)的类型。有些时候,光凭函数实参是无法准确推断出模板参数的,这时候就需要使用 <> 来主动提供模板参数了,比如 max<int>(3, 5);。值得注意的是,模板参数有时是由编译器推断出来的,推断的依据是调用函数时传入的实参。当编译器推断出模板的形参类型后,编译器会实例化一个特定版本的函数。函数模板只有被调用(实例化)时,编译器才会生成对应的代码,仅定义模板编译器是不会生成任何代码的。由于编译器在生成代码的时候,需要找到函数的函数体,所以函数模板的定义通常是写在 .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
#include <iostream>

using namespace std;

// 定义函数模板
template <typename T>
T maxNum(T a, T b) {
return (a > b) ? a : b;
}

int main() {
// 调用时,编译器根据实参(10, 20)推断 T 为 int
int result1 = maxNum(10, 20);

// 调用时,编译器根据实参(3.14, 2.71)推断 T 为 double
double result2 = maxNum(3.14, 2.71);

// 如果实参类型不一致,编译器无法唯一确定 T,会导致模板参数推断失败
// double result3 = max(10, 3.14);

// 显式指定模板参数
double result3 = maxNum<double>(10, 3.14);

return 0;
}
非类型模板参数

关键点

在定义函数模板时,因为 T 前边有一个 typename / class,这表示 T 代表一个类型,是一个类型参数。那么在模板参数列表里面,还可以定义非类型参数,这里的非类型参数代表的是一个值。既然非类型参数代表一个值,那么就不能用 typename / class 这种关键字来修饰这个值,而是必须要用具体的类型名称来指定非类型参数。比如,非类型参数 S 如果是个整型,就可以写成 template<typename T, int S>。当函数模板实例化时,这种非类型参数的值通常需要由用户显式指定(一般情况下编译器不会自动推断),并且这些值都必须是常量表达式,因为函数模板的实例化是在编译阶段完成的。另外,无论是函数模板还是类模板,非类型模板参数都可以指定默认值,比如 template <typename T, int A = 5>,但需要注意非类型模板参数的默认值必须从右向左连续定义。特别注意,浮点型不能作为非类型模板参数,比如 floatdouble

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

using namespace std;

// 定义函数模板(使用非类型模板参数)
template <int A, int B>
int sum() {
return A + B;
}

// 定义函数模板(使用非类型模板参数)
template <unsigned L1, unsigned L2>
int charscomp(const char (&c1)[L1], const char (&c2)[L2]) {
return strcmp(c1, c2);
}

// 定义函数模板(使用类型模板参数和非类型模板参数)
template <typename T, int A = 5>
void repeatPrint(T value) {
for (int i = 0; i < A; ++i) {
cout << value << " ";
}
cout << endl;
}

void test01() {
cout << "========== test01() ======" << endl;
// 显式指定非类型模板参数
int result1 = sum<3, 5>();
cout << result1 << endl;
}

void test02() {
cout << "========== test02() ======" << endl;
// 编译器会自动推断非类型模板参数
int result1 = charscomp("test2", "test");
cout << result1 << endl;
}

void test03() {
cout << "========== test03() ======" << endl;
// T 被推断为 int,而 A 显式指定为 2
repeatPrint<int, 2>(10);

// T 被推断为 double,而 A 显式指定为 3
repeatPrint<double, 3>(3.14);

// T 被推断为 const char*,而 A 显式指定为 4
repeatPrint<const char*, 4>("Hi");

// T 被推断为 float,而 A 默认指定为 5
repeatPrint<float>(6.7f);
}

int main() {
test01();
test02();
test03();
return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
7
8
9
========== test01() ======
8
========== test02() ======
50
========== test03() ======
10 10
3.14 3.14 3.14
Hi Hi Hi Hi
6.7 6.7 6.7 6.7 6.7

类模板

类模板的概述

类模板可以用来实例化一个特定的类。由于编译器不能为类模板自动推断模板参数类型,因此为了使用类模板,必须在模板名后边使用 <> 来提供额外的信息,这些信息其实就是对应着模板参数的具体类型或值。类模板的核心优势在于:同一套代码,可以应付不同的数据类型,代码精简多了,避免了为每种类型重复编写类似的类。在 C++ 模板中,只要一个名称依赖于模板参数,编译器在第一阶段无法判断它是类型还是变量,默认按非类型解析;因此无论是在类模板还是函数模板中,都必须使用 typename 显式声明它是类型。

对比项函数模板类模板
模板实参推断支持自动推断不支持(C++17 之前),需显式指定
使用方式max(1, 2)vector<int>
实例化时机调用时实例化(编译期实例化)使用类型时实例化(编译期实例化)
函数 / 成员函数生成调用时生成成员函数按需实例化(懒生成)
类模板的使用

关键点

C++ 类模板的成员函数是指在类模板中定义的函数,这些函数同样依赖于模板参数,因此在类外实现类模板的成员函数时,需要保留模板参数声明(比如 template<typename T>)并使用作用域限定符(如 MyVector<T>::func())进行定义。由于模板是在编译期进行实例化的,成员函数的实现通常需要放在头文件中,以便编译器在使用时能够生成具体类型的代码。此外,在成员函数中如果使用了依赖于模板参数的类型(如 MyVector<T>::iterator),编译器默认无法判断其是类型还是变量,此时必须使用 typename 进行显式说明(如 typename MyVector<T>::iterator)。总体来说,类模板的成员函数与普通类成员函数在语法上类似,但需要额外注意模板参数、依赖名称解析以及编译期实例化机制。

  • MyVector.hpp,简单模拟实现 STL 的 vector 容器,不考虑扩容、移动语义等实现
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
114
115
116
117
118
119
120
#pragma once

#include <iostream>

using namespace std;

// 定义类模板
template <typename T>
class MyVector {
public:
// 定义迭代器类型
using iterator = T*;

// 构造函数
MyVector();

// 析构函数
~MyVector();

// 拷贝构造函数
MyVector(const MyVector& v);

// 赋值运算符
MyVector& operator=(const MyVector& v);

// 返回迭代器的起始位置
iterator begin();

// 返回迭代器的最后一个元素的下一个位置
iterator end();

// 往容器末尾添加元素
bool push_back(const T& t);

private:
T* m_data; // 底层数组
size_t m_size; // 元素个数
size_t m_capacity; // 最大容量
};

// 构造函数
template <typename T>
MyVector<T>::MyVector() {
m_size = 0;
m_capacity = 10;

// 申请内存空间
m_data = new T[m_capacity];
}

// 析构函数
template <typename T>
MyVector<T>::~MyVector() {
if (m_data) {
delete[] m_data;
m_data = nullptr;
}
}

// 拷贝构造函数
template <typename T>
MyVector<T>::MyVector(const MyVector<T>& v) {
m_size = v.m_size;
m_capacity = v.m_capacity;

// 深拷贝
m_data = new T[m_capacity];
for (size_t i = 0; i < m_size; ++i) {
m_data[i] = v.m_data[i];
}
}

// 赋值运算符
template <typename T>
MyVector<T>& MyVector<T>::operator=(const MyVector<T>& v) {
// 防止自赋值
if (this == &v) {
return *this;
}

// 申请新的内存空间
T* new_data = new T[v.m_capacity];
for (size_t i = 0; i < v.m_size; ++i) {
new_data[i] = v.m_data[i];
}

// 释放旧的内存空间
delete[] m_data;

m_data = new_data;
m_size = v.m_size;
m_capacity = v.m_capacity;

return *this;
}

// 返回迭代器的起始位置
template <typename T>
typename MyVector<T>::iterator MyVector<T>::begin() {
return m_data;
}

// 返回迭代器的最后一个元素的下一个位置
template <typename T>
typename MyVector<T>::iterator MyVector<T>::end() {
return m_data + m_size;
}

// 往容器末尾添加元素
template <typename T>
bool MyVector<T>::push_back(const T& t) {
if (m_size == m_capacity) {
return false;
}

m_data[m_size] = t;
++m_size;

return true;
}
  • main.hpp
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
#include <iostream>
#include "MyVector.hpp"

using namespace std;

void show(MyVector<int>& v) {
for (MyVector<int>::iterator it = v.begin(); it != v.end(); ++it) {
cout << *it << " ";
}
cout << endl;
}

int main() {
MyVector<int> v1;
v1.push_back(1);
v1.push_back(3);
v1.push_back(5);
show(v1);

MyVector<int> v2 = v1;
show(v2);

v2 = v1;
show(v2);

return 0;
}

程序运行输出的结果如下:

1
2
3
1 3 5 
1 3 5
1 3 5
非类型模板参数

关键点

在定义类模板时,因为 T 前边有一个 typename / class,这表示 T 代表一个类型,是一个类型参数。那么在模板参数列表里面,还可以定义非类型参数,这里的非类型参数代表的是一个值。既然非类型参数代表一个值,那么就不能用 typename / class 这种关键字来修饰这个值,而是必须要用具体的类型名称来指定非类型参数。比如,非类型参数 S 如果是个整型,就可以写成 template<typename T, int S>。当类模板实例化时,这种非类型参数的值通常需要由用户显式提供(一般情况下编译器不会自动推断),并且这些值都必须是常量表达式,因为类模板的实例化是在编译阶段完成的。另外,无论是函数模板还是类模板,非类型模板参数都可以指定默认值,比如 template <typename T, int A = 5>,但需要注意非类型模板参数的默认值必须从右向左连续定义。特别注意,浮点型不能作为非类型模板参数,比如 floatdouble

  • MyVector.hpp,简单模拟实现 STL 的 vector 容器,不考虑扩容、移动语义等实现
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
114
115
116
117
118
119
120
#pragma once

#include <iostream>

using namespace std;

// 定义类模板(使用类型模板参数和非类型模板参数)
template <typename T, int C = 10>
class MyVector {
public:
// 定义迭代器类型
using iterator = T*;

// 构造函数
MyVector();

// 析构函数
~MyVector();

// 拷贝构造函数
MyVector(const MyVector& v);

// 赋值运算符
MyVector& operator=(const MyVector& v);

// 返回迭代器的起始位置
iterator begin();

// 返回迭代器的最后一个元素的下一个位置
iterator end();

// 往容器末尾添加元素
bool push_back(const T& t);

private:
T* m_data; // 底层数组
size_t m_size; // 元素个数
size_t m_capacity; // 最大容量
};

// 构造函数
template <typename T, int C>
MyVector<T, C>::MyVector() {
m_size = 0;
m_capacity = C > 0 ? C : 10;

// 申请内存空间
m_data = new T[m_capacity];
}

// 析构函数
template <typename T, int C>
MyVector<T, C>::~MyVector() {
if (m_data) {
delete[] m_data;
m_data = nullptr;
}
}

// 拷贝构造函数
template <typename T, int C>
MyVector<T, C>::MyVector(const MyVector<T, C>& v) {
m_size = v.m_size;
m_capacity = v.m_capacity;

// 深拷贝
m_data = new T[m_capacity];
for (size_t i = 0; i < m_size; ++i) {
m_data[i] = v.m_data[i];
}
}

// 赋值运算符
template <typename T, int C>
MyVector<T, C>& MyVector<T, C>::operator=(const MyVector<T, C>& v) {
// 防止自赋值
if (this == &v) {
return *this;
}

// 申请新的内存空间
T* new_data = new T[v.m_capacity];
for (size_t i = 0; i < v.m_size; ++i) {
new_data[i] = v.m_data[i];
}

// 释放旧的内存空间
delete[] m_data;

m_data = new_data;
m_size = v.m_size;
m_capacity = v.m_capacity;

return *this;
}

// 返回迭代器的起始位置
template <typename T, int C>
typename MyVector<T, C>::iterator MyVector<T, C>::begin() {
return m_data;
}

// 返回迭代器的最后一个元素的下一个位置
template <typename T, int C>
typename MyVector<T, C>::iterator MyVector<T, C>::end() {
return m_data + m_size;
}

// 往容器末尾添加元素
template <typename T, int C>
bool MyVector<T, C>::push_back(const T& t) {
if (m_size == m_capacity) {
return false;
}

m_data[m_size] = t;
++m_size;

return true;
}
  • main.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
#include <iostream>
#include "MyVector.hpp"

using namespace std;

template <int C>
void show(MyVector<int, C>& v) {
for (typename MyVector<int, C>::iterator it = v.begin(); it != v.end(); ++it) {
cout << *it << " ";
}
cout << endl;
}

int main() {
MyVector<int, 5> v1;
v1.push_back(1);
v1.push_back(3);
v1.push_back(5);
show(v1);

MyVector<int, 5> v2 = v1;
show(v2);

v2 = v1;
show(v2);

return 0;
}

程序运行输出的结果如下:

1
2
3
1 3 5 
1 3 5
1 3 5

模板进阶

typename 的使用场合

在 C++ 中,typename 常见的三种使用场合:

  • (1) 函数模板的定义:

    1
    2
    3
    4
    5
    // 这里可以将 typename 替换为 class
    template <typename T>
    T maxNum(T a, T b) {
    ......
    }
  • (2) 类模板的定义:

    1
    2
    3
    4
    5
    // 这里可以将 typename 替换为 class
    template <typename T>
    class MyVector {
    ......
    }
  • (3) 类外访问类模板的类型成员:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    template <typename T>
    class MyVector {
    public:
    using iterator = T*;

    iterator begin();

    ......
    }

    template <typename T>
    // 这里访问类模板的类型成员 iterator 时,必须在最前面加上 typename,且不能替换成 class,否则编译器会误将 iterator 解析成类的静态成员
    typename MyVector<T>::iterator MyVector<T>::begin() {
    ......
    }

函数指针的使用方式

函数指针作为其他函数的参数

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

using namespace std;

// 定义函数指针类型(第一种写法,C 语义风格)
typedef int (*FunType)(int, int);

// 定义函数指针类型(第二种写法,C++ 现代写法)
// using FunType = int(*)(int, int);

// 定义普通函数
int sum(int a, int b) {
return a + b;
}

// 函数指针作为其他函数的参数
int testFunc(int i, int j, FunType func) {
// 通过调用函数指针调用函数
return func(i, j);
}

int main() {
int result = testFunc(3, 5, sum);
cout << result << endl;
return 0;
}

程序运行输出的结果如下:

1
8

函数模板的趣味写法

在函数模板中,使用函数指针和函数对象

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

using namespace std;

// 定义函数指针类型(第一种写法,C 语义风格)
typedef int (*FunType)(int, int);

// 定义函数指针类型(第二种写法,C++ 现代写法)
// using FunType = int(*)(int, int);

// 定义函数模板(支持使用函数指针或函数对象作为其他函数的参数)
template <typename T, typename F>
int testFunc(const T &i, const T &j, F func) {
return func(i, j);
}

// 定义普通函数
int sum(int a, int b) {
return a + b;
}

// 定义函数对象(仿函数),重载了 operator(),可以像函数一样使用对象
class Test {
public:
// 构造函数
Test() {
cout << "Test()" << endl;
}

// 析构函数
~Test() {
cout << "~Test()" << endl;
}

// 拷贝构造函数
Test(const Test &t) {
cout << "Test(const Test&)" << endl;
}

// 重载圆括号操作符
int operator()(int i, int j) const {
return i + j;
}
};

void test01() {
cout << "------------ test01() ---------------" << endl;

// 函数指针作为其他函数的参数
int result = testFunc(1, 3, sum);
cout << result << endl;
}

void test02() {
cout << "------------ test02() ---------------" << endl;

// 定义函数对象(可调用对象)
Test obj;

// 函数对象作为其他函数的参数(会额外调用 Test 类的拷贝构造函数)
int result2 = testFunc(5, 6, obj);
cout << result2 << endl;
}

void test03() {
cout << "------------ test03() ---------------" << endl;

// 函数对象作为其他函数的参数(更高效的写法,直接使用临时对象,不会额外调用 Test 类的拷贝构造函数)
int result3 = testFunc(7, 8, Test());
cout << result3 << endl;
}

int main() {
test01();
test02();
test03();
return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
------------ test01() ---------------
4
------------ test02() ---------------
Test()
Test(const Test&)
~Test()
11
~Test()
------------ test03() ---------------
Test()
~Test()
15

函数指针与函数对象的简单介绍

  • 函数指针(Function Pointer)

    • 定义:指向函数地址的指针
      • C 语言的定义风格:typedef int (*FunType)(int, int);
      • 现代 C++ 的定义风格:using FunType = int(*)(int, int);
    • 本质:存储函数入口地址,可用于间接调用函数
    • 调用方式:通过指针调用,例如 fp(a, b)(*fp)(a, b)
    • 特点:不具备状态、语法较复杂、灵活性较低但开销小,常用于 C 风格接口或回调函数
  • 函数对象(Function Object / 仿函数)

    • 定义:重载了 operator() 的类或结构体对象
    • 本质:一个 “可调用的对象”(对象 + 函数行为)
    • 调用方式:通过对象名加 () 调用,例如 obj(a, b)
    • 特点:可以携带状态(成员变量)、支持内联优化、可用于模板参数(STL 中广泛使用,比如 std::lessstd::greater
  • 函数指针与函数对象主要区别

对比函数指针函数对象
是否能存状态
是否可内联优化一般不行
灵活性

默认模板参数的使用

函数模板使用默认模板参数

  • 模板参数设置默认类型(typename T = int
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;

// 定义函数模板(使用模板参数默认值)
template <typename T = int>
T add(T a, T b) {
return a + b;
}

int main() {
cout << add(1, 2) << endl; // 使用默认 T=int
cout << add<double>(1.5, 2.3); // 指定 T=double
}
  • 函数参数设置默认值(T b = 10
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;

template <typename T>
T add(T a, T b = 10) {
return a + b;
}

int main() {
cout << add(5, 3) << endl; // 8
cout << add(5) << endl; // 15(b 使用默认值 10)
}
  • 两者结合使用
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;

template <typename T = int>
T add(T a, T b = 10) {
return a + b;
}

int main() {
cout << add(5) << endl; // T=int, b=10 → 15
cout << add<double>(5.5) << endl; // T=double, b=10 → 15.5
}

类模板使用默认模板参数

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

using namespace std;

// 定义类模板(使用默认模板参数)
template <typename T, size_t size = 10>
class MyArray {
public:
// 重载 [] 操作运算符,返回非 const 对象
T& operator[](size_t index) {
if (index < 0 || index > size - 1) {
throw out_of_range("index out of range");
}
return array[index];
}

// 重载 [] 操作运算符,返回 const 对象
const T& operator[](size_t index) const {
if (index < 0 || index > size - 1) {
throw out_of_range("index out of range");
}
return array[index];
}

private:
T array[size];
};

int main() {
MyArray<int> array; // size = 10
array[0] = 10;
array[1] = 20;
cout << array[0] << ", " << array[1] << endl;


MyArray<int, 5> array2; // size = 5
array2[0] = 40;
array2[1] = 50;
cout << array2[0] << ", " << array2[1] << endl;

return 0;
}

程序运行输出的结果如下:

1
2
10, 20
40, 50

成员函数模板的使用

在 C++ 中,无论是普通类还是类模板,其成员函数(包括构造函数)都可以声明为函数模板(称为成员函数模板)。但由于虚函数(virtual)机制依赖于运行时多态,而函数模板是在编译期间实例化的,因此成员函数模板不能被声明为虚函数(virtual)。

特别注意

对于类模板,其成员函数(包括普通成员函数和成员函数模板)通常采用 "按需实例化"(Lazy Instantiation)机制。只有当程序中实际使用(例如发生函数调用)某个成员函数时,编译器才会对该函数进行实例化。如果某个成员函数从未被使用,则不会被实例化。

普通类的成员函数模板
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>

using namespace std;

// 普通类
class Test {
public:
// 成员函数模板
template <typename T>
T sum(T v1, T v2) {
return v1 + v2;
}
};

int main() {
Test obj;

// 函数模板支持自动推断模板参数类型
int result = obj.sum(3, 5);

cout << result << endl; // 输出 8

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

using namespace std;

// 类模板
template <typename A>
class Test {
public:
// 成员函数模板
template <typename B>
void func(A v1, B v2) {
cout << v1 << ", " << v2 << endl;
}
};

int main() {
// 类模板不支持自动推断模板参数类型
Test<int> obj;

// 函数模板支持自动推断模板参数类型
obj.func(3, 5.3);

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

using namespace std;
template <typename A>
class Test {
public:
// 成员函数模板
template <typename B>
void func(A v1, B v2);
};

template <typename A>
template <typename B>
void Test<A>::func(A v1, B v2) {
cout << v1 << ", " << v2 << endl;
}

int main() {
// 类模板不支持自动推断模板参数类型
Test<int> obj;

// 函数模板支持自动推断模板参数类型
obj.func(3, 5.3);

return 0;
}

模板显式实例化的使用

为了避免在多个 .cpp 文件中重复实例化同一个模板(包括类模板和函数模板),C++ 提供了 “显式实例化”(Explicit Instantiation)机制。通过在某一个 .cpp 源文件中进行显式实例化,可以将模板实例化集中到单一个翻译单元,从而避免重复生成代码并减少编译和链接开销。实际上,编译器和链接器通常会对重复的模板实例进行合并(如 COMDAT 机制),但显式实例化仍然可以用于控制模板实例化的位置,并在一定程度上减少编译时间和代码膨胀。

特别注意

  • 显式实例化(Explicit Instantiation)既适用于类模板,也适用于函数模板,其本质是为指定类型生成对应的模板实例代码。
  • 函数模板的显式实例化:其本质是在某个 .cpp 源文件中为指定类型(如 int)生成对应的函数实现,其他翻译单元通过 extern template 声明避免函数模板重复实例化。
  • 类模板的显式实例化:其本质是在某个 .cpp 源文件中为某个具体类型(如 MyClass<int>)生成完整的实例代码(包括类结构及其所有已定义的成员函数实现),其他翻译单元通过 extern template 声明避免类模板重复实例化。
类模板 + 显式实例化
  • MyClass.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef MYCLASS_H
#define MYCLASS_H

#include <iostream>

using namespace std;

// 类模板声明
template <typename T>
class MyClass {
public:
void print(T value);
};

// 声明:显式实例化,可以写在多个地方(告诉编译器,MyClass<int> 这个实例已经在别处生成,当前翻译单元不要再实例化)
extern template class MyClass<int>;

#endif
  • MyClass.cpp
1
2
3
4
5
6
7
8
9
10
#include "MyClass.h"

// 类模板声明定义
template <typename T>
void MyClass<T>::print(T value) {
cout << "value: " << value << endl;
}

// 定义:显式实例化,只写在一个地方(告诉编译器,在当前 .cpp 源文件中,为 MyClass<int> 这个具体类型生成完整的实例代码)
template class MyClass<int>;
  • main.cpp
1
2
3
4
5
6
7
#include "MyClass.h"

int main() {
MyClass<int> obj;
obj.print(42);
return 0;
}
函数模板 + 显式实例化
  • func.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef FUNC_H
#define FUNC_H

#include <iostream>

using namespace std;

// 函数模板声明
template <typename T>
void printValue(T value);

// 声明:显式实例化,可以写在多个地方(告诉编译器,printValue<int>(int) 这个实例已经在别处生成,当前翻译单元不要再实例化)
extern template void printValue<int>(int);

#endif
  • func.cpp
1
2
3
4
5
6
7
8
9
10
#include "func.h"

// 函数模板定义
template <typename T>
void printValue(T value) {
cout << "value: " << value << endl;
}

// 定义:显式实例化,只写在一个地方(告诉编译器,在当前 .cpp 源文件中,为 printValue<int>(int) 这个具体类型生成完整的实例代码)
template void printValue<int>(int);
  • main.cpp
1
2
3
4
5
6
#include "func.h"

int main() {
printValue(42); // 使用 printValue<int>
return 0;
}