C++(模板进阶)


目录

前言:

本章学习目标:

1.非类型模版参数 

1.1使用方法 

1.2注意事项

1.3 实际引用

2.模版特化 

 2.1概念

2.2函数模板特化

 2.3类模板特化

2.3.1全特化

2.3.2偏特化 

 3.模版分离编译

​编辑 3.1失败原因

​编辑 3.2解决方案

4 总结 



前言:

本章节是在学习完STL之后,对高阶模版进行的总结,模板给泛型编程注入了灵魂,模板提高了程序的灵活性,模板包括:非类型模版参数、全特化、偏特化等,同时本文还会对模板声明和定义不能分离的问题做出介绍。

本章学习目标:

1. 非类型模板参数
2. 类模板的特化
3. 模板的分离编译

1.非类型模版参数 

模板参数分类 :类型形参与非类型形参。
类型形参即:    出现在模板参数列表中,跟在class或者typename之类的参数类型名称。
非类型形参:    就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。

1.1使用方法 

非类型模版参数 既然是使用常量作为参数,那么浮点数、类对象以及字符串是不允许作为非类型模版参数的,   非类型模版参数必须在编译期就能确认结果。

利用非类型模版参数创建一个可以自由调整大小的整形数组代码如下:

#include <assert.h>
using namespace std;template <size_t N>
class arr
{
public:int& operator[](size_t pos){assert(pos >= 0 && pos < N);return _arr[pos];}size_t size() const{return N;}private:int _arr[N];
};
int main()
{arr<5> a1;arr<10> a2;arr<15> a3;cout <<a1.size() << endl;cout << a2.size() << endl;cout << a3.size() << endl;return 0;
}

定义一个模板类型的静态数组

template <class T ,size_t N>
class arr
{
public:T& operator[](size_t pos){assert(pos >= 0 && pos < N);return _arr[pos];}size_t size() const{return N;}private:int _arr[N];
};
int main()
{arr<int,5> a1;arr<double,10> a2;arr<char,15> a3;cout <<a1.size()<< typeid(a1).name() << endl;cout << a2.size() << typeid(a2).name()<< endl;cout << a3.size() << typeid(a3).name()<< endl;return 0;
}

1.2注意事项

 非类型模板参数要求类型为 整型家族,其他类型是不被编译器通过的,如果我们使用非整形的家族成员就会报错,代码如下:

//浮点型,非标准
template<class T, double N>
class arr 
{/*……*/ 
};

 整形家族:char     short      int      bool         long           longlong

1.3 实际引用

 在 C++11 标准中,引入了一个新容器 array,它就使用了 非类型模板参数,为一个真正意义上的 泛型数组,这个数组是用来对标传统数组的。

注意: 部分老编译器可能不支持使用此容器

新引入arry非类型模版参数的代码 如下:

#include <iostream>
#include <cassert>
#include <array>  //引入头文件using namespace std;int main()
{int arrOld[10] = { 0 };	//传统数组array<int, 10> arrNew;	//新标准中的数组//与传统数组一样,新数组并没有进行初始化//新数组对于越界读、写检查更为严格 优点arrOld[15];	//老数组越界读,未报错arrNew[15];	//新数组则会报错arrOld[12] = 0;	//老数组越界写,不报错,出现严重的内存问题arrNew[12] = 10;	//新数组严格检查return 0;
}

array 是泛型编程思想中的产物,支持了许多 STL 容器的功能,比如 迭代器 和 运算符重载 等实用功能,最主要的改进是 严格检查越界行为。

需要提醒的是:arry能做到严格的越界检查 得益于 []的重载,对下标进行了严格检查。

2.模版特化 

 2.1概念

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结,需要特殊处理,比如:使用 日期类对象指针 构建优先级队列后,若不编写对应的仿函数,则比较结果会变为未定义 代码如下:

// 函数模板 -- 参数匹配
template<class 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;
}

