C++模板【上篇】 —详解模板基础语法
文章目录
- 前言
- 1. 泛型编程
- 2. 模板的类别
- 2.1 函数模板
- 2.2 类模板
- 3. 模板的实例化
- 3.1 函数模板的实例化
- 3.1.1 隐式实例化
- * 编译器实例化原理
- 3.1.2 显示实例化
- 3.2 类模板的实例化
前言
在这篇文章中,主要介绍一些模板的基础的语法和一些细节,同时了解泛型编程。我们主要从如下的角度进行谈论:
- 泛型编程
- 模板的分类
- 模板的实例化(简要说明实例化原理)
1. 泛型编程
我们先来看一个这样的场景,例1.1:
//实现一个交换函数
void swap(int& x, int& y)
{int tmp = x;x = y;y = tmp;
}
但是这个函数只能实现整数的交换……
如果我们想要实现double
、char
、指针……类型的转换由该如何办呢?是否需要将每一个都写一遍,当一个CV工程师?
例1.2:
void swap(double& x, double& y)
{double tmp = x;x = y;y = tmp;
}
void swap(char& x, char& y)
{char tmp = x;x = y;y = tmp;
}
// ……
通过函数重载是可以很好的解决这个问题,但是这样是否未免太过于麻烦了?很显然地有很大的缺陷:
- 重载的函数仅仅是类型不同,代码复用率比较低。如果有新类型出现时,就需要用户自己增加对应的函数。
- 函数的维护成本高,当一个函数出现问题过后,对于另外的同类函数也需要跟着修改……
对于这样的问题C++提出了一个新的语法:模板。就好像给出一个模具,想要什么“属性”的物体,直接往模具上套就可以了。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段!模板就是泛型编程的基础。
模板分为:
- 函数模板
- 类模板
2. 模板的类别
模板主要运用在
- 函数模板
- 类模板
这也是我们主要介绍的语法。关于模板的实例化,我们在第三节进行细致的讲解。在这里主要介绍语法,搭建框架。
2.1 函数模板
语法如下:
template<class T> // 这个也可以是:template<typename T>
// …… 函数声明 + 函数定义
对于上面所述的交换函数,例2.1:
template<class T>
void swap(T& x, T& y)
{T tmp = x;x = y;y = tmp;
}
这里声明模板关键字:template<type_parameter list>
。模板参数列表每一个被作为类型的标识符(就像函数形参名一样)都需要前面带一个关键字:class/typename
。模板参数列表当然可以不止一个。例2.2:
template<class T1, typename T2>
void func(T1 x, T2 y)
{// 业务处理
}
注意:
引用和指针都需要指明用&和*指明类型。
- 目的是:为了让编译器更好地识别和方便进行解引用。
否则: 对于引用来说,编译器不会把它识别为引用,而是普通的值传递;对于指针 来说,如果想得到参数解引用过后的值,请把函数参数带上*
指明指针类型。上面的例2.1和2.2已经给出了作为引用的例子了,下面给出一个指针的例子。
例2.3:
template<class T>
void swap(T* x, T* y)
{T tmp = *x;*x = *y;*y = tmp;
}
这样才能更好地实现函数体。
const也可以作为函数参数的一部分
例2.4:
template<class T1, typename T2>
void func(const T1 x, const T2 y)
{// 业务处理
}
关于这个例子,稍后在实例化(第三节)的时候进行讲解!
2.2 类模板
先给出语法:
template<class T1, class T2, ..., class Tn> //类型参数列表
class class_name
{
// 类内成员定义
}
对于自定义类型来说,有了类模板简直是如虎添翼。让代码得到了极大地复用。
考虑如下场景:现在我要创建int
,char
,double
三个类型的顺序表(vector
)……
如果不使用类模板的代码就不再演示了,给出采用类模板进行的编写的代码
例2.5:
template<class T>
class Vector
{
public :Vector(size_t capacity = 10): _pData(new T[capacity]), _size(0), _capacity(capacity){}// 使用析构函数演示:在类中声明,在类外定义。
~Vector();void PushBack(const T& data);void PopBack();
// ...size_t Size() {return _size;}T& operator[](size_t pos){assert(pos < _size);return _pData[pos];}
private:T* _pData;size_t _size;size_t _capacity;
};// 注意:类模板中函数放在类外进行定义时,需要加模板参数列表
template <class T>
Vector<T>::~Vector()
{
if(_pData)
delete[] _pData;
_size = _capacity = 0;
}
那我们在使用的时候就可以直接根据不同的类型进行创建不同的vector
了!
注意:
类模板的类名不再是纯粹的类名了
- 例如上面的
class vector
中的vector
不再是类名了,而vector<T>
才是这个类的类名。因为类模板类名不是真正的类,只有实例化过后的类才是真正的类!
类模板的方法是可以分离于类域的
- 但是在类域外部进行定义的时候要指明类域:类名
<T>
。同时记得继续带上关键字template
!因为template
管辖的范围只有紧接着的函数或者类。(不可以一个template
关键字给多个函数或者模板)
不要尝试将类模板分文件编译
- 这个问题值得重视!不仅仅是类模板,同时还有函数模板。在C++模板下篇小编会和大家一起探讨。
3. 模板的实例化
在第二节中主要讲解了模板的框架搭建,这里主要讲解如何进行操作了。
对于实例化而言,主要分为两种:
- 隐式实例化
- 显示实例化
主要运用
- 函数模板:隐式实例化、显示实例化
- 类模板:显示实例化
3.1 函数模板的实例化
3.1.1 隐式实例化
来看上面的例2.1
#include<iostream>
using namespace std;
template<class T>
void myswap(T& x, T& y) //因为std标准库中有swap实现,这里做一个区分
{T tmp = x;x = y;y = tmp;
}int main()
{int a = 10, b = 20;printf("交换之前:a = %d, b = %d\n", a, b); //printf还是挺方便的,哈哈哈myswap(a, b);printf("交换之前:a = %d, b = %d\n", a, b);return 0;
}
运行这个程序,结局也如我们所愿。可是总有些人动一些歪脑筋
例3.1:
#include<iostream>
using namespace std;
template<class T>
void myswap(T& x, T& y)
{T tmp = x;x = y;y = tmp;
}int main()
{int a = 10;double b = 1.0;printf("交换之前:a = %d, b = %d\n", a, b);//myswap(a, b); // 很不幸,这是错误的!printf("交换之前:a = %d, b = %d\n", a, b);return 0;
}
为什么编译器会报错呢?
在这里先浅浅地谈一下编译器实例化的原理:
* 编译器实例化原理
来看这样一个例子
例 *.1:
#include<iostream>
using namespace std;
template<class T>
void myswap(T& x, T& y)
{T tmp = x;x = y;y = tmp;
}int main()
{int a = 10, b = 20;myswap(a, b);double m = 1.0, n = 2.1;myswap(m, n);char x = 'a', y = 'b';myswap(x, y); return 0;
}
查看这个程序的汇编代码
对比上面的函数名和函数地址,显然我们可以得到的是:
对于不同类型的参数调用的不是同一个函数!当然对于同一种类型,只需要生成一个函数即可。
根据汇编我们大概了解了:
-
在编译阶段,编译器会根据函数模板实际出入的参数对函数模板进行推演,从而生成类型匹配的函数以供调用。
- 例如:在上面使用
int
的时候,编译器生成了一个myswap< int >
的函数;还有myswap< double >
、myswap< char >
。
- 例如:在上面使用
换句话说编译器帮我们完成了函数编写的工作。而我们只需要维护好这一份模板代码即可。
现在回到刚刚的问题:为什么编译器会报错呢?
- 很显然地是:编译器无法推出类型来,从而实例化出函数!!但是,对于我们普通的函数而言是可以支持隐式类型转换的,但是模板是坚决不允许进行所谓的类型转换的!
解决办法:
- 强制类型转换
- 显示实例化
这里先介绍强制类型转换:
例3.2:
#include<iostream>
using namespace std;
template<class T>
void myswap(T& x, T& y)
{T tmp = x;x = y;y = tmp;
}int main()
{int a = 10;double b = 1.0;printf("交换之前:a = %d, b = %d\n", a, b);myswap(a, (int)b); // 很不幸,还是错误的!printf("交换之前:a = %d, b = %d\n", a, b);return 0;
}
一脸笑意运行,突然发现,还是错误的……
为什么啊?
(int)b
给出下面的疑问:
- 这是一个什么行为?类型转换啊!
- 类型转换会干嘛?产生临时变量啊!
- 临时变量怎么了?具有常性啊!
这下知道了,原来是常性出了问题,意思就是需要给函数参数加一个const
(这里可以阅读我的另外一片关于类型转换细节的文章 链接:类型转换细节)
#include<iostream>
using namespace std;
template<class T>
void myswap(T& x, const T& y)
{T tmp = x;x = y;//y = tmp; //这就是不可以取的了
}int main()
{int a = 10;double b = 1.0;printf("交换之前:a = %d, b = %d\n", a, b);myswap(a, (int)b); // 很不幸,还是错误的!printf("交换之前:a = %d, b = %d\n", a, b);return 0;
}
对于这个函数而言就是无意义的,因为我需要改变值啊。但是这个细节需要注意!!
3.1.2 显示实例化
对于上面的问题,我们还有一个办法就是:显示实例化
例3.3:
#include<iostream>
using namespace std;
template<class T>
void myswap(T& x, T& y)
{T tmp = x;x = y;y = tmp;
}int main()
{int a = 10;double b = 1.0;printf("交换之前:a = %d, b = %d\n", a, b);myswap<int>(a, b); //告诉编译器,我要使用intprintf("交换之前:a = %d, b = %d\n", a, b);return 0;
}
这样就十分可取了。告诉了编译器,我需要什么样子的函数了。
当然显示实例化肯定是有自己的用武之地的,考虑下面的场景:
例3.4:
template<class T>
T* Alloc(int n)
{T* ptr = new T[n];//开辟n个T类型的数据// …… 处理return ptr;
}int main()
{int* ptr1 = Alloc<int>(10); //需要int类型delete[] ptr1; //不得不说这样的接口设计不好。只是为了举例子return 0;
}
显示实例化还有更多应用的场景!!
小编提出如下问题:我的函数模板会和我实际写的函数发生矛盾吗?
—不会
这里给出函数模板实例化和普通函数的存在性原则:
- 一个非模板函数和一个同名的模板函数可以同时存在,而且该函数模板还可以被实例化为这个非模板函数。
- 对于非模板函数和同名的函数模板,如果其它条件都相同,在调用的时候会优先调用非模板函数而不会从该模板实例化一个模板函数。
同时,如果模板可以生成一个具有更好匹配的函数,那么将选择模板。
3.2 类模板的实例化
关于类模板的实例化,一般都是显示实例化。因为类中的数据,根本不知道你需要什么样子的数据。
例3.6:
template<typename T>
class Stack
{
public:void Push(T x);Stack(int capacity = 4):_a(new T[capacity]), _capacity(capacity), _top(0){}~Stack(){delete[] _a;_capacity = 0;_top = 0;}Stack(const Stack& st):_capacity(st._capacity),_top(st._top){_a = new T[st._capacity];memcpy(_a.st._a, sizeof(T) * st._capacity);}
private:T* _a;int _capacity;int _top;
};template<typename T>
void Stack<T>::Push(T x)
{_a[_top++] = x;
}int main()
{Stack<int> st1; //存放int类型Stack<char> st2; // 存放char类型Stack<vector<int>> st3; //存放自定义类型// ...return 0;
}
我们下篇再见!!