目录
前言
我们以前在写代码的时候会遇到功能相似,代码内容基本一样的函数,唯一的不同点可能算是参数和返回值的不同。
最简单的例子就是不同类型数据的求和(Add)等数学函数。学习C语言的时候我们遇到这种情况无非是把函数名改一改,参数改一改,多写几个函数,顶多再整一个函数指针数组来方便使用。这种做法显得非常笨,但显然这是语言限制了我们。
学习C++时我们接触到了重载函数,使函数名不用再变来变去,增强了可读性,但仍然要写多个函数,依然不够方便。
事实上,这些函数既然功能相似,代码内容相差不大,能不能写一个模板,把相同的代码保留下来,不同的部分用一个可变的符号来代替?答案是可以,C++后来为了解决这个问题,创造了模板这个东西,浅显来看,就是一个模子,你需要什么类型的,就给你创造什么类型的,非常人性化。
函数模板
定义
函数模板就是一类函数的集合,根据使用者所传的参数类型,来生成相应类型的代码,构成相应类型的函数供使用者使用。
格式
template<typename T1, typename T2, ... , typenameTn>
返回值类型 函数名(参数列表){函数内容}
实例
1.
#include<iostream>
using namespace std;
template<typename T>
T Add(const T& a, const T& b)
{
return a + b;
}
int main()
{
cout << Add(1, 2) << endl;
cout << Add(1.1, 2.2) << endl;
return 0;
}
2. 如果是两种不同的类型数据呢?简单,就用两个typename呗!
#include<iostream>
using namespace std;
template<typename T1, typename T2>
T1 Add(const T1& a, const T2& b)
{
return a + b;
}
int main()
{
cout << Add(1.1, 2) << endl;
cout << Add(1, 2.2) << endl;
return 0;
}
当然了,如果涉及到不同类型的之间的转换,可能返回值会精度丢失,这个就得使用者自己去限制参数的传参顺序了。
3.事实上,模板也同样支持缺省功能。
#include<iostream>
using namespace std;
template<typename T1 = int , typename T2 = int>//全缺省
T1 Add(const T1& a, const T2& b)
{
return a + b;
}
template<typename T1 = int, typename T2 = double>
void Func()
{
cout << sizeof(T1) << endl;
cout << sizeof(T2) << endl<< endl;
}
int main()
{
Func();
Func<double, int>();//<double , int>为显式实例化,下面的也是,后面会有讲到
Func<double>();
return 0;
}
函数模板的原理
在程序编译的过程中,会将函数模板作为蓝图,根据后面的代码所用到的数据类型来推演生成对应的函数。用到多少种类型的数据,就会生成多少个函数。
函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。
模板参数实例化分为:隐式实例化 和显式实例化。
隐式实例化
让编译器根据实参推演模板参数的实际类型
#include<iostream>
using namespace std;
template<typename T>
T Add(const T& a, const T& b)
{
return a + b;
}
int main()
{
cout << Add(1, 2) << endl;
cout << Add(1.1, 2.2) << endl;
return 0;
}
Add(1,2)就是隐式实例化,参数传的是1和2,是const int型数据,因此编译器会自动推演生成int型的Add函数。
显式实例化
在函数名后的<>中指定模板参数的实际类型
#include<iostream>
using namespace std;
template<typename T>
T Add(const T& a, const T& b)
{
return a + b;
}
int main()
{
int a = 10;
double b = 2.5;
cout << Add<int>(a, b) << endl;//显式指定为int型
cout << Add<double>(a, b) << endl;//显式指定为double型
return 0;
}
不同的显式指定造成了不同的结果。
模板参数的匹配原则
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数,对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而 不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
#include<iostream>
using namespace std;
int Add(int a,int b)
{
return a + b;
}
template<typename T1 = double , typename T2 = double>
T2 Add(const T1& a, const T2& b)
{
return a + b;
}
int main()
{
cout << Add(1, 2) << endl;
cout << Add(2.3, 2.2) << endl;
return 0;
}
类模板
定义
类模板和函数模板的定义相似,在类的前面加可变类型参数T。是一系列不同类型数据的类的集合。
格式
template<class T1, class T2, ... , calss Tn>
类模板名
{
// 类内成员定义
};
实例
1.
#include<iostream>
using namespace std;
template<class T = int>
class A
{
public:
A(T a = 10)
:_a(a)
{
_p = new T[10];
}
~A()
{
delete[] _p;
}
private:
T _a;
T* _p;
};
int main()
{
A<double>* pa=new A<double>[10];//类模板的实例化,后面会讲
delete[] pa;
return 0;
}
2.类模板中函数放在类外进行定义时,需要加模板参数列表
#include<iostream>
using namespace std;
template<class T = int>
class A
{
public:
A(T a = 10)
:_a(a)
{
_p = new T[10];
}
~A()
{
delete[] _p;
}
void Print();
private:
T _a;
T* _p;
};
template<class T>//这里注意不能写成缺省,直接写成T就行
void A<T>::Print() //加模板参数列表T
{
cout << _a << endl;
}
int main()
{
A<> a(5);
a.Print();
return 0;
}
类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的 类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
#include<iostream>
using namespace std;
template<class T = int>
class A
{
public:
A(T a = 10)
:_a(a)
{
_p = new T[10];
}
~A()
{
delete[] _p;
}
void Print();
private:
T _a;
T* _p;
};
template<class T>
void A<T>::Print()
{
cout << _a << endl;
}
int main()
{
A<int> a(5);
a.Print();
A<double> b(5.5);
b.Print();
return 0;
}
注:其中A是类名,A<int> 和 A<double> 才是类型
模板的分离编译
定义
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有 目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
模板声明与定义分离
如果我们把一个模板的声明放在头文件中,把定义放在源文件中,结果会怎么样呢?
//Add.h
#pragma once
using namespace std;
template<typename T1 = double, typename T2 = double>
T2 Add(const T1& a, const T2& b);
//Add.cpp
#include"Add.h"
template<typename T1 = double, typename T2 = double>
T2 Add(const T1& a, const T2& b)
{
return a + b;
}
//test.cpp
#include<iostream>
#include"Add.h"
int main()
{
cout << Add(1, 2) << endl;
cout << Add(2.3, 2.2) << endl;
return 0;
}
链接错误解析
可以看出此时发生了链接错误,原因何在?
实际上理解起来也并不麻烦,在头文件中声明不定义,代码预编译时展开头文件只有函数的声明在 test,cpp 中展开,而 Add.cpp 中的模板函数没有实例化,编译器也不知道要生成哪个数据类型的函数,也就是实例化不明确。这时在 test,cpp 中调用Add函数时就会发生这样一件事:找到了模板的地址,但找不到对应实例化过的相应类型的函数地址。因此链接错误也就不可避免了。
解决方案
1.将声明和定义放到一个文件 "xxx.hpp" 里面或者xxx.h中,这样在预编译时就会在对应的原文件中展开,从而使模板实例化与定义在同一个文件中,编译器才会推演生成对应的函数。
//Add.h
#pragma once
using namespace std;
template<typename T1 = double, typename T2 = double>
T2 Add(const T1& a, const T2& b)
{
return a + b;
}
//test.cpp
#include<iostream>
#include"Add.h"
int main()
{
cout << Add(1, 2) << endl;
cout << Add(2.3, 2.2) << endl;
return 0;
}
2.模板定义的位置显式实例化。相当于手动告诉编译器要生成怎样类型的函数了。这样做虽然也可以解决问腿,但是一旦数据类型很多或是比较复杂使,实际操作起来并不方便。
//Add.h
#pragma once
using namespace std;
template<typename T1 = double, typename T2 = double>
T2 Add(const T1& a, const T2& b);
//Add.cpp
#include"Add.h"
template<typename T1 = double, typename T2 = double>
T2 Add(const T1& a, const T2& b)
{
return a + b;
}
//显示实例化
template
int Add<int , int>(const int& a,const int& b);
template
double Add<double ,double>(const double& a, const double& b);
//test.cpp
#include<iostream>
#include"Add.h"
int main()
{
cout << Add(1, 2) << endl;
cout << Add(2.3, 2.2) << endl;
return 0;
}
总结
模板的优点:
1.模板复用了代码,节省资源。
2.增强了代码的灵活性
模板的缺点:
1.模板会导致代码膨胀问题,也会导致编译时间变长。这与模板的底层原理相关,缺点无法逆转,算是在优点之下的一种妥协了。
2.出现模板编译错误时,错误信息非常凌乱,不易定位错误。因为模板的实例化是在使用到时才会进行,编译错误使编译器报错可能没有那么人性化,因此常常会出现一些奇奇怪怪的错误提示。
模板的内容很多,一下子也不好把握全部内容,这篇博客仅仅对模板初学的一些情况做了阐述,内容多有偏失的地方还请给位读者老爷指正。我们下篇博客见~