当前位置: 首页 > news >正文

泛型的诗意——深入C++模板的艺术与科学(模版进阶)

前言:

        在之前,小编讲述了模版的初阶内容,当时小编讲述了模版的书写,方便之后容器的讲解以及模拟实现,现在小编已经带领各位学习了很多容器,模版初阶的知识已经用的很多了,今天小编讲述一下全新的知识点,它就是模版的进阶部分,下面开始奏响模版初阶的交响曲~

img

正文:

1.非类型模版参量

1.1.相关的介绍

        模版参数分为类型模版形参和非类型形参;其中,类型参数是指出现在模版的列表中,跟在class或者typename之类的参数类型名称,就比如下图代码的写法:

template<class T>int add(T& x,T& y);

        非类型模版形参就是用一个常量作为类(函数)模版的一个参数,在类(函数)模版参数中可以将该参量当做常量来使用,如下面的使用:

template<class T,size_t N = 10> //可以给一个缺省参数给予非模版形参class Array{private:T _arr[N]; //这样的话可以定一个定长的数组};
int main()
{Array<int> s1; //可以构建一个数组长度为10的数组,也可以根据自己的兴趣来定Array<int,123> s2; //也可以根据自己的兴趣定一个长度的数组,虽然说很鸡肋,但在某一个方面还是很实用的
}

        可能此时有很多读者朋友会有一点疑惑,各位可能想起之前我们在C语言阶段学习过的一个小的知识点——宏(define),宏也可以定义一个变量为一个固定的数,所以此时我们如果让N为10不可以设置一个定长的数组吗?就类似下面代码所写:

template<class T,size_t N = 10> //可以给一个缺省参数给予非模版形参class Array{private:T _arr[N]; //这样的话可以定一个定长的数组};
int main()
{Array<int> s1; //可以构建一个数组长度为10的数组,也可以根据自己的兴趣来定Array<int,123> s2; //也可以根据自己的兴趣定一个长度的数组,虽然说很鸡肋,但在某一个方面还是很实用的
}

        相信很多读者朋友看到这会发现这个代码的局限性,此时我们并可以随心所欲的去设置一个定长的Array,每次生成的Array都是一个长度,可能会违背我们的初心,所以不要在产生宏就可以替代非模板形参,它俩是互相替代不了的,都在各位的领域里闪闪发亮,非模板参数可能以后在我们的讲解过程中出现的次数不会太多,不过各位读者朋友还是要知道这个知识点的,防患于未然,下面小编讲述一下它的注意事项。

1.2.注意事项

        1.浮点数、类对象以及字符串不可以作为非模版参数(后两者应该截止到目前都不可以,浮点数在C++20就可以支持了)。

        2.非类型的模版参数必须在编译期间就可以确认结果,也就是说我们写非类型模版参量,要不是给缺省值,要不就是显示去写,不然会报错。

2.模版的特化(本文重点)

2.1.概念

        在通常情况下,使用模版可以实现一些与类型无关的代码,但对于一些特殊类型的,需要进行特殊处理,比如说:实现一个专门用来进行比较大小的函数模版:

template <class T>
bool Less(T left,T right)
{return left < right;
}
int main()
{cout << Less(1,2) << endl;  //可以比较,结果是正确的。//这里我用一下之前小编实现过的时间类,具体可以看我之前的文章Date s1(2024,11,3);Date s2(2024,11,5);cout << Less(s1,s2) << endl;  //可以进行比较,不过这里使用的是<运算符的重载,结果也是正确的。Date* p1 = &s1;Date* p2 = &s2;cout << Less(p1,p2) << endl;  //比较之后,结果随机,因为每次我们调用构造函数,地址都是不确定的,所以此时我们进行的比较,是地址大小的比较,而不是内容的比较,所以此时我们就想在写一个函数,使得可以比较里面内容。
}

        可以看到,Less在绝大多数情况下都可以正常的比较,但是在某种特殊的场景下,这个函数就不可以被使用了,在上面的代码中,p1指向的内容明显比p2的要小,但是在经过我们多次运行程序的时候,发现此时的结果就是一个随机值,有时候正确有时候错误,让人摸不着头脑,不知道为什么会有这样的结果,其实此时是涉及到了地址的开辟知识,地址的开坯都是随机的,而不是按部就班的,所以此时我们比较的是两个地址之间的大小,由于地址有随机性,自然而然的就无法到达预期结果。

        不仅函数有这样的情况,有时候类也有类似的情况,所以此时,就需要对模版进行特化。即:在原有模版的基础上,针对特殊类型进行特殊化的实现方式。模版特化分类类模版特化和函数模版特化。

