C++初阶(类模板+模板函数)
引入
void mySwapint(int &a, int &b)
{
int tmp = a;
a = b;
b = tmp;
}
void mySwapdouble(double& a, double& b)
{
double tmp = a;
a = b;
b = tmp;
}
不知道大家看到这么几个代码心里是什么感觉,有没有觉得这里一直在写重复的代码,看起来十分地不舒服,只要出现一个新的类型的数互换,就要重新写一段代码,显得很麻烦。这样的代码总结就是:复用性低 和 可维护性差。
为了提高代码的复用性,C++提出了泛型编程,也就是写一种与类型无关的代码,提高代码的复用性。或者说是提供一个模板,让不同类型的代码可以根据模板代码生成与其对应的代码。
模板又分为函数模板和类模板。下面就来给大家一一介绍。
函数模板
概念: 函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
写法: template<class T1, class T2…>(其中,也可以用typename来代替class,T代表的是一种类型)
交换两个数的函数模板可以如下写出:
template<class T> // typename
void Swap(T& left, T& right)
{
T tmp = left;
left = right;
right = tmp;
}
原理
模板原理: 函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。
函数生成阶段: 编译的预处理阶段。
编译器根据传入的实参来推演实例化出对应的函数和类。
模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
template<class T> // typename
void Swap(T& left, T& right)
{
T tmp = left;
left = right;
right = tmp;
}
1.隐式的实例化
int main()
{
int a = 10;
int b = 20;
Swap(a, b);//编译器在函数模板被调用的时候,进行第二次编译
/*
void mySwap(int &a,int &b)//第一次编译
{
T tmp = a;
a = b;
b = tmp;
} 二次编译的时候,会把第一次编译的T换成具体的int类型
*/
}
2.显示的实例化
int main()
{
int a = 10;
int b = 20;
Swap<int>(a, b);//用参数列表告诉编译器我只传int类型
//Swap<double>(a,b)报错,a和b都是int类型,但是你传入doule的列表
//指定了类型,传入的的时候不能不一致
Swap<>(a,b);//不写<>里面的东西也可以
//Swap2<>();err 调用的时候必须让编译器知道泛型T具体是什么类型
}
模板参数匹配原则
//普通函数
void myPlus(int a,int b)
{}
//1.函数模板和普通函数可以重载
template<class T>
void myPlus(T a,T b)
{
return a+b;
}
template<class T>
void myPlus(T a)
{
return a;
}
void test
{
int a = 10;
int b = 20;
//2.如果普通函数和函数模板都可以实现功能,普通函数优先调用
myPlus(a, b);
//3.可以使用<>空参数列表强制调用函数模板
myPlus<>(a, b);
//4.如果函数模板可以产生更好的匹配,那么优先调用函数模板
char c1 = 'a';
char c2 = 'b';
myPlus(c1, c2);
//模板函数不允许自动类型转换
myPlus(6,3.2);//模板不允许自动类型转化,如果有不同类型参数,只允许使用非模板函数,因为普通函数可以进行自动类型转换,将 3.2 转换成了 int 类型再与 6 进行比较
}
- 模板函数不允许自动类型转换
使用模板函数要注意的问题
<>
中的每一个类型参数在模板函数参数列表中必须至少使用一次。
template<typename T1, typename T2>
void func(T1 t)
{}
函数模板声明了两个参数 T1 与 T2,但在使用时只使用了 T1,没有使用 T2,这种模板函数的声明就是不正确。
- 全局作用域中声明的与模板参数同名的对象、函数或类型,在函数模板中将被隐藏。
int num;
template<typename T>
void func(T t)
{
T num;
cout<<num<<endl; //输出的是局部变量num,全局int类型的num被屏蔽
}
在函数体内访问的 num 是 T 类型的变量 num,而不是全局 int 类型的变量 num。
- 模板参数名在同一模板参数列表中只能使用一次,但可在多个函数模板声明或定义之间重复使用。
template<typename T, typename T> //错误,在同一个模板中重复定义模板参数
void func1(T t1, T t2){}
template<typename T>
void func2(T t1){}
template<typename T> //在不同函数模板中可重复使用相同的模板参数名
void func3(T t1){}
- 模板的定义和多处声明所使用的模板参数名不是必须相同。
//模板的前向声明
template<typename T>
void func1(T t1, T t2);
//模板的定义
template<typename U>
void func1(U t1, U t2)
{}
- 如果函数模板有多个模板参数,则每个模板参数前都必须使用关键字 class 或 typename 修饰。
template<typename T, typename U> //两个关键字可以混用
void func(T t, U u){}
template<typename T,U> //错误,每一个模板参数前都必须有关键字修饰
void func(T t, U u){}
类模板
用法:类模板就是把类中的数据类型参数化
template<class T1, class T2,…>
class 类模板名
{};
template<class NameType, class AgeType>
class Maker
{
public:
Maker(NameType name, AgeType age)
{
this->name = name;
this->age = age;
}
public:
NameType name;
AgeType age;
}
类模板的使用以及注意事项
- 类模板的使用
//1.类模板不会自动推到函数类型,要显示的告诉编译器是什么类型
Maker<string, int>m("翠花",18);
//2.注意传入的参数,传入参数类型要程序员自己把握
Maker<int ,int> m2("18","20");
//Maker<> m3("aaa",18);err,必须通过参数列表告诉编译器什么类型
类模板和模板函数的区别:
类模板不会自动推导数据类型,要显示的告诉编译器是什么类型,而模板函数可以根据实参来推导数据类型
- 类模板的默认参数
template<class NameType ,class AgeType = int>
class Maker2
{
public:
Maker2(NameType name, AgeType age)
{
this->name = name;
this->age = age;
}
public:
NameType name;
AgeType age;
};
void test()
{
//如果有默认类型,那么<>里面可以少写类型
Maker2<string> m("翠花",20);
//以传入的为标准
Maker2<string ,double> m2("小明",20.22);
}
//默认参数类型的后面泛型类型都必须是默认参数
template<class NameType ,class AgeType = int,class T = int>
class Maker3
{};
函数模板作为类模板成员
类模板中的成员函数还可以是一个函数模板。成员函数模板只有在被调用时才会被实例化
#include <iostream>
using namespace std;
template <class T>
class A
{
public:
template <class T2>
void Func(T2 t) { cout << t; } //成员函数模板
};
int main()
{
A<int> a;
a.Func('K'); //成员函数模板Func被实例化
a.Func("hello");
return 0;
}
类模板作为函数参数
template<class NameType, class AgeType>
class Maker
{
public:
Maker(NameType name, AgeType age)
{
this->name = name;
this->age = age;
}
public:
NameType name;
AgeType age;
}
//1.指定传入的数据类型
void func(Maker<string ,int> &m)
{}
//2.参数模板化(常用)
template<class T1,class T2>
void func2(Maker<T1,T2> &m)
{}
//3.整个类模板化
template<class T>
void func3(T &m)
{}
类模板的继承
1.普通类继承类模板
//普通类继承类模板
template<class T>
class Father
{
public:
Father()
{
m = 20;
}
public:
T m;
};
//普通类,Faher要告诉编译器父类的泛型数据类型具体是什么类型
class Son :public Father<int>
{};
2.类模板继承类模板
//类模板继承类模板
template<class T1,class T2>
//要告诉编译器父类的泛型数据类型具体是什么类型
class Son2 :public Father<T2>
{};
- 必须告诉编译器父亲的泛型类型是什么数据类型
类模板成员函数类外实现
template<class NameType, class AgeType>
class Maker
{
public:
//类内实现
Maker(NameType name, AgeType age);
/*
{
this->name = name;
this->age = age;
}
*/
void PrintMaker();
/*
{
cout << this->name << this->age << endl;
}
*/
public:
NameType name;
AgeType age;
};
//类模板的类外实现
//要写成函数模板
template<class NameType,class AgeType>
Maker<NameType, AgeType>::Maker(NameType name, AgeType age)
{
cout << "构造函数" << endl;
this->name = name;
this->age = age;
}
template<class NameType, class AgeType>
void Maker<NameType, AgeType>::PrintMaker()
{
cout << this->name << this->age << endl;
}
非类型模板参数
模板参数分类类型形参与非类型形参。
- 类型模板形参:出现在模板参数列表中,跟在class或者typename后面的参数类型名称。(这个我们之前有讲过)
- 非类型模板形参:用一个常量作为模板的一个参数,必须是整形家族中的类型参数,否则不行。他在模板中可以当常量使用。
// 类型模板参数
namespace wxj
{
// 非类型模板参数 N 是一个常量参数,只能是整形家族的:int short char long long long 自定义类型和其他类型都不能作费类型模板参数
// 必须在编译期就能确认结果
template<class T, size_t N = 10>
class Array
{
public:
Array()
:_size(N)
{}
T& operator[](size_t i)
{
return _arr[i];
}
const T& operator[](size_t i) const
{
return _arr[i];
}
size_t size()
{
return _size;
}
bool empty()
{
return _size == 0;
}
private:
T _arr[N];
size_t _size;
};
void TestArray()
{
Array<int, 5> arr;
for (size_t i = 0; i < arr.size(); ++i)
{
arr[i] = i;
}
for (size_t i = 0; i < arr.size(); ++i)
{
cout << arr[i] << " ";
}
cout << endl;
}
}
int main()
{
wxj::TestArray();
return 0;
}
注意:
- 非类型形参必须是整形家族中的类型,浮点数和类对象都不行。
- 非类型的模板形参必须在编译期间就能确认结果。
模板的特化
模板特化:在原模板类的基础上,针对特殊类型所进行的特殊化的实现。分为函数模板特化 和类模板特化。
函数模板的特化
特化的步骤
- 必须先有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对尖括号<>,里面指定需要的特化的类型
- 函数形参列表:必须和函数模板的基础参数类型完全一致
// 模板的特化 模板的特殊化
template<class T>
bool IsEqual(T& left, T& right)
{
return left == right;
}
// 特化 针对某些类型进行特殊化处理
template<>
bool IsEqual<const char* const>(const char* const& left, const char* const& right)
{
return strcmp(left, right) == 0;
}
注意: 一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。
bool IsEqual(char* left, char* right)
{
return strcmp(left, right) == 0;
}
类模板的特化
类模板的特化分为全特化和偏特化。
全特化: 对类模板参数列表的类型全部都确定(明确指定)
template <class T1, class T2>
class Date
{
public:
Date()
{
cout << "Date<T1, T2>" << endl;
}
private:
T1 _d1;
T2 _d2;
};
// 全特化
template<>
class Date<int, double>
{
public:
Date()
{
cout << "Date<int, double>" << endl;
}
private:
int _d1;
double _d2;
};
偏特化: 堆类模板的参数列表中部分参数进行确定化分为部分特化和参数进一步限制
- 部分特化
// 部分
template<class T2>
class Date<int, T2>
{
public:
Date()
{
cout << "Date<int, T2>" << endl;
}
private:
int _d1;
T2 _d2;
};
2.参数进一步限制 如下有T*和T&,是模板的类型转为指针类型和引用类型
// 参数进一步限制 堆模板参数更进一步的条件限制
template <class T1, class T2>
class Date<T1*, T2&>
{
public:
Date(int& a)
:_d2(a)
{
cout << "Date<T1*, T2&>" << endl;
}
private:
T1* _d1;
T2& _d2;
};
实例:我们试着实例化几个对象,看他们用的是哪个模板
int main()
{
Date<int, int> d1;
Date<int, double> d2;
Date<int, float> d3;
int a = 10;
Date<int*, int&> d4(a);
return 0;
}
模板的分离编译
分离编译: 我们对这个应该是不陌生的,就是把函数的声明放在一个叫.h的文件中,实现都放在一个叫.cpp的文件中,这样方便我们管理。
结论:
- 函数模板不能分离编译,普通函数可以
- 类模板中的成员函数,只有源程序代码中出现调用这些成员函数的代码时,这些成员函数才会出现在一个实例化了的类模板中
- 类模板中的成员函数模板,只有源程序代码中出现调用这些成员函数模板的代码时,这些成员函数模板的具体实例化才会出现在一个实例化了的类模板中
- 总而言之一句话,模板只有被使用的时候才会被实例化
类模板和友元
1.友元类内实现
//类模板
template<class NameType, class AgeType>
class Maker {
//这不是成员函数,这是友元函数
friend void printMaker(Maker<NameType, AgeType> &p)
{
cout << "类内实现" << p.name << p.age << endl;
}
public:
Maker(NameType name, AgeType age);
{
this->name = name;
this->age = age;
}
private:
NameType name;
AgeType age;
};
void test01()
{
Maker<string, int> m("悟空", 18);
printMaker(m);
}
2.友元类外实现
//友元函数类外实现
//告诉编译器有printMaker2函数的实现,声明一下函数
//友元在类外实现要写成函数模板
template<class NameType, class AgeType>
class Maker2;
template<class NameType, class AgeType>
void printMaker2(Maker2<NameType, AgeType>& p);
template<class NameType, class AgeType>
class Maker2 {
//1.在函数名和括号之间加上<>参数列表
friend void printMaker2<>(Maker2<NameType, AgeType> &p);
//2.编译器不知道这个函数下面有没有实现,需要知道函数结构
public:
Maker2(NameType name, AgeType age);
{
this->name = name;
this->age = age;
}
private:
NameType name;
AgeType age;
};
template<class NameType, class AgeType>
void printMaker2(Maker2<NameType, AgeType>& p)
{
cout << p.name << p.age << endl;
}
void test02()
{
Maker<string, int> m("悟空", 18);
printMaker(m);
}