C++---模板进阶(非类型模板参数,模板的特化,模板分离编译)
我们都学习和使用过模板,而这篇文章我们来将一些更深入的知识。在此之前,我们在使用C++编程时可以看到模板是随处可见的,它能支持泛型编程。模板包括函数模板和类模板,我们有的人可能会说是模板函数和模板类,但严格讲这样说是错误的。而在我们实际使用中,类模板用的场景是比函数模板多的,如STL中vector,list等都是类模板,而算法中sort,find等是函数模板。
非类型模板参数
模板参数分为类型模板形参,非类型形参。
类型形参:出现在模板参数列表中,跟在class或typename关键字之后的参数类型名称。(我们之前一直使用的)
非类型形参:用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用。
举个例子:
我们想要创建一个大小固定,可以存储不同类型元素的数组类,我们可以这样写:
#include<iostream>
using namespace std;
#define N 10template<class T>
class Array
{
private:int _a[N];
};
int main()
{Array<int> arr1;Array<int> arr2;return 0;
}
这时实例化出来的arr1,arr2都是大小为10的定长数组。但我们假如说想改变一个数组的长度呢?例如我们想将数组长度改为20,怎么办呢?有同学说我们可以将N define为20呀。但是假如我们想要一个数组长为10,另一个长为20呢?
这时候我们的非类型模板参数就有作用了,我们可以将Array定义为下面的形式:
#include<iostream>
using namespace std;//这里的N叫做非类型模板参数,它是一个常量
template<class T, size_t N>
class Array
{
private:int _a[N];
};
int main()
{Array<int, 10> arr1;Array<int, 20> arr2;int n = 10;Array<int, n> arr3;//error 这里非类型模板参数是常量,这里会报错return 0;
}
对于非类型模板参数的使用场景,STL中的deque和array容器中都用了。deque中的非类型模板参数用来传一个一个常量来控制buff的大小。
我们下面介绍一下array容器:
C++11支持array,它的结构类似于vector,只不过vector是动态数组,而array是静态的。它不提供头插,头插,尾插,尾删,任意位置插入删除这种操作,因为它根本不存在这种说法,而且它可以直接使用operator[ ] 访问修改任意位置的数据。
array的缺陷:
我们不推荐使用array,array底层是在栈上开辟空间的,而栈空间又是很有限的,例如:在32位的linux下栈空间只有8M,所以一般开大空间时极不推荐使用array,相比之下vector就很有优势。而且在知道要开多大空间的情况下,vector 也可以通过reserve一次性开好空间,在后续的使用中还可以自动增容,而array空间是固定的,不灵活。所以我们一般不使用array。
注意:
- 非类型模板参数只允许使用整形家族(int,short,long,long long,char),而浮点型,类对象,字符串是不允许作为非类型模板参数的
- 非类型模板参数需要在编译的时候就确认结果,因为编译器在编译阶段就需要根据传入的非类型模板参数生成对应的类或函数
模板的特化
我们先举个例子,理解一下模板的特化,看下面的代码:
template<class T>
bool IsEqual(const T& left, const T& right)
{ return left == right;
}
int main()
{cout << IsEqual(1, 2) << endl;char p1[] = "hello";char p2[] = "world";cout << IsEqual(p1, p2) << endl;return 0;
}
我们实现了IsEqual函数用来判断两个参数是否相等。我们总共调用了两次,第一次比较的是两个整数,是没问题的;而第二次实际上是有问题的,我们本意是想比较两个字符串是否相等,可是我们仔细看看:我们实际上比较的p1,p2两个指针。我们可能会想先判断一下传过来的参数的类型是什么,再进行不同的操作,就像下面这样:
template<class T>
bool IsEqual(const T& left, const T& right)
{if(T == char*)//error,C++中不支持类型比较{return *left==*right;}else{...}return left == right;
}
int main()
{cout << IsEqual(1, 2) << endl;//okchar* p1 = "hello";char* p2 = "world";cout << IsEqual(p1, p2) << endl;//errreturn 0;
}
但是很可惜,C++中不支持类型比较,所以上面写的是错误的。
那么这里就需要我们的模板特化出手了,模板特化的目的就是在原模板类的基础上,针对某些类型进行特殊化处理。模板特化又分为函数模板特化和类模板特化。
函数模板的特化
- 首先必须有一个基础的函数模板
- 关键字template后面接一对空的尖括号<>
- 函数名后跟一对<>,尖括号内指定需要特化的类型
- 函数形参表必须要和函数模板的基础参数类型完全相同,如果不同,编译器可能会报一些奇怪的错误
我们上面的代码就可以改为:
#include<iostream>
using namespace std;template<class T>
bool IsEqual(const T& left, const T& right)
{return left == right;
}template<>
bool IsEqual<const char *&>(const char*& left,const char*& right)
{return strcmp(left, right)==0;
}
int main()
{cout << IsEqual(1, 2) << endl;const char* p1 = "hello";const char* p2 = "hello";cout << IsEqual(p1, p2) << endl;return 0;
}
说明:其实一般情况下,如果函数模板遇到不能处理或者处理有误的类型,可以直接将该函数直接给出,这叫做模板的匹配原则:有现成的完全匹配的,就直接调用,没有现成调用的,实例化模板生成。上面的代码就可以这样写:
#include<iostream>
using namespace std;template<class T>
bool IsEqual(const T& left, const T& right)
{return left == right;
}bool IsEqual(const char*& left,const char*& right)
{return strcmp(left, right)==0;
}
int main()
{cout << IsEqual(1, 2) << endl;const char* p1 = "hello";const char* p2 = "hello";cout << IsEqual(p1, p2) << endl;return 0;
}
对于模板匹配原则和函数模板特化,两者底层没有任何差别,如果能使用函数模板特化的时候更推荐使用函数模板特化。
类模板特化
1.全特化
就是将模板参数列表中所有参数确定化,例如:
#include<iostream>
using namespace std;template<class T1,class T2>
class A
{
public:A(){cout << "T" << endl;}
private:T1 a1;T2 a2;
};template<>
class A<int,int>
{
public:A(){cout << "int" << endl;}
private:
};int main()
{A<double, double>a1;A<int, int>a2;return 0;
}
2.偏特化
也叫半特化:即对模板参数进行一定的确定化,例如:
template<class T1>//第二个参数是int类型就会调用这个
class A<T1, int>
{
public:A(){cout << "T1,int" << endl;}
private:
};template<class T2>//第一个参数是int类型就会调用这个
class A<int, T2>
{
public:A(){cout << "int,T2" << endl;}
private:
};template<class T1,class T2>//两个参数都是指针类型就会调用这个
class A<T1*, T2*>
{
public:A(){cout << "T1*,T2*" << endl;}
private:
};template<class T1, class T2>//两个参数都是引用类型就会调用这个
class A<T1&, T2&>
{
public:A(){cout << "T1&,T2&" << endl;}
private:
};template<class T1, class T2>//第一个参数是指针类型,第二个参数是引用类型就会调用这个
class A<T1*, T2&>
{
public:A(){cout << "T1*,T2&" << endl;}
private:
};int main()
{A<double, int>a;A<int, char>b;A<double*, int*>c;A<char&, int&>d;A<double*, char&>e;return 0;
}
上面这几种特化版本都是偏特化。
偏特化并不仅仅是指特化,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本,例如我们可以限制他的类型是引用,指针之类的。
关于特化的场景我们等到哈希表的时候会给大家介绍。
模板分离编译
我们在工作的时候写的项目都会由若干个源文件共同实现,每个源文件独立编译生成目标文件,最后将所有目标我呢见链接起来形成单一的可执行文件的过程称为分离编译模式。
模板的分离编译
在前面我们模拟实现string,vector,list等STL容器的时候,都没有将声明和定义分开。我们实际上是习惯将声明放在头文件中,将定义放在.cpp文件中的。我们没有分离的原因就是C++的模板不支持分离编译。
我们以这段代码为例:
//Func.h代码:
#pragma once
#include<iostream>
using namespace std;void Print();template<class T>
void F(T a);//Func.cpp代码
#include"Func.h"void Print()
{cout << "print" << endl;
}template<class T>
void F(T a)
{cout << "F(T a)" << endl;
}//test.cpp代码
#include"Func.h"int main()
{Print();F(10);//这里编不过return 0;
}
不支持分离编译的原因:
我们都知道:程序的编译过程分为:预处理,编译,汇编,链接四个步骤
其中预处理会做的事情就是:头文件展开,宏替换,条件编译,去掉注释,在linux环境下就生成了.i文件
编译:检查语法错误后,生成汇编代码,在linux环境下生成.s文件

汇编:将汇编代码转成二进制机器码,在linux环境下生成.o文件
链接:会把.o文件里F或Print这样没有地址的地方,用被修饰过的函数名去Func.o中的符号表找对应的地址,然后填上地址就像上图中的地址一样。然后再把目标文件合并成可执行程序。
而问题就出现在链接的时候,编译器拿着我们的函数名去符号表中找时,能找到Print()函数,但是却找不到F()函数,这是因为在编译阶段,Print()函数有定义,可以生成。但是F()函数是一个函数模板,他不能生成,因为我们不知道T是什么类型。模板其实是调用时生成的。
如何解决
一、
这种方法实际上是不可行的,就是让编译器在编译的时候去各个地方查找实例化,例如我们在Func.i中看到一个模板,我们就去Test.i中找实例化,但是这样的话,如果是一个大项目的化,有几十几百个文件,对于编译器的实现就复杂了。所以实际在链接前,各文件间是不会交互的
二、
显示指定实例化,编译器看到后就知道要把这个模板参数T实例化成什么类型。但是这样做的问题也很大:就是我换个类型就又链接不上了,那我们就只能使用一种类型就实例化一次,这样就很麻烦。
三、
最后这种方法就很粗暴,STL中用的也是这种方法,就是不分离编译,声明和定义都放在一个.h文件里。这样的话,.h文件中就包含了模板的定义,就不需要链接的时候去查找了,在编译的阶段就能直接填地址了。而我们在项目中一般把这种文件命名为后缀为.hpp的文件,也就是说它即是.h文件,又是.cpp文件。
同样我们的类模板也不支持分离编译,最好的办法也是不分离,写在同一个文件中。
我在这里还要在解释我上文中的一句话:模板其实是调用时实例化生成的。我们在一个类模板或者函数模板中,假如写出了语法错误,假如我们没有实例化运行的话,编译器是检查不出错误的,这就是因为模板是调用时实例化的,没有实例化的时候,编译器不会去检查函数模板或类模板内部的语法错误。
总结
模板优点:
1.模板复用了代码,节约资源,更快的迭代开发,有了模板才有了C++的标准模板库STL的诞生
2.增强了代码的灵活性
模板缺点:
1.模板会导致代码膨胀问题,也会导致编译时间变长
2.假如我们使用模板时出现错误,编译器提示的错误信息会非常凌乱,而且准确度不高,我们不能盲目地相信模板的报错。可能只是一点小错误,最后却报出了一大堆错误,这时候我们要优先看第一个错误。
以上就是本章的全部内容,谢谢大家!
相关文章:
C++---模板进阶(非类型模板参数,模板的特化,模板分离编译)
我们都学习和使用过模板,而这篇文章我们来将一些更深入的知识。在此之前,我们在使用C编程时可以看到模板是随处可见的,它能支持泛型编程。模板包括函数模板和类模板,我们有的人可能会说是模板函数和模板类,但严格讲这样…...
锂电池寿命预测 | Matlab基于SSA-SVR麻雀优化支持向量回归的锂离子电池剩余寿命预测
目录 预测效果基本介绍程序设计参考资料 预测效果 基本介绍 【锂电池剩余寿命RUL预测案例】 锂电池寿命预测 | Matlab基于SSA-SVR麻雀优化支持向量回归的锂离子电池剩余寿命预测(完整源码和数据) 1、提取NASA数据集的电池容量,以历史容量作…...
整理好了!2024年最常见 20 道 Kafka面试题(十)
上一篇地址:整理好了!2024年最常见 20 道 Kafka面试题(九)-CSDN博客 十九、Kafka的消费者如何实现幂等性? 在Kafka中,幂等性指的是消费者处理消息时,即使多次接收到同一条消息,也能…...
Paper Survey——3DGS-SLAM
之前博客对多个3DGS SLAM的工作进行了复现及代码解读 学习笔记之——3DGS-SLAM系列代码解读_gs slam-CSDN博客文章浏览阅读1.9k次,点赞15次,收藏45次。最近对一系列基于3D Gaussian Splatting(3DGS)SLAM的工作的源码进行了测试与…...
搜索与图论:深度优先搜索
搜索与图论:深度优先搜索 题目描述参考代码 题目描述 参考代码 #include <iostream>using namespace std;const int N 10;int n; int path[N]; bool st[N];void dfs(int u) {// u n 搜索到最后一层if (u n){for (int i 0; i < n; i) printf("%d …...
AMD显卡和英伟达显卡哪个好?
显卡是计算机中负责处理图形和视频输出的硬件设备,主要分为两种类型:AMD的A卡和NVIDIA的N卡。那么AMD显卡和英伟达显卡哪个好?怎么选? 答:不能一概而论地说哪个好,因为它们各有优势,选择应基于…...
5.31.8 学习深度特征以实现判别定位
1. 介绍 尽管没有对物体的位置提供监督,但卷积神经网络 (CNN) 各层的卷积单元实际上可以充当物体检测器。尽管卷积层具有这种出色的物体定位能力,但当使用全连接层进行分类时,这种能力就会丧失。最近,一些流行的全卷积神经网络,如 Network in Network (NIN) [13] 和 Goog…...
uniapp小程序多线程 Worker 实战【2024】
需求 最近遇到个小程序异步解码的需求,采用了WebAssembly,涉及大量的计算。由于小程序的双线程模型只有一个线程处理数据,因此智能寻求其它的解决方案。查看小程序的文档,发现小程序还提供一个异步线程的Worker方案,可…...
C语言基础——数组(2)
ʕ • ᴥ • ʔ づ♡ど 🎉 欢迎点赞支持🎉 个人主页:励志不掉头发的内向程序员; 专栏主页:C语言基础; 文章目录 前言 一、二维数组的创建 1.1 二维数组的概念 1.2二维数组的创建 二、二维数组…...
封装PHP用于发送GET和POST请求的公共方法
封装了ThinkPHP用于发送GET和POST请求的公共方法。这个方法可以放在你的公共函数文件中,或者创建一个独立的类来管理这些请求。 <?php namespace app\common\utils;use think\facade\Log; use think\exception\HttpException;class HttpRequest {/*** 发送GET请…...
MongoDB~基础知识记录
为何要学Mongodb 工作以来,使用最多、了解最多的是MySQL。但技术的发展一定是依据痛点来的,就比如我遇到的痛点,一个业务、一个平台能力、存储的一个对象,随着产品和运营的需求,不断的进行变更,每一次的变…...
DSP28335模块配置模板系列——ADC配置模板
一、配置步骤 1.使能并配置高速时钟HSPCLK、ADC校验 EALLOW;SysCtrlRegs.PCLKCR0.bit.ADCENCLK 1; EDIS;EALLOW;SysCtrlRegs.HISPCP.all ADC_MODCLK; // HSPCLK SYSCLKOUT/(2*ADC_MODCLK)ADC_cal();EDIS; 这里ADC_MODCLK3,所以HSPCLK时钟为150/625Mhz 2.配…...
字符串转换为字节数组、16进制转换为base64、base64转换为字符串数组、base64转换为16进制(微信小程序)
1、字符串转换为字节数组 // 字符串转为字节数组 function stringToByteArray(str) {var array new Uint8Array(str.length);for (var i 0; i < str.length; i) {array[i] str.charCodeAt(i);}return array; } 2、16进制转换为base64 // 16进制转换为base64 function H…...
c++中, 直接写浮点数, 是float 还是 double?
如果直接一个浮点数, 那么他默认是float还是double呢? 测试用例 #include <iostream> using namespace std;int main() {auto x 0.2;float f 0.2;double d 0.2;cout << "x Size : " << sizeof(x) << " bytes" << endl…...
C++核心编程友元的应用
文章目录 1.友元1.什么是友元2.全局函数做友元2.类做友元3.成员函数做友元 1.友元 1.什么是友元 在C中,友元(friend)是一种允许一个类或函数访问另一个类的非公有(private 或 protected)成员的机制。这种机制打破了类…...
C#,JavaScript实现浮点数格式化自动保留合适的小数位数
目标 由于浮点数有漂移问题,转成字符串时 3.6 有可能得到 3.6000000000001,总之很长的一串,通常需要截取,但按照固定长度截取不一定能使用各种情况,如果能根据数值大小保留有效位数就好了。 C#实现 我们可以在基础库里…...
Android基础-工程目录结构说明
Android工程的项目目录结构是开发Android应用时的基础,它组织和存储了应用的所有源代码、资源和配置文件。了解并熟悉这个目录结构对于提高开发效率和代码管理至关重要。下面将详细阐述Android工程的项目目录结构。 1. 工程根目录 Android工程的根目录通常包含多个…...
浅谈提示词发展现状,Prompt 自动优化是未来。
#封面手绘于本科期间,当年在知乎上写的第一篇关于 AI 的文章就用的这个封面,聊表纪念。 这次我们来聊聊 Prompt. 本来想取一个类似“提示词不存在了…”,或是“再见,Prompt 课程…”的标题,但最近很多大佬的谬赞让我感…...
揭秘智能测径仪省钱之道!每年能为每条产线省上百万!
在当今竞争激烈的市场环境下,企业们都在不断寻求提高生产效率、降低成本的方法。而智能测径仪的出现,为圆形钢材、螺纹钢等生产企业实现这一目标提供了有力的支持。 智能测径仪被广泛应用于高线、铸管、圆钢、螺纹钢、钢筋等的轧制生产线中,进…...
echaerts图例自动滚动并隐藏翻页按钮
效果图 代码 legend: {itemHeight: 14,itemWidth: 14,height: "300", //决定显示多少个// 通过 CSS 完全隐藏翻页按钮pageButtonItemGap: 0,pageButtonPosition: end,pageIconColor: transparent, // 隐藏翻页按钮pageIconInactiveColor: transparent, // 隐藏翻页按…...
RestClient
什么是RestClient RestClient 是 Elasticsearch 官方提供的 Java 低级 REST 客户端,它允许HTTP与Elasticsearch 集群通信,而无需处理 JSON 序列化/反序列化等底层细节。它是 Elasticsearch Java API 客户端的基础。 RestClient 主要特点 轻量级ÿ…...
docker详细操作--未完待续
docker介绍 docker官网: Docker:加速容器应用程序开发 harbor官网:Harbor - Harbor 中文 使用docker加速器: Docker镜像极速下载服务 - 毫秒镜像 是什么 Docker 是一种开源的容器化平台,用于将应用程序及其依赖项(如库、运行时环…...
黑马Mybatis
Mybatis 表现层:页面展示 业务层:逻辑处理 持久层:持久数据化保存 在这里插入图片描述 Mybatis快速入门 中,Ingress是一个API对象,它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress,你可…...
《Playwright:微软的自动化测试工具详解》
Playwright 简介:声明内容来自网络,将内容拼接整理出来的文档 Playwright 是微软开发的自动化测试工具,支持 Chrome、Firefox、Safari 等主流浏览器,提供多语言 API(Python、JavaScript、Java、.NET)。它的特点包括&a…...
dedecms 织梦自定义表单留言增加ajax验证码功能
增加ajax功能模块,用户不点击提交按钮,只要输入框失去焦点,就会提前提示验证码是否正确。 一,模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...
NLP学习路线图(二十三):长短期记忆网络(LSTM)
在自然语言处理(NLP)领域,我们时刻面临着处理序列数据的核心挑战。无论是理解句子的结构、分析文本的情感,还是实现语言的翻译,都需要模型能够捕捉词语之间依时序产生的复杂依赖关系。传统的神经网络结构在处理这种序列依赖时显得力不从心,而循环神经网络(RNN) 曾被视为…...
深入解析C++中的extern关键字:跨文件共享变量与函数的终极指南
🚀 C extern 关键字深度解析:跨文件编程的终极指南 📅 更新时间:2025年6月5日 🏷️ 标签:C | extern关键字 | 多文件编程 | 链接与声明 | 现代C 文章目录 前言🔥一、extern 是什么?&…...
在WSL2的Ubuntu镜像中安装Docker
Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包: for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