造成每次结果不一样是因为 我们每次都日期类对象比较的时候 系统每次分配的地址都是随机的,我们对地址进行比较是不符合实际情况的。 

此时,就 需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特 化中分为 函数模板特化 类模板特化

2.2函数模板特化

1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。

上述日期进行比较的函数模版进行特化之后,代码如下:

// 函数模板 -- 参数匹配
template<class T>
bool Less(T left, T right)
{return left < right;
}
// 对Less函数模板进行特化
template<>
bool Less<Date*>(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;
}

 2.3类模板特化

 模板特化主要用在类模板中,它可以在泛型思想之上解决大部分特殊问题,并且类模板特化还可以分为:全特化和偏特化,适用于不同场景

后面用的日期类举例比较多,先把日期类放出来

class Date
{
public:Date(int year = 1970, int month = 1, int day = 1): _year(year), _month(month), _day(day){}bool operator<(const Date& d)const{return (_year < d._year) ||(_year == d._year && _month < d._month) ||(_year == d._year && _month == d._month && _day < d._day);}bool operator>(const Date& d)const{return (_year > d._year) ||(_year == d._year && _month > d._month) ||(_year == d._year && _month == d._month && _day > d._day);}private:int _year;int _month;int _day;
};

2.3.1全特化

 全特化指 将所有的模板参数特化为具体类型,将模板全特化后,调用时,会优先选择更为匹配的模板类

//原模板
template<class T1, class T2>
class Test
{
public:Test(const T1& t1, const T2& t2):_t1(t1),_t2(t2){cout << "template<class T1, class T2>" << endl;}private:T1 _t1;T2 _t2;
};//全特化后的模板
template<>
class Test<int, char>
{
public:Test(const int& t1, const char& t2):_t1(t1), _t2(t2){cout << "template<>" << endl;}private:int _t1;char _t2;
};int main()
{Test<int, int> T1(1, 2);Test<int, char> T2(20, 'c');return 0;
}

对模板进行全特化处理后,实际调用时,会优先选择已经特化并且类型符合的模板。

2.3.2偏特化 

 偏特化,指 将泛型范围进一步限制,可以限制为某种类型的指针,也可以限制为具体类型

//原模板---两个模板参数
template<class T1, class T2>
class Test
{
public:Test(){cout << "class Test" << endl;}
};//偏特化之一:限制为某种类型
template<class T1>
class Test<T1, int>
{
public:Test(){cout << "class Test<T, int>" << endl;}
};//偏特化之二:限制为不同的具体类型
template<class T>
class Test<T*, T*>
{
public:Test(){cout << "class Test<T*, T*>" << endl;}
};int main()
{Test<double, double> t1;Test<char, int> t2;Test<Date*, Date*> t3;return 0;
}

 

偏特化(尤其是限制为某种类型)在 泛型思想 和 特殊情况 之间做了折中处理,使得 限制范围式的偏特化 也可以实现 泛型

  • 比如偏特化为 T*,那么传 int*char*Date* 都是可行的

应用实例:

 有如下专门用来按照小于比较的类模板 :Less

#include<vector>
#include <algorithm>
template<class T>
struct Less
{bool operator()(const T& x, const T& y) const{return x < y;}
};
int main()
{Date d1(2022, 7, 7);Date d2(2022, 7, 6);Date d3(2022, 7, 8);vector<Date> v1;v1.push_back(d1);
v1.push_back(d2);v1.push_back(d3);// 可以直接排序,结果是日期升序sort(v1.begin(), v1.end(), Less<Date>());vector<Date*> v2;v2.push_back(&d1);v2.push_back(&d2);v2.push_back(&d3);// 可以直接排序,结果错误日期还不是升序,而v2中放的地址是升序// 此处需要在排序过程中,让sort比较v2中存放地址指向的日期对象// 但是走Less模板,sort在排序时实际比较的是v2中指针的地址,因此无法达到预期sort(v2.begin(), v2.end(), Less<Date*>());return 0;
}
通过观察上述程序的结果发现,对于日期对象可以直接排序,并且结果是正确的。但是如果待排序元素是指 针,结果就不一定正确。因为: sort 最终按照 Less模板中方式比较,所以只会比较指针,而不是比较指针指 向空间中内容,此时可以使用类版本特化来处理上述问题
// 对Less类模板按照指针方式特化
template<>
struct Less<Date*>
{bool operator()(Date* x, Date* y) const{return *x < *y;}
};

 3.模版分离编译

 

