C++语言的特性之一:它支持泛型编程,而泛型编程主要就是利用了模板来实现
举几个模板的例子:
1.我们设计了一个容器类用来存储其他对象或值,例如一个队列,当我们要存储不同数据类型,甚至是自定义类型时,如果没有模板的话,我们需要给每一个数据类型写一个队列的类。当我们使用模板以后,我们就能将具体的类型作为参数传递给这个模板类,就能用不同的代码生成存储不同类型值的队列。这就是模板类
2.我们都知道一个函数swap,用来交换两个数。同样,当我们要交换不同数据类型,甚至时自定义类型时,我们就需要模板函数,只需每次输入不同参数,就能实现一个函数被用在不同数据类型中。这就是函数模板
模板的概念:
分类
模板分为:模板类和函数模板两大类。
使用
在使用时,先声明,接下来的类或者函数为模板:
template<typename Type>
1.typename 可用 class代替,因为在c++11中才提供了不易混淆的typename关键字。
2.其中Type是将类型参数化
3.template是模板的意思
4.两种使用方式:1.自动类型推导:必须推导出一致的类型T才行。
2.模板必须确定出T的类型才可以使用。
模板的局限性以及模板重载
#include<iostream>
#include<string>
using namespace std;
//学习模板并不是为了写模板,而是在STL中能够使用系统提供的模板
//当a ,b是一个数组时,就无法实现了,无法数组b给a赋值
template<typename T>
void f(T a, T b) {
a = b;
}
template<typename T>
void com(T a, T b) {
if (a > b) {
}
}
//如果T的数据类型为Person这样的自定义数据类型,也无法正常运行
template<typename T>
bool compare(T &a, T &b) {
if (a == b) {
return true;
}
else {
return false;
}
}
void test01() {
int a = 10;
int b = 10;
bool ret = compare(a, b);
if (ret) {
cout << "a == b" << endl;
}
else {
cout << "a != b" << endl;
}
}
//因此提供了模板的重载,可以给特定的类型提供具体化的模板
//利用具体化Person的版本实现代码,具体化优先调用
class Person {
public:
Person() :m_Name("张三"), m_Age(18) {
}
Person(string name, int age) {
this->m_Age = age;
this->m_Name = name;
}
string m_Name;
int m_Age;
};
template<> bool compare(Person& p1, Person& p2) {
if (p1.m_Name == p2.m_Name && p1.m_Age == p2.m_Age) {
return true;
}
else {
return false;
}
}
void test02() {
Person p1("Tom", 10);
Person p2("Tom", 10);
bool ret = compare(p1, p2);
if (ret) {
cout << "p1 == p2" << endl;
}
else {
cout << "p1 != p2" << endl;
}
}
int main() {
test01();
test02();
return 0;
}
为了满足函数模板对于自定数据类型(除此之外还有内置数据类型)的使用,提供了模板的重载,如在比较两个类时,可以重载比较的函数。如代码里的compare()函数。 该过程也称为模板的具体化,方法是在重载的函数前加上template<>,即可
模板类:
模板类的结构
1.类模板以下面这样的代码开头:
template<typename Type>
关键字template告诉编译器,接下来要定义一个模板。Type叫做泛型标识符,泛型标识符也可用其他的字符替代,我们常用T来表示。这个也称为类型参数,意味着它类似于变量,但是赋给他们的只能是类型,而不能是数字、字符等。
当类有两个成员时,可以在开头这样声明:
template<typename T1, typename T2>
模板类的使用方法
- 类模板没有自动类型推导
- 类模板在模板参数列表中可以有默认参数
在写了模板类的开头以后,用Type(或者T,看自己选择哪一个),多个成员时T1,T2.....替代类中的参数类型名。而且在开头中可以写类模板的默认参数:
template<class NameType, class AgeType = int>
之后在创建类对象时候,,如果用有参构造函数创建,需要在类名后的<>加入参数的类型名,如Person类,两个参数,一个string类型的m_Name,一个int类型的m_Age:
Person<string, int> p1("孙悟空", 999);
另外就是无自动类型推导这个注意事项:
void test01() {
//让编译器自动类型推导,会报错
//Person p("孙悟空", 1000);
//因此我们显示指定类型
Person<string, int> p("孙悟空", 1000);
p.showPerson();
}
类模板对象做函数参数
类模板实例化出的对象,向函数传参的方式
三种方式
1.指定传入的类型:直接显示对象的数据类型 如:将函数参数设为:(Person<string ,int> &p)
void printPerson(Person <string, int> &p) {}
2.参数模板化:将这个对象中的参数变为模板进行传递
template <typename T1, typename T2>
void printPerson2(Person<T1, T2> &p){}
3.整个类模板化:将这个对象类型模板化进行传递
template <typename T>
void printPerson3(T &p)
在我们日常使用时,指定传入类型使用的多
#include<iostream>
using namespace std;
template<class T1,class T2>
class Person {
public:
Person(T1 name, T2 age) {
this->m_Age = age;
this->m_Name = name;
}
void showPerson() {
cout << "name:" << this->m_Name << endl;
cout << "age:" << this->m_Age << endl;
}
T1 m_Name;
T2 m_Age;
};
//1.指定传入的类型: 直接显示对象的数据类型
// 也就是用一个函数,函数类型为一个类,然后在函数中
// 用传入的类,来调用类中的函数。
void printPerson(Person <string, int> &p) {
p.showPerson();
}
void test01() {
Person <string, int>p1("张三", 18);
printPerson(p1);
}
// 2.参数模板化: 将这个对象中的参数变为模板进行传递(像函数模板)
template <typename T1, typename T2>
void printPerson2(Person<T1, T2> &p) {
p.showPerson();
cout << "T1的类型为:" << typeid(T1).name();
cout << "T2的类型为:" << typeid(T2).name();
}
void test02() {
Person<string, int> p2("猪八戒", 999);
printPerson2(p2);
}
// 3.整个类模板化 将这个对象类型模板化进行传递
template <typename T>
void printPerson3(T &p) {
p.showPerson();
cout << "T的类型为:" << typeid(T).name();
}
void test03() {
Person<string, int > p3("唐僧", 18);
printPerson3(p3);
}
int main() {
test01();
test02();
test03();
return 0;
}
类模板成员函数类外实现
在类外实现时,如本例中构造函数:
Person<T1,T2>::Person(int age, string name) {.....}
除作用域本就需要加外,还需加模板参数列表,并写入类有参构造函数的参数类型。
template<typename T1,class T2>
class Person
{
public:
//构造函数类外实现
Person(int age, string name);
/*this->m_Age = age;
this->m_Name = name;*/
void showPerson();
/*cout << "姓名:" << this->m_Name << endl;
cout << "年龄:" << this->m_Age << endl;*/
T1 m_Name;
T2 m_Age;
};
template<typename T1, class T2>
Person<T1,T2>::Person(int age, string name) {
this->m_Age = age;
this->m_Name = name;
}
template<typename T1, class T2>
void Person<T1, T2>::showPerson() {
cout << "姓名:" << this->m_Name << endl;
cout << "年龄:" << this->m_Age << endl;
}
类模板与友元
全局函数类外声明:
1.需要加空模板参数列表
2.如果全局函数 是 类外实现,需要让编译器提前知道这个函数的存在。
//类外实现
//1.多出了4-10这几行代码
//2.需要在声明中加入空模板参数列表
template<class T1, class T2>
class Person;
template<class T1, class T2>
void printPerson2(Person<T1, T2>& p) {
cout << "姓名 " << p.m_Name << " 年龄 " << p.m_Age << endl;
}
template<class T1, class T2>
class Person {
public:
Person(T1 name, T2 age) {
this->m_Age = age;
this->m_Name = name;
}
//全局函数类内声明
friend void printPerson( const Person<T1, T2> &p){
cout << "姓名 " << p.m_Name << " 年龄 " << p.m_Age << endl;
}
//全局函数类外声明
friend void printPerson2<>(Person<T1, T2>& p);
//通过类内的全局函数打印
//void showPerson() {
// cout << "name:" << this->m_Name << endl;
// cout << "age:" << this->m_Age << endl;
//}
private:
string m_Name;
int m_Age;
};
类模板分文件编写
类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到
解决方法:直接包含.cpp源文件,而不是包含.h文件。需要让编译器看得到.cpp文件才行,因为.h中类模板中函数只声明,定义的话在调用时候才有,而且我们的.cpp文件中包含着.h文件。也就是将声明和实现写到同一个文件中,并更改后缀.cpp为.hpp,hpp是约定的名称,并不是强制要求。不分开写了,而且该文件后缀用.hpp,用的时候包含.hpp文件
分文件编写:
.h 文件和.cpp文件分开
函数模板:
设定的模板仅且必须作用于紧跟着的下一个函数,因此下一个函数必须参与T的确定
两种使用方式举例:
1.自动类型推导:必须推导出一致的类型T才行。
如该例:c 的类型为 char, 而a、b的类型为int,当比较a、c或者b、c的时候就会出错
template<typename T>
void myswap(T &a, T &b) {
T temp = a;
a = b;
b = temp;
}
void test01() {
//必须推导出一致的类型T才行
int a = 10;
int b = 20;
char c = 'a';
//1.自动类型推导
myswap(a, c);
//2.显示指定类型
myswap<int>(a, b);
cout << a << endl;
cout << b << endl;
}
2:模板必须确定出T的类型才可以使用。
//模板必须确定出T的类型才可以使用。
template<class T>
void func(){
cout << "func函数的调用";
}
void test02() {
//func(); //报错,此时虽然没有用T,但是func()仍然是一个函数模板,需确定T的类型
func<int>(); //显示指定类型后不报错
}
函数模板的调用规则
调用规则:
1.如果普通函数和函数模板都可以实现,优先调用普通函数
2.可以通过空模板参数列表来强制调用函数模板
即在调用时,显示指定为空
myPrint<>(a, b);
3.函数模板也可以发生重载
4.如果函数模板可以产生更好的匹配,优先调用函数模板
如实例代码中,c1,c2是字符类型,若使用普通函数还需要转换成Ascll码,但使用模板函数不需要转换,因此会调用函数模板
如果提供了函数模板,最好不要提供普通函数,否则容易出现二义性。
使用时,建议使用指定类型的方式,调用函数模板。因为我们自己输入参数时可以确定通用类型T。
#include<iostream>
using namespace std;
//此处普通函数若只有声明,没有实现,在test中,仍然优先调用普通函数
//但是只有声明,会报错。即使报错,编译器也不使用函数模板
//因此只能通过空模板参数列表来调用函数模板
void myPrint(int a, int b) {
cout << "调用的是普通函数" << endl;
}
template <typename T>
void myPrint(T a, T b) {
cout << "调用的是函数模板" << endl;
}
template<typename T>
void myPrint(T a, T b, T c) {
cout << "调用的是重载的模板" << endl;
}
void test01() {
int a = 10;
int b = 20;
//优先调用普通函数
myPrint(a, b);
//空模板参数列表,强制调用函数模板
myPrint<>(a, b);
//函数模板也能重载
int c = 100;
//myPrint(a, b, c);
//如果函数模板产生更好的匹配,优先调用函数模板
char c1 = 'a';
char c2 = 'b';
myPrint(c1, c2); //此时普通函数可隐式类型转换,但用函数模板不用转换,因此用函数模板
}
int main() {
test01();
}
与普通函数的区别
1.普通函数调用时可发生隐式类型转换(自动类型转换)
2.函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
3.如果利用显示指定类型的方式,可以发生隐式类型转换
#include<iostream>
using namespace std;
int myAdd01(int a, int b) {
return a + b;
}
template<typename T>
T myAdd02(T a, T b) {
return a + b;
}
void test01() {
int a = 10;
int b = 20;
//此处普通函数发生隐式类型转换
char c = 'c';
cout << myAdd01(a, c) << endl; //输出109
//此时的话,a,c类型不同,而模板函数用自动类型推导必须推导出一致的类型才行
//cout << mydd02(a, c) << endl;
cout << myAdd02(a, b) << endl; //输出30
//在显示指定类型时,指定了类型为double,因此把两个int类型隐式转换成double
cout << myAdd02<double>(a, b) << endl; //输出30
//在显示指定类型时,指定了类型为int,因此把字符类型隐式转换成int
cout << myAdd02<int>(a, c) << endl; //输出109
}
int main() {
test01();
}
模板函数为成员函数
普通类中成员函数 和 类模板中成员函数创建时机是有区别的:
普通类中的成员从一开始就可以创建,类模板中的成员函数在调用时才创建
#include<iostream>
using namespace std;
class Person1{
public:
void showPerson1() {
cout << "Person1 show" << endl;
}
};
class Person2 {
public:
void showPerson2() {
cout << "Person2 show" << endl;
}
};
template <class T>
class Myclass {
public:
T obj;
//类模板中的成员函数func1()和func2()
//此处不报错,没有二义性
//此时obj不清楚是Person1的成员,还是Person2的成员,因此它俩的成员函数就没有创建出来
//但是因为类模板中成员函数在调用时才创建,因此可以编译通过
void func1() {
obj.showPerson1();
}
void func2() {
obj.showPerson2();
}
};
void test01() {
//此时obj确定了为Person1类型
Myclass<Person1> obj;
obj.func1();
//报错此时Myclass里的obj是Person1类的一个对象,不可调用Person2里的成员
//m.func2();
}