2.2.函数模版的特化

函数模版特化的步骤:

  1. 必须先有一个基础的函数模版(也就是说模版特化的前提就是要先有一个原模版,在原模版的基础上进行特化)

  2. 关键字template后面加一对空的<>(可能很多读者朋友感觉这个步骤很奇怪,但这就是语法要求,我们也不能怎么样)

  3. 函数名后跟一对尖括号,尖括号里面放置要特定特化的类型(对于这个等会在代码的例子中各位就可以感受到)

  4. 函数形参表:必须和模版函数的基础参数类型完全相同,如果不同编译器会报错,并且这个错误会很奇怪(也就是说我们想要特化,就得对照着原模版的参数进行特化,而不是随心所欲的乱改参数,这就不就叫做特化了)

        下面小编讲述一下如何进行一个函数模版的特化,我就随便举一个例子来帮助各位去了解它的用法:

template<class T>void add(T x, T y)
{cout << "void add(T x, T y)" << endl;
}
template<>
void add<int>(int x,int y)
{cout <<"void add<int>(int x,int y)" << endl;
}
int main()
{add(1,2);add(1.1,1.2);return 0;
}

        可以看出编译器在使用模板函数的时候,对于特化和原模板函数,它会优先去调用特化函数,这个就像是之前小编在模板初阶的时候,讲述如果遇到特殊的函数,我们可以不用模板化,直接在写一个现成的函数就好了,这是方法之一,现在我们又多了一个方法,那就是讲它特化,对于这两种方法的选择,小编还是认为直接再写一个函数要方便的多,因为有时候的特化真的就是可能会出错,就比如下面代码如何进行特化:

template<class T>void add(const T& x,const T& y)
{cout << "void add(const T& x,const T& y)" << endl;
}
//如果此时我们想要比较的是指针类型怎么办

        很多的读者朋友遇到这个情况都可能会出错,因为此时我们在特化的时候,并不知道const到底指向哪里,这里我们要知道const修饰指针的一个规则:左值右指,const在”星号“的左边是限制的指针本身而不是它指向的值,const在“星号”的右边是限制的指针指向的内容不能改变,而不是指针本身,此时我们想要修饰的应该是x(指针本身),x的类型应该是&类型的,所以此时我们指针应该就是修饰整体,下面小编展示它的特化的写法:

template<>
void add<int*>(int * const & x,int * const& y)
{cout << "void add<int*>(int * const & x,int * const& y)" << endl;
}

        这么一看,特化的时候我们很有可能出错,与其从这思索如何特化,还不如再写一个函数,这样的话出错的概率就变小了,所以在函数模版上,小编认为能写写成的就写现成的,最好不要去写特化,对于普通类型还好说,对于上述加上const类型的,有时候我们真的会分不清const究竟要修饰哪里,不害臊的说,小编到这里还对const有点疑惑,所以小编对于上述的特殊类型的写法,还是喜欢写如下现成的:

void add(int* x,int* y)
{cout <<"void add(int* x,int* y)" << endl;
}

        这便就是函数模版特化的内容,其实真的学起来还是比较容易的,只不过小编还是不推荐函数进行特化,因为有时候特化太容易出错了,为了减少错误,咱们还是老老实实的去写一个现成的函数吧,出错的概率还小一点,对了,对于刚开始小编举的比较函数的例子,下面小编给出解决上述问题的两种方法:特化或者现成:

template <class T>
bool Less(T left,T right)
{return left < right;
}
//特化
template <>
bool Less<Date&>(Date* left,Date* right)
{return *left < *right;
}
//现成的
bool Less(Date* left,Date* right)
{return *left < *right;
}

2.3.类模版的特化

        函数模版的特化分为两种,分别是全特化偏特化,下面小编分别进行讲述。

2.3.1.全特化

        全特化就和它的名字一样,就是把模版参数的列表全部进行特化处理,就比如下面代码所示:

template<class T1 , class T2>class wang{public:wang(){cout << "wang<T1,T2>" << endl;}private:T1 _a;T2 _b;};
template<>
class wang<int,double> //和函数模板一样,此时我们把要特化的放在类名的后面
{public:wang(){cout << "wang<int,double>" << endl;}private:int _a;double _b;
};

        上面就是类模版的全特化,我们进行把类模版参数全部特殊化处理就好了,这个用法各位读者朋友要熟知,以后碰到相关的习题的时候不至于一问三不知,只不过全特化一般来说我们也是不常写的,等会小编要讲述的偏特化用处还是很大的,并且我们在把类模版给实例化以后,如果我们写的正好是全特化的,那么系统会自动调用特化的,这个就和现成和模版一个关系,全特化以后的类,其实就算不上一个真正的模板了,因为此时我们把模版参数全给定了,编译器无须自动转换了,对于现成的和需要自己制作的,编译器也是会偷懒的,所以这个点也要知道,上面函数模版的时候我忘记说了,就在这给各位讲述了吧。下面开始进入偏特化的讲解。

2.3.2.偏特化

        偏特化就是任何针对模版参数进一步进行条件限制涉及的特化版本,也就是说我们特化的时候只针对部分模版参数进行特化,这时候可能有很多读者朋友会问,既然就是对部分的模版参数进行偏特化,那么为啥不叫半特化呢?其实偏特化不仅仅就是针对部分模版参数进行特化,有的时候还会堆参数的进一步限制,等会我就分两种表现方式来讲述一下偏特化,下面我们先设置好一个模板类,从而可以更好的去进行讲述:

template<class T1, class T2>class wang{public:wang(){cout << "wang<T1,T2>" << endl;}private:T1 _a;T2 _b;};

        偏特化是有两种表现方式的,下面我分别讲述:

1.部分特化

        部分特化就是各位读者朋友理解的,对模板参量部分进行特殊化处理,就比如下面代码所示:

template<class T1>class wang<T1,int>{public:wang(){cout << "wang<T1,int>" << endl;}private:T1 _a;int _b;};

        上面就是部分特化的书写,其实还是很容易去理解的,下面我们在说说另一种表现的偏特化。

2.参数进一步的限制

        偏特化还可以对参数进行进一步的限制,可以让参数便的更加有局限性,就比如下面的写法:

//对参数进行两个指针的特化
template<class T1, class T2>class wang<T1*,T2*>{public:wang(){cout << "wang<T1*,T2*>" << endl;}private:T1 _a;T2 _b;};

        上面虽然模版参数还是按兵不动的,但是其实已经给他限制上了,此时我们的T1,T2变成了指针类型,这就是我们对于它进行的限制,当然不仅仅可以变成指针类型,有时候也可以是引用类型,这里小编就考验一下各位,此时如果我写的是:wang<int,double>,请问此时的T1,T2会被转换成什么,3,2,1,其实这里的T1,T2是会被转换为int和double类型,因为我们在实例化的时候,是先把T1,T2实例出来,然后再给他限制,这个特点各位读者朋友要好好地去理解。

        以上便就是关于类模版特殊的知识点,各位读者朋友好好知晓偏特化的两个表现方式,不要以后仅仅知道偏特化就是对部分参量进行特化,而把对于参数进行进一步的限制给直接忘记,下面我们进入下一部分的讲述。

img

3.分离编译

3.1.分离编译是什么

        一个程序(项目)由若干个源文件共同实现,而每一个源文件单独编译生成目标文件(obj),最后把所有目标文件链接起来形成单一的可以执行文件的过程叫做分离编译模式。小编之前写过的各种数据结构,都是这么写过来的,我一般头文件放结构体和函数的声明,源文件放定义,这就是一个简单的分离编译,一般之后我们写大型项目的时候,都会用分离编译的方式进行书写,一是显的没有那么乱,代码不会聚在一起显的冗余;二是可以更好的去各位每个人分配工作,每个人都有每个人的代码任务,这样的话效率会大大的提升,并且由减少了耦合度,增大了代码的可读性以及容易更改,但是记住一件事,模版是不可以分离编译,想要知道原因的话就继续往后进行阅读吧。

3.2.模版的分离编译

        如果我们把模版声明和定义的分离的话,就会出现下面的情况:

//test.h
#include<iostream>
using namespace std;
template<class T>
void add(T x);
//test01.cpp
template<class T>
void add(T x)
{cout << x << endl;
}
//test.cpp
#include"test.h"
int main()
{add(1);return 0;
}

        此时我们如果编译的时候,是不会报错的,但是当我们进行运行的时候,就会发现此时会报错误,这个错误是一个明显的链接错误,通过前面的LINK就知道是在链接的时候出错了,,可能很多读者朋友会好奇为啥子这里会进行链接错误,这就涉及到了一个编译的知识了,由于我学艺不精,于是找了个解释给各位读者朋友进行解释:

        上面就是对于这个知识点的解释,各位读者朋友可以看这个图片进行了解,目前的我无法很好的进行解答,我就不各位讲述错误的知识点,下面小编给出解决模板分离编译的两种方法:

3.3.解决方法
  1. 把声明和定义都放在一个头文件里面。这个方法小编还是很推荐的,小编在之前对于STL里面的容器和适配器都是把声明和定义放在一个头文件的,那时候我并没有详细解释为啥这样做,这里想必各位就知道为啥这样了。

  2. 模板定义的位置显示实例化。这个方法不实用,小编极力不推荐。于其这样我还不如正常写个函数或者类得了。

        小编在CSDN上面找到了一个二十年前一个大佬写的一篇关于分类编译的文章,各位读者朋友如果想要详情了解的话可以看看那个大佬的文章:为什么C++编译器不能支持对模板的分离式编译-CSDN博客,他写的真的很牛。

4.模板的总结

【优点】

  1. 模板复用了代码,节省资源,更快的迭代开发,C++标准库(STL)应运而生

  2. 增加了代码的灵活性

【缺点】

  1. 模板会导致代码膨胀问题,也会导致编译时间变长(毕竟要在编译的时候把那些模板参数转成我们想要的类型)

  2. 出现编译错误时,编译会非常的乱,不易定位错误。

img

5.总结

        本文到这里也就结束了,模板进阶的知识各位一定要好好的去了解,里面的内容可能以后我们不会用到,但是在一些选择题中可能就会考到相关的问题,所以小编认为这个知识点也是很重要的,各位大佬一定要好好掌握,截至到这,小编也是完成了对于C++的初步讲述,之后的C++学习难度就要往上增了,希望我能好好的掌握吧,初阶我还有很多知识点没讲出来,希望在年前,我可以完成对于vector,list,string的模拟实现把,那么各位大佬们,我们下一篇文章见啦!

img

相关文章:

  • PostSwigger Web 安全学习:CSRF漏洞3
  • 【学习笔记1】一站式大语言模型微调框架LLaMA-Factory
  • C#本地使用离线ocr库识别图片中文本,工具包PaddleOCRSharp
  • Git Bash 下使用 SSH 连接出现 “Software caused connection abort” 问题
  • 从基础到实战的量化交易全流程学习:1.3 数学与统计学基础——概率与统计基础 | 基础概念
  • Spark Streaming实时数据处理实战:从DStream基础到自定义数据源集成
  • 如何避免爬虫因Cookie过期导致登录失效
  • Kubernetes学习笔记-配置Service对接第三方访问
  • iOS 类与对象底层原理
  • 深度学习常见框架:TensorFlow 与 PyTorch 简介与对比
  • Postman接口测试: postman设置接口关联,实现参数化
  • 超级创新思路:基于CBAM-Transformer的强化学习时间序列预测模型(Python\matlab实现)
  • 【仿Mudou库one thread per loop式并发服务器实现】服务器边缘测试+性能测试
  • 小结: DHCP
  • Haply MinVerse触觉3D 鼠标—沉浸式数字操作,助力 3D 设计与仿真
  • 【QT】QT多线程
  • MySQL----查询
  • 计算机组成原理系列3--存储系统
  • 【C语言操作符详解(一)】--进制转换,原反补码,移位操作符,位操作符,逗号表达式,下标访问及函数调用操作符
  • 《代码整洁之道》全书归纳
  • 当AI开始深度思考,人类如何守住自己的慢思考能力?
  • 涨价应对关税变化是短期之策,跨境电商塑造新品牌开辟“新蓝海”
  • 日均新开三家“首店”,上海的“首发经济”密码是什么?
  • 云南鲁甸县一河滩突然涨水致4死,有人在救人过程中遇难
  • 杨荫凯已任浙江省委常委、组织部部长
  • 为国出征指纹却无法识别?他刷新了我军在这一项目的最好成绩