一、什么是模板
C++中的模板分为类模板和函数模板,并不是一个实际的类或函数,这指的是编译器不会自动为其生成具体的可执行代码。只有在具体执行时,编译器才帮助其实例化。
二、为什么引入模板
拿我们最常见的交换函数来举例子,如果我们编写一个交换int类型变量的函数,如下:
bool Swap(int &a, int &b) {
int c = a;
a = b;
b = c;
return true;
}
但如果我们想要编写一个通过函数来交换float的函数,我们又要写另一个函数,如下:
bool Swap(float &a, float &b) {
float c = a;
a = b;
b = c;
return true;
}
那接下来如果需要交换double,char,bool,甚至一些结构体或者类的交换函数,那么我们的工作量就会很大。细心观察会发现,其实上边两个函数唯一不同的便是交换的变量类型不同,如果我们把变量类型设置为一个抽象的X,在具体需要交换的时候,再去根据具体的类型去自动适应,这便是C++的template模板的巧妙之处。
三、使用方式
C++模板采用template关键字修饰
3.1 函数模板
具体写法如下:
template <typename 参数类型1, typename 参数类型2,...>
返回值类型 函数名称(形参1, 形参2,...) {
函数体
}
上边代码的typename关键字也可以用class代替。下边给出一个交换函数模板的例子:
template <typename T>
bool Swap(T &a, T &b) {
T c = a;
a = b;
b = c;
return true;
}
3.2 类模板
具体写法如下:
template <typename 模板名称1, typename 模板名称2,...>
class 类名称{
类成员
}
注意,在实例化类模板对象时,需要在类的结尾加上<具体模板名称>。 比如以下例子:
template <typename T1, typename T2>
class NNUM {
private :
T1 num1;
T1 num2;
public :
NNUM(T1 num1, T1 num2) {
this->num1 = num1;
this->num2 = num2;
}
void print_NNUM(T2 str) {
cout << "str : [" << str << "]" << endl;
cout << "num1 : <" << this->num1 << ">, " << "num2 : <" << num2 << ">" << endl;
}
};
这里是一个用了2个模板类型(T1和T2)的类模板,那么我们在实例化时候就要按照以下代码,将具体需要使用的T1与T2类型体现出来:
NNUM<int, string> n1 = NNUM<int, string>(123, 456);
四、模板函数和普通函数的区别
区别在于是否会发生隐式类型转换。例子如下:
/*函数模板*/
template <typename T>
T fun1(T num1, T num2) {
return num1 * num2;
}
/*普通函数*/
float fun2(float num1, float num2) {
return num1 * num2;
}
int main() {
int x = 222;
float y = 3.0;
/*函数模板*/
//以下这条语句报错,因为函数模板无法隐式地将int类型的x转换为float类型
//cout << fun1(x, y) << endl; //语句1
/*普通函数*/
//这条语句正确 222.0 * 3.0 = 666.0
cout << fun2(x, y) << endl; //语句2
//以下这条语句正确,因为显示调用函数模板,可以实现自动类型转换
cout << fun1<float>(x, y) << endl; //语句3
return 0;
}
以上代码中,fun1为函数模板,fun2为普通函数。
当使用函数模板传入的参数为int和double时,语句1报错,因为函数模板无法隐式地将int类型的x转换为float类型,无法完成隐式自动转换。同样的使用放入fun2的普通函数中就可以正常运行和输出。语句3采用显示调用的方式,同样也可以实现自动类型转换。
✳建议:当使用模板时,最好显示调用
五、函数模板和普通函数的调用规则
当函数模板和普通函数同时存在时,程序会选择哪一个呢?
//函数模板
template<typename T>
void funct(T a, T b) {
cout << "函数模板" << endl;
}
//普通函数
void funct(int a, int b) {
cout << "普通函数" << endl;
}
5.1 情况一: 二者均可调用
当函数模板和普通函数均可调用时,程序会优先调用普通函数。
程序测试示例和运行结果如下所示:
//函数模板
template<typename T>
void funct(T a, T b) {
cout << "函数模板" << endl;
}
//普通函数
void funct(int a, int b) {
cout << "普通函数" << endl;
}
int main() {
funct(1, 2);
return 0;
}
---------------------------------------------------------------------------------------------------------------------------------
☆特别注意
当普通函数只有声明,没有定义的时候,程序同样会优先调用普通函数。示例如下:
//函数模板定义
template<typename T>
void funct(T a, T b) {
cout << "函数模板" << endl;
}
//普通函数声明
void funct(int a, int b);
int main() {
funct(1, 2);
return 0;
}
上述代码中,普通函数funct只有声明而未定义,因此编译后程序会报错误信息函数未定义:
5.2 情况二:使用空模板参数列表
在使用空模板参数列表的情况下,程序会强制调用函数模板。示例与运行结果如下
//函数模板
template<typename T>
void funct(T a, T b) {
cout << "函数模板" << endl;
}
//普通函数
void funct(int a, int b) {
cout << "普通函数" << endl;
}
int main() {
funct<>(1, 2);//这里使用了“空模板参数列表”
return 0;
}
5.3 情况三:函数模板的重载
函数模板允许重载。
重载的作用在于:代码在编译阶段对模板代码编译。而在代码的执行阶段,才会判断具体调用时是否发生错误。例子如下:
//复数类
class Complex {
int real;//实部
int vir;//虚部
public:
Complex(int r, int v) : real(r), vir(v){}
};
//模板比较函数
template<typename T>
bool myCompare(T &t1, T &t2) {
if (t1 == t2) {
return true;
}
else {
return false;
}
}
int main() {
Complex c1(1, 2);
Complex c2(3, 4);
Complex c3(3, 4);
cout << "c1 == c2 : " << boolalpha << myCompare(c1, c2) << endl;
cout << "c2 == c3 : " << boolalpha << myCompare(c2, c3) << endl;
return 0;
}
这样的程序编译是没有错误的,但运行时会报错,错误信息如下:
原因:当执行时,模板匹配到了我们的Complex类,但Complex类型并没有相应的==运算符。
解决方式:
1. 重载运算符==
利用全局函数重载Complex的==运算符。
关于运算符的重载,请参考我之前写的《C++运算符重载》。
// ==运算符的重载
bool operator==(Complex& c0, Complex& c1) {
cout << "执行的是:运算符重载" << endl;
if (c0.real == c1.real && c0.vir == c1.vir) {
return true;
}
else {
return false;
}
}
2. 模板重载
//模板的重载
template<>
bool myCompare(Complex& c0, Complex& c1) {
if (c0.real == c1.real && c0.vir == c1.vir) {
return true;
}
else {
return false;
}
}
在函数执行时,会优先匹配这个函数的内容进行执行,因为它不需要适配T类型,直接就是Complex类型的,更容易和函数调用适配。执行结果如下:
3. 二者的优先级
如果二者同时存在:模板重载 > 运算符重载
六、模板类和普通类的函数创建时机
模板类和普通类创建的时间是不同的:
普通类的成员函数在程序编译时一开始就被创建 |
类模板的成员函数在调用时创建 |
也就是说,当类模板使用出现错误的时候,在编译阶段,程序可能不会报错。例子如下:
class Dog {
public :
Dog& showDog() {
cout << "汪" << endl;
return *this;
}
};
class Cat {
public :
Cat& showCat() {
cout << "喵" << endl;
return *this;
}
};
template<typename T>
class ShowAnimal {
public:
T animal;
void show1() {
animal.showDog();
}
void show1() {
animal.showCat();
}
}
以上代码编译不会报错。以上例子中,Dog类与Cat类分别拥有showDog()和showCat()成员函数。当类模板ShowAnimal创建它的show1()与show2()成员函数时,其实并没有在编译阶段真正创建这两个函数,因此在编译阶段是没有错误的。
但是,执行时候会报错。因为如果T为Cat类,则show1()成员函数出错,因为没有animal没有showDog();如果T为Dog类,则show2()成员函数出错,因为没有animal没有showCat()。
我们再看一下普通函数的例子:
class Cat {
public:
Cat& showCat() {
cout << "喵" << endl;
return *this;
}
};
class Show {
public :
Cat cat;
void show() {
cat.showDog();//这句话会直接报错,无法通过编译
}
};
在ShowShow类的show()成员函数中,因为cat没有showDog(),
系统在未创建对象时,就已经创建了这个函数,因此在编译阶段系统会报错。
七、类模板作为函数的形参
(※小知识※:如果想查看某一类型的名称,请使用typeid(XXX).name(),返回的是字符串)
(比如: cout << typeid(int).name() << endl; 输出的是int。当然,类型也可以是模板T)
当需要将类模板作为形参类型时,总共有一下三种方式:
7.1 直接指定模板的类型
这个很简单,就是在< >中声明模板的真实类型,示例如下:
<template T>
class Animal {
public :
T type; //假设在调用时,指定的是string类型
Animal(T t) : type(t){}
void show() {
cout << type << endl;
}
}
//测试函数:参数为一个Animal类型的对象
void test(Animal<string> animal) {
animal.show();
}
//主函数
int main() {
Animal<string> dog("狗");
test(dog);//指明string类型的调用
return 0;
}
7.2 将模板类的参数模板化
在形参的模板类型处依旧使用模板,将整个函数变成一个模板函数。例子如下:
<template T>
class Animal {
public :
T type; //假设在调用时,指定的是string类型
Animal(T t) : type(t){}
void show() {
cout << type << endl;
}
}
//测试函数:参数为一个Animal类型的对象
template<typename T>
void test(Animal<T> animal) {
animal.show();
}
//主函数
int main() {
Animal<string> dog("狗");
test(dog);//指明string类型的调用
return 0;
}
7.3 使用模板引用类型
将形参类型整体设置为模板的引用,依靠编译器执行时自动识别。例子如下:
<template T>
class Animal {
public :
T type; //假设在调用时,指定的是string类型
Animal(T t) : type(t){}
void show() {
cout << type << endl;
}
}
//测试函数:参数为一个Animal类型的对象
template<typename T>
void test(T& animal) {
animal.show();
}
//主函数
int main() {
Animal<string> dog("狗");
test(dog);//指明string类型的调用
return 0;
}
八、继承模板类
1. 当子类企图集成一个类模板父类时,父类必须指定具体的模板类型。原因:如果不指定类型,编译器无法确定父类具体大小,也就无法集成给子类。(无论父类访问权限如何,子类都将全部继承,只是 是否可见和可访问罢了)
2. 如果父类未指定模板具体类型,则子类必须也是一个类模板。
为了便于直观理解,例子如下:
//模板类父类
template <typename T>
class Animal {
public :
T type;
Animal(T t) : type(t) {}
};
//指定父类模板的类型,子类可以正常继承
class Dog : public Animal<string> {
public :
Dog(string t) : Animal(t) {}
};
//未指定父类模板类型,子类也必须是模板类
template <typename T>
class Cat : public Animal {
public :
Cat(T t) : Animal(t) {}
};
九、类模板 成员函数的类外定义
类外定义时,不光需要声明域,同时也需要:
1 | 将这个函数声明为函数模板 |
2 | 在域名声明具体的模板类型 |
拿上边的Animal类举一个简单的例子:
//模板类父类
template <typename T>
class Animal {
public :
T type;
Animal(T t);//构造函数
void showType();//展示type函数
};
//构造函数的类外定义
template<typename T>
Animal<T>::Animal(T t) : type(t) {}
//展示type函数的类外定义
template<typename T>
void Animal<T>::showType() {
cout << T << endl;
}
十、类模板 多文件编写
由于类模板是在运行时才创建的,因此有时会出现声明(.h)与源码(.cpp)对应不上的情况。
解决方式有以下两个:(1)直接包含.cpp文件
(2)声明与定义写到同一个尾缀为.hpp的文件(hpp不是强制的,是习惯)