C++ ---- 模板

张开发
2026/5/28 22:25:18 15 分钟阅读
C++ ---- 模板
1. 模板引入在我们学习过程中经常使用交换函数通过函数重载我们可以实现多个交换函数根据类型不同实现交换但是这么写总归是有点麻烦的有没有一种方法可以能只写一次交换然后所有类型都可以用呢 为了处理这个问题C迎来了模板这个概念。模板是泛型编程的基础泛型编程是编写与类型无关的通用代码是代码复用的一种手段。模板分为函数模板和类模板。关键字是 template2.函数模板2.1、函数模板概念函数模板代表了一个函数家族该函数模板与类型无关在使用时被参数化根据实参类型产生函数的特定类型版本。2.2、模板的结构结构template typename T1, typename T2,......, typename Tn返回值类型 函数名 (参数列表) {注意typename是用来定义模板参数关键字也可以使用class(切记不能使用struct代替class)示例1示例22.3、函数模板的原理函数模板是一个蓝图它本身并不是函数是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。编译器在识别的时候将模板 T 类型转换成传参的参数的类型编译器自己生成一个double/ int / char类型的交换函数。类似于函数重载不过不需要我们手动去写。在编译器编译阶段对于模板函数的使用编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如当用double类型使用函数模板时编译器通过对实参类型的推演将T确定为double类型然后产生一份专门处理double类型的代码对于字符类型也是如此。2.4、函数模板的实例化用不同类型的参数使用函数模板时称为函数模板的实例化。模板参数实例化分为隐式实例化和显式实例化。1.隐式实例化让编译器根据实参推演模板参数的实际类型在下面代码中我们传参后编译器自动识别参数的类型然后把该模板实例化为对于的函数在上面代码我们可以看出T Add(const T x, const T y)我们传入的参数必须是一样的但是如果我们调用Add时传入一个 int类型一个 double 类型这时编译器会怎么处理呢由此我们看到编译器编译失败编译器报错参数 T 不明确因为在编译期间当编译器看到该实例化时需要推演其实参类型 通过实参a将T推演为int通过实参c将T推演为double类型但模板参数列表中只有一个T 编译器无法确定此处到底该将T确定为int 或者 double类型而报错注意在模板中编译器一般不会进行类型转换操作因为一旦转化出问题编译器就需要背黑锅 Add(a1, d1);那怎么解决这个问题呢有两种处理方式1. 用户自己来强制转化 2. 使用显式实例化1、 用户自己强制转化。2.显式实例化在函数名后的中指定模板参数的实际类型在函数名后的 指定类型让两个参数都强制转化为 里面的类型。如果类型不匹配编译器会尝试进行隐式类型转换如果无法转换成功编译器将会报错。2.5 模板参数的匹配原则1、 一个非模板函数可以和一个同名的函数模板同时存在而且该函数模板还可以被实例化为这个非模板函数相当于函数重载。int Add(int left, int right) { return left right; } // 通用加法函数 templateclass T T Add(T left, T right) { return left right; } void Test() { Add(1, 2); Addint(1, 2); } int main() { Test(); return 0; }2、对于非模板函数和同名函数模板如果其他条件都相同在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数 那么将选择模板如图所示当参数都是 int 类型时先去调用更适配的非模板函数但是当非模板函数不匹配时调用模板函数实例化出匹配的函数3、模板函数不允许自动类型转换但普通函数可以进行自动类型转换3. 类模板3.1 类模板的定义格式结构templateclass T1, class T2, ..., class Tnclass 类模板名{// 类内成员定义};示例定义一个类型为 T 的栈后面我们传参时传入int 类型那就是int 类型的栈传入double就是double 类型的栈// 类模版 templatetypename T class Stack { public: Stack(size_t capacity 4) { _array (T*)malloc(sizeof(T) * capacity); if (nullptr _array) { perror(malloc申请空间失败); return; } _capacity capacity; _size 0; } void Push(const T data); private: T* _array; size_t _capacity; size_t _size; }; // 模版不建议声明和定义分离到.h 和.cpp会出现链接错误 // 要分离也分离在.h // 注意类模板中函数放在类外进行定义时需要加模板参数列表 templateclass T void StackT::Push(const T data) { // 扩容 _array[_size] data; _size; }3.2 类模板的实例化类模板实例化与函数模板实例化不同类模板实例化需要在类模板名字后跟 然后将实例化的类型放在 中即可类模板名字不是真正的类而实例化的结果才是真正的类。示例int main(){Stackint st1; // intStackdouble st2; // doublereturn 0;}4、非类型模板参数模板参数分为类类型形参、非类型形参。类型形参即出现在模板参数列表中跟在class或者typename之类的参数类型名称。非类型形参就是用一个常量作为类(函数)模板的一个参数在类(函数)模板中可将该参数当成常量来使用。示例在我们之前的学习中当我们定义一个静态的栈时我们想要这个栈是多大一般采用宏定义的方式想要多少就定义多少如下图但是当我们想要第一个栈存10个第二个栈存100个数据那怎么办呢我们可以定义一个非类型模板参数常量参数如下图#include iostream using namespace std; templatetypename T, size_t N // 非类型模板参数 class Stack { private: int _a[N]; int _top; }; int main() { Stackint, 10 st1; // 能存储10个数据大小的栈 Stackint, 100 st2; //能存储100个数据大小的栈 return 0; }监视器结果如下注意1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。2. 非类型的模板参数必须在编译期就能确认结果。5、类模板的特化5.1、概念通常情况下使用模板可以实现一些与类型无关的代码但对于一些特殊类型的可能会得到一些错误的结果需要特殊处理比如实现了一个专门用来进行小于比较的函数模板示例// 函数模板 -- 参数匹配 templateclass T bool Less(T left, T right) { return left right; } int main() { cout Less(1, 2) endl; // 可以比较结果正确 Date d1(2022, 7, 7); Date d2(2022, 7, 8); cout Less(d1, d2) endl; // 可以比较结果正确 Date* p1 d1; Date* p2 d2; cout Less(p1, p2) endl; // 可以比较结果错误 return 0; }结果可以看到Less绝对多数情况下都可以正常比较但是在特殊场景下就得到错误的结果。上述示例中p1指向的d1显然小于p2指向的d2对象但是Less内部并没有比较p1和p2指向的对象内容而比较的是p1和p2指针的地址这就无法达到预期而错误。此时就需要对模板进行特化。即在原模板类的基础上针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化与类模板特化5.2、函数模板特化函数模板的特化步骤1. 必须要先有一个基础的函数模板2. 关键字 template 后面接一对空的尖括号 3. 函数名后跟一对尖括号尖括号中指定需要特化的类型4. 函数形参表: 必须要和模板函数的基础参数类型完全相同如果不同编译器可能会报一些奇怪的错误。示例// 函数模板 -- 参数匹配 templateclass T bool Less(T left, T right) { return left right; } // 特化 --- 对Less函数模板进行特化 template bool LessDate*(Date* left, Date* right) { return *left *right; } int main() { cout Less(1, 2) endl; Date d1(2022, 7, 7); Date d2(2022, 7, 8); cout Less(d1, d2) endl; Date* p1 d1; Date* p2 d2; cout Less(p1, p2) endl; // 调用特化之后的版本而不走模板生成了 return 0; }注意一般情况下如果函数模板遇到不能处理或者处理有误的类型为了实现简单通常都是将该函数直接给出。在前面我们说过函数模板和函数可以同时存在所以我们也可以用函数解决这个问题bool Less(Date* left, Date* right){return *left *right;}该种实现简单明了代码的可读性高容易书写因为对于一些参数类型复杂的函数模板特化时特别给出因此函数模板不建议特化。5.3、类模板特化5.3.1、全特化全特化即是将模板参数列表中所有的参数都确定化。示例1当模板参数是int和char时使用该类除此之外全使用这个特殊版本。// 将第二个参数特化为int template class T1 class DataT1, int // 只要第二个参数为 int 全用这个 { public: Data() { cout DataT1, int 偏特化 endl; } private: T1 _d1; int _d2; };5.3.2 偏特化偏特化任何针对模版参数进一步进行条件限制设计的特化版本。比如对于以下模板类templateclass T1, class T2 class Data { public: Data() { cout DataT1, T2 原模版 endl; } private: T1 _d1; T2 _d2; };偏特化有以下两种表现方式部分特化 将模板参数类表中的一部分参数特化。// 将第二个参数特化为int template class T1 class DataT1, int // 只要第二个参数为 int 全用这个 { public: Data() { cout DataT1, int 偏特化 endl; } private: T1 _d1; int _d2; };结果参数更进一步的限制偏特化并不仅仅是指特化部分参数而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。6、 模板的分离编译6.1、什么是分离编译一个程序项目由若干个源文件共同实现而每个源文件单独编译生成目标文件最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。6.2、模板的分离编译假如有以下场景模板的声明与定义分离开在头文件中进行声明源文件中完成定义// a.h templateclass T T Add(const T left, const T right); // a.cpp templateclass T T Add(const T left, const T right) { return left right; } // main.cpp #includea.h int main() { Add(1, 2); Add(1.0, 2.0); return 0; }结果这段代码会链接失败原因是C 模板不支持传统的“声明放 .h、实现放 .cpp”的分离编译方式a.h中只有模板的声明而模板的具体实现写在a.cpp中当main.cpp调用Add(1,2)和Add(1.0,2.0)时编译器在编译main.cpp阶段生成一个main.o文件而a.cpp虽然包含了模板实现编译阶段生成a.o文件但因为没有在a.cpp中实际使用这些模板也不会自动生成对应类型的代码最终导致链接器找不到Addint和Adddouble的定义报“无法解析的外部符号”错误。7.模板总结【优点】1.模板复用了代码节省资源更快的迭代开发C的标准模板库(STL)因此而产生2.增强了代码的灵活性【缺陷】1.模板会导致代码膨胀问题也会导致编译时间变长2.出现模板编译错误时错误信息非常凌乱不易定位错误

更多文章