C++从入门到实战(十三)C++函数模板与类模板初阶讲解
- 前言
- 一、为什么需要模板
-
- [1. 函数重载的问题](#1. 函数重载的问题)
- [2. 泛型编程和模板的作用](#2. 泛型编程和模板的作用)
- 二、函数模板
- 三、类模板
-
- [3.1 类模板是什么?](#3.1 类模板是什么?)
- [3.2 类模板的定义格式](#3.2 类模板的定义格式)
- [3.3 类模板的实例化:从 "模具" 生成具体类](#3.3 类模板的实例化:从 “模具” 生成具体类)
- [3.4 类模板的注意事项](#3.4 类模板的注意事项)
-
- 1.模板参数声明不能省略
- [2. 声明和定义不能分离到.h 和.cpp](#2. 声明和定义不能分离到.h 和.cpp)
- [3. 支持多个模板参数](#3. 支持多个模板参数)
前言
- 在上一篇博客中,我们围绕 C/C++ 内存管理展开讨论,深入解析了内存分布模型、C 与 C++ 内存管理的核心差异,并初步认识了 C++ 中new与delete的基本用法,为理解 C++ 内存管理体系打下基础。
- 从本文开始,我们将暂别内存管理主题,转而聚焦 C++ 模板编程的核心模块 ------函数模板初阶
我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343我的C++知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12880513.html?spm=1001.2014.3001.5482
一、为什么需要模板
1. 函数重载的问题
在C++里,要是你想实现交换两个变量值的功能,对不同类型的变量(像int
、double
、char
),就得写不同的Swap
函数。
- 就像下面这样:
cpp
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
不过,这种函数重载的做法存在一些弊端:
- 代码复用率低 :这些重载函数只是处理的变量类型不同,代码的逻辑是一样的。要是以后有新的类型,就得手动再写对应的
Swap
函数。 - 可维护性差 :如果其中一个
Swap
函数出了问题,可能所有重载函数都得检查一遍,因为它们的代码逻辑本质相同,一处出错可能其他地方也有类似问题。
2. 泛型编程和模板的作用
为了解决上面的问题,C++引入了泛型编程的概念。
- 泛型编程的目标是编写和具体类型无关的通用代码,以此实现代码复用。而模板就是泛型编程的基础。
可以把模板想象成一个模具。当你需要不同类型的具体代码时,就往这个模具里填入不同的"材料"(也就是类型),这样就能得到不同"材料"的"铸件"(也就是具体类型的代码)。

比如,使用模板来实现Swap
函数:
现在看一下,后面我们会详细讲解
cpp
template <typename T>
void Swap(T& left, T& right) {
T temp = left;
left = right;
right = temp;
}
这里的template <typename T>
就声明了一个模板,T
是一个类型参数,它代表任意类型。在使用这个Swap
函数时,编译器会根据传入的变量类型,自动用具体类型替换T
,生成对应的代码。这样就不用为每种类型都写一个单独的函数,既提高了代码复用率,也让代码更易于维护。

二、函数模板
- 函数模板就像是一个函数家族,它和具体的类型没关系。在使用的时候,会根据传入的实参类型,生成特定类型的函数版本。
2.1 函数模板格式
函数模板的定义格式是:template<typename T1, typename T2,......,typename Tn>
,后面接着返回值类型、函数名和参数列表。
- 比如Swap函数模板:
cpp
template<typename T>
void Swap( T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
这里的typename是用来定义模板参数的关键字,也能用class,但不能用struct代替class
cpp
#include <iostream>
using namespace std;
template<typename T>
void Swap( T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
int main() {
int a = 5, b = 10;
cout << "Before swap: a = " << a << ", b = " << b << endl;
Swap(a, b); // 调用模板函数,传入int类型的参数
cout << "After swap: a = " << a << ", b = " << b << endl;
return 0;
}

2.2 函数模板的原理
-
函数模板就像一个蓝图,它本身不是真正的函数,而是让编译器根据使用方式生成特定类型函数的模具。
-
这样就把原本我们要做的重复工作交给了编译器。
-
在编译阶段,编译器会根据传入的实参类型,推演出对应的类型,然后生成专门处理该类型的代码。
-
例如用double类型使用模板时,编译器会把T确定为double类型,生成处理double类型的代码
2.3 函数模板的实例化
- 用不同类型的参数使用函数模板,就叫做函数模板的实例化。
- 分为隐式实例化和显式实例化。
(1)隐式实例化:
让编译器根据实参来推导出模板参数的实际类型。比如Add函数模板:
cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
cpp
#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;
cout << Add(a1, a2) << endl;
cout << Add(d1, d2) << endl;
}

- 当我们写成这样时
cpp
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
Add(a1, d1);
- 编译器会报错,因为编译器无法确定T是int还是double
- 处理方式:需要自己强制转换
- 在模板里,编译器一般不会进行类型转换,怕出问题。
(2)显式实例化:
在函数名后的<>里指定模板参数的实际类型。
- 比如:
cpp
int main(void)
{
int a = 10;
double b = 20.0;
// 显式实例化
Add<int>(a, b);
return 0;
}
如果类型不匹配,编译器会尝试隐式类型转换,转换不成功就会报错
cpp
#include <iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main(void)
{
int a = 10;
double b = 20.0;
// 显式实例化
cout << Add<int>(a, b) << endl;
return 0;
}

2.4 模板参数的匹配原则
1. 非模板函数与同名函数模板共存
- 在 C++ 里,一个非模板函数和同名的函数模板能够同时存在。
- 而且函数模板可以实例化为与非模板函数相同功能的函数。
cpp
#include <iostream>
// 专门处理 int 类型的加法非模板函数
int Add(int left, int right) {
return left + right;
}
// 通用加法函数模板
template<class T>
T Add(T left, T right) {
return left + right;
}
int main() {
// 调用 Add(1, 2),会和非模板函数匹配
std::cout << "调用 Add(1, 2) 的结果: " << Add(1, 2) << std::endl;
// 调用 Add<int>(1, 2),会调用编译器特化的 Add 版本
std::cout << "调用 Add<int>(1, 2) 的结果: " << Add<int>(1, 2) << std::endl;
return 0;
}

在上述代码中,定义了一个专门处理 int
类型的非模板 Add
函数,以及一个通用的 Add
函数模板。
- 当调用
Add(1, 2)
时,由于实参类型是int
,编译器会优先选择非模板函数。而调用Add<int>(1, 2)
时,明确指定了使用函数模板 ,编译器会对模板进行实例化,生成处理int
类型的函数。
2. 调用时的优先选择规则
当非模板函数和同名函数模板其他条件相同时,调用时会优先选择非模板函数。
- 不过,要是模板能产生更匹配的函数,就会选择模板。下面通过代码来进一步说明:
cpp
#include <iostream>
// 专门处理 int 类型的加法非模板函数
int Add(int left, int right) {
return left + right;
}
// 通用加法函数模板,可处理不同类型的参数
template<class T1, class T2>
auto Add(T1 left, T2 right) {
return left + right;
}
int main() {
// 调用 Add(1, 2),和非模板函数完全匹配
std::cout << "调用 Add(1, 2) 的结果: " << Add(1, 2) << std::endl;
// 调用 Add(1, 2.0),模板函数能生成更匹配的版本
std::cout << "调用 Add(1, 2.0) 的结果: " << Add(1, 2.0) << std::endl;
return 0;
}
在这段代码中,调用 Add(1, 2)
时,实参类型都是 int
,与非模板函数完全匹配,所以会调用非模板函数。
- 而调用
Add(1, 2.0)
时,实参类型分别是int
和double
,非模板函数无法直接匹配,此时模板函数能生成更匹配的版本,编译器就会选择模板函数进行实例化并调用。

3. 类型转换规则差异
模板函数不允许自动类型转换,而普通函数可以进行自动类型转换。
cpp
#include <iostream>
// 普通加法函数
int Add(int left, int right) {
return left + right;
}
// 函数模板
template<class T>
T Add(T left, T right) {
return left + right;
}
int main() {
int a = 1;
double b = 2.0;
// 普通函数可以进行自动类型转换
std::cout << "普通函数调用 Add(a, b) 的结果: " << Add(a, b) << std::endl;
// 模板函数不允许自动类型转换,下面这行代码会报错
// std::cout << "模板函数调用 Add(a, b) 的结果: " << Add(a, b) << std::endl;
// 若要使用模板函数,需手动进行类型转换
std::cout << "模板函数调用 Add(a, static_cast<int>(b)) 的结果: " << Add(a, static_cast<int>(b)) << std::endl;
return 0;
}
在上述代码中,调用普通函数 Add(a, b)
时,编译器会自动将 double
类型的 b
转换为 int
类型。
- 而调用模板函数
Add(a, b)
时,由于模板函数不允许自动类型转换,会导致编译错误。若要使用模板函数,需要手动进行类型转换,如Add(a, static_cast<int>(b))
。
三、类模板
3.1 类模板是什么?
-
想象你要做一个 "栈"(Stack)类,用来存储数据。
-
如果数据可能是整数、小数、字符等不同类型,难道要为每种类型写一个独立的Stack类吗?
-
类模板就是解决这个问题的 "通用模具":它定义一个与类型无关的类,通过传入具体类型,生成针对该类型的专属类。
3.2 类模板的定义格式
cpp
template <class T1, class T2, ..., class Tn> // 模板参数列表,T是类型占位符
class 类模板名 {
// 类的成员(变量/函数)可以使用T1、T2等模板参数
// 例如:成员变量类型是T,成员函数参数类型是T&
};
cpp
#include <iostream>
using namespace std;
template <typename T> // 等价于template <class T>,T代表任意类型
class Stack {
public:
// 构造函数:初始化数组和容量
Stack(size_t capacity = 4) {
_array = new T[capacity]; // 数组元素类型是T
_capacity = capacity;
_size = 0;
}
// 声明成员函数(参数类型是T&)
void Push(const T& data); // 向栈中添加数据
private:
T* _array; // 存储数据的数组,类型是T*
size_t _capacity; // 容量
size_t _size; // 已存储元素数量
};
// 类外定义成员函数时,需要再次指定模板参数
template <class T> // 必须重复模板参数声明
void Stack<T>::Push(const T& data) { // 用Stack<T>表明这是T类型的成员函数
if (_size < _capacity) {
_array[_size] = data; // 存入数据,类型是T
_size++;
}
}
3.3 类模板的实例化:从 "模具" 生成具体类
类模板本身不是真正的类,必须通过实例化指定具体类型,才能生成可用的类。
- 语法:
类模板名<具体类型> 对象名;
- 显式指定类型:必须在类名后用<>明确写出类型(不能像函数模板那样隐式推导)。
- 生成专属类:每个不同的类型实例化,都会生成一个独立的类。
例如上面代码中的
cpp
int main() {
Stack<int> st1; // 实例化出存储int的栈类,生成Stack<int>类
Stack<double> st2; // 实例化出存储double的栈类,生成Stack<double>类
st1.Push(10); // 向int栈中添加数据
st2.Push(3.14); // 向double栈中添加数据
return 0;
}
- Stack是模板名,不是类型;Stack才是具体的类型(就像 "模具压出的产品")。
- 每个实例化的类(如
Stack<int>、Stack<double>
)都是独立的,互不干扰。
3.4 类模板的注意事项
1.模板参数声明不能省略
- 在类外定义成员函数时,必须像
template <class T>
一样重复声明模板参数,否则编译器不知道T是什么。
2. 声明和定义不能分离到.h 和.cpp
- 模板需要在编译阶段根据实例化的类型生成代码,如果声明在.h、定义在.cpp,编译器可能找不到定义,导致链接错误。
3. 支持多个模板参数
- 可以定义多个类型参数,例如:
cpp
template <class T, class U> // 两个类型参数T和U
class Pair {
T first;
U second;
};
Pair<int, double> p(10, 3.14); // 实例化时传入两个类型
以上就是这篇博客的全部内容,下一篇我们将继续探索C++中STL更多精彩内容。
我的个人主页,欢迎来阅读我的其他文章
https://blog.csdn.net/2402_83322742?spm=1011.2415.3001.5343我的C++知识文章专栏
欢迎来阅读指出不足
https://blog.csdn.net/2402_83322742/category_12880513.html?spm=1001.2014.3001.5482
|--------------------|
| 非常感谢您的阅读,喜欢的话记得三连哦 |
