
📚 系列文章导航
【从零开始学 C++】专题正在持续更新中🔥
第一篇:C++ 入门 ------ 从 C 到 C++ 的跨越
第二篇:类和对象 (上)------ 面向对象的开端
第三篇:类和对象 (中)------ 构造析构与运算符重载
第四篇:类和对象 (下)------ 静态成员与友元
第五篇:C++ 内存管理 ------new 与 delete 的奥秘
第六篇:模板初阶 ------ 让你的代码 "万能" 起来 👈 你在这里
第七篇:STL 简介和入门(敬请期待)
【从零开始学 C++】专题
目录
[📚 系列文章导航](#📚 系列文章导航)
[1. 基础概念解释](#1. 基础概念解释)
[2. 代码示例](#2. 代码示例)
[1. 函数模板的概念](#1. 函数模板的概念)
[2. 函数模板的格式](#2. 函数模板的格式)
[3. 函数模板的原理](#3. 函数模板的原理)
[4. 函数模板的实例化](#4. 函数模板的实例化)
[5. 模板参数的匹配原则](#5. 模板参数的匹配原则)
[1. 类模板的定义格式](#1. 类模板的定义格式)
[2. 类模板的实例化](#2. 类模板的实例化)
[🎯 本篇总结](#🎯 本篇总结)
[📢 下一篇预告](#📢 下一篇预告)
哈喽各位小伙伴们大家好!我是你们的小小风吖,今天我们来学习 C++ 中又一个超级重要的概念 ------模板!
不知道大家有没有过这种经历:写了一个交换函数,只能交换 int 类型;后来要交换 double,又得重写一遍;再后来要交换 char,又得再写一遍... 是不是觉得特别麻烦?
cpp
void Swap(int& a, int& b) { int t = a; a = b; b = t; }
void Swap(double& a, double& b) { double t = a; a = b; b = t; }
void Swap(char& a, char& b) { char t = a; a = b; b = t; }
// ... 还有完没完啊!
今天学完模板,你就会发现:原来这些重复的代码,只需要写一遍就够了! 让我们一起进入泛型编程的世界吧~
(一)泛型编程
1. 基础概念解释
大白话时间:
想象一下,你是一个做月饼的师傅。如果做豆沙月饼要一套模具,做五仁月饼又要一套模具,做蛋黄莲蓉还要一套模具... 那你家里得堆多少模具啊?
但聪明的师傅会怎么做呢?------做一个通用的月饼模具,往里面填什么馅料,就出来什么口味的月饼! 这就是 "泛型" 的思想。
泛型编程 就是:编写与类型无关的通用代码。就像那个通用的月饼模具,我们写一个通用的 "代码模具",编译器会根据我们传入的 "材料"(数据类型),自动生成对应类型的代码。
而模板就是 C++ 中实现泛型编程的基础工具。模板分为两大类:
-
函数模板:针对函数的通用模具
-
类模板:针对类的通用模具
💡 实用小贴士:模板是 STL(标准模板库)的基础,我们后面要学的 vector、list、map 这些容器,全都是用模板实现的!学好模板,STL 就成功了一半!
2. 代码示例
【示例 1:没有模板时的痛苦 ------ 函数重载】
这个例子展示:没有模板的情况下,要实现不同类型的交换,需要写 N 多重复代码。
cpp
#include <iostream>
using namespace std;
// 交换int类型
void Swap(int& left, int& right) {
int temp = left;
left = right;
right = temp;
cout << "调用了int版本的Swap" << endl;
}
// 交换double类型
void Swap(double& left, double& right) {
double temp = left;
left = right;
right = temp;
cout << "调用了double版本的Swap" << endl;
}
// 交换char类型
void Swap(char& left, char& right) {
char temp = left;
left = right;
right = temp;
cout << "调用了char版本的Swap" << endl;
}
int main() {
int a = 10, b = 20;
Swap(a, b);
cout << "a = " << a << ", b = " << b << endl << endl;
double c = 1.1, d = 2.2;
Swap(c, d);
cout << "c = " << c << ", d = " << d << endl << endl;
char e = 'x', f = 'y';
Swap(e, f);
cout << "e = " << e << ", f = " << f << endl;
return 0;
}
运行结果:
cpp
调用了int版本的Swap
a = 20, b = 10
调用了double版本的Swap
c = 2.2, d = 1.1
调用了char版本的Swap
e = y, f = x
⚠️ 问题来了:如果我明天要交换 float 类型,后天要交换 long 类型,大后天还要交换自定义类型... 难道我要一直写下去吗?这就是模板要解决的问题!
(二)函数模板
1. 函数模板的概念
大白话时间:
函数模板就是一个 "函数模具"。你告诉编译器:"我这里有一个模具,你帮我按照这个模具,根据我给的类型生成具体的函数。"
就像活字印刷术一样:以前雕版印刷,每一页要刻一个版;活字印刷就厉害了,你要什么字,就把对应的活字拿出来组合。函数模板就是那个 "活字" 的思想。
官方一点的定义:函数模板代表了一个函数家族,它和类型无关,在使用时会根据你传入的实参类型,自动产生对应类型的函数版本。
代码示例
【示例 2:函数模板初体验 ------ 万能 Swap】
这个例子展示:用函数模板,只写一遍代码,就能支持所有类型的交换!
cpp
#include <iostream>
using namespace std;
// 这就是函数模板!T是一个"虚拟类型"
template<typename T> //这里也可以用<class T> 用class和typename都可以
void Swap(T& left, T& right) {
T temp = left;
left = right;
right = temp;
cout << "调用了模板生成的Swap" << endl;
}
int main() {
// 交换int类型
int a = 10, b = 20;
Swap(a, b); // 编译器自动生成int版本的Swap
cout << "int交换: a = " << a << ", b = " << b << endl << endl;
// 交换double类型
double c = 1.1, d = 2.2;
Swap(c, d); // 编译器自动生成double版本的Swap
cout << "double交换: c = " << c << ", d = " << d << endl << endl;
// 交换char类型
char e = 'x', f = 'y';
Swap(e, f); // 编译器自动生成char版本的Swap
cout << "char交换: e = " << e << ", f = " << f << endl;
return 0;
}
运行结果:
cpp
调用了模板生成的Swap
int交换: a = 20, b = 10
调用了模板生成的Swap
double交换: c = 2.2, d = 1.1
调用了模板生成的Swap
char交换: e = y, f = x
🎉 太神奇了! 我们只写了一遍代码,居然支持了三种类型!而且你还可以用它交换 float、long、甚至你自己定义的类型!这就是模板的威力!
2. 函数模板的格式
大白话时间:
函数模板的格式很简单,记住这个公式就行:
cpp
template<typename T1, typename T2, ...>
返回值类型 函数名(参数列表) {
// 函数体,里面可以用T1、T2这些虚拟类型
}
关键点说明:
-
template:告诉编译器 "我要定义模板了" -
<typename T>:尖括号里是模板参数列表,typename是关键字,T是我们给虚拟类型起的名字(可以随便起,叫 T、K、V 都行,习惯上用大写字母) -
注意 :
typename也可以写成class,效果完全一样!但是不能写成 struct哦!
💡 新手避坑 :模板参数列表里的每个类型前面,都要写 typename!比如template<typename T1, T2>是错的,应该是template<typename T1, typename T2>
代码示例
【示例 3:多模板参数的函数模板】
这个例子展示:模板可以有多个类型参数,实现更灵活的通用函数。
cpp
#include <iostream>
#include <string>
using namespace std;
// 两个模板参数:T1和T2可以是不同的类型
template<typename T1, typename T2>
void Print(const T1& t1, const T2& t2) {
cout << "第一个参数:" << t1 << ",类型是:" << typeid(T1).name() << endl;
cout << "第二个参数:" << t2 << ",类型是:" << typeid(T2).name() << endl;
cout << "------------------------" << endl;
}
int main() {
// int和double组合
Print(10, 3.14);
// char和string组合
Print('A', string("Hello C++"));
// string和bool组合
Print(string("模板真好玩"), true);
return 0;
}
运行结果:
cpp
第一个参数:10,类型是:i
第二个参数:3.14,类型是:d
------------------------
第一个参数:A,类型是:c
第二个参数:Hello C++,类型是:NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
------------------------
第一个参数:模板真好玩,类型是:NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
第二个参数:1,类型是:b
------------------------
💡 小知识 :typeid(xxx).name()可以查看类型的名字,虽然输出的名字有点奇怪(比如 i 就是 int,d 就是 double),但调试的时候很好用!
3. 函数模板的原理
大白话时间:
很多新手会问:"模板函数是怎么做到支持所有类型的?难道它真的是一个万能函数?"
答案是:NO! 模板本身不是 一个真正的函数!它只是一个模具!
真正的过程是这样的:
-
编译阶段 :编译器看到你调用
Swap(a, b),发现 a 和 b 是 int 类型 -
编译器就拿着你的模板模具,把 T 全部替换成 int,生成一个真正的 int 版本的 Swap 函数
-
然后你调用的其实是这个生成出来的 int 版本函数
-
同理,调用
Swap(c, d)时,c 和 d 是 double,编译器又生成一个 double 版本的 Swap 函数
所以说:模板是把本来应该我们做的重复劳动,交给编译器去做了! 我们写一遍,编译器帮我们生成 N 遍。
💡 重要理解 :模板是在编译期处理的,不是运行期!生成的代码里,int 版本和 double 版本是两个完全不同的函数,就像你自己写的重载函数一样。
代码示例
【示例 4:验证模板原理 ------ 查看地址】
这个例子证明:不同类型的模板函数,地址是不一样的,说明它们是不同的函数!
cpp
#include <iostream>
using namespace std;
template<typename T>
T Add(const T& a, const T& b) {
return a + b;
}
int main() {
int a = 10, b = 20;
double c = 1.1, d = 2.2;
// 调用Add<int>
cout << "Add(a, b) = " << Add(a, b) << endl;
// 调用Add<double>
cout << "Add(c, d) = " << Add(c, d) << endl;
// 打印函数地址,看看是不是同一个函数
cout << endl;
cout << "Add<int>的函数地址:" << (void*)Add<int> << endl;
cout << "Add<double>的函数地址:" << (void*)Add<double> << endl;
if ((void*)Add<int> == (void*)Add<double>) {
cout << "两个函数地址相同,是同一个函数" << endl;
} else {
cout << "两个函数地址不同,是不同的函数!" << endl;
cout << "这说明编译器根据模板生成了两份不同的代码!" << endl;
}
return 0;
}
运行结果:
cpp
Add(a, b) = 30
Add(c, d) = 3.3
Add<int>的函数地址:0x401550
Add<double>的函数地址:0x401570
两个函数地址不同,是不同的函数!
这说明编译器根据模板生成了两份不同的代码!
🎉 完美验证! 两个函数地址不一样,说明它们确实是两个独立的函数!这就是模板的工作原理。
4. 函数模板的实例化
大白话时间:
"实例化" 这个词听起来很高大上,其实就是:用模板模具生成真正函数的过程。
就像你用月饼模具做月饼的过程,模具本身不是月饼,把模具压下去,出来的那个东西才是真正的月饼 ------ 这个 "压下去做出月饼" 的过程,就是实例化。
模板实例化分两种:
-
隐式实例化:编译器自动帮你推导出类型是什么,你什么都不用管
-
显式实例化:你手动告诉编译器,我就要用这个类型!
💡 新手避坑:隐式实例化时,编译器不会做类型转换!比如你传一个 int 和一个 double 给同一个 T,编译器会报错说 "类型矛盾",而不是自动把 int 转成 double!这时候就要用显式实例化。
代码示例
【示例 5:隐式实例化 vs 显式实例化】
这个例子展示两种实例化方式,以及什么时候必须用显式实例化。
cpp
#include <iostream>
using namespace std;
template<class T> // 这里用class代替typename,效果一样
T Add(const T& left, const T& right) {
return left + right;
}
int main() {
int a = 10;
double b = 20.5;
// ========== 1. 隐式实例化:编译器自动推演类型 ==========
cout << "Add(a, 20) = " << Add(a, 20) << endl; // T推演成int
cout << "Add(1.5, b) = " << Add(1.5, b) << endl; // T推演成double
// ========== 2. 下面这行会报错! ==========
// Add(a, b);
// 错误原因:a是int,b是double,编译器不知道T该是什么
// 编译器不会做类型转换!
// ========== 3. 解决方法1:强制类型转换 ==========
cout << "Add(a, (int)b) = " << Add(a, (int)b) << endl;
// ========== 4. 解决方法2:显式实例化!手动指定类型 ==========
cout << "Add<int>(a, b) = " << Add<int>(a, b) << endl; // 都转成int
cout << "Add<double>(a, b) = " << Add<double>(a, b) << endl; // 都转成double
cout << endl << "显式实例化成功!手动指定T的类型就是这么简单!" << endl;
return 0;
}
运行结果:
cpp
Add(a, 20) = 30
Add(1.5, b) = 22
Add(a, (int)b) = 30
Add<int>(a, b) = 30
Add<double>(a, b) = 30.5
显式实例化成功!手动指定T的类型就是这么简单!
💡 总结 :当编译器无法自动推演类型,或者推演的结果不符合你的预期时,就用<类型>手动指定!但是就结果而言,我们强行将double类型转化为int类型时,会导致结果出现偏差,所以我们在使用显示类型转化时,要就具体情况来转换
5. 模板参数的匹配原则
大白话时间:
现在问题来了:如果我既有一个普通的 Add 函数(专门处理 int),又有一个 Add 模板,那我调用 Add (1, 2) 时,编译器会调用哪一个?
这就涉及到模板的匹配原则了,记住三条铁律:
-
普通函数优先:如果有现成的普通函数刚好匹配,就用普通函数,不用模板生成
-
更好匹配优先:如果模板能生成比普通函数更匹配的版本,那就用模板
-
模板不做自动类型转换:普通函数可以做隐式类型转换,但模板函数不会
💡 实用技巧 :如果你就是想调用模板版本,哪怕有普通函数,那就用显式实例化:Add<int>(1, 2),这样一定会调用模板!
代码示例
【示例 6:模板与普通函数的匹配规则】
这个例子完整演示三条匹配原则,超级重要!
cpp
#include <iostream>
using namespace std;
// 普通函数:专门处理int的加法
int Add(int left, int right) {
cout << "【调用了普通函数Add(int, int)】" << endl;
return left + right;
}
// 函数模板:通用加法
template<class T>
T Add(T left, T right) {
cout << "【调用了模板Add<T>】,T的类型是:" << typeid(T).name() << endl;
return left + right;
}
// 两个参数可以是不同类型的模板
template<class T1, class T2>
T1 Add(T1 left, T2 right) {
cout << "【调用了模板Add<T1, T2>】" << endl;
return left + right;
}
int main() {
cout << "===== 测试1:两个int参数 =====" << endl;
Add(1, 2); // 刚好匹配普通函数,优先调用普通函数
cout << endl << "===== 测试2:显式指定用模板 =====" << endl;
Add<int>(1, 2); // 强制调用模板版本
cout << endl << "===== 测试3:两个double参数 =====" << endl;
Add(1.1, 2.2); // 普通函数不匹配,用模板生成double版本
cout << endl << "===== 测试4:一个int一个double =====" << endl;
Add(1, 2.2); // 普通函数需要把2.2转成int,而模板可以生成更匹配的版本
// 所以这里会调用模板!
return 0;
}
运行结果:
cpp
===== 测试1:两个int参数 =====
【调用了普通函数Add(int, int)】
===== 测试2:显式指定用模板 =====
【调用了模板Add<T>】,T的类型是:i
===== 测试3:两个double参数 =====
【调用了模板Add<T>】,T的类型是:d
===== 测试4:一个int一个double =====
【调用了模板Add<T1, T2>】
**===== 总结匹配原则 =====
- 有完全匹配的普通函数,优先用普通函数
- 模板能生成更好的匹配,就用模板
- 显式实例化一定调用模板**
🎉 **完美!**这三条原则记住了,模板匹配的问题就难不倒你了!
(三)类模板
1. 类模板的定义格式
基础概念解释
大白话时间:
函数模板是针对函数的模具,那类模板就是针对类的模具啦!
想想我们之前学的栈:如果我要一个存 int 的栈,要写一个 Stack 类;要一个存 double 的栈,又要写一个;要一个存 string 的栈,还要写一个... 是不是又回到了之前的问题?
类模板就是解决这个的:写一个通用的 Stack 类模板,要存什么类型,就把类型告诉编译器,编译器帮你生成对应类型的栈类。
类模板的格式:
cpp
template<class T1, class T2, ...>
class 类名 {
// 类里面可以用T1、T2这些类型
};
⚠️ 超级重要的坑 :类模板的成员函数如果要在类外面定义,必须加上模板参数!而且类模板不建议声明和定义分离到.h 和.cpp 两个文件,会出现链接错误!这个坑我们后面进阶篇会详细讲。
代码示例
【示例 7:实现一个通用的栈类模板】
这是一个完整可运行的栈类模板,包含了常用的接口。
cpp
#include <iostream>
#include <string>
using namespace std;
// 通用的栈类模板
template<class T>
class Stack {
public:
// 构造函数:默认容量是4
Stack(size_t capacity = 4)
: _array(new T[capacity])
, _capacity(capacity)
, _size(0)
{}
// 析构函数
~Stack() {
delete[] _array;
_array = nullptr;
_capacity = _size = 0;
}
// 入栈
void Push(const T& data);
// 出栈
void Pop() {
if (_size > 0) {
_size--;
}
}
// 获取栈顶元素
T& Top() {
return _array[_size - 1];
}
// 获取栈的大小
size_t Size() {
return _size;
}
// 判断是否为空
bool Empty() {
return _size == 0;
}
private:
T* _array; // 用T类型的数组存储数据
size_t _capacity;
size_t _size;
};
// ⚠️ 注意:类模板的成员函数在类外定义时,必须加上模板参数!
template<class T>
void Stack<T>::Push(const T& data) {
// 这里简化处理,不写扩容逻辑了
if (_size < _capacity) {
_array[_size] = data;
_size++;
}
}
int main() {
cout << "========== int类型的栈 ==========" << endl;
Stack<int> st1; // 存int的栈
st1.Push(1);
st1.Push(2);
st1.Push(3);
while (!st1.Empty()) {
cout << st1.Top() << " ";
st1.Pop();
}
cout << endl << endl;
cout << "========== double类型的栈 ==========" << endl;
Stack<double> st2; // 存double的栈
st2.Push(1.1);
st2.Push(2.2);
st2.Push(3.3);
while (!st2.Empty()) {
cout << st2.Top() << " ";
st2.Pop();
}
cout << endl << endl;
cout << "========== string类型的栈 ==========" << endl;
Stack<string> st3; // 存string的栈
st3.Push("Hello");
st3.Push("C++");
st3.Push("模板");
while (!st3.Empty()) {
cout << st3.Top() << " ";
st3.Pop();
}
cout << endl;
return 0;
}
运行结果:
cpp
========== int类型的栈 ==========
3 2 1
========== double类型的栈 ==========
3.3 2.2 1.1
========== string类型的栈 ==========
模板 C++ Hello
🎉 太牛了! 一个类模板,支持 int、double、string 三种类型!而且你还可以用它存你自己定义的任何类型!
2. 类模板的实例化
基础概念解释
大白话时间:
类模板的实例化和函数模板有点不一样:
-
函数模板可以隐式实例化(编译器自动推类型)
-
类模板必须显式实例化!你必须手动告诉编译器类型是什么!
也就是说,你不能这样写:
cpp
Stack st; // ❌ 错误!类模板不能隐式实例化
必须这样写:
cpp
Stack<int> st; // ✅ 正确!显式指定类型
还有一个重要概念:Stack是类模板名 ,不是真正的类;Stack<int>才是真正的类型!
就像:"月饼模具" 不是月饼,"用模具做出来的豆沙月饼" 才是真正的月饼!
代码示例
【示例 8:类模板实例化的注意事项】
这个例子展示类模板实例化的各种细节和注意点。
cpp
#include <iostream>
#include <typeinfo>
using namespace std;
template<class T>
class MyClass {
public:
MyClass() {
cout << "MyClass被实例化了,T的类型是:" << typeid(T).name() << endl;
}
void Print() {
cout << "这是一个MyClass对象" << endl;
}
private:
T _data;
};
int main() {
cout << "===== 类模板必须显式实例化! =====" << endl;
// MyClass c; // ❌ 编译错误!类模板不能隐式实例化
MyClass<int> c1; // ✅ int类型
MyClass<double> c2; // ✅ double类型
MyClass<char> c3; // ✅ char类型
cout << endl << "===== 验证:不同实例化是不同类型 =====" << endl;
cout << "MyClass<int>的类型信息:" << typeid(MyClass<int>).name() << endl;
cout << "MyClass<double>的类型信息:" << typeid(MyClass<double>).name() << endl;
if (typeid(MyClass<int>) == typeid(MyClass<double>)) {
cout << "它们是同一个类型" << endl;
} else {
cout << "它们是完全不同的类型!" << endl;
cout << "记住:MyClass是模板名,MyClass<int>才是真正的类型!" << endl;
}
cout << endl << "===== 类模板也可以有多个模板参数 =====" << endl;
cout << "比如我们后面要学的map:map<string, int>" << endl;
cout << "第一个参数是key的类型,第二个参数是value的类型" << endl;
return 0;
}
运行结果:
cpp
===== 类模板必须显式实例化! =====
MyClass被实例化了,T的类型是:i
MyClass被实例化了,T的类型是:d
MyClass被实例化了,T的类型是:c
===== 验证:不同实例化是不同类型 =====
MyClass<int>的类型信息:7MyClassIiE
MyClass<double>的类型信息:7MyClassIdE
它们是完全不同的类型!
记住:MyClass是模板名,MyClass<int>才是真正的类型!
===== 类模板也可以有多个模板参数 =====
比如我们后面要学的map:map<string, int>
第一个参数是key的类型,第二个参数是value的类型
🎯 本篇总结
恭喜你看到这里!模板初阶的内容我们就学完了,来回顾一下今天的重点:
|----------|----------------------|
| 知识点 | 核心要点 |
| 泛型编程 | 编写与类型无关的代码,模板是基础 |
| 函数模板 | 函数的模具,编译器根据类型生成具体函数 |
| 模板原理 | 编译期处理,不同类型生成不同函数 |
| 实例化 | 隐式(自动推)和显式(手动指定)两种 |
| 匹配原则 | 普通函数优先,更好匹配优先,显式强制模板 |
| 类模板 | 类的模具,必须显式实例化 |
| 重要区别 | 类模板名≠真正的类型,实例化后才是 |
💡 一句话总结:模板就是让编译器帮我们写重复代码的工具!
📢 下一篇预告
模板初阶到这里就先告一段落啦~
其实模板的知识点远不止这些,背后还有很多深层语法、细节坑点和进阶用法等着大家深挖,水真的特别深!
下一篇博客我们就正式告别模板基础,进军STL核心殿堂~
我会带大家从零入门STL容器、常用算法、迭代器底层逻辑,手把手拆解常用容器的用法、底层结构和实战场景,帮你轻松拿捏C++ STL,敬请期待吧!!
💬 如果这篇文章对你有帮助,别忘了点赞收藏关注三连哦!有任何问题欢迎在评论区留言,我会一一回复!
我是的小小的风,我们下一篇再见!👋