1.非模板类型参数
模板参数可以分为类型形参和非类型形参
类型形参:跟在class或者typename后的参数类型名称
非类型形参:将一个常量作为类的模板参数,在模板里可以将该参数作为常量使用如下
template <class T,int N = 10>//T为类型形参,N为非类型形参
class Array
{
public:
Array(int* n,int num)
{
for(int i=0;i<num;i++)
{
_array[i] = n[i];
_size++;
}
}
T& operator[](int size)
{
return _array[size];
}
const T& operator[](int size) const
{
return _array[size];
}
bool empty()
{
return _size == 0;
}
int size()
{
return _size;
}
private:
T _array[N];
int _size= 0;
};
需要注意以下几点
- 浮点数,类对象和字符串不能作为非类型形参
- 非类型形参的值必须在编译后就是一个确定的值。
2.模板的特化
2.1 有了模板函数可以屏蔽类型的不同,但是有些类型的参数需要特殊处理
template <class T>//一个判是否相等的模板函数
bool isequl(T need1,T need2)
{
if(need1 == need2)
return true;
return false;
}
可以比较int,char,double等类型的参数,但是如果比较指针类型的参数,它会比较两指针的存的地址,而不是指向地址的值。这不是我们希望的,所以需要模板的特化
模板的特化:在原本的模板函数的基础上,根据特殊的类型进行特殊的实现方式。
模板特化分为函数模板特化与类模板特化。
- 必须要先有一个基础的函数模板
- 关键词tempalte后面要有空尖括号<>
- 函数名后面的尖括号,尖括号的里面写特化的类型
- 函数形参表的参数必须和函数模板相同,否则会有大量的错误。
所以针对判断值是否相同,针对int指针可以这么写
template<>
bool isequl<int*>(int* need1,int* need2)
{
if(*need1 == *need2)
return true;
return false;
}
注意,如果遇到模板函数无法处理或者处理错误的情况下,一般直接给出相应的函数。
bool isequl<int*>(int* need1,int* need2)
{
if(*need1 == *need2)
return true;
return false;
}
类特化的方式分为两类:全特化,偏特化
- 全特化
将所有的模板参数都确定为具体类型。
template <class T>
class text
{
public:
text()
{
cout<<"template <class T>"<<endl;
}
private:
T a;
};
template<>
class text<int>
{
public:
text()
{
cout<<"template <class int>"<<endl;
}
private:
T a;
};
- 偏特化:进一步限制类型设置的特化
下面为基本的模板类
template <class T1,class T2>
class text
{
public:
text()
{
cout<<"template <class T>"<<endl;
}
private:
T1 a;
T2 b;
};
偏特化有两种形式
- 部分特化
将模板类的一部分参数具体化
template<class T1>//注意因为具体化了T2,所以这里只写T1
class text<T1,int>//将T2具体为了int类型
{
public:
text()
{
cout<<"template <class int>"<<endl;
}
private:
T1 a;
int b;
};
- 参数进一步限制
偏特化并不仅仅指一部分参数具体化,还指出将模板参数更进一步的条件限制所设置的特化版本
template<class T1,class T2>
class text<T1*,T2*>//两个参数被特化为指针类型
{
public:
text()
{
cout<<"template <class int>"<<endl;
}
private:
T1 a;
int b;
};
类似的,可以把参数的类型特化为引用类型
class text<T1&,T2&>
注意只有类模板有偏特化,函数模板并没有,
一方面是因为C++规定了函数模板没有
you can’t partially specialize them – pretty much just because the language says you can’t
另一方面也因为有函数重载的存在,作用重复了。
如果试图对函数模板进行特化会产生一系列的错误。
3.类型萃取
假如我们要做一个拷贝函数可以考虑用memcpy函数进行拷贝,但是因为有深浅拷贝的问题,假如是string或者是涉及深拷贝的自定义类型,就需要for循环进行深拷贝,而对于大部分的内置类型,需要浅拷贝。
如果我们可以提前判断拷贝的数据类型就能调用相应的函数
类型萃取就是这样的思路,是实现不同数据类型对于同一函数的不同操作。
#include "pch.h"
#include <iostream>
using namespace std;
//类的静态成员函数可以直接访问类的静态数据和函数成员。而访问非静态
//数据成员必须通过参数传递的方式得到对象名,然后通过对象名来访问。
struct TrueTyp
{
static bool get()//内置类型
{
return true;
}
};
struct falseType
{
static bool get()//涉及深拷贝类型和自定义类型
{
return true;
}
};
template <class T>
class Typejudge
{
public:
typedef falseType isTypedef;
};
template <>
class Typejudge<char>
{
public:
typedef TrueType isTypedef;
};
template <>
class Typejudge<int>
{
public:
typedef TrueType isTypedef;
};
template <>
class Typejudge<double>
{
public:
typedef TrueType isTypedef;
};
template <>
class Typejudge<float>
{
public:
typedef TrueType isTypedef;
};
template <>
class Typejudge<long>
{
public:
typedef TrueType isTypedef;
};
template<class T>
void copy(T* dst, T* scr, int size)
{
if (Typejudge<T>::isTypedef::get())
{
memcpy(dst, scr, size);
}
else
for (int i = 0; i < size; i++)
{
dst[i] = scr[i];
}
}
需要注意的是这里调用的函数为静态函数,类的静态成员函数可以直接访问类的静态数据和函数成员
4.模板分离编译
-
分离编译
可以理解为将一份代码分解为多个部分,如.h,.c等部分
一个程序由若干份源文件组成,这些源文件编译成为目标文件,最后所有的目标文件链接起来构成可执行文件,这个过程就叫做分离编译。 -
对模板的分离编译
程序会报错,因为在编译的四个过程中:预编译,编译,汇编,链接,作用如下
1.预编译:展开头文件,宏替换,去掉注释,条件编译(ifndef等语句的判断)
2.编译:将代码转换成汇编代码,并且做了两件很重要的事。
编译器在每个文件中保存一份函数地址符表,保存当前文件里的每个函数的地址
生成一条条的汇编代码,其中调用函数的代码会生成call指令,call 指令后面跟着一条jmp指令的汇编代码地址,之后跟着的才是“被调用函数的汇编后第一条指令”的地址
但是这个地址是在链接阶段才填上的。 -
汇编:将汇编代码转换成机器码
-
链接:编译器将多个.o文件链接在一起,形成可执行文件.
需要注意的是这个时候编译器会将每个call指令的地址补齐,方式是从当前的函数地址符表,如果没有,继续向其他的文件的函数地址符表找,如果找不到,则链接失败。
//test.h
#pragma once
#include "iostream"
using namespace std;
template<class T>
T Add(const T& num1, const T& num2);
//test.cpp
#include "test.h"
template<class T>
T Add(const T& num1, const T& num2)
{
return num1 + num2;
}
//main.cpp
#include "test.h"
int main()
{
Add(1, 2);
return 0;
}
运行上面的代码会出错误,错误信息。
原因是因为:预编译生成后,就生成了test.i和main.i文件
test.i有函数的声明实现,main.i有函数的调用和声明
因此编译器做了一件事情,fun.i生成fun.s里面保存了函数的地址,在地址符表里,同理main.i生成了main.s里面有call指令,call指令后面的地址暂时空下,等链接时填入,再汇编生成,main.o和fun.o文件
同时我们知道,模板在没有实例化时是没有代码的。
所以编译失败原因是因为,在展开头文件时,fun.o里只有函数的声明实现,没有它的实例化代码,也就没有函数地址,main.o的call指令里的地址无法填入,导致编译失败
解决方法有两个
- 将模板的声明和定义写在一起
- 在函数的声明里写上实例化的函数,但是这种不常用。
如
template<class T>
T Add(const T& num1, const T& num2)
{
return num1 + num2;
}
template<>//实例化的模板函数
int Add<int>(const int& num1, const int& num2)
{
return num1 + num2;
}