模板template
模板
概念
模板是C++支持参数化多态的工具,使用模板可以使用户为类或者函数声明一种一般模式,使得类中的某些数据成员或者成员函数的参数、返回值取得任意类型,分为函数模板和类模板
❗注意:
-
函数模板和类模板都不支持把声明写在头文件(.h),定义写到源文件(.cpp)中,因为会引发Link Error,最好写到一起。具体原因就是模板的编译分离:http://t.csdn.cn/OCyHh和http://t.csdn.cn/6ju4e
-
函数模板和类模板只有在实例化时才会创建存放该函数或者对象的内存
函数模板
函数模板的定义
template<typename T> //声明一个模板,T为一般类型
void MySwap(T& a, T& b){
T tmp = a;
a = b;
b = tmp;
}
🌸说明:
- 上面函数模板声明时的 typename 也可以换成 class
- 函数模板用 typename,类模板用 class (也可以全都用 class )
函数模板的使用
template<class T>
T func(){
cout << "this is func " << endl;
}
void test()
{
func(); //error,没有与参数列表匹配的函数模板 "func" 实例
func<void>();
}
❗注意:函数模板必须要明确指出数据类型才可以使用
案例
设计一个模板,可以将数组的元素排序
template<class T>
void SelectSort(T* arr, int len){
T max = 0;
for (int n = len - 1; n > 0; n--){
for (int i = 0; i <= n; ++i){
if (arr[max] < arr[i]){
max = i;
}
}
MySwap<T>(arr[max], arr[n]);
max = 0;
}
}
函数和函数模板的区别
函数可以发生隐式类型转化,后者不行
int add1(int a,int b){
return a + b;
}
template<class T>
T add2(T a, T b){
return a + b;
}
void test(){
int a = 10;
char b = 'b';
add1(a, b); //可以运行,因为b在函数中隐式转换成了int类型
//隐式实例化(自动推导类型)
add2(a, b); //error,说明隐式实例化不会发生类型转换
//注意:隐式实例化只能是相同类型
add2(2.0, 2); //error,2.0是double类型,而2是int
add2(10, 20); //30
//显式指定类型
add2<int>(a, b); //没有报错,说明可以发生隐式类型转换
}
普通函数与函数模板的调用规则
- 如果函数模板和普通函数都可以实现,优先调用普通函数。如果此时普通函数只有声明没有定义,那么会报错
- 可以通过空模板参数列表来强制调用函数模板
- 如果函数模板可以产生更好的匹配,优先调用函数模板
void myPrint(int a, int b){
cout << "调用的是普通函数" << endl;
}
template<class T>
void myPrint(T a,T b){
cout << "调用的是模板函数" << endl;
}
void test(){
int a = 0, b = 0;
myPrint(a, b); //调用的是普通函数
myPrint<>(a, b); //空模板,调用的是模板函数
char c = 'c';
char d = 'd';
myPrint(c, d); //调用的是模板函数
//编译器会觉得将char隐式转换成int太麻烦,不如使用模板推导
}
🌸总结:
- 现成 > 半匹配 > 转换
- 提供了函数模板后就尽量不要再提供普通函数
函数模板的重载
函数模板也可以发生重载
template<class T>
void myPrint(T a,T b){
cout << "调用的是模板函数" << endl;
}
void myPrint(T a, T b, T c){
cout << "调用的是重载的模板函数" << endl;
}
指定类型少于模板参数的情况
template<typename T1, typename T2, typename T3>
void add(T1 x, T2 y, T3 z){
cout << typeid(x).name() << " ";
cout << typeid(y).name() << " ";
cout << typeid(z).name() << " ";
}
void test(){
add<int>(1, 2.0, 3); //int double int
add<char>(2, 3.0, 'a'); //char double char
add<int, char>(1, 2, 'a'); //int char char
add<int, char>('a', 2.0, 1); //int char int
//报出警告double强转为char
}
🌸由上面的例子我们知道:
- 函数显示指明类型 add<int, char> 就相当于 T1 = int ,T2 = char,但是对于 T3,其会根据传入的参数自动推导类型
- 如果参数类型和指明的类型不符会发生强制转换
类模板
类模板的定义和使用
template<class name,class age = int> // < >里面的叫模板参数列表
class Person {
public:
Person(name name, age age):age(age),name(name) {}
name name;
age age;
void showperson(){
cout << "age:" << age;
cout << "name:" << name << endl;
}
void showperson1();
};
template<class name, class age> //类外定义的函数必须重新声明模板
void Person<name, age>::showperson1(){
cout << "showperson1" << endl;
}
void test(){
Person<string, int>p1(10, "张三");
p1.showperson();
}
类模板和函数模板的区别
接上面的代码:
void test(){
//1. 类模板没有自动类型推导
Person p2(10, "张三"); //error
//类模板只能显式实例化,显式实例化的类型不同,类不同
Person<string, int> p2(10, "孙悟空");
//2. 类模板在模板参数列表中可以有默认参数
Person<string> p3(10, "猪八戒"); //模板中的age默认为int类型
Person<string> p4('c', "李四"); //模板中可以发生隐式转换
}
类模板的类名(类的类型)
template<class T>
class A {}; //这里A不是类名,A<T>才是,或者说实例化结果才是类名
类模板中的成员函数
类模板中的成员函数只有在调用时才会创建,即分配内存(普通类的成员函数一开始就创建了)
class example1 {
public:
void show1(){
cout << "show1" << endl;
}
};
class example2 {
public:
void show2(){
cout << "show2" << endl;
}
};
template<class T>
class example3 {
public:
T a;
void show3(){
a.show1();
}
void show4(){
a.show2();
}
};
//当代码运行到这里时,并不会报错
//因为此时模板类并不知道a的数据类型,所以类模板中的成员函数还没有创建
void test(){
example3<example2> p;
p.show3(); //error
p.show4(); //不会报错,因为没有调用show3()
}
类模板实例化对象向函数传参
一共有三种传入方式:
- 指定传入的类型:直接显示对象的数据类型
- 参数模板化:将对象中的参数变为模板进行传递
- 整个类模板化:将这个类模板化进行传递
template<class T1,class T2>
class animal {
public:
T1 age;
T2 string;
animal(T1 age,T2 name):age(age),name(name){}
};
//指定传入的类型
void func(animal<int, string> p);
//参数模板化
template<class T1,class T2>
void func(animal<T1, T2>& p); //相当于函数模板配合类模板
//整个类模板化
template<class T>
void func(T& p){
cout << typeid(T).name() << endl;
}
void test(){
animal<int, string> p(10, "cat");
func(p);
}
非类型模板参数
非类型实参:用一个常量作为模板的一个参数,在模板中可以作为常量使用,可以用来开辟数组等
❗注意:这个常量不能是浮点数、类对象和字符串
template<class T, size_t N = 10>
class Stack{
public:
size_t GetSize(){
return N;
}
private:
T a[N] = { 0 }; //这里的N是常量,默认为缺省值10
};
void test(){
cout << Stack<int>().GetSize(); //匿名对象的用法
Stack<int, 20> st;
cout << st.GetSize(); //20,如果有参数,那么就不会用缺省值
}
❗注意:非类型的模板参数必须在编译时就能确定结果
类模板与继承
template<class T>
class base {
public:
T a;
};
//继承时父类必须要说明参数列表
class son :public base //error,父类中缺少参数列表
class son:public base<int> {
public:
int b;
};
//如果我们想要灵活的指定父类的参数列表,那么子类也要是类模板
template<class T1,class T2>
class son1:public base<T1> {
public:
T2 a;
};
void test(){
son1<int, char> p;
}
类模板成员函数类外实现
template<class T1,class T2>
class tap {
public:
T1 a;
T2 c;
tap(T1, T2);
void func(T1, T2);
};
template<class T1, class T2> //需要重新声明模板
//tap::tap(T1 age, T2 name) //error
tap<T1, T2>::tap(T1 age, T2 name){ //构造函数
this->a = age;
this->c = name;
}
template<class T1, class T2>
void tap<T1,T2>::func(T1 age,T2 name){ //成员函数
this->a = age;
this->c = name;
}
模板文件编写
book.h文件:
#pragma once
#include <iostream>
#include <string>
using namespace std;
template<class T1,class T2>
class book {
public:
book(T1 price, T2 name);
void book_show();
private:
T1 price;
T2 name;
};
book.cpp文件
#include "book.h"
template<class T1,class T2>
book<T1, T2>::book(T1 price, T2 name){
this->name = name;
this->price = price;
}
template<class T1, class T2>
void book<T1, T2>::book_show(){
cout << "name:" << this->name;
cout << " price:" << this->price << endl;
}
main.cpp文件:
#include "book.h"
int main(){
book<int, string> p(20, "小王子");
p.book_show(); //error
}
代码运行时会链接错误,因为函数定义是在 book.cpp 中,而我们只包含了 book.h 文件,解决方法:
//1. main.cpp中直接引用book.cpp文件
#include "book.cpp"
//2(建议). 创建一个book.hpp文件,里面放类的声明和成员函数的定义
#include "book.hpp"
book.hpp文件:
#pragma once
#include <iostream>
#include <string>
using namespace std;
template<class T1, class T2>
class book {
public:
book(T1 price, T2 name);
void book_show();
private:
T1 price;
T2 name;
};
template<class T1, class T2>
book<T1, T2>::book(T1 price, T2 name){
this->name = name;
this->price = price;
}
template<class T1, class T2>
void book<T1, T2>::book_show(){
cout << "name:" << this->name;
cout << " price:" << this->price << endl;
}
模板的特化
模板的特化:模板的特殊化,也就是针对一些类型特殊处理。比如比较函数,比较内置类型(int、double等)都差不多,都是看值相不相等,但是如果是比较两个对象,此时就得对其进行专门的处理
函数模板特化
使用情景
class Person {
public:
int age;
string name;
Person(int a,string str):age(a),name(str){}
};
template<class T>
bool cmp_person(T& p1,T& p2){
return p1 == p2; //如果传入的是person对象,error
}
解决方法:模板特化
template<> //模板特化也就是模板重载版本(函数重载)
bool cmp_person<Person>(Person& p1, Person& p2)
{
return p1.age == p2.age &&
p1.name == p2.name;
}
void test(){
Person p1(10, "小明");
Person p2 = p1;
if (cmp_person(p1, p2)) //此时可以对两个person对象比较
{
cout << "相等" << endl;
}
}
函数模板特化步骤
🌸函数模板的特化步骤:
- 先有一个基础的函数模板
- 关键字 template 后面接一对空的尖括号 <>
- 函数名后跟一对尖括号,尖括号中指定需要特化的类型
- 函数形参表:必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误
❗注意:一般情况下遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出**(即函数模板不常用)**
用函数模板特化进行字符串比较:
template<typename T>
bool IsEqual(T& a, T& b){
return a == b;
}
template<> //函数模板的特化
bool IsEqual<const char*>(const char*& a, const char*& b){
return strcmp(a, b);
}
void test(){
const char* a = "hello world";
const char* b = "hello world";
cout << IsEqual(a, b);
}
🌸对于上面的例子:
- 如果没有写第二个模板的特化,那么默认就是比较a和b的地址,这样显然不对
- 当我们写了模板的特化后,就会默认调用第二个函数来比较 a 和 b
- 这里调用第二个特化模板是因为第二个比第一个模板更加匹配
- 上面的例子不太合理,因为如果 a 和 b 的字符串内容一样,那么a和b的地址应该相同,因为常量字符串只有一份
其他注意情况
- 函数模板不支持半特化
template<typename T1, typename T2>
void add(T1 a, T2 b) {}
template<class T> //这里只能是 < >,里面不能有参数(类模板除外)
void add<int, T>(int x, T y){} //error
//正确做法
template<>
void add<int>(int x, int y){}
- 特例化指明的类型在参数列表中都要用到
template<typename T1, typename T2>
void add(T1 a, T2 b) {}
template<>
void add<int, char>(int x, int y){} //error,这里的char类型没有用到
对于上面的代码还有一种解释:因为我们已经指明 T2 = char,而函数参数 y 的类型为 int ,函数参数列表必须要和模板函数的基础参数类型完全相同
❗注意:下面的代码没有报错,但不代表其他的就不会报错
template<>
void add<const int, char>(int x, char y){}
- 当指明类型少于模板参数时的情况
template<class T1, class T2, class T3>
void test1(T1 x, T2 y, T3 z){}
template<>
void test1<int>(int x, char y, double z){} //没有报错
//这里我们只指明了T1=int,而 T2、T3 我们没有指明
//所以 T2、T3 可以直接在特化模板参数列表中说明
void test1<int, string>(int x, char y, string z) {}
//error,因为这里我们指定的 T2 为 string 类型
//但是我们参数列表中 y 的类型为 char
//因为我们没有指定 T3 的类型,所以 z 的类型可以在参数列表中说明
template<class T1, class T2>
void tmp(T1 x, T2 y, T1 z) {}
template<>
void tmp<int>(int x, string y, char z) {}
//error,因为我们指定了 T1 类型为 int ,那么 x 和 z 的类型必须为 int
//而z在参数列表中的类型为char
template<>
void tmp<int, char>(int x, char y, int z){} //不会报错
template<>
void tmp<>(int x, char y, int z) {}
//我们就算<>里什么都不写也不会报错(此时这里的<>可以省略)
🌸总结:我们指定了类型的模板参数,其在参数列表中类型必须要和指定类型一致,我们没有指定的,可以在参数列表中自己定义
- 模板参数必须要和特例化模板的参数个数一样,并且两个函数的返回值类型一样
template<class T1, class T2>
void add(T1 x, T2 y) {}
template<>
void add<int>(int x){} //error,缺少参数y
template<>
int add<int, char>(int a, char b){}
//error,返回值类型必须和原模板一样
- 关于 const 修饰
template<class T>
void cmp(const T& a,const T& b){}
template<>
void cmp<const char*>(const char* const& a,const char* const& b){}
//这里必须要有两个 const,不然会报错
//上面第二个 const 是针对函数模板参数中的 const T&
template<>
void cmp<char*>(const char*& x, const char*& y) {} //error
template<>
void cmp<char*>(char* const& x,char* const& y){} //没有报错
//一般不要给T加修饰,T本身就可以代表类型
template<class T>
void fun(const T* x, const T* y){}
template<>
void fun<int>(int* const x, int* const y){} //error,莫名奇妙的错
template<>
void fun<int>(int const* x,int const* y){} //莫名奇妙的正确
类模板特化
全特化
全特化:将模板参数列表的所有参数类型都确定
template<class T1, class T2>
class temp{
public:
temp() { cout << "temp<T1, T2>"; }
private:
T1 tmp1;
T2 tmp2;
};
template<>
class temp<int, char>{
public:
temp() { cout << "temp<int, char>"; }
private:
int tmp1;
char tmp2;
};
void test(){
temp<double, string> t1; //temp<T1, T2>
temp<int, char> t2; //temp<int, char>
}
偏特化
类模板的偏特化不仅仅是特化部分参数,而是针对模板参数更近一步的条件限制,所设计出来的一个特化版本
template<class T1, class T2>
class emp{
public:
emp() { cout << "基础模板"; }
private:
T1 elm1;
T2 elm2;
};
//两个参数偏特化为指针类型
template<class T1, class T2>
class emp<T1*, T2*>{
public:
emp() { cout << "指针模板"; }
private:
T1 elm1;
T2 elm2;
};
//两个参数偏特化为引用类型
template<class T1, class T2>
class emp<T1&, T2&>{
public:
emp(const T& a, const T& b) :elm1(a), elm2(b)
{ cout << "引用模板"; }
private:
const T1& elm1;
const T2& elm2;
};
//一个指定参数
template<class T>
class emp<int, T>{
public:
emp() { cout << "int模板"; }
private:
int elm1;
T elm2;
};
void test(){
emp<int*, int*> e1; //指针模板
emp<int&, int&> e2(1, 3); //引用模板(由于引用必须初始化,所以这里需要传参)
emp<int, string> e3; //int模板
emp<string, char> e4; //基础模板
}
❗注意:
- 如果需要传引用的话,这时候 T& 版本必须自己实现,指针的话没必要, 编译器会自动推导
- 这里特例化出指针和引用的版本是为了对参数进一步的限制