目录泛型编程函数模板函数模板概念函数模板格式函数模板的原理函数模板的实例化1.隐式实例化让编译器根据实参推演模板参数的实际类型2.显式实例化在函数名后的中指定模板参数的实际类型模板参数的匹配原则类模板泛型编程泛型编程是一种编程思想核心是编写与类型无关的代码一份代码可以适配多种不同的数据类型不用为 int、char、自定义类等每种类型单独重写一套逻辑。C 里主要依靠模板来实现泛型编程分为函数模板和类模板。泛型编程和面向对象不同面向对象是通过继承和多态在运行时实现类型复用而泛型编程在编译期完成类型实例化。编译阶段编译器会根据使用的类型生成对应代码运行时不会有额外的类型判断开销效率更高。泛型编程最典型的应用就是 C 标准库 STL容器、算法、迭代器整套体系都是基于模板实现。sort 算法可以对数组、vector、list 等不同容器的不同类型数据排序就是泛型思想的体现。函数模板如何实现一个通用的交换函数呢void Swap(int left, int right) { int temp left; left right; right temp; } void Swap(double left, double right) { double temp left; left right; right temp; } void Swap(char left, char right) { char temp left; left right; right temp; }使用函数重载虽然可以实现但是有以下几个不好的地方1. 重载的函数仅仅是类型不同代码复用率比较低只要有新类型出现时就需要用户自己增 加对应的函数2. 代码的可维护性比较低一个出错可能所有的重载均出错。那能否告诉编译器一个模板让编译器根据不同的类型利用该模板来生成代码呢如果在C中也能够存在这样一个模具通过给这个模具中填充不同材料(类型)来获得不同材料的铸件(即生成具体类型的代码那将会节省许多头发。巧的是前人早已将树栽好我们只需在此乘凉。函数模板概念函数模板是 C 泛型编程里的工具可以理解成函数的模板模具它不是一个真实的函数只是告诉编译器一套通用代码逻辑类型暂时用参数代替。当代码调用这个模板的时候编译器会根据传入的数据类型自动生成对应类型的函数不用我们手动写多份重复代码。函数模板不会在编译时直接生成所有类型的函数只有当代码中用到这个模板编译器才会针对当前使用的类型生成对应函数这个过程叫做模板实例化。函数模板格式函数模板有两种写法typename 和 class 都可以用来声明模板类型参数效果相同。第一种用typenametemplatetypename T 返回值类型 函数名(参数列表) { 函数体 }第二种classtemplateclass T 返回值类型 函数名(参数列表) { 函数体 }template 是固定关键字代表这是模板尖括号里面是模板参数。T 是类型参数相当于类型占位符可以自己取名不一定要叫 T。如果有多个模板参数中间用逗号隔开。就拿swap函数举例templatetypename T void Swap(T left, T right) { T temp left; left right; right temp; }int main() { int a 3, b 5; double c 3.3, d 5.5; char ch1 e, ch2 f; Swap(a, b); Swap(c, d); Swap(ch1, ch2); cout a - b endl; cout c - d endl; cout ch1 - ch2 endl; return 0; }注意template 这一行必须写在函数前面不能和函数写在同一行。模板参数名字不能和普通变量重名。函数模板的原理函数模板本身不是可执行函数只是一份代码模板相当于给编译器的一套代码草稿。编译阶段编译器会根据调用处传入的实参类型自动推导类型生成对应类型的真实函数这个过程叫做模板实例化。当调用 Swapint或者直接传入 int 类型变量让编译器自动推导时编译器会拿模板里的 T 替换成 int生成一份 int 版本的 Swap 函数。如果后面调用时传入 double编译器再单独生成一份 double 版本的 Swap。每一种不同的类型都会产生一份独立的函数代码。模板不会在编译一开始就生成所有类型的函数只有代码中真正用到这个模板的时候编译器才会实例化出对应函数没有使用到的类型不会生成代码。这个特点叫做按需实例化。类型推导的时候如果模板参数无法确定编译就会报错。模板的语法检查分为两次第一次在定义模板的时候只检查基础语法第二次在实例化的时候结合具体类型检查类型相关逻辑。templatetypename T void Swap(T left, T right) { T temp left; left right; right temp; }int a 1, b 2; Swap(a, b); //编译器实例化得到下面这份函数 //void Swap(int left, int right) //{ // int temp left; // left right; // right temp; //} double x 1.1, y 2.2; Swap(x, y); //编译器实例化得到double版本的Swap函数简单总结函数模板的原理就是编译期根据不同类型复制替换模板代码生成多个重载函数这个工作由编译器自动完成不用程序员手动写多份重载。函数模板的实例化函数模板本身只是一段模板代码不是真正可以调用的函数。实例化就是编译器根据模板结合具体的类型参数生成一份真实函数代码的过程。1.隐式实例化让编译器根据实参推演模板参数的实际类型隐式实例化是函数模板实例化里最常用的方式。当代码中调用模板函数编译器会根据传入实参的类型自动推导模板参数 T然后用这个类型替换模板里所有的 T生成对应类型的真实函数整个过程不需要程序员手动指定类型。编译器在遇到函数调用时拿实参的类型匹配模板参数自动完成类型推导然后实例化出对应的函数。如果后续再用相同类型调用这个模板编译器不会重复生成函数直接复用之前已经生成好的代码。templatetypename T void Swap(T left, T right) { T temp left; left right; right temp; }int a 10, b 20; Swap(a, b);这里调用 Swap (a,b)实参 a 和 b 都是 int 类型编译器推导出 T 就是 int自动把模板里所有 T 替换成 int生成下面这个真实函数。void Swap(int left, int right) { int temp left; left right; right temp; }隐式实例化有几个关键点。第一只有当代码执行调用动作的时候编译器才会生成函数如果只写模板但全程没有调用不会产生任何实例代码。第二类型推导必须成功如果多个实参推导出的类型不一致编译就报错。第三同一个类型多次调用模板只会实例化一次不会重复生成多份相同函数。templateclass T T Add(const T x, const T y) { return x y; } int main() { int a1 10, a2 20; double d1 10.0, d2 20.0; //正常隐式实例化调用 Add(a1, a2); Add(d1, d2); //错误隐式实例化调用 Add(a1, d1); /* 该语句不能通过编译因为在编译期间当编译器看到该实例化时需要推演其实参类型 通过实参a1将T推演为int通过实参d1将T推演为double类型但模板参数列表中只有一个T 编译器无法确定此处到底该将T确定为int 或者 double类型而报错 注意在模板中编译器一般不会进行类型转换操作因为一旦转化出问题编译器就需要背黑锅 */ // 此时有两种处理方式1. 用户自己来强制转化 2. 使用显式实例化 Add(a1, (int)d1); return 0; }2.显式实例化在函数名后的中指定模板参数的实际类型显式实例化就是程序员主动告诉编译器模板要用什么类型强制编译器生成对应类型的函数就算代码里没有调用这个函数也会生成对应的函数实体。隐式实例化是调用的时候自动推导类型而显式实例化手动指定模板参数类型。语法格式template 返回值类型 函数名类型(参数列表);以swap函数举例templatetypename T void Swap(T left, T right) { T temp left; left right; right temp; } //显式实例化强制生成int版本Swap函数 template void Swapint(int, int);代码会让编译器把模板中的 T 全部替换成 int生成 int 版本的 Swap 函数。哪怕后面代码没有调用 Swapint这个函数代码依然会被生成到程序中。显式实例化的特点不需要调用函数就能生成函数实体。隐式实例化只有调用才会生成代码显式实例化写一句声明就直接生成。手动指定类型不依赖实参推导不会出现类型推导冲突的问题。一般用于多文件场景如果模板定义放在 cpp 文件在需要使用的地方显式实例化减少编译时间。显式实例化声明是用来生成函数实体的。还有一种是显式指定模板参数的调用写法这个不属于实例化声明只是调用函数int a1,b2; Swapint(a,b);#includeiostream using namespace std; templatetypename T void Swap(T left, T right) { T temp left; left right; right temp; } //显式实例化主动生成int版本函数 template void Swapint(int, int); //显式实例化主动生成char版本函数 template void Swapchar(char, char); int main() { //这里可以直接使用已经实例化好的函数 int x1,y2; Swap(x,y); return 0; }模板参数的匹配原则模板参数匹配的原则是优先进行实参类型推导编译器会根据函数调用传入的实参类型自动推导模板参数的类型这个过程会优先发生不需要我们手动指定模板参数。当实参可以成功推导出一致的类型时就会使用推导出来的类型实例化模板。如果同一个模板参数从不同实参推导出不一样的类型类型推导就会失败编译直接报错。当类型推导无法完成或者想要强制指定类型的时候可以在函数名后面的尖括号中显式给出模板参数手动指定的类型优先级高于自动推导。如果同时存在普通函数和同名的函数模板调用时会优先匹配普通函数只有普通函数不匹配的时候才会去匹配函数模板再进行实例化。如果函数模板可以实例化出和普通函数完全匹配的版本编译器依然优先选择普通函数不会使用模板实例化出来的函数。对于带多个模板参数的模板函数如果部分参数手动指定剩下的参数依旧可以尝试依靠实参推导得到。类模板类模板的定义需要以 template 关键字开头尖括号内部写模板参数之后再定义类模板参数相当于类内类型的占位符类里面的成员变量、成员函数都可以使用这个占位类型。基础格式如下。templatetypename T class 类名 { //类内可以使用T作为类型 T _member; public: //成员函数声明 函数返回值 函数名(参数); };如果存在多个模板参数在尖括号内用逗号隔开。templatetypename T1, typename T2 class 类名 { T1 _a; T2 _b; };代码实现了一个类模板 Stack能够创建存储任意类型元素的栈。templatetypename T是模板声明T 作为类型占位符。类里面的数组指针 T* _array代表数组存放元素的类型要等到实例化的时候才确定。类模板的成员函数写在类外的时候前面必须重新写 template 模板头类名必须写成 StackT告诉编译器这是属于 Stack 类模板的成员函数。#includeiostream using namespace std; // 类模版 templatetypename T class Stack { public: Stack(size_t capacity 4) { _array new T[capacity]; _capacity capacity; _size 0; } void Push(const T data); private: T* _array; size_t _capacity; size_t _size; }; //类外实现成员函数必须再加template模板头类名后面要写StackT templateclass T void StackT::Push(const T data) { //简单扩容判断如果容量满了就扩容这里简化写 if(_size _capacity) { size_t newcap _capacity * 2; T* tmp new T[newcap]; for(size_t i 0; i _size; i) { tmp[i] _array[i]; } delete[] _array; _array tmp; _capacity newcap; } _array[_size] data; _size; } int main() { Stackint st1; //实例化int版本的栈 Stackdouble st2; //实例化double版本的栈 st1.Push(10); st2.Push(3.14); return 0; }