写在前面
泛型程序设计(英文:generic programming)是程序设计语言的一种风格或范式。 泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。 各种程序语言和其编译器、运行环境对泛型的支持均不同。
模版就是泛型编程的工具,也是C++面向对象的一个体现。我们需要掌握的就是模版是什么、分类、模版怎么定义、模版怎么特化、模版分离编译的解决办法。
模版
函数模版
函数模版代表着一个函数家族,该函数模版与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
格式:
template<typename T1, typename T2, ..., typename Tn>
//typename 是用来定义模版参数的关键字,也可以使用class,但不能用struct
函数的定义...
举例:
template<class T>
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
理解:
函数模版本身不是函数,是编译器使用特定的方式来产生特定的具体类型函数的模版。
在编译器编译阶段,对于函数模版的使用,编译需要根据传入的实参类型来推演生成对应类型的函数,以供调用。
函数模版的实例化
用不同类型的参数使用函数模版,就是函数模版的实例化。
分为:隐式实例化和显式实例化。
-
隐式实例化:编译器自己推到模版参数的实际类型。
template<class T> void Swap(T& a, T& b) { T tmp = a; a = b; b = tmp; } int main(){ int a1 = 10, a2 = 20; double d1 = 10.00, d2 = 20.00; Swap(a1, a2); Swap(d1, d2); return 0; }
在模版中,编译器一般不会进行隐式类型转换,因为编译器要依仗传入参数的类型来进行推演,最终在编译阶段实例化出函数。所以如果传入类型不同的两个值,并且使用隐式实例化,会报错。
改正:1. 用户自己强转 2. 使用显式实例化
-
显式实例化:函数名后<>中指定模版参数的实际类型。
int main()
{
int a1 = 10, a2 = 20;
int d1 = 10.00, d2 = 20.00;
Swap<int>(a1, a2);
Swap<double>(d1, d2);
Swap<int>(a1, d2);//可以
return 0;
}
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功就会报错。
模版参数匹配原则
- 一个非模版函数可以与一个同名的函数模版同时存在,而且该函数模版还可以被实例化为这个非模版函数。
- 对于非模版函数和同名函数模版,如果其他条件都相同,在调用时会优先调用非模版函数而不会从该模版产生出一个实例。如果模版可以产生一个具有更好匹配的函数,那么将选择模版。
- 函数模版不允许自动类型转换,但是普通函数可以进行自动类型转换。
类模版
定义格式:
template<class T1, class T2, ... , class Tn>
class ClassName{
//
};
注意:
类模版中的函数放在类外定义时,需要在前面加模版参数列表,如下:
template<class T>
class A{
private:
int a_;
public:
A(int a = 10) :a_(a){}
~A();
//...
}
template<class T>
A<T>::~A(){
//...
}
类模板的实例化
类模板的实例化与函数模板的实例化不同,类模板的实例化需要在类模板的名字后面+<实例化类型>。
类模板的名字不是真正的类,实例化的结果才是真正的类。
(上面的例子里,A不是真正的类,A才是真正的类。)
非类型模版参数
模版参数分为两种,分别是类型形参 和 非类型形参。
- 类型形参:出现在模版参数列表中,跟在class 或者 typename 之后的参数类型名称。
- 非类型形参:用一个常量作为类(或函数)模版的一个参数,在类(函数)模版中可以将该参数当成常量来使用。
格式:
template<class T, size_t N = 10>
class A{
private:
size_t size_;
int a_;
public:
size_t size() const { return _size; }
...
}
注意:
- 浮点数、类对象以及字符串不允许作为非类型模版参数。
- 非类型的模版参数必须在编译期间就能确认结果。
模版特化
通常情况下,可以使用模版实现一些与类型无关的代码,但对于一些很特殊的类型可能会得到一些错误的结果,或者对于特殊的类型必须经过特殊处理。这时候就需要用到模版特化。
比如一个小于的模版函数。
template<class T>
bool ComLess(T& left, T& right){
return left < right;
}
class Date{
private:
int year_;
int month_;
int day_;
public:
Date(int year = 0,int month = 1,int day = 1)
:year_(year),month_(month),day_(day)
{}
};
int main(){
cout << ComLess(1, 2) << endl;
cout << ComLsee(Date(1000,1,1), Date()) << endl;//如果Date类中没有 <的重载,那么就无法比较
return 0;
}
但是如果你想对Date 类的指针,或者int 的指针进行比较,由于ComLess 内部并没有比较指向的对象的内容,而是会直接比较指针的地址,就无法达到比较的目的。
所以就需要对模版进行特化。在原模版类的基础上,针对特殊类型所进行特殊化的实现方式。模版特化中分为函数模版特化 和 类模版特化。
函数模版特化
特化步骤:
- 必须先有一个基础的函数模版。
- 关键字template 后面接一对空的<>
- 函数名后跟 <你想指定特化的类型>
- 函数形参表,必须要和模版参数的基础参数类型完全相同,如果不同编译器会报错。
举例:
template<class T>
bool ComLess(T& left, T& right) const
{
return left < right;
}
template<>
bool ComLess<int*>(int* pa1, int* pa2) const
{
return *pa1 < *pa2;
}
注意:
一般情况下如果函数模版碰到了不能处理或者处理错误的类型,为了实现简单,通常都是将这个函数直接给出。
类模版特化
全特化
全特化指的是将模版参数列表中的所有参数都确定化。
//未经特化的类模版
template<class T1,class T2>
class Data{
private:
T1 d1_;
T2 d2_;
public:
Data(){}
};
//全特化:
template<>
class Data<int, char>
{
private:
int d1_;
char d2_;
public:
...
}
偏特化
偏特化指任何针对模版参数进一步进行条件限制设计的特化版本。就是函数模版的某个类型被限制成特定的类型了。
偏特化并不仅仅是指的是特化部分参数,而是针对模版参数更进一步的条件限制所设计出来的一个特化版本。
有两种表现方法:
-
部分特化。即将模版参数类表中的一部分参数特化。
template<class T1> class Data<T1, int> { private: T1 d1_; int d2_; }
-
参数更进一步的限制。比如把参数偏特化为指针类型,把参数偏特化为引用类型。如下
//偏特化为指针类型 template<typename T1, typename T2> class Data<T1*, T2*> { private: T1 d1_; T2 d2_; public: Data(){cout << "Data<T1*, T2*> "<< endl;} } //偏特化为引用类型 template<typename T1, typename T2> class Data<T1&, T2&> { private: const T1& d1_; const T2& d2_; }
模版分离编译
分离编译:一个程序或者是项目,由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最终将所有的目标文件链接起来形成单一的可执行文件的过程。
模版的分离编译:模版的声明和定义分离开,在头文件中进行声明,在源文件中完成定义。
C/C++程序要运行,需要经历以下步骤:
预处理->编译->汇编->链接。编译阶段做的工作是:堆程序按照语言特性进行词法、语法、语义分析,错误检查无误后生成汇编代码。头文件不参与编译,编译器对工程中的多个源文件是分离开单独编译的。
链接:将多个.o文件(.obj) 文件合并成一个,并处理没有解决的地址问题。
如果我们定一个test.h 文件,在其中进行模版类(函数)的声明,定一个test.cc 文件,其中进行模版类(函数)的实现;定义一个main.cc 文件,其中进行该函数的调用。
那么在a.cpp 中,编译器没有看到对模版的实例化,所以不会生成具体的加法函数;
在main.o(main.obj)文件中调用了模版,指定了参数的类型。在链接的时候编译器才会找到他们的地址,但是这两个函数没有实例化,所以没有生成具体的代码,所以链接的时候会报错。
解决办法:
- 将声明和定义放到一个 .hpp 文件中, 或者放到 .h文件中也可以。推荐
- 模版定义的位置显式实例化。不实用。
模版总结
优点:
- 模版复用了代码,节省资源,更快的迭代开发,这也是C++的标准模版库的出现的原因。
- 增强了代码的灵活性。
缺点:
- 模版会导致代码膨胀,也会导致编译的时间变长。
- 出现模版编译错误时,错误信息经常很凌乱,不容易定位错误。
模版小节完。