文章目录
- 函数模板
- 函数模板的原理
- 函数模板的实例化
- 模板参数的匹配原则
- 类模板
- 类模板的定义格式
- 类模板的实例化
- 非类型模板参数
- typename 与class
- 模板的特化
- 函数模板特化
- 类模板特化
- 全特化
- 偏特化
- 模板的分离编译
函数模板
函数模板的原理
template <typename T> //模板参数 ——类型
void Swap(T& x1, T& x2)
{
T tmp = x1;
x1 = x2;
x2 = tmp;
}
int main()
{
int a = 0, b = 1;
double c = 1.1, d = 2.2;
swap(a, b);
swap(c, d);
int* p1 = &a;
int* p2 = &b;
swap(p1, p2);
return 0;
}
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此
函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化
1、隐式实例化:让编译器根据实参推演模板参数的实际类型
template<class T>
T Add(const T& left , const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
//函数模板根据调用,自动推导模板参数的类型 ,实例化对应的参数
cout << Add(a1, a2) << endl;
cout << Add(d1, d2) << endl;
//实参传递的类型, 推演T的类型
cout << Add( a1, (int)d1 ) << endl;
cout << Add( (double)a1, d1) << endl;
//显示实例化 ,用指定的类型实例化 ,相当于隐式类型转换
cout << Add<int> (a1, d1) << endl;
cout << Add<double>(a1, d1) << endl;
return 0;
}
2、显式实例化:在函数名后的<>中指定模板参数的实际类型
template<class T>
T Add(const T& left , const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
//函数模板根据调用,自动推导模板参数的类型 ,实例化对应的参数
cout << Add(a1, a2) << endl;
cout << Add(d1, d2) << endl;
//实参传递的类型, 推演T的类型
cout << Add( a1, (int)d1 ) << endl;
cout << Add( (double)a1, d1) << endl;
//显示实例化 ,用指定的类型实例化 ,相当于隐式类型转换
cout << Add<int> (a1, d1) << endl;
cout << Add<double>(a1, d1) << endl;
return 0;
}
template<class T >
T * Alloc(int n )
{
return new T[n];
}
int main()
{
//有些函数无法自动推导函数模板的类型,实例化对应的参数,只能显式实例化
double *p1 = Alloc <double>(10);
return 0;
}
模板参数的匹配原则
类模板
类模板 ,无法推演实例化,所以类模板都是显式实例化
class Stack
{
public :
Stack(int capacity = 3)
{
_array= new T[capacity];
_size = 0;
_capacity = 0;
}
void Push(const T & data)
{
_array[_size++] = data;
}
~Stack()
{
free(_array);
_size = _capacity = 0;
}
private :
T * _array;
int _size;
int _capacity;
};
int main()
{
Stack <int> s1(); // int
Stack <double> s2();//double
Stack <char> s3();//char
//Stack <int ,doule> s2();
return 0;
}
类模板的定义格式
函数类模板的声明和定义分离
template<class T>
class Stack
{
public:
//声明
Stack(int capacity );
void Push(const T& data)
{
_array[_size++] = data;
}
~Stack()
{
free(_array);
_size = _capacity = 0;
}
private:
T* _array;
int _size;
int _capacity;
};
//定义
template<class T>
Stack<T>::Stack(int capacity )
{
_array = new T[capacity];
_size = 0;
_capacity = 0;
}
int main()
{
Stack <int> s1(); // int
Stack <double> s2();//double
Stack <char> s3();//char
//Stack <int ,doule> s2();
return 0;
}
对于普通类,类名和类型是一样的,但是对于类模板 ,类名和类型是不一样的 上面的代码中Stack是类名 ,但是Stack < T >是类型
类模板的实例化
非类型模板参数
模板参数可分为类型形参和非类型形参
类型形参: 出现在模板参数列表中,跟在class或typename关键字之后的参数类型名称。
非类型形参: 用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
如果此时有一个需求,实现一个静态数组的类,就需要用到非类型模板参数
template<class T, size_t N> //N:非类型模板参数
// N是常量 ,且N必须是整形
class StaticArray
{
public:
size_t arraysize()
{
return N;
}
private:
T _array[N]; //利用非类型模板参数指定静态数组的大小
};
int main()
{
StaticArray<int, 10> a1; //定义一个大小为10的静态数组
cout << a1.arraysize() << endl; //10
StaticArray<int, 100> a2; //定义一个大小为100的静态数组
cout << a2.arraysize() << endl; //100
return 0;
}
注意:
1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
2. 非类型的模板参数必须在编译期就能确认结果。
typename 与class
一般来说,typename 和class 没有什么区别,但是在有一种情景下是有区别的
template<class Container>
void Print( const Container& v )
{
//Container::const_iterator it = v.begin();是不行的
// 因为编译不确定Container::const_iterator是类型还是对象
// typename的作用就是明确告诉编译器这里是类型,等模板实例化再去找
typename Container::const_iterator it = v.begin();
while (it != v.end() )
{
cout << *it << " ";
it++;
}
cout << endl;
}
int main()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
Print(v);
list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
Print(lt1);
return 0;
}
以上情景需要使用typename
模板的特化
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理
模板的特化 即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。
模板特化中分为函数模板特化 和 类模板特化。
函数模板特化
1、首先必须要有一个基础的函数模板。
2、关键字template后面接一对空的尖括号<>。
3、函数名后跟一对尖括号,尖括号中指定需要特化的类型。
4、函数形参表必须要和模板函数的基础参数类型完全相同,否则不同的编译器可能会报一些奇怪的错误。
template<class T>
bool Less(T left ,T right)
{
return left < right;
}
//函数模板的特化
template<>
bool Less<int * >(int * left, int* right)
{
return *left < *right;
}
int main()
{
int a = 1, b = 2;
cout << Less(1, 2);
cout << endl;
cout << Less(&a, &b);
return 0;
}
类模板特化
不仅函数模板可以进行特化,类模板也可以针对特殊类型进行特殊化实现,并且类模板的特化又可分为全特化和偏特化(半特化)。
全特化
全特化即是将模板参数列表中所有的参数都确定化。
例如,对于以下类模板:
template<class T1, class T2>
class Data
{
public:
Data() {cout<<"Data<T1, T2>" <<endl;}
private:
T1 _d1;
T2 _d2;
};
template<>
class Data<int, char>
{
public:
Data() {cout<<"Data<int, char>" <<endl;}
private:
int _d1;
char _d2;
};
void TestVector()
{
Data<int, int> d1;
Data<int, char> d2;
}
偏特化
偏特化:任何针对模版参数进一步进行条件限制设计的特化版本。
偏特化有以下两种表现方式:
1、部分特化 ,将模板参数类表中的一部分参数特化
template<class T1, class T2>
class Data
{
public:
Data()
{
cout<<"Data<T1, T2>" <<endl;
}
private:
T1 _d1;
T2 _d2;
};
// 将第二个参数特化为int
template <class T1>
class Data<T1, int>
{
public:
Data()
{
cout<<"Data<T1, int>" <<endl;
}
private:
T1 _d1;
int _d2;
};
int main()
{
Data<double, int> d1;//偏特化
Data<int, double> d2;//调用基础的模板
return 0;
}
2、参数更进一步的限制
template<class T1, class T2>
class Data
{
public:
Data()
{
cout<<"Data<T1, T2>" <<endl;
}
private:
T1 _d1;
T2 _d2;
};
//偏特化:对类型的进一步限制
template<class T1, class T2>
class Data<T1*, T2*>
{
public:
Data() { cout << "Data<T1*, T2*>" << endl; }
private:
};
int main()
{
Data<int, int > d1;
Data<int, double > d2;
Data<int*, double > d3;
Data<int*, double* > d4;
Data<void*, void* > d5;
return 0;
}
模板的分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
在分离编译模式下,我们一般创建三个文件
一个头文件用于进行函数声明
一个源文件用于对头文件中声明的函数进行定义
最后一个源文件用于调用头文件当中的函数
举个例子:如果对一个加法函数模板进行分离编译
如果这三个文件生成可执行文件时,会在链接阶段产生报错
这是为什么呢?
C / C++程序要运行起来一般要经历以下四个步骤:
预处理、编译、汇编、链接
如果需要详细的了解这四个步骤,请点击这里
这三个文件经过预处理后就只剩下两个文件了
Visual Studio平台:
预处理后就进入编译阶段,
虽然在 main.i 当中有调用Add函数的代码,但是在 main.i 里面也有Add函数模板的声明,在编译阶段并不会发现任何语法错误,在编译阶段将 Add.i 和 main.i 翻译成了汇编语言,即将 Add.i 和 main.i 变成了Add.s 和 main.s
进入汇编阶段,利用 Add.s 和 main.s 这两个文件分别生成了两个目标文件,
即将 Add.s 和 main.s变成了Add.o和 main.o
最后将Add.o和 main.o进行链接操作生成a.out,
但在链接时发现,在main函数当中调用的两个Add函数实际上并没有被真正定义
原因是函数模板T还没有实例化,可以将模板定义的位置显式实例化。(这种方法不实用,不推荐使用)。
总结:
编译阶段看有没有声明,声明是一种承诺
在编译阶段,检查声明,查看函数名、参数、返回值是否对上,如果对上,则编译阶段通过
进入链接阶段,编译器会拿着修饰后的函数去其他文件符号表查找,如果查到,则链接阶段通过
模板分离编译失败的原因:
在函数模板定义的地方(Add.cpp)没有进行实例化,而在需要实例化函数的地方(main.cpp)没有模板函数的定义,无法进行实例化。
解决方法
- 将声明和定义放到一个文件 “xxx.hpp” 里面或者xxx.h其实也是可以的。推荐使用这种。
- 模板定义的位置显式实例化。(这种方法不实用,不推荐使用)。
如果你觉得这篇文章对你有帮助,不妨动动手指给点赞收藏加转发,给鄃鳕一个大大的关注你们的每一次支持都将转化为我前进的动力!!!