前言
ADD函数很好写,但是如果我们要有int类型的,double类型的,char类型的等等各种类型,难道要写这么多不同的ADD函数吗,这么写简直太麻烦了,所以有了泛型编程的概念。
一、泛型编程
实现一个通用的ADD函数有很多办法,就比如函数重载,但是函数重载又有很多的缺点,比如:
1.重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数。
2.代码的可维护性比较低,一个出错可能所有的重载都出错。
那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生产代码呢?
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
函数模板
函数模板的概念:函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
函数模板格式:
template<typename p1,typename p2........>
注意:typename是用来定义模板参数 关键字, 也可以使用class(切记:不能使用struct代替class)
template <class T> void Swap(T& d1, T& d2) { T tmp = d1; d1 = d2; d2 = tmp; } int main() { int a = 10, b = 50; Swap(a, b); double c = 2.36, d = 6.15; Swap(c, d); return 0; }
那么上图中的两次调用Swap函数是同一个函数吗?这里不是同一个函数,我们先来验证一下:
通过汇编代码我们发现两个函数并不是同一个,他们有着不同的地址。这是因为模板就像是印刷的模具一样,生成的函数就是印刷出来的书,我们看的是书并不是印刷的板,每本书用的印刷的板都是一样的但是书中的内容是不一样的。
函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模 板就是将本来应该我们做的重复的事情交给了编译器。
下面我们来看一些关于模板的小细节:
template<class T> T Add(const T& left, const T& right) { return left + right; } int main() { int a1 = 10, a2 = 20; double d1 = 10.22, d2 = 20.32; cout << Add(a1, a2) << endl; cout << Add(d1, d2) << endl; return 0; }
首先上面的代码是可以正常编译的,那么如果写成下面这样呢?
这里有两种解决方式,一种是强制类型转换,一种是显式实例化。
int main() { int a1 = 10, a2 = 20; double d1 = 10.22, d2 = 20.32; //实参传递给形参,自动推演模板类型 cout << Add(a1, (int)d1) << endl; cout << Add(d1, d2) << endl; //显示实例化 cout << Add<int>(a1, d2) << endl; cout << Add<double>(a1, d1) << endl; return 0; }
在函数名后加<(需要的类型)>就可以完成显式实例化,编译器直接以显式的类型去计算。
模板参数的匹配原则
// 专门处理int的加法函数 int Add(int left, int right) { return left + right; } // 通用加法函数 template<class T> T Add(T left, T right) { return left + right; } void Test() { Add(1, 2); // 与非模板函数匹配,编译器不需要特化 Add<int>(1, 2); // 调用编译器特化的Add版本 } int main() { Test(); return 0; }
我们可以看到上面代码中有两个Add函数,当他们类型都为int的时候为什么没有函数重定义呢?
这是因为一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
那么像上图这样的情况编译器会调用哪个呢?答案是第一个,因为对于非模板函数和同名函数模板,如果其他条件都相同,在调用时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的一个函数,那么将选择模板。
什么意思呢?就是说已经存在一个能用的函数了编译器就不会花时间去实例化一个相同的函数,而是调用那个已经存在的函数。
如果我们必须要调用那个模板有什么方法吗?
我们直接在函数名后显示实例化就会调用模板了。
类模板
template<class T> class Stack { public: Stack(int capacity = 4) { _a = new T[capacity]; _top = 0; _capacity = capacity; } ~Stack() { delete[] _a; _a = nullptr; _top = _capacity = 0; } paivate: T* _a; size_t _top; size_t _capacity; }; int main() { Stack<int> sl; Stack<double> st; return 0; }
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
注意:模板的声明和定义分开与普通类不一样,如下图:
模板的声明和定义分离只限于在一个文件中,如果声明和定义在两个不同的文件会出现链接错误。
模板的类创建对象必须显示实例化在类名后面加<>确定其类型。
总结
模板的出现让我们在写一些代码相同但是类型不同的函数或者类的时候方便了很多,以前这些都是由我们自己写出来的,有了模板就可以由编译器去做这件事,并且编译器做的比我们更好,因为人写总是会出现一些粗心的错误,编译器却不会。