早在 模板初阶 中,我们就已经知道了 模板不能进行分离编译,会引发链接问题

 3.1失败原因

 声明与定义分离后,在进行链接时,无法在符号表中找到目标地址进行跳转,因此链接错误

下面是 模板声明与定义写在同一个文件中时,具体的汇编代码执行步骤

test.h 

#pragma once//声明
template<class T>
T add(const T x, const T y);//定义
template<class T>
T add(const T x, const T y)
{return x + y;
}

 main.cpp

#include <iostream>
#include "Test.h"using namespace std;int main()
{add(1, 2);return 0;
}

 

 声明与定义在同一个文件中时,可以直接找到函数的地址

编译器 生成可执行文件的四个步骤:

  1. 预处理:头文件展开、宏替换、条件编译、删除注释,生成纯净的C代码
  2. 编译:语法 / 词法 / 语义 分析、符号汇总,生成汇编代码
  3. 汇编:生成符号表,生成二进制指令
  4. 链接:合并段表,将符号表进行合并和重定位,生成可执行程序

当模板的 声明 与 定义 分离时,因为是 【泛型】,所以编译器无法确定函数原型,即 无法生成函数,也就无法获得函数地址,在符号表中进行函数链接时,必然失败 

 3.2解决方案

 解决方法有两种:

  1. 在函数定义时进行模板特化,编译时生成地址以进行链接
  2. 模板的声明和定义不要分离,直接写在同一个文件中
//定义
//解决方法一:模板特化(不推荐,如果类型多的话,需要特化很多份)
template<>
int add(const int x, const int y)
{return x + y;
}

 

//定义
//解决方法二:声明和定义写在同一个文件中
template<class T>
T add(const T x, const T y)
{return x + y;
}

这也就解释了为什么涉及 模板 的类,其中的函数声明和定义会写在同一个文件中 (.h),著名的 STL 库中的代码的声明和定义都是在一个 .h 文件中

为了让别人一眼就看出来头文件中包含了 声明 与 定义,可以将头文件后缀改为 .hpp,著名的 Boost 库中就有这样的命名方式 

 

4 总结 

 

模板是 STL 的基础支撑,假若没有模板、没有泛型编程思想,那么恐怕 "STL" 会变得非常大

模板的优点:

模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生
增强了代码的灵活性
模板的缺点:

模板会导致代码膨胀问题,也会导致编译时间变长
出现模板编译错误时,错误信息非常凌乱,不易定位错误位置
总之,模板 是一把双刃剑,既有优点,也有缺点,只有把它用好了,才能使代码 更灵活、更优雅

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/208074.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SQL 中的 MIN 和 MAX 以及常见函数详解及示例演示

SQL MIN() 和 MAX() 函数 SQL中的MIN()函数和MAX()函数用于查找所选列的最小值和最大值&#xff0c;分别。以下是它们的用法和示例&#xff1a; MIN() 函数 MIN()函数返回所选列的最小值。 示例&#xff1a; 查找Products表中的最低价格&#xff1a; SELECT MIN(Price) F…

设计循环队列,解决假溢出问题

什么是假溢出&#xff1f; 当我们使用队列这种基本的数据结构时&#xff0c;很容易发现&#xff0c;随着入队和出队操作的不断进行&#xff0c;队列的数据区域不断地偏向队尾方向移动。当我们的队尾指针指向了队列之外的区域时&#xff0c;我们就不能再进行入队操作了&#xff…

