C++——模版(二)

前言

我们前面讲过模版的一,不知道大家还有没有所印象,如果大家不太能回忆起来可以再去前面看一下,那通过我们讲解了几个容器之后,相信大家现在应该已经对模版很熟悉了,那模版还剩下一些其他的内容我们就在这里进行讲解了,那我们就正式开始本篇文章啦


一、class和typenama的区别

在之前我们讲的时候,我们说class和typename定义模版参数的关键字,用哪个都可以,没有区别

template<class T>
void swap1(T& x, T& y)
{T tmp = x;x = y;y = tmp;
}template<typename T>
void swap2(T& x, T& y)
{T tmp = x;x = y;y = tmp;
}

对于上面这种情况来说确实没有任何的区别,但是在一个地方他们是有区别的,下面看一个例子

void Print(const vector<int>& v)
{vector<int>::const_iterator it = v.begin();while (it != v.end()){cout << *it << " ";++it;}cout << endl;
}int main()
{vector<int> v = { 1, 2, 3, 4, 5 };Print(v);return 0;
}

 当我们这样写去调用Print函数要去打印这个vector的时候是没有任何问题的,注意一下这里要用const迭代器,因为这个形参v就是const类型的,但是这样写又只能去打印vector的数据,那如果想做到任意类型的容器都能打印就得用模版,那我们来改一下

template<class Container>
void Print(const Container& c)
{Container::const_iterator cit = c.begin();while (cit != c.end()){cout << *cit << " ";++cit;}cout << endl;
}int main()
{vector<int> v = { 1, 2, 3, 4, 5 };Print(v);return 0;
}

那现在Container会被实例化成一个容器,去它里面取它的const迭代器,然后去遍历,看着好像没什么问题呀,但是这段代码会编译不通过

 编译器报错说const_iterator前必须以typename做前缀,这是什么意思呢?实际上这是因为编译器分不清这里的Container::const_iterator了,因为这里分两种情况,第一种是类型,第二种是对象,如果是类型这样写没问题,如果是对象就是错的,像上面的第一种写法,明确告诉编译器了这是vector<int>,这里已经实例化了,就直接去取const_iterator就可以,但是除了内嵌类型以外,静态成员变量也可以去这样取,编译器在这里就是不知道是哪种情况了,我们得告诉它一下,所以我们需要加上一个typename告诉编译器这是一个类型,等到模版实例化了再去找,然后去取里面的const_iterator

template<class Container>
void Print(const Container& c)
{// 告诉编译器这是一个类型typename Container::const_iterator cit = c.begin();while (cit != c.end()){cout << *cit << " ";++cit;}cout << endl;
}int main()
{vector<int> v = { 1, 2, 3, 4, 5 };Print(v);return 0;
}

如果是静态成员变量就这样写

class A
{
public:int begin(){return 0;}static int const_iterator;
};int A::const_iterator = 1;int main()
{A aa;A::const_iterator cit = aa.begin();return 0;
}

编译器分不清的就是这两种,所以我们才要去告诉编译器,那总结起来就是:类模版中取内嵌类型要加上关键字typename

上篇文章我们讲到的优先级队列这里就用到了


二、非类型模版参数

我们先来看一个静态栈

#define N 10// 静态栈
template<class T>
class Stack
{
private:T _a[N];int _top;
};
int main()
{//做不到Stack<int> st1; // 10Stack<int> st2; // 100return 0;
}

 这就是一个静态栈,栈里面最多存N个数据,那现在是无法做到一个栈存10个,一个栈存100个的,就像typedef一样,只能保证所有的栈中存的都是一个类型的数据,如果要存其他类型那就得去改typedef,要保证同时存不同的类型那就要定义多个栈,不满足复用的思想,对此,引入了模版,那模版参数除了可以是类型,还可以是非类型的

template<class T, size_t N = 10>
class Stack
{
private:T _a[N];int _top;
};int main()
{//可以做到Stack<int> st1; // 10Stack<int, 100> st2; // 100return 0;
}

那我们把元素个数也写成模版的,这就是非类型模版参数,可以给上一个缺省值,没传就开小一点,如果知道自己要用多少就直接开好空间,这样是不是就比以前好多了,所以我们说模版的本质就是去复用

需要注意的是:1、非类型模版参数必须是整形

                         2、非类型模版参数是常量,不能修改

// 浮点数不可以
//template<class T, double N>
template<class T, size_t N = 10>
class Stack
{
public:void func(){// 常量,不能修改N++;}
private:T _a[N];int _top;
};

array

非类型模版参数的一个典型应用就是array,array是C++11新增加的一个容器,是一个定长数组

那我们来试一下它的使用,它支持迭代器,所以可以用范围for,存储5个数据,打印出来看一下

 可以看到的是,array并不会初始化,那它有什么用呢?实际上它就是没有多大的用处,array是一个非常鸡肋的设计,那我们为什么不可以直接定义一个数组?为什么不能用vector?vector还可以用n个val初始化,要是真的说跟普通数组有什么区别的话,那就是越界的检查,普通数组对于越界读是检查不出来的,对于越界写是一种抽查,可能能检查出来,也可能检查不出来,我们实现了string和vector应该就知道了,[]在实现前会去断言一下有效性,要小于size,这也就是array越界读写都报错的机制,array在实践中是几乎没有什么用处的


三、模版的特化

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些
错误的结果,需要特殊处理,就像上一篇文章的日期类对象的指针,那现在比较的就是指针,而不是对象的大小
template<class T>
bool Less(const T& left, const T& right)
{return left < right;
}int main()
{int a = 1, b = 2;cout << Less(a, b) << endl;cout << Less(&a, &b) << endl;return 0;
}

Less内部并没有比较内容,而比较的是指针,这就无法达到预期而错误,这样的情况就是需要特殊处理的情况,此时,就需要对模板进行特化。即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式,模板特化中分为函数模板特化与类模板特化


函数模版的特化

先来看一下函数模版的特化的步骤

函数模板的特化步骤:
1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误
template<class T>
bool Less(const T& left, const T& right)
{return left < right;
}// 特化
template<>
bool Less<int*>(int* left, int* right)
{return *left < *right;
}

除了这样写还可以这样,直接不写成模版的,写成一个普通的函数

bool Less(int* left, int* right)
{return *left < *right;
}

这样写是不叫特化的,只是一种处理特殊情况的方式,函数模版和非函数模版是可以同名的,而且模版的匹配规则是在类型匹配的前提下有现成的就直接用,那刚才的Less(&a, &b)就会走这个普通的函数,推荐这样去写,不要像第一种那样写,这种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化。

除了这些原因以外,还有一个原因就是容易出错,我们再变一下,那现在是传值传参,我们把引用加上减少拷贝,加上引用又不改变最好加const

template<class T>
bool Less(const T& left, const T& right)
{return left < right;
}// 特化 编译报错
template<>
bool Less<int*>(int* left, int* right)
{return *left < *right;
}

现在会编译报错,编译器说下面的Less不是特化, 那这是为什么呢?我们在上面的函数模版的步骤4中可以看到,要求特化版本要和基础版本的参数类型完全相同,现在就是类型不匹配了,不满足步骤4,这里的T就是int*,那就把参数对照着写

template<class T>
bool Less(const T& left, const T& right)
{return left < right;
}// 特化 编译报错
template<>
bool Less<int*>(const int*& left, const int*& right)
{return *left < *right;
}

依然编译报错,模版的报错信息会很长,而且不太准,这里的错出在这个const,在基础版本中,const修饰的是left,left不能修改,而现在的特化版本中const修饰的是*left,那就变成了left指向的内容不能修改但是left本身可以修改,所以依然是类型不匹配,那我们就把const改成只修饰left,放在*的右边

template<class T>
bool Less(const T& left, const T& right)
{return left < right;
}template<>
bool Less<int*>(int* const& left, int* const& right)
{return *left < *right;
}

这样写就可以了,所以函数模版的特化是可能出现幺蛾子的,我们就直接写普通的函数就可以了

如果要把所有指针类型的都要特殊处理也可以这么写,也就是最后一种

template<class T>
bool Less(const T& left, const T& right)
{return left < right;
}template<>
bool Less<int*>(int* const& left, int* const& right)
{return *left < *right;
}bool Less(int* left, int* right)
{return *left < *right;
}template<class T>
bool Less(T* left, T* right)
{return *left < *right;
}int main()
{int a = 1, b = 2;cout << Less(a, b) << endl;cout << Less(&a, &b) << endl;double c = 1.1, d = 2.2;cout << Less(&c, &d) << endl;return 0;
}

类模版的特化

类模版的特化又分为两种,一个是全特化,一个是偏特化

全特化就是将模板参数列表中所有的参数都确定化

template<class T1, class T2>
class Data
{
public:Data() { cout << "Data<T1, T2>" << endl; }
};// 全特化
template<>
class Data<int, double>
{
public:Data() { cout << "Data<int,double>" << endl; }
};int main()
{Data<int, int> d1;  // Date<T1, T2>Data<int, double> d2;  // Date<int, double>return 0;
}

偏特化就是针对模版参数进一步进行条件限制设计的特化版本

偏特化又分为两种,第一种是 将模板参数类表中的一部分参数特化
template<class T1, class T2>
class Data
{
public:Data() { cout << "Data<T1, T2>" << endl; }
};// 偏特化:特化部分参数
template<class T1>
class Data<T1, double>
{
public:Data() { cout << "Data<T1, double>" << endl; }
};int main()
{Data<int, int> d1;  // Date<T1, T2>Data<int*, double> d2;  // Date<T1, double>return 0;
}

第二种就针对模板参数更进一步的条件限制所设计出来的一个特化版本

template<class T1, class T2>
class Data
{
public:Data() { cout << "Data<T1, T2>" << endl; }
};// 偏特化:可能是对某些类型的进一步限制
template<class T1, class T2>
class Data<T1*, T2*>
{
public:Data() { cout << "Data<T1*, T2*>" << endl; }
};template<class T1, class T2>
class Data<T1&, T2&>
{
public:Data() { cout << "Data<T1&, T2&>" << endl; }
};int main()
{Data<int, int> d1;  // Date<T1, T2>Data<double*, double*> d2; // Date<T1*, T2*>Data<int&, double&> d3; // Data<T1&, T2&>return 0;
}

那针对于我们之前的优先级队列插入一个日期类的指针时,因为默认的仿函数比较的是指针的问题,需要在外部自己写一个传进来,但现在有了特化,所以我们可以这么写

template<class T>
struct Less
{bool operator()(const T& x, const T& y){return  x < y;}
};template<>
struct Less<Date*>
{bool operator()(Date* const& x, Date* const& y) const{return *x < *y;}
};

现在就不需要自己传了,在调用的时候就会走特化版本,再去调日期类的<,我们的代码就灵活了很多


三、模版的声明与定义分离

我们之前说模版是不能声明与定义分离的,会报错,那我们就看看它报错的原因是什么,会什么会报错

// func.h
template<class T>
struct A
{T Add(const T& x, const T& y);
};struct B
{int Sub(const int& x, const int& y);
};// func.cpp
template<class T>
T A<T>::Add(const T& x, const T& y)
{return x + y;
}int B::Sub(const int& x, const int& y)
{return x - y;
}// test.cpp
int main()
{int a = 1, b = 2;A<int> aa;aa.Add(a, b);B bb;bb.Sub(a, b);return 0;
}

C/C++程序要运行前需要经历预处理->编译->汇编->链接,编译就是进行词法分析,语法分析,语义分析,看有没有语法错误,如果没有错误就生成汇编代码,多个源文件是分开编译的,首先Add和Sub函数都能通过编译,这是因为编译只看声明,声明是一种承诺,等到链接的时候再去其他文件的符号表查找,但是奇怪的是,Add和Sub在func.cpp中都定义了,但是只能查找到Sub,不能查找到Add,这里的原因是,Sub因为定义了,所以在符号表中可以查找到,虽然Add也定义了,但是Add并没有实例化,也就没有生成对应的代码,那自然就没有进符号表,所以找不到,报的是链接错误


解决方法有两种,第一种是显示在定义的文件中,显示实例化

template<class T>
T A<T>::Add(const T& x, const T& y)
{return x + y;
}int B::Sub(const int& x, const int& y)
{return x - y;
}template
class A<int>;

在func.cpp文件中这样写,告诉编译器实例化成int,那就有了具体的函数,链接时就可以找到,但是这种方法不实用,一般不用


第二种方法就是同一个文件内声明与定义分离

template<class T>
struct A
{T Add(const T& x, const T& y);
};struct B
{int Sub(const int& x, const int& y);
};template<class T>
T A<T>::Add(const T& x, const T& y)
{return x + y;
}

在.h文件里定义Add,这里要注意Sub函数不能在.h里实现,因为func.cpp和test.cpp都包含了func.h,那就会在两个.cpp文件中展开,就冲突了,所以Sub函数要不就直接在类内实现了,要不就放在func.cpp里实现


总结

模版会让我们的灵活性变得更强,包括之前的适配器,仿函数也可以看出来,但是模版的缺点就是出错了会非常难看,非常长,也很凌乱,但是,模版的优点还是远大于它的缺点的,如果没有模版那就要多写很多代码,那维护起来也就更加困难,本篇文章到这里就结束了,如果觉得小编写的不错,大家可以给一个三连,感谢大家支持!!!

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

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

相关文章

算法与数据结构(旋转链表)

题目 思路 每个节点向右移动k个位置&#xff0c;其实就是从头开始遍历&#xff0c;将n-k个节点顺序插入到链表的尾部。 如上图所示的示例1&#xff0c;先将1插入到5的后面&#xff0c;再将2插入到1的后面&#xff0c;最后将3插入到2的后面即可。 代码详解 定义一个cur变量用…

TOGAF之架构标准规范-信息系统架构 | 应用架构

TOGAF是工业级的企业架构标准规范&#xff0c;信息系统架构阶段是由数据架构阶段以及应用架构阶段构成&#xff0c;本文主要描述信息系统架构阶段中的应用架构阶段。 如上所示&#xff0c;信息系统架构&#xff08;Information Systems Architectures&#xff09;在TOGAF标准规…

智能优化算法:莲花算法(Lotus flower algorithm,LFA)介绍,提供MATLAB代码

一、 莲花算法 1.1 算法原理 莲花算法&#xff08;Lotus flower algorithm&#xff0c;LFA&#xff09;是一种受自然启发的优化算法&#xff0c;其灵感来源于莲花的自清洁特性和授粉过程。莲花的自清洁特性&#xff0c;即所谓的“莲花效应”&#xff0c;是由其叶片表面的微纳…

CSS 媒体查询:从入门到精通,打造跨设备完美体验

在当今移动互联网时代&#xff0c;用户访问网站的设备早已不再局限于桌面电脑&#xff0c;手机、平板等各种屏幕尺寸的设备层出不穷。为了确保用户在不同设备上都能获得良好的浏览体验&#xff0c;响应式网页设计应运而生。而 CSS 媒体查询&#xff0c;正是实现响应式设计的核心…

【Python LeetCode 专题】树

LeetCode 题目104. 二叉树的最大深度(gif 图解)方法一:后序遍历(DFS)方法二:层序遍历(BFS)872. 叶子相似的树(DFS 遍历)1448. 统计二叉树中好节点的数目(DFS 遍历)437. 路径总和 III(前缀和 + DFS 回溯)1372. 二叉树中的最长交错路径(DFS)236. 二叉树的最近公共…

Spring有哪些缺点?

大家好&#xff0c;我是锋哥。今天分享关于【Spring有哪些缺点?】面试题。希望对大家有帮助&#xff1b; Spring有哪些缺点? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Spring框架是一个广泛使用的企业级Java开发框架&#xff0c;提供了丰富的功能和强大的灵…

MySQL数据库——索引潜规则(回表查询、索引覆盖、索引下推)

大家好&#xff0c;这里是编程Cookbook。本文详细介绍MySQL索引的三个潜规则——回表查询、索引覆盖、索引下推&#xff0c;以提升数据库的性能。 文章目录 索引回顾聚集索引&#xff08;Clustered Index&#xff09;非聚集索引&#xff08;Secondary Index/辅助索引/二级索引&…

VScode运行后出现黑窗口

原文链接&#xff1a;VScode运行出黑窗口 1.安装插件&#xff1a;C/C Compile Run 2.快捷键【CtrlShiftp】,点击【首选项&#xff1a;打开用户设置】

使用大语言模型(Deepseek)构建一个基于 SQL 数据的问答系统

GitHub代码仓库 架构 从高层次来看&#xff0c;这些系统的步骤如下&#xff1a; 将问题转换为SQL查询&#xff1a;模型将用户输入转换为SQL查询。 执行SQL查询&#xff1a;执行查询。 回答问题&#xff1a;模型根据查询结果响应用户输入。 样本数据 下载样本数据&#xf…

【前端小点】vue3项目内根据主题读取不同文件夹下的图片资源(图片文件)

项目要求实现一键换肤的功能&#xff0c;不仅仅是主题颜色上的替换&#xff0c;还有图片素材的替换&#xff0c;主题颜色替换的方案大同小异&#xff0c;下面仅对图片素材的一件替换进行方法描述。 主要思路 使用本地仓库对当前主题进行存储&#xff0c;系统根据主题去加载不同…

vxe-table实现动态列

vxe-table实现动态列 1.动态列解释2.解决步骤2.1将后端返回的动态列表头&#xff0c;按照格式拼接在固定列表头上2.2将后端返回的列表数据按照键值对格式组装 1.动态列解释 正常列表是有固定的列&#xff1b;我的需求是&#xff0c;最初只知道表格的固定两列&#xff0c;查询数…

Windows 11 使用容器(Docker Podman)

文章目录 背景1、相关网站1.1、WSL1.2、Docker1.3、Podman 2、环境3、安装部署3.1、安装 WSL3.2、Docker3.2.1、Docker Desktop3.2.1.1、安装3.2.1.2、拉取镜像3.2.1.3、启动容器 3.3、Podman3.3.1、安装3.3.2、使用3.3.3、异常处理 总结 背景 Windows 系统中使用容器&#xf…

UE_C++ —— Gameplay Modules

目录 一&#xff0c;Module Creation INI File Setup 二&#xff0c;Multiple Gameplay Modules 三&#xff0c;Limitations 编译成 DLL 的游戏相关类的集合&#xff1b;正如引擎本身由一组模块构成一样&#xff0c;每个游戏也是由一个或多个游戏模块构成的&#xff1b;这些…

蓝桥杯定时器实现led闪烁

step1.配置定时器&#xff0c;TIM1时高级定时&#xff0c;TIM2是通用定时器&#xff0c;用TIM2就行&#xff0c;用内部时钟源&#xff0c;记住相关公式&#xff0c;定时器中断配置时要使能&#xff0c;且生成代码后也要在mian中写使能函数 step2.写代码 配置生成代码后多出的…

二:前端发送POST请求,后端获取数据

接着一&#xff1a;可以通过端口访问公网IP之后 二需要实现&#xff1a;点击飞书多维表格中的按钮&#xff0c;向服务器发送HTTP请求&#xff0c;并执行脚本程序 向服务器发送HTTP请求&#xff1a; 发送请求需要明确一下几个点 请求方法&#xff1a; 由于是向服务器端发送值…

内外网文件传输 安全、可控、便捷的跨网数据传输方案

一、背景与痛点 在内外网隔离的企业网络环境中&#xff0c;员工与外部协作伙伴&#xff08;如钉钉用户&#xff09;的文件传输面临以下挑战&#xff1a; 安全性风险&#xff1a;内外网直连可能导致病毒传播、数据泄露。 操作繁琐&#xff1a;传统方式需频繁切换网络环境&…

elasticsearch在windows上的配置

写在最前面&#xff1a; 上资源 第一步 解压&#xff1a; 第二步 配置两个环境变量 第三步 如果是其他资源需要将标蓝的文件中的内容加一句 xpack.security.enabled: false 不同版本的yaml文件可能配置不同&#xff0c;末尾加这个 xpack.security.enabled: true打开bin目…

OpenCV二值化处理

1.1. 为什么需要二值化操作 二值化操作将灰度图像转换为黑白图像&#xff0c;即将图像中的像素值分为两类&#xff1a;前景&#xff08;通常为白色&#xff0c;值为 255&#xff09;和背景&#xff08;通常为黑色&#xff0c;值为 0&#xff09;。二值化的主要目的是简化图像&…

[Android]浏览器下载的apk文件无法识别无法安装问题

在Android电话机上&#xff0c;用浏览器下载apk进行版本更新&#xff0c;出现下载文件没被识别为apk&#xff0c;导致无法安装问题。 原来的下载链接&#xff1a; https://mojsetup.obs.cn-southwest-2.myhuaweicloud.com/callphone-release-1.0.4.apk 修改后的下载链接&…

如何使用SSH连接设备?很简单!

前言 小白发现最近写的文章都与SSH息息相关&#xff0c;于是就有了这一篇文章&#xff0c;免得在后续的文章又不断重复如何SSH连接设备。 有需要的小伙伴自然就会看到这里&#xff0c;也不会影响到其他小伙伴的阅读体验。 至于文章里的广告嘛……就当是小伙伴们给小白的一点…