《从函数模板到类模板:OP泛型编程进化论》
文章目录
- 1. 什么是模板
- 2. 函数模板
- 2.1. 函数模板存在的意义
- 2.2. 函数模板的语法定义
- 2.3. 函数模板的使用
- 2.3.1. 隐式实例化
- 2.3.2. 显示实例化
- 2.4. 函数模板实例化时的匹配原则
- 3. 类模板
- 3.1. 类模板存在的意义
- 4.1 类模板的语法定义
- 4.2 类模板的使用
- 5. 模板无法进行文件的声明和定义分离
- 6. 模板机制总览:编译器如何通过模板生成代码?
- 6.1. 函数模板
- 6.2. 类模板
1. 什么是模板
- 在中秋节时我们吃的或买到的月饼都是各式各样的,在同一种月饼的制作过程中因为需要做很多个,所以不可能纯手工制作进而就有了摸具,有了摸具就有一部分的制作过程可以省去且现状大小都是一样的但还是有一部分制作过程是人工完成的,所以摸具只带来了半自动化的这么一个作用。
- 了解了制作月饼的摸具,其实模板就和月饼的摸具一样,减少了程序员的写代码时间把大部分重复的工作交给编译器去完成,但模板和摸具一样也是一个半自动化,具体为什么是半自动化大家请听后面我详细到来。
2. 函数模板
2.1. 函数模板存在的意义
- 大家在学C++之前基本都有学过C语言了吧;在学习C语言期间有这么一个题目:写一个函数将两个数相加并返回两数的和,很简单吧基本有编程基础的都能写出来。
int Add(int x, int y)
{return x + y;
}
- 此时再加一个要求呢,要求能进行char、double、float等这些类型的加法运算,那也很简单C++有函数重载这一语法就可以复制粘贴整形加法的代码改一下类型即可。
int Add(int x, int y)
{return x + y;
}float Add(float x, float y)
{return x + y;
}double Add(double x, double y)
{return x + y;
}char Add(char x, char y)
{return x + y;
}
- 但大家都有发现吧这些代码除了类型不同外其他都是一样的,这样的重复工作大家做多了是不是也很烦!此时C++函数模板的好处就来了。
template <class T>
T Add(T x, T y)
{return x + y;
}
- 写一个模板,所有类型都可以调用这样就很好解决了我们在写代码时可以省略掉代码相同, 但类型不同重复性的工作了。
2.2. 函数模板的语法定义
- 函数模板大家可以理解为它代表了一个函数的家族,与类型无关,在使用时被对传入的参数推导实例化一个对应参数的函数出来。
- 模板的语法由两个部分组成,分别是模板关键字和参数类型
template<class T>
- template为模板的关键字,后面<>内为参数类型关键字和类型名称
- 参数类型的关键字有两个:
class | 类的关键字,在这里是作模板参数类型的关键字 |
typename | 作用和名称一样,类型名称,也是模板参数类型的关键字 |
这两个类型关键字,在刚学模板时可以认为两个关键字没有区别
参数名称写什么都可以,没什么特殊情况下我比较喜欢写T(type的首字母)
template<class T>
template<typename T>
- 写多个参数时和函数一样加逗号分隔即可
template<class T1, class T2>
- 也可以这样写
template<typename T3, typename T4>
- 甚至可以这样写
template<class T3, typename T4>
template<typename T3, class T4>
- 但不能这样写
//错误的写法,语法不支持这样写,会编译报错!
template<class T1, T2>
template<typename T1, T2>
- 定义时:定义在函数的头上即可。
- 函数的参数类型和返回值类型替换成模板参数的类型名称即可(没有返回值(void)就无需替换)。
- 需要注意的是: 如果你传入的是一个类类型的话建议加上引用减少调用拷贝构造,且你不想让它指向的内容被修改时建议加上const避免被修改。
template <class T>
T Add(T x, T y)
{return x + y;
}template <typename T>
T Add(T x, T y)
{return x + y;
}
2.3. 函数模板的使用
2.3.1. 隐式实例化
#include<iostream>
using namespace std;template <class T>
T Add(T x, T y)
{return x + y;
}int main()
{int a = 10;int b = 20;int ret = Add(a, b);cout << ret << endl;return 0;
}
- 上述代码中 Add(a,b) 就是隐式实例化后进行调用,不写具体类型由编译器推导传入参数的类型然后实例化一份对应参数的函数出来。
2.3.2. 显示实例化
#include<iostream>
using namespace std;template <class T>
T Add(T x, T y)
{return x + y;
}int main()
{int a = 10;int b = 20;int ret = Add<int>(a, b);cout << ret << endl;return 0;
}
- 上述代码中 Add< int >(a, b) 就是显示实例化后进行调用,明确知道传入的参数是某个固定类型就显示写不用编译器推导。
2.4. 函数模板实例化时的匹配原则
举例:在某天的早上到中午这段时间你都在刷题、写代码;不知不觉到了午饭饭点现在的你又饿又累且然后家里没人(没人就代表了没人做饭)此时有两个选择:
-
- 选法1:
(1). 自己手动做饭(但你此刻又饿又累做饭又要花去一些时间)
(2). 看了一下自己的钱包,还有点余额点了个外卖吃(并且这个外卖送餐的距离离你家很近)。 - 上述选法1中的 (1)和(2)你会怎么选,如果是我的话我肯定选(2),点完躺一会饭就到了即省时又省力。
- 选法1:
-
- 选法2:
(1). 自己手动做饭(但你此刻又饿又累做饭又要花去一些时间)
(2). 看了一下自己的钱包,余额为0.00点不了外卖吃。 - 上述选法2中的 (1)和(2)你会怎么选,余额不足这就没办法选(2)了,只能将就着做饭吃了。
- 选法2:
- 函数模板实例化时的匹配原则也和上述类似:
int Add(int x, int y)
{cout << "int Add(int x, int y)" << endl;return x + y;
}template <class T>
T Add(T x, T y)
{cout << "T Add(T x, T y)" << endl;return x + y;
}int main()
{Add(1,2);Add(1.1,2.2);return 0;
}
上述代码运行结果:
- 总结:编译器会根据匹配原则进行调用:有现成的就会去调用现成的,没有现成的就将就的走函数模板实例化出来一个对应类型的函数然后调用。
3. 类模板
3.1. 类模板存在的意义
- 大家在学C++之前应该有学过数据结构里的栈了吧;这里我写一个简易的栈出来给大家看看。
class stack
{
public:stack(size_t n = 4):_st(new int[n]){ }void push(const int& val){//这里只是浅浅的给大家写个基础的栈所有扩容我就不写了。_st[_size++] = val;}void pop(){assert(_size >= 0);_size--;}int& top(){return _st[_size - 1];}private:int* _st;size_t _size = 0;size_t _capacity = 0;
};int main()
{stack s;s.push(1);s.push(2);s.push(3);return 0;
}
-
这个栈这样写没问题,可是那天有给需求要我一个栈存int类型的数据还要一个栈存double类型的数据,这样的话是不是得copy一下前面写的int类型的栈然后把int类型改成double类型,这时两个类除了增删查改的类型不同其他基本一样,这是不是也是在做一些重复性的工作写多了也很烦。
-
此刻C++类模板的好出就来了。
#include<iostream>
#include<assert.h>
using namespace std;template<class T>
class stack
{
public:stack(size_t n = 4):_st(new T[n]){ }void push(const T& val){//这里只是浅浅的给大家写个基础的栈所有扩容我就不写了。_st[_size++] = val;}void pop(){assert(_size >= 0);_size--;}T& top(){return _st[_size - 1];}private:T* _st;size_t _size = 0;size_t _capacity = 0;
};int main()
{stack<int> s1;stack<double> s2;s1.push(1);s1.push(2);s1.push(3);s2.push(1.1);s2.push(2.2);s2.push(3.3);return 0;
}
- 使用类模板之后,想要什么类型的栈就显示实例化什么类型的栈即可,是不是很方便。
4.1 类模板的语法定义
- 类模板大家可以理解为它代表了一个类的家族,与类型无关,在使用时语法规定必须显示实例化定义。
- 类模板的语法和函数模板的语法是一样的
template<class T>
template<typename T>
注意:类模板是可以使用缺省参数的
template<class T = int>
- 定义时:定义在类的头上即可。
(1). 将类处理数据的类型都替换成类模板参数的名称即可。
template<class T>
class stack
{
public:stack(size_t n = 4):_st(new T[n]){ }void push(const T& val){//这里只是浅浅的给大家写个基础的栈所有扩容我就不写了。_st[_size++] = val;}void pop(){assert(_size >= 0);_size--;}T& top(){return _st[_size - 1];}private:T* _st;size_t _size = 0;size_t _capacity = 0;
};
4.2 类模板的使用
- 类模板与函数模板不同的是类模板在定义时必须使用显示实例化进行定义(这是语法规定)
#include<iostream>
#include<assert.h>
using namespace std;template<class T>
class stack
{
public:stack(size_t n = 4):_st(new T[n]){ }void push(const T& val){//这里只是浅浅的给大家写个基础的栈所有扩容我就不写了。_st[_size++] = val;}void pop(){assert(_size >= 0);_size--;}T& top(){return _st[_size - 1];}private:T* _st;size_t _size = 0;size_t _capacity = 0;
};int main()
{stack s1;//error C2955: “stack”: 使用 类 模板 需要 模板 参数列表stack<int> s;//必须显示实例化定义s.push(1);s.push(2);s.push(3);return 0;
}
5. 模板无法进行文件的声明和定义分离
- 不管是函数模板还是类模板都不能文件的声明和定义分离,因为链接时不知道是什么类型的模板从而实例化变成了。
- 解决方法:后续会再写一篇文章进行讲解!
//Test.cpp
#include"Add.h"int main()
{Add(1, 2);Add(1.1, 2.2);return 0;
}
#pragma once
//Add.h
#include<iostream>
using namespace std;template <class T>
T Add(T x, T y);
//Add.cpp
#include"Add.h"template <class T>
T Add(T x, T y)
{return x + y;
}
- 链接时报错:
6. 模板机制总览:编译器如何通过模板生成代码?
- 编译器会通过模板去实现一份对应参数的函数模板或类模板,本质上就是将重复的工作交给编译器来做。