
🎈主页传送门****:良木生香
🔥个人专栏:《C语言》 《数据结构-初阶》 《程序设计》《鼠鼠的C++学习之路》
🌟人为善,福随未至,祸已远行;人为恶,祸虽未至,福已远离

前言:在上一篇文章中,我们学习了C++的内存管理,学习了如何使用new/delete,他们两个的组成原理以及他们与C语言中malloc/free之间的关系和差别,那么今天我们就再来学习一个新的知识点---C++模板
目录
一、模板的概念
这里有一个新的概念,叫做泛型编程,什么是泛型编程呢?不着急,我们本篇文章将会围绕着泛型编程来展开讲解。
先从一个简单的问题开始:当我们想要实现不同数据类型之间的数据交换,我们该怎么做呢?是像下面这样吗?
cpp
//各类型的数据交换函数
void Swap_int(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
void Swap_double(double& a, double& b) {
double temp = a;
a = b;
b = temp;
}
void Swap_char(char& a, char& b) {
char temp = a;
a = b;
b = temp;
}
以上的方法使用函数重载虽然可以实现,但是有以下几个不好的地方:
- 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
- 代码的可维护性比较低,一个出错可能所有的重载均出错
那能否 告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码 呢?
这时候C++模板就诞生了,语法如下:
cpp
// 模板声明关键字 + 模板参数列表
template <typename 类型参数名1, typename 类型参数名2, ...>
// 函数定义
返回值类型 函数名(参数列表) {
函数体;
}
其中,template是告诉编译器就接下来的函数是个模板函数,<>里面放的是模板参数列表,typename表示的是声明一个泛型类型(也可以是class,现阶段可以认为两者没有差异),类型参数名代表的是自定义类型占位符,可以是T,U等等。
二、函数模板
使用模板的话,Swap()函数就可以写成:
cpp
template <typename T>
void Swap(T& a, T& b) {
int temp = a;
a = b;
b = temp;
}
那么使用的时候就像下面这样:
cpp
#include<iostream>
using namespace std;
template <typename T>
void Swap(T& a, T& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int a = 10, b = 20;
char c = 'x', d = 'y';
double e = 2.5, f = 3.6;
Swap(a, b);
cout << a << " " << b << endl;
Swap(c, d);
cout << c << " " << d << endl;
Swap(e, f);
cout << e << " " << f << endl;
return 0;
}
运行的结果为:

这时候我们再来想一个问题:三种类型的交换函数,调用的是同一个函数吗?我们调用反汇编看看:



通过反汇编的结果来看,很显然三种类型的交换函数调用的不是同个函数。在我框出来的地方可以清楚的看到,template会根据传进来的参数类型去创建不同类型的函数,而不同类型的函数相当于构成函数构成函数重载,可以参考下面这张图:

2.1、函数模板的原理
函数模板本身就是一个蓝图,它本身并不是函数,就像我们之前学习的类,所以就是将之前本来应该是我们程序员的做的事交给了编译器去做,提升了很大的效率空间,就像我们的洗衣机,洗碗机等等这些半自动化机器。
在template <typename T>中,也可以写成 template<class T>,在现阶段可以认为class和typename没有任何区别,但是有一些场景只能用tpyename,后续的学习中我们会讲到。
2.2、模板的实例化
与类的实例化相似,模板也有实例化,将参数传给模板而生成对应类型。
对于上面所写的Swap()函数,我们在创建模板的时候只写了一个自定义类型占位符,那么在传参的时候只能传同一类型的参数进去,即int和int,double和double,不能int和double混用。如果实在想混用两种不同的类型,有两种方法:
1、使用强制类型转换:
cpp
#include<iostream>
using namespace std;
template <typename T>
T add(T a, T b) {
return a + b;
}
int main() {
int a = 1;
double b = 2.5;
auto ret = add((double)a, b);
cout << ret << endl;
return 0;
}
在使用函数模板的时候将int强制类型转换成double类型,这样子就能实现两种不同类型的数据进行相加了,同时他们的结果也会被自动推断出double:

现在讲的方法都属于隐式类型转换实例化,下面就来介绍一下第二种方法;
2、显示实例化:
显式实例化就是先将这个函数的返回值直接确定,让不属于这个返回值类型的数据自动转换成符合返回值的类型:
cpp
#include<iostream>
using namespace std;
template <typename T>
T add(T a, T b) {
return a + b;
}
int main() {
int a = 1;
double b = 2.5;
//auto ret = add((double)a, b);
auto ret = add<double>(a, b);
auto ret2 = add<int>(a, b);
cout << ret << endl;
return 0;
}
直接在函数实例化的地方给函数类型进行表明,这样就能保证相加的类型是相同的了。
当然,还有一种邪修(bushi),就是直接修自定义类型参数名:
cpp
#include<iostream>
using namespace std;
template<typename T,typename U>
auto add(T a, U b) {
return a + b;
}
int main() {
int a = 10;
double b = 22.5;
cout << add(a, b) << endl;
return 0;
}
在返回值的部分让编译器自己决定返回什么类型的。
但是呢,显式实例化的真正用途并不在这里,而是在下面的场景中:
3、真正的使用场景:
当我们在传参是没有用到自定义类型占位符T,那就只能显式实例化了,像这样:
cpp
template <typename T>
void Func(size_t n) {
cout << n*2 << endl;
}
在Func()函数中没有用到类型T,这是后如果在main函数中想要直接调用,就会出现下面这种中编译错误的信息:

就是编译器推导不出T是什么类型的,但是函数中又必须要出现T类型的数据,所以这时候只能我们自己显式实例化:
cpp
#include<iostream>
using namespace std;
template <typename T>
void Func(size_t n) {
cout << n*2 << endl;
}
int main() {
int a = 10;
Func<int>(10);
return 0;
}
2.3、函数模板的匹配原则
讲完了函数模板的实例化,现在我们就来讲讲函数模板的参数匹配规则。
当实现同个功能的函数但类型不同,与模板同时可以存在吗?像这样子:
cpp
#include<iostream>
using namespace std;
//模板
template <typename T>
void Swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
//整形交换函数
void Swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int a = 30, b = 40;
Swap(a, b);
cout << a << " " << b << endl;
return 0;
}
整形的交换函数和模板同时存在,并且主函数中的变量又是整形,编译器会调用模板还是现成的整形交换函数?当然是现成的交换函数:

如果是double的话就只能调用模板了:

调用哪个编译器都能很好的给我们展示出来。
编译器会优先找更匹配的,比如:
cpp
#include<iostream>
using namespace std;
//模板
template <typename T1,typename T2>
auto add(T1 a,T2 b){
return a + b;
}
//整形加法
int add(int a, int b) {
return a + b;
}
int main() {
int a = 10;
double b = 20.333;
cout << add(a, b) << endl;
return 0;
}
即使有现成的整形加法,但是使用模板会更加匹配,所以编译器会使用模板: 
三、类模板
3.1、类模板的原理
类模板指的是一种模板,而模板类指的是通过类模板实例化出来的类。下面我们就来讲讲类模板
以前我们在实现像栈,队列,链表等数据结构的时候,通常只存储同一类型或者单一类型的数据:
cpp
#include<iostream>
using namespace std;
//模板
template <typename T>
class Stack {
private:
T* _top;
size_t _size;
size_t _capacity;
public:
Stack(size_t n = 4)
:_size(0),
_top(new T[n]),
_capacity(n)
{
}
};
int main() {
Stack<int> st1;
return 0;
}
之前的自己实现的数据结构都已经够用了,为什么还要在出类模板这么个东西呢?就是为了,同一种数据结构能够存储不同类型式数据,当我有了以上代码之后,我就能将double,int,char等类型的数据都放进同一种数据结构中:
cpp
Stack<int> st1; //存放int类型
Stack<double> st2; //存放double类型
stack<char> st3; //存放char类型
函数模板大多数适用多种类型,可以隐式实例化,但是类模板通常是针对一种类进行操作,做好显式实例化,通过这种方法就实现了我们之前说的:泛型编程。即有了一个蓝图就能实现各种各类型的数据操作。
3.2、类模板的使用
有了类模板这个好东西,我们就衍生出了STL中的容器,像<stack>,<queue>之类的数据结构,他们的本质就是类模板,通过传进来数据自动推导数据类型。以后写数据结构就能调用STL中的函数了,就不用自己像在C语言阶段那样自己造轮子了。
还有一点就是,以前我们写代码通常是将声明放在.h中,将定义放在.cpp中,但是类模板就不能将定义和声明分离,如果真想分离,那也只能在用一个文件中分离:
cpp
#include <iostream>
using namespace std;
// 1. 类模板声明 + 定义(类体)
template <typename T>
class Stack
{
private:
T* _data;
int _top;
int _capacity;
public:
// 只在类里声明,不写实现
Stack(int cap = 4);
// 析构函数
~Stack();
// 成员函数只声明
void push(const T& val);
T top() const;
void pop();
bool empty() const;
};
// -------------------------------------------------------
// 2. 类外实现成员函数(真正的"声明与定义分离")
// -------------------------------------------------------
// 构造函数
template <typename T>
Stack<T>::Stack(int cap)
{
_capacity = cap;
_data = new T[_capacity];
_top = 0;
}
// 析构函数
template <typename T>
Stack<T>::~Stack()
{
delete[] _data;
}
// push
template <typename T>
void Stack<T>::push(const T& val)
{
_data[_top++] = val;
}
// top
template <typename T>
T Stack<T>::top() const
{
return _data[_top - 1];
}
// pop
template <typename T>
void Stack<T>::pop()
{
--_top;
}
// empty
template <typename T>
bool Stack<T>::empty() const
{
return _top == 0;
}
// -------------------------------------------------------
// 3. main 测试
// -------------------------------------------------------
int main()
{
Stack<int> st;
st.push(10);
st.push(20);
st.push(30);
cout << st.top() << endl;
st.pop();
cout << st.top() << endl;
return 0;
}
这就是类模板的声明和定义分离的使用。
还有一点就是,类模板的模板参数可以实现缺省参数:
单一参数时:
cpp
#include<iostream>
using namespace std;
template <typename T = int>
class A {
public:
T x1;
T x2;
};
int main() {
A<> a1; //不传入参数时就会默认为int类型
A<double> a2;
return 0;
}
当有多个参数时,就可以参考缺省参数那一块的知识:
cpp
#include<iostream>
using namespace std;
//template <typename T1 = int ,typename T2 = double,typename T3 = int> //全缺省
template <typename T1, typename T2 = int, typename T3 = double>
class B{
};
int main() {
B<int> b1;
B<int, int > b2;
B<int, int, double> b3;
return 0;
}
缺省值只能从右往左给。
那么以上就是本次所有的内容了
文章是自己写的哈,有什么描述不对的、不恰当的地方,恳请大佬指正,看到后会第一时间修改,感谢您的阅读~~~~
播主手写笔记:




