【C++】非类型模板参数 | array容器 | 模板特化 | 模板为什么不能分离编译

目录

一、非类型模板参数

二、array容器

三、模板特化

为什么要对模板进行特化

函数模板特化

补充一个问题

类模板特化

全特化与偏特化

全特化

偏特化

四、模板为什么不能分离编译

为什么

怎么办

五、总结模板的优缺点


一、非类型模板参数

模板参数分两类:类型形参 与 非类型形参。

类型形参:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。

非类型形参:用一个常量作为类 (函数) 模板的一个参数,在类 (函数) 模板中可将该参数当成常量来使用。

为什么需要非类型模板参数呢?我们先来看这样一个场景。

我定义了一个静态栈MyStack:

#define N 100
template<class T>
class MyStack
{
private:int arr[N];int top;
};
int main() {MyStack<int> st;return 0;
}

这样实例化出的栈,大小都是100。那假如我想要两个栈,一个大小为100,一个为200,要怎么办呢?

这就是#define所解决不了的问题了。

为此,我们引入了非类型模板参数,来看看它是怎么处理这个问题的:

template<class T,int N>   //非类型模板参数N
class MyStack
{
private:int arr[N];int top;
};
int main() {MyStack<int, 100> st1;MyStack<int, 200> st2;return 0;
}

这样实例化出的st1,大小为100;st2,大小为200。

注意:

1.整形家族可以作为非类型模板参数,包括char、short、int、long、longlong。最常见的是int。

浮点数、类对象以及字符串是不允许作为非类型模板参数的。

2.非类型的模板参数必须在编译期就能确认结果。

3.非类型模板参数是是常量,是不能修改的。

不信来修改下试试:

template<class T,int N>
class MyStack
{
public:void func() {N = 200;   //修改N}
private:int arr[N];int top;
};
int main() {MyStack<int, 100> st1;st1.func();return 0;
}

二、array容器

学习了非类型模板参数,我们就可以了解下array容器。这个容器很少用,了解即可。

array,大小固定的数组

相比之前学过的动态数组vector,array的功能就显得有些鸡肋:array有的功能,vector也有;而vector有的功能,array未必有。

一方面,vector是开在堆上的,大小可以变化;而array开在栈上,大小固定。但这也是array的优势,正因为是栈上分配内存,所以比vector的效率更高。

但总的来说,用array的地方,我们往往也能用vector,vector还更好用。所以这个容器很少用。

三、模板特化

模板特化的概念:

在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。

当模板被使用时,编译器会针对特定的类型使用特定的实现,而非使用通用的实现,从而提高代码的效率。

为什么要对模板进行特化

假如我们想要对9和2进行大小比较,现用两种方式比,然而,比较结果却出现了分歧:

template<class T>
bool IfLess(const T& x, const T& y) {return x < y;
}
int main() {int a = 2, b = 9;int* pa = &a, * pb = &b;cout << IfLess(a, b) << endl;   cout << IfLess(pa, pb) << endl;return 0;
}

存进a、b变量里,得到的是正确的结果。而通过传指针的方式,得到的是错误的结果。

这是因为,这里直接比较了指针的大小,而无法对指针先解引用 再比较。

此时,就需要对模板进行特化。我们希望达到的效果是:传指针也能比大小。

模板特化中分为函数模板特化类模板特化

函数模板特化

函数模板的特化步骤:

1.必须要先有一个基础的函数模板

2.关键字template后面接一对空的尖括号<>

3.函数名后跟一对尖括号,尖括号中指定需要特化的类型

4.函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。

如,这里就没有做到完全相同,所以报错了:

应改为:

template<class T>
bool IfLess(T& x,T& y) {return x < y;
}
​
template<>
bool IfLess<int*>(int*& x, int*& y) {   //注意形参要和基础参数完全相同!return *x < *y;
}

函数模板特化的示例:

#include<iostream>
using namespace std;
​
template<class T>
bool IfLess(T& x,T& y) {return x < y;
}
​
template<>
bool IfLess<int*>(int*& x, int*& y) {return *x < *y;
}
​
int main() {int a = 2, b = 9;int* pa = &a, * pb = &b;cout << IfLess(a, b) << endl;cout << IfLess(pa, pb) << endl;return 0;
}

在调用时,函数会根据实参类型,调用对应的函数模板:

补充一个问题

思考:为什么这样写不对?

#include<iostream>
using namespace std;
template<class T>
bool IfLess(const T& x, const T& y) {return x < y;
}
​
template<>
bool IfLess<int*>(const int*& x,const int*& y) {  //明明基础参数类型完全一致,为啥报错?return *x < *y;
}
int main() {……
}

编译不通过:

这里看似 和基础参数类型完全一致,实际上只是做到了形式上的一致,内涵上却大相径庭。

在基础函数参数中,const修饰形参x、y,保证它们不被改变;那迁移到 特化的参数中,对应的是:const修饰两个指针x、y,使之不被改变。

而const int*& x,const修饰的是指针x指向的内容不受改变,即const修饰 *x。

正确的写法是:int* const& x,让const直接修饰指针x。

📖如果你还是一头雾水,那说明“指针常量and常量指针”的知识点你没掌握好,现在我予以补充。

指针常量:int* const p。const直接修饰指针p,p自身的值不能改变,即p的指向是不能变的,而*p可以改变。

p就相当于一个常量了,此常量的类型为指针,因而叫指针常量。

常量指针:const int* p或int const* p,const修饰的是*p,即指针指向的内容是不能变的,而指针的指向可以改变。

因为指向的内容不能变,就相当于指向了一个常量,所以叫常量指针。

类模板特化

当我们需要针对某些特定类型进行特殊处理时,就需要对类模板进行特化。

如,我们定义了一个类模板 用于计算两个数的和:

#include <iostream>
using namespace std;
​
template<class T1,class T2>
class Adder
{
public:T1 add(T1 x, T2 y) {return x + y;}
};
​
int main() {Adder<int,double> a;int ret1 = a.add(1, 1.1);   //我想要ret1是整形,ret2是浮点型double ret2 = a.add(1, 1.1);cout << ret1 << endl;cout << ret2 << endl;
​return 0;
}

我想要1+1.1的结果,一个取整为2,一个保留小数为2.1。然而,结果却:

结果都是整形。因为返回类型T1是int,这会直接把2.1截断,返回2,存进ret2里。

来看看 当引入类模板特化,是怎么解决这个问题的吧:

#include <iostream>
using namespace std;
​
template<class T1,class T2>
class Adder
{
public:T1 add(T1 x, T2 y) {return x + y;}
};
​
template<>
class Adder<int,double>
{
public:double add(int x, double y) {return x + y;}};
​
int main() {Adder<int,double> a;int ret1 = a.add(1, 1.1);double ret2 = a.add(1, 1.1);cout << ret1 << endl;cout << ret2 << endl;
​return 0;
}

所以说,当需要针对特殊情况做特殊处理时,可以考虑使用类模板特化。

全特化与偏特化

全特化

将所有的模板参数都确定化。(我们刚刚给出的那几个例子,都是全特化的)

例:

template<class T1,class T2>
class Adder
{
public:T1 add(T1 x, T2 y) {return x + y;}
};
​
template<>
class Adder<int,double>
{
public:double add(int x, double y) {return x + y;}
};

注意看,全特化时,template<>尖括号里一定是空的。在尖括号内不添加类型,表示完全特化。

(但不能凭<>是否为空,来区分是全or偏特化。偏特化的<>也可以为空)

偏特化

偏特化又叫半特化。下面这2种情况都属于偏特化:

1.将参数表中一部分参数进行特化。

例:

template<class T1,class T2>
class Adder
{
public:T1 add(T1 x, T2 y) {return x + y;}
};
​
template<class T1>   //只特化了T2,T1还得保留
class Adder<T1,double>
{
public:double add(T1 x, double y) {return x + y;}};

2.对参数进行更进一步的限制。

例:如果我们想要 传参时,确保是引用传参,该怎么做呢?

template<class T1, class T2>
class Adder
{
public:T1 add(T1 x, T2 y) {cout << "调用了普通类模板" << endl;return x + y;}
};
​
template<class T1,class T2>
class Adder<T1& ,T2&>    //限制两个参数是引用
{
public:double add(const T1& x, const T2& y) {cout << "调用了特化类模板" << endl;return x + y;}
​
};
int main() {Adder<int,double> a;a.add(1, 1.1);
​Adder<int&, double&> b;  //调用特化的类模板,这样的话,就一定是传引用传参b.add(1, 1.1);
​return 0;
}

四、模板为什么不能分离编译

为什么

最开始接触模板时,就说过:模板不能分离编译。那为什么呢?我们要知其然,还要知其所以然。现在,我通过一个例子来说明这个原因。

我现在在一个project里创建了三个文件:Add.h、Add.cpp、main.cpp,分别用于声明、定义和测试。

//Add.h
#include<iostream>
using namespace std;
template<class T1,class T2>
T1 Add(T1 x,T2 y);
​
//Add.cpp
#include"Add.h"
template<class T1, class T2>
T1 Add(T1 x, T2 y) {return x + y;
}
​
//main.cpp
#include"Add.h"
int main() {cout << Add(1, 2.0) << endl;return 0;
}

运行报错:

原因说明:

我们先来回顾下,程序是怎么运行的。

程序在运行时,要经历四个阶段:预处理、编译、汇编、链接。

a.在预处理阶段,头文件被展开、宏被替换、注释被删除;

b.在编译阶段,编译器对各个源文件分别进行语法检查,然后将其转化成汇编代码。(注意:此阶段已经没有头文件了)

c.由于机器只能识别0、1串,所以我们的代码要经过汇编,从给人看的字符,变成给机器看的01。在汇编阶段,会形成符号表(用于存放变量、函数的地址)。

d.链接时,计算机遇到不认识的变量、函数,就会去符号表里找它的地址。通过链接,一个项目里的多个文件,才能合成一个关联的整体。

模板之所以不能分离编译,就是在链接阶段出岔子了。

我们说过,模板就像一张图纸。Add.cpp里的函数模板,因为没有确定的类型,所以并未实例化出具体的函数,自然无法在符号表中生成对应的地址。

在mian.cpp中,模板隐式实例化出 函数Add<int,double>,想要调用,却在符号表里找不到地址。所以报错:Add<int,double>为无法解析的外部符号。

怎么办

既然不能分离编译,那一般怎样处理模板呢?这里提供两种方法。

1.(推荐!)用到模板的地方,就不要分离编译~

将声明和定义放到同一个文件 "xxx.hpp" (或者"xxx.h")。

2.(不推荐)模板定义的位置显式实例化。

你要调用的函数,都显式实例化放在Add.cpp中:

template<class T1, class T2>
T1 Add(T1 x, T2 y) {return x + y;
}
//显示实例化
template
int Add(int x, double y);

这样写不好,因为如果你要调用好几种不同参数类型的Add,你就都要显式实例化,这就造成了代码的冗余。

五、总结模板的优缺点

优点:

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

2.增强了代码的灵活性

缺点:

1.模板会导致代码膨胀问题,也会导致编译时间变长

2.出现模板编译错误时,错误信息非常凌乱,不易定位错误

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

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

相关文章

数据结构预算法--链表(单链表,双向链表)

1.链表 目录 1.链表 1.1链表的概念及结构 1.2 链表的分类 2.单链表的实现(不带哨兵位&#xff09; 2.1接口函数 2.2函数的实现 3.双向链表的实现&#xff08;带哨兵位&#xff09; 3.1接口函数 3.2函数的实现 1.1链表的概念及结构 概念&#xff1a;链表是一种物理存储结…

黑豹程序员-架构师学习路线图-百科:Knife4j API接口文档管理

文章目录 由来&#xff1a;接口文档第一代&#xff1a;Swagger第二代&#xff1a;Knife4j界面 由来&#xff1a;接口文档 古老编程是一个语言前后端通吃&#xff0c;ASP、JSP、PHP都是如此。 但随着项目规模变大&#xff0c;项目团队也开始壮大&#xff0c;岗位职责开始细分&a…

如何从零开始手写一个消息中间件(从宏观角度理解消息中间件的技术原理)

如何从零开始手写一个消息中间件&#xff08;从宏观角度理解消息中间件的技术原理&#xff09; 什么是消息中间件消息中间件的作用逐一拆解消息中间件的核心技术消息中间件核心技术总览IOBIONIOIO多路复用AIOIO多路复用详细分析selectpollepoll Java中的IO多路复用 协议序列化消…

【halcon】halcon 函数文件 以及 脚本引擎如何调用外部函数文件 上篇

前言 halcon有几种文件&#xff1a; 本地程序函数&#xff08;.hdev&#xff09;外部函数文件&#xff08;.hdvp)库函数(.hdp) 说多了容易混淆&#xff0c;今天就说&#xff0c;我觉得最有用的&#xff1a;外部函数文件&#xff08;.hdvp) 步骤 先写一段halcon脚本&#x…

Nuxt.js——基于 Vue 的服务端渲染应用框架

文章目录 前言一、知识普及什么是服务端渲染什么是客户端渲染&#xff1f;服务端渲染与客户端渲染那个更优秀&#xff1f; 二、Nuxt.js的特点Nuxt.js的适用情况&#xff1f; 三、Vue是如何实现服务端渲染的&#xff1f;安装依赖使用vue安装 Nuxt使用npm install安装依赖包使用n…

C语言——打印1000年到2000年之间的闰年

闰年&#xff1a; 1、能被4整除不能被100整除 2、能被400整除 #define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h> int main() {int year;for(year 1000; year < 2000; year){if((year%4 0) && (year%100!0) || (year%400 0)){printf("%d ",ye…

Java学习_对象

对象在计算机中的执行原理 类和对象的一些注意事项 this关键字 构造器 构造器是一种特殊的方法 : 特殊之处在于&#xff0c;名字必须与所在类的名字一样&#xff0c;而且不能写返回值类型 封装 封装的设计规范&#xff1a;合理隐藏、合理暴露 实体类 成员变量和局部变量的区别 …

vite基础学习笔记:14.路由跳转(二)携带query参数

说明&#xff1a;自学做的笔记和记录&#xff0c;如有错误请指正 1. 路由跳转&#xff08;携带query参数&#xff09; &#xff08;1&#xff09;第一层路由&#xff08;点击卡片路由跳转至新页面-携带query参数&#xff09; 知识点&#xff1a; query传参对应的是path和qu…

vscode 终端进程启动失败: shell 可执行文件“C:\Windows\System32\WindowsPower

vscode 终端进程启动失败: shell 可执行文件“C:\Windows\System32\WindowsPower 第一次用vscode&#xff0c;然后遇到这个问题&#xff0c;在设置里搜索 terminal.integrated.defaultProfile.windows 将这里的null改成"Command Prompt" 重启就可以了

【Git】深入了解Git及其常用命令

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于Git的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.Git是什么 二.SVN和Git的区别 三.Git的…

通过SD卡给某摄像头植入可控程序

0x01. 摄像头卡刷初体验 最近研究了手上一台摄像头的sd卡刷机功能&#xff0c;该摄像头只支持fat32格式的sd卡&#xff0c;所以需要先把sd卡格式化为fat32&#xff0c;另外微软把fat32限制了最大容量32G&#xff0c;所以也只能用不大于32G的sd卡来刷机。 这里使用32G的sd卡来…

docker-compose安装es以及ik分词同义词插件

目录 1 前言 2 集成利器Docker 2.1 Docker环境安装 2.1.1 环境检查 2.1.2 在线安装 2.1.3 离线安装 2.2 Docker-Compose的安装 2.2.1 概念简介 2.2.2 安装步骤 2.2.2.1 二进制文件安装 2.2.2.2 离线安装 2.2.2.3 yum安装 3 一键安装ES及Kibana 3.1 yml文件的编写…

【Redis系列】Redis的核心命令(上)

哈喽&#xff0c;大家好&#xff0c;我是小浪。那么上篇博客教会了大家如何在Linux上安装Redis&#xff0c;那么本篇博客就要正式开始学习Redis啦&#xff0c;跟着俺的随笔往下看~ 1、启动Redis 那么如何启动Redis呢&#xff1f;最常用的是以下这个命令&#xff1a; redis-cl…

堆的应用-----Top k 问题

目录 前言 Topk问题 1.问题描述 2.解决方法 3.代码实现&#xff08;C/C&#xff09; 前言 在人工智能算法岗位的面试中&#xff0c;TopK是问得最多的几个问题之一&#xff1a; 到底有几种方法&#xff1f; 这些方案里蕴含的优化思路究竟是怎么样的&#xff1f; 为啥T…

如何用Excel软件制作最小二乘法①

一、用自带的选项&#xff08;不推荐&#xff09;&#xff0c;因为感觉只是近似&#xff0c;虽然结果一样 1.在Excel中输入或打开要进行在excel中输入或打开要进行最小二乘法拟合的数据&#xff0c;如图所示。 2.按住“shift”键的同时&#xff0c;用鼠标左键单击以选择数据&a…

电路综合-基于简化实频的SRFT集总参数切比雪夫低通滤波器设计

电路综合-基于简化实频的SRFT集总参数切比雪夫低通滤波器设计 6、电路综合-基于简化实频的SRFT微带线切比雪夫低通滤波器设计中介绍了使用微带线进行切比雪夫滤波器的设计方法&#xff0c;在此对集总参数的切比雪夫响应进行分析。 SRFT集总参数切比雪夫低通滤波器综合不再需要…

typora保护机制与注册逆向分析

、起因 一直比较喜欢Typora的简洁与美观&#xff08;尝试过用 vscode 搭配插件编辑 markdown 文件&#xff0c;体验还是要差一些的&#xff09;&#xff0c;突然发现自己windows机器上很久前安装的typora不让用了&#xff0c;提示&#xff1a; 幸好原始安装文件还在&#xf…

ASP.NETWeb开发(C#版)-day1-C#基础+实操

目录 .NET实操&#xff1a;创建项目执行 C#基础语法数据类型变量实操001_变量如何在一个解决方案 中创建另一个项目实操002结构实操003-if else实操004-多分支多行注释按钮实操&#xff1a;循环 面向对象基础如何在同一个项目下创建新的.cs文件实操-类的定义与访问实操-练习实操…

55基于matlab的1.高斯噪声2.瑞利噪声3.伽马噪声4.均匀分布噪声5.脉冲(椒盐)噪声

基于matlab的1.高斯噪声2.瑞利噪声3.伽马噪声4.均匀分布噪声5.脉冲&#xff08;椒盐&#xff09;噪声五组噪声模型&#xff0c;程序已调通&#xff0c;可直接运行。 55高斯噪声、瑞利噪声 (xiaohongshu.com)

深度解剖Linux权限的概念

> 作者简介&#xff1a;დ旧言~&#xff0c;目前大二&#xff0c;现在学习Java&#xff0c;c&#xff0c;c&#xff0c;Python等 > 座右铭&#xff1a;松树千年终是朽&#xff0c;槿花一日自为荣。 > 目标&#xff1a;牢记Linux权限的概念。 > 毒鸡汤&#xff1a;你…