目录
1.泛型编程
我们在学习C语言的时候,有一个十分难受的问题,比如我们要实现一个交换函数
void Swap(int& ra, int& rb)
{
int tmp = ra;
ra = rb;
rb = tmp;
}
这个交换函数虽然能够实现交换,但是它只能用于整形的交换,如果我们要进行浮点数的交换或者字符的交换,则还需要在写一些与类型对应的函数。
但是我们能发现,不管什么类型的交换函数,他们的代码逻辑都是一样的,唯一的不同就是参数的类型以及tmp的类型,如何编写出一个与类型无关的代码来实现这个逻辑呢?这就要用到泛型编程的概念了
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段,模板是泛型编程的基础。
而模板又分为函数模板和类模板
2.函数模板
模板是怎么写的呢?
首先模板需要用到一个关键字 template ,然后加上一个尖括号,尖括号中用来接收我们要使用的类型的参数
template<class T>
//template<typename T>
为什么要用 class 或者 typename 来接收类型参数呢?在模板上是不能写明具体的类型的,如果写明具体的类型,这就不是模板而是具体的函数了,要做到与类型无关,就需要用一个虚拟类型,而class 和typename 就是虚拟的类型,他么们不是具体的类型。而上面的T则相当于形参,也就是我们要使用的类型。
如何定义模板函数呢?
template<class T>
void Swap(T& ra, T& rb)
{
T tmp = ra;
ra = rb;
rb = tmp;
}
我们声明一个模板只能在后面写一个模板函数或者模板类,而不能连续写几个。
这就是一个基本的函数模板了。
int i1 = 1;
int i2 = 2;
Swap(i1, i2);
cout << i1 << " " << i2 << endl;
double d1 = 1.1;
double d2 = 2.2;
Swap(d1, d2);
cout << d1 << " " << d2 << endl;
这样一来我们就能实现不同的类型的交换函数了。
但是,我们实现交换逻辑的时候调用的是上面的函数模板吗?
如果在vs中,我们进行调试的话,逐条代码执行,我们发现在Swap按f11之后就跳到了我们的函数模板那里,但是事实上,我们调用的并不是函数模板,而这里也只是vs编译器为了便于我们调试而设计的。
实际上交换时所调用的其实是编译器用这个模板实例化出来的函数,就如同一个模具,我们能用这个模具做出很多相似功能的物品,但是我们最后使用的不是这个模具,而是用模具造出来的物品。而且,不同的类型根本就不可能调用同一个函数,尽管他们的代码逻辑一样,但是他们相应的逻辑需要的函数栈帧都不一样了,怎么可能调用的是同一个函数呢。
当我们打开反汇编就能看到,实际上调用的是不同的函数,他们的地址都不是一样的
真正调用的是用函数模板实例化出来的的具体函数,在程序编译期间,编译器会通过我们的函数调用的代码,来推演实例化出具体的函数,让然后将函数的地址关联到相应的调用的地方,在程序执行的时候,我们就能通过函数的地址来调用相应的对应着参数类型的函数
推演实例化就是编译器通过函数调用传的参数,以及函数模板,来推断函数模板中的 T 是哪种类型,然后把模板中的 T 替换成相应的具体的类型,生成一个对应的函数。
但是在函数调用的过程中,我们还有一种情况就是隐式类型转换。比如下面这一个Add函数
int Add(const int x,const int y)
{
return x + y;
}
对于这样一个形参用const修饰的加法函数,我们是可以传 整形以外的类型来调用函数的,因为这之中会存在一层隐式类型转换,将其它类型的实参先隐式转换出一个 const int 的而临时变量,在传给形参拷贝
int main()
{
int x = 1;
double y = 2.2;
cout << Add(x, y) << endl;
return 0;
}
但是如果我们用函数模板来实现呢?能不能实现这样的功能呢?
template<typename T>
T Add(const T x, const T y)
{
return x + y;
}
因为我们调用 Add 函数传的两个参数 是 int 和double 类型的,在编译期间,编译器就无法区推演出模板中的T到底是 int 还是double ,所以说这段程序甚至过不了编译 。
那么这种情况如果我们硬要实现,该怎么做呢?
第一种方法就是显式实例化,显式实例化就是我们自己显式指定 T 的参数类型,用我们指定的参数类型 替换 T生成一个具体的函数。转而去调用我们实现实例化的函数
int x = 1;
double y = 2.2;
cout << Add<int>(x, y) << endl;
但是这种方法有一个很大的问题,就是我们必须在调用的时候指定实例化的函数类型,如果我们不是在这里调用的时候指定实例化,而是在前面实例化出函数,再通过Add调用的话
int x = 1;
Add<int>;
Add<double>;
double y = 2.2;
cout << Add(x, y) << endl;
这时候,编译器会因为我们调用的参数与匹配不上上面的已经实例化出来的具体函数,编译器会继续去推演类型,这就回到了最初的问题,所以我们一般不推荐这种做法。
第二种方法就是我们不只接受一个类型的参数,在函数模板中用两个参数类分别接受调用函数时两个参数的类型,
template<class T1,class T2>
T1 Add(const T1 x, const T2 y)
//T2 Add(const T1 x, const T2 y)
{
return x + y;
}
但是这种方法的话,我们就要注意返回值的类型用T1还是T2,这也是一个不小的问题。
所以我们再调用模板函数的时候传参最好要符合模板的格式。
模板函数是否会和我们自己写的具体函数冲突呢?
template<class T>
void Swap(T& ra, T& rb)
{
T tmp = ra;
ra = rb;
rb = tmp;
}
void Swap(int& ra, int& rb)
{
int tmp = ra;
ra = rb;
rb = tmp;
}
当我们的模板函数与自己实现的函数功能相同的时候,
编译器会去调用我们自己定义的函数,因为我们已经有一份函数能够实现这个功能了,所以编译器没必要再多生成一个。
而如果,我们显式实例化一份int 类型的交换函数出来,会不会出现函数重定义的情况呢?
int x = 1;
int y = 2;
Swap<int>(x, y);
这种情况编译器不会报错,这说明了模板函数的命名修饰规则和普通的函数命名修饰规则不一样,这才能同时存在两份相同的函数。
3.类模板
在C语言我们实现数据结构的时候,通常对一个具体的类型 typedef 称DataType,这样一来,当我们想要存储其它类型的数据时,我们只需要修改这一行代码就能完成存储数据类型的转换。但是,这却并不能在同一个程序中同时存储不同的类型,
typedef int STDataType;
typedef double STDataType;//???
class Stack
{
public:
//... ...
private:
STDataType *a;
int top;
int capacity;
};
这种方法并不是真正的泛型,只是更方便我们的代码维护。那么要实现在一个程序中能够存储多个类型的数据结构我们要怎么做呢?这就是我们下面的类模板
类模板与函数模板类似,首先用一个关键字template以及一个虚拟类型来接收具体类型,然后在下面进行类的定义,只不过是把具体的类型换成了泛型
template<class T>
class Stack
{
public:
Stack(int capacity = 4)
:_capacity(capacity)
{
_a = (T*)malloc(sizeof(T) * capacity);
assert(_a);
_top = 0;
}
void push(T x)
{
//扩容... ...
_a[_top] = x;
_top++;
}
//....
private:
T* _a;
int _top;
int _capacity;
};
这样一个简单的类模板就定义好了。
但是类模板的使用只能显式实例化,因为类对象初始化的时候我们是不会给要存储的数据类型的,一般都是直接用类名定义出一个对象。
那么类模板的使用也很简单,只需要在定义对象的时候显式实例化就行了,其他的与普通的类的使用并无太大区别
类模板一般是没有类型推演的,统一要显式实例化。
在使用类模板的时候,还要注意一个细节,就是赋值操作。即便我们实现了赋值运算符的重载,
Stack<double> s2(5);
Stack<int> s1;
s1 = s2;//s1和s2已经是不同的类了,不能进行赋值操作
对于上面的 s1 和 s2 ,他们已经是用类模板实例出来的不同的类了,不同类之间是不能进行赋值操作的。
4.模板不支持分离编译
分离编译指的就是,使用与定义分离,也就是下面这种情况
类模板声明放在头文件中,而类模板中的模板函数的定义与声明分离,而调用函数的地方又在其他的文件。
这种情况会出现链接失败的问题
这是因为各个源文件之间在进行链接之前没有任何交互的,且头文件在预处理的时候就在包含他的地方展开了。编译链接完生成目标文件的时候,在Stack.o中,由于只有声明和定义,而没有实际的函数调用,所以在Stack.o中是没有实例化函数的,所以在Stack.o文件的符号表中,是没有相关函数的地址的。而在main.o中,由于没有函数的定义,虽然它调用了模板函数,但是在他的文件里,只有模板函数的声明,没有定义,所以编译器在编译的时候,虽然能够推演出具体的类型,但是他只能替换声明中的T,所以在main.o中,相当于只有函数的声明,而没有实现,所以他的符号表中这个函数关联的是一个无效的地址。
最后在链接的时候,由于Stack.o中没有函数的实例化,所以符号表中没有函数地址,而在main.o中,只有函数声明,函数关联的是一个无效的地址,所以最终就会出现链接错误,报错应该是函数未定义之类的。
他的解决方案也有两个
(一):在定义的文件中直接显式实例化一套函数出来,这样一来,在Stack.o文件的符号表中就会有有效地址。
注意这里显式实例化的格式。
但是这种解决方案不是很好,因为这意味着,我们要在Stack文件中相当于把所有需要用到的类型都实例化一套出来,每次运行程序都需要区去检查我们需要的类型是否实例化,代码的移植性不是很好。
第二种解决方案: 不分离,我们可以直接将模板类和函数的声明和定义都写在.h文件中,而不是声明与定义分离,这样一来包含头文件就行了,在调用函数的文件中就实例化了,而不用在链接的时候去别的符号表中搜索。