Go 语言函数、参数和返回值详解

函数是一组语句&#xff0c;可以在程序中重复使用。函数不会在页面加载时自动执行。函数将通过调用函数来执行。 创建函数 要创建&#xff08;通常称为声明&#xff09;一个函数&#xff0c;请执行以下操作&#xff1a; 使用 func 关键字。指定函数的名称&#xff0c;后跟括…

打不开clickonce问题解决过程

1.用户电脑user文件夹下有xx和xx.1两个账户,原先安装在xx账户下,后修了电脑原数据保留在xx.1,新创建XX,之后clickonce就打不开了表现为没有反应,,删除注册表和appdata都只能正常安装,但是不能打开,没有任何报错,发现在我的电脑下打开有这样的提示,,在用户电脑上没有 尝试通过修…

电大搜题——让学习变得轻松高效

作为一名现代学者&#xff0c;您一定时刻关注着教育领域的进展和创新。今天&#xff0c;我将向大家介绍一个名为“电大搜题”的神奇工具&#xff0c;它将为您的学习之路带来一场完美的革命。 在快节奏的现代社会中&#xff0c;学习已经成为每个人追求成功的必经之路。然而&…

攻防世界-web-Confusion1

1. 题目描述 打开链接&#xff0c;如图 点击Login和Rigister&#xff0c;都报错 但是有提示 指出了flag所在的位置&#xff0c;题目中直接能获取到的信息暂时就这么些了 2. 思路分析 既然告诉了我们flag文件的位置&#xff0c;那么要读取到这个文件&#xff0c;要么是任意文…

设计模式-创建型模式-工厂方法模式

一、什么是工厂方法模式 工厂模式又称工厂方法模式&#xff0c;是一种创建型设计模式&#xff0c;其在父类中提供一个创建对象的方法&#xff0c; 允许子类决定实例化对象的类型。工厂方法模式是目标是定义一个创建产品对象的工厂接口&#xff0c;将实际创建工作推迟到子类中。…

双流网络论文精读笔记

精读视频&#xff1a;双流网络论文逐段精读【论文精读】_哔哩哔哩_bilibili Two-Stream Convolutional Networks for Action Recognition in Videos 传统的神经网络难以学习到物体的运动信息&#xff0c;双流网络则通过光流将物体运动信息抽取出来再传递给神经网络 给模型提供…

未来制造业的新引擎:工业机器人控制解决方案

制造业正经历着一场革命性的变革 在这个变革的浪潮中&#xff0c;工业机器人成为推动制造业高效生产的关键力量。然而&#xff0c;要发挥机器人的最大潜力&#xff0c;一个强大而智能的控制系统是必不可少的。在这个领域&#xff0c;新一代的工业机器人控制解决方案正崭露头角&…

Gradle常用命令与参数依赖管理和版本决议

一、Gradle 常用命令与参数 本课程全程基于 Gradle8.0 环境 1、Gradle 命令 介绍 gradle 命令之前我们先来了解下 gradle 命令怎么在项目中执行。 1.1、gradlew gradlew 即 Gradle Wrapper&#xff0c;在学习小组的第一课时已经介绍过了这里就不多赘述。提一下执行命令&am…

python解决登录图形验证码

摘要:测试过程中经常遇到图片验证码,以下主要是调用百度OCR图片识别获取验证码,实现登录 1、百度云申请创建应用

Autoware.universe部署06:使用DBC文件进行UDP的CAN通信代码编写

目录标题 一、安装DBC文件编辑工具VectorCANdb二、编写DBC文件2.1 CAN通信协议2.2 编写DBC文件2.2.1 根据CAN协议设置signals2.2.2 设置报文2.2.3 建立节点 三、根据DBC文件编写ROS2驱动程序四、实际通信调试 根据CAN协议编写DBC文件&#xff0c;通过DBC文件编写ROS2包进行UDP通…