C++ 模板初阶:泛型编程基础
前言
C++ 模板是实现泛型编程的核心工具,允许编写与类型无关的代码,从而提升复用性和灵活性。模板在编译时实例化,根据实际使用的类型生成具体代码,因此不会产生额外的运行时开销。
一、模板基础
1.1 为什么需要模板?
在编写函数或类时,如果希望它们能处理多种数据类型(如 int、double、string),传统方法是使用函数重载,但这会产生大量重复代码或丢失类型信息。
模板允许将类型作为参数,编译器根据调用时传入的具体类型生成对应的代码。
场景: 需要编写一个求两个数最大值的函数,支持 int、double 和 string(按字典序)。
① 传统方法:函数重载
#include <iostream>
#include <string>
using namespace std;
// 为 int 重载
int max(int a, int b) {
cout << "int version\n";
return a > b ? a : b;
}
// 为 double 重载
double max(double a, double b) {
cout << "double version\n";
return a > b ? a : b;
}
// 为 string 重载
string max(const string& a, const string& b) {
cout << "string version\n";
return a > b ? a : b;
}
int main() {
cout << max(3, 5) << "\n"; // int version
cout << max(3.14, 2.7) << "\n"; // double version
cout << max("hello", "world") << "\n"; // string version
return 0;
}
缺点:
- 代码重复:每个类型都要写一遍几乎相同的函数体。
- 扩展性差:若要支持新类型(如
char、float),必须添加新重载。- 维护困难:修改算法(如改为返回较小值)需要改动所有重载函数。
② 模板方法:函数模板
#include <iostream>
#include <string>
using namespace std;
template <typename T>
T max(T a, T b) {
cout << "template version for " << typeid(T).name() << "\n";
return a > b ? a : b; // 要求 T 支持 operator>
}
int main() {
cout << max(3, 5) << "\n"; // T = int
cout << max(3.14, 2.7) << "\n"; // T = double
cout << max("hello", "world") << "\n"; // T = string
// 甚至支持新类型,只要它实现了 operator>
cout << max('a', 'b') << "\n"; // T = char
return 0;
}
优点:
- 一份代码适用于所有类型,只要该类型支持
operator>。- 无需为每种类型单独编写,维护成本低。
- 编译器根据实际调用生成对应版本的函数,效率与手写重载相同。
1.2 泛型编程思想
模板是 C++ 支持泛型编程的基础,其核心思想是'参数化类型'——将类型也视为一种参数,让算法和数据结构独立于具体类,从而编写与类型无关的通用代码,使之能够进行代码复用。
1.3 模板的分类
一般而言,模板分为两类:函数模板和类模板。
注意: 一个模板只能服务于一个函数或者一个类。
二、函数模板
2.1 函数模板的概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2.2 函数模板的定义
函数模板定义以 template 关键字开头,后跟模板参数列表,然后是函数声明。
模板函数的语法:
- 关键字
template <class 模板名称>或template<typename 模板名称>- 例如:
template<class T>或template<typename T>- 备注:这里的模板名称为
T(可自定义为任意名称)- 提示:
typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)
通过函数模板定义函数:
template<class T>
void Swap(T& a, T& b) {
// 函数实现
}
这里的返回类型也可以是模板参数:
template<class T>
T max(T a, T b) {
// 函数实现
}
通过模板定义多个模板,实现接受不同类型的参数:
template<class T1, class T2>
// ...
提示: A. 当只有一个模板参数时,一般须传入相同类型的实参。 B. 当有两个模板参数时,可以传不同类型的实参。
代码示例 1:通过函数模板编写一个交换两个数的函数。
#include <iostream>
using namespace std;
template<class T>
void Swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
int main() {
int m = 10, n = 20;
double a = 1.2, b = 2.5;
Swap(m, n);
Swap(a, b);
return 0;
}
代码示例 2:通过函数模板编写一个求两个数最大值的函数。
#include <iostream>
using namespace std;
template <typename T>
T max(T a, T b) {
return a > b ? a : b;
}
int main() {
int i = max(3, 5); // T 被推导为 int
double d = max(3.14, 2.7); // T 被推导为 double
return 0;
}
代码示例 3:通过模板定义多个模板,以实现接受不同类型的参数。
#include <iostream>
using namespace std;
template<class T1, class T2>
void func2(const T1& x, const T2& y) {
cout << x << " " << y << endl;
}
int main() {
int a = 10;
double b = 3.14;
func2(a, b); // T1 被推导为 int 类型,T2 被推导为 double 类型
return 0;
}
2.3 函数模板的原理
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具,所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。
例如:当用 double 类型使用函数模板时,编译器通过对实参类型的推演,将 T 确定为 double 类型,然后产生一份专门处理 double 类型的代码,对于 char 类型也是如此。
2.4 函数模板的实例化
不同类型的参数使用函数模板时,称为函数模板的实例化,模板参数实例化分为:隐式实例化和显式实例化。
1. 隐式实例化
让编译器根据实参推演模板参数的实际类型。
代码示例:通过函数模板实现两数相加。
#include <iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right) {
return left + right;
}
int main() {
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, a2);
Add(d1, d2);
return 0;
}
注意: 如果只有一个模板参数时,传入两个不同类型,该语句不能通过编译。
#include <iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right) {
return left + right;
}
int main() {
int a1 = 10;
double d1 = 3.14;
Add(a1, d1); // 编译报错
return 0;
}
报错原因: 因为在编译期间,当编译器看到该实例化时,需要推演其实参类型: 通过实参
a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,编译器无法确定此处到底该将T确定为int或者double类型而报错。注意: 在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅。
解决办法一:用户自己来强制转化
#include <iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right) {
return left + right;
}
int main() {
int a1 = 10;
double d1 = 3.14;
Add(a1, (int)d1); // 成功,显式转换
return 0;
}
解决办法二:显式实例化
#include <iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right) {
return left + right;
}
int main() {
int a1 = 10;
double d1 = 3.14;
Add<int>(a1, d1); // 显式指定 T 为 int
return 0;
}
详情解释:显式指定模板参数 例如:
Add<int>(a1, d1)或Add<double>(a1, d1)。 这意味着你在调用时明确告诉编译器:'模板参数T就是int(或double),不要再推导了',此时编译器会跳过类型推导过程,直接使用你指定的类型来实例化函数模板。编译器会检查这个调用是否合法:
- 对于
Add<int>(a1, d1),生成的函数签名是int Add(const int&, const int&)。- 实参
a1是int,可以直接绑定到const int&。- 实参
d1是double,但double可以隐式转换为int,因此编译器会生成一个临时int对象(值为d1截断后的整数)传递给函数。 这种隐式转换是允许的,因此调用成功。同理,
Add<double>(a1, d1)会将a1从int隐式转换为double,也可以正常调用。
2. 显式实例化
在函数名后的 <> 中指定模板参数的实际类型。
代码示例:显示实例化 Add 函数的模板参数
#include <iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right) {
return left + right;
}
int main() {
int a = 10;
double b = 20.0;
// 显式实例化
Add<int>(a, b);
return 0;
}
2.5 模板参数的匹配原则
匹配原则一: 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
代码示例:验证匹配原则一
// 专门处理 int 的加法函数
int Add(int left, int right) {
return left + right;
}
// 通用加法函数
template<class T>
T Add(T left, T right) {
return left + right;
}
void Test() {
Add(1, 2); // 与非模板函数匹配,编译器不需要特化
Add<int>(1, 2); // 调用编译器特化的 Add 版本
}
匹配原则二:
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调用时会优先调用非模板函数而不会从该模板产生出一个实例。
- 如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
代码示例:验证匹配原则二
// 专门处理 int 的加法函数
int Add(int left, int right) {
return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right) {
return left + right;
}
void Test() {
Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的 Add 函数
}
三、类模板
3.1 类模板的概念
类模板不是具体的类,而是一个'类的蓝图'。它使用模板参数(通常是类型参数)来描述类中成员的类型,这些成员包括数据成员、成员函数的参数和返回值等。
3.2 类模板的定义
类模板的定义必须以 template 关键字开头,后面紧跟模板参数列表(用尖括号 <> 包围)。这是类模板语法的核心标志,告诉编译器接下来的类是一个模板,可以参数化类型或值。
基本语法定义如下所示:
template<class T1, class T2, ..., class Tn> 或 template<typename T1, typename T2, ..., typename Tn> class 类模板名 { // 类内成员定义 };提示:
template <typename T>和template <class T>中的T是一个占位符类型名,在使用时会被实际类型替换。- 类定义内可以使用
T声明成员变量、成员函数参数和返回类型。- 成员函数如果在类外定义,必须再次声明模板参数,并使用完整的类名
<T>限定。
代码示例:演示语法
template <typename T> // T 是类型参数,也可用 class 代替 typename
class Stack {
public:
void push(const T& x);
T pop();
private:
T data[100];
int top;
};
代码示例:成员函数的外部定义
成员函数如果在类外定义,必须再次声明模板参数,并使用完整的类名 <T> 限定:
template <typename T>
class Stack {
public:
void push(const T& x);
T pop();
private:
T data[100];
int top;
};
template <typename T>
void Stack<T>::push(const T& x) {
data[++top] = x;
}
template <typename T>
T Stack<T>::pop() {
return data[top--];
}
3.3 类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟 <>,然后将实例化的类型放在 <> 中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
简单来说:对于模板类而言,类名不能代表类型,类名需要绑定模板参数才能成为类型。
代码示例:演示 stack 类代码实例化
#include <iostream>
#include <cstring>
using namespace std;
template<class T>
class Stack {
public:
Stack(int n = 4) : _array(new T[n]), _capacity(n), _size(0) {}
// 类内声明 类外定义的写法
void Push(const T& x);
~Stack() { delete[] _array; }
private:
T* _array;
size_t _capacity;
size_t _size;
};
template<class T>
void Stack<T>::Push(const T& x) {
if (_size == _capacity) {
// 扩容处理:开新空间 -> 拷贝内容 -> 释放旧空间 -> 指向新空间 -> 更新容量
int newcapacity = _capacity * 2;
T* tmp = new T[newcapacity];
memcpy(tmp, _array, _capacity * sizeof(T)); // 修复:需乘以 sizeof(T)
delete[] _array;
_array = tmp;
_capacity *= 2;
}
_array[_size++] = x;
}
int main() {
// 类模板都需要显示实例化,不能做到隐式实例化
Stack<int> st1; // 整形 int 类
st1.Push(1);
st1.Push(2);
st1.Push(3);
Stack<double> st2; // 浮点数 double 类
st2.Push(1.1);
st2.Push(1.2);
st2.Push(1.3);
Stack<double>* pst = new Stack<double>();
return 0;
}
本文涵盖了 C++ 模板的基础概念、函数模板与类模板的语法及实例化机制,以及相关的匹配原则。理解这些内容是掌握 C++ 泛型编程的关键。


