C++:模板

C++:模板

    • 函数模板
      • 显式实例化
      • 模板参数缺省
      • 参数匹配规则
    • 类模板
      • 类名与类型
      • 类成员的声明定义分离
    • 非类型模板参数
    • 模板特化
      • 函数模板特化
      • 类模板特化
        • 全特化
        • 偏特化
          • 部分特化
          • 限制特化


在讲解模板前,我提出一个问题:
如何实现一个通用的swap交换函数?

也许你可以这样:

void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
......

把每种类型都进行重载,写出n多种交换函数。
但这很明显是一个费力不讨好的方法,不仅会让代码冗余重复,而且写出这样的代码,也会耗费精力。

那么有没有一种办法,我们给编译器一个模板,编译器自动生成函数?

C++就样提供了模板,而C++最重要的STL库,也就是起源于模板的。

模板分为函数模板与类模板,我们先通过函数模板来了解模板的大部分规则:

函数模板

功能:

函数模板代表了一个函数家族,在使用时被参数化,根据实参的类型生成特定类型的版本

也就是说,我们可以通过一个函数模板,让编译器生成一整个同类型的函数家族。

语法:

template <typename T1, typename T2 ......>
template <class T1, class T2 ......>

以上两种都是创建模板的方式,classtypename在里面是一样的功能,没有区别。在STL中多用class,所以本博客也以class为主。
而被classtypename定义是模板参数,它可以被任意类型代替。如果你不能理解,我们不妨看看示例。

示例:

template <class T>
T Add(T x, T y)
{return x + y;
}

以上就是一个函数模板,T是一个模板参数,它代表一个类型。如果T是int,那么以上函数就是:

int Add(int x, int y)
{return x + y;
}

这个函数是不是很熟悉了?
也就是说,T是可以被替换的类型,那么我们要如何确定这个T的类型?
编译器会根据调用函数时传入的参数,自动判断类型

比如以下调用:

Add(1, 5);
Add(3.0, 5.0);

对于Add(1, 5);,其两个参数都是int类型,那么此时模板就会生成一个int类型的Add函数。
对于Add(3.0, 5.0);,其两个参数都是double类型,此时模板就会生成一个double类型的Add函数。

以此类推,我们不论想要多少种类型,只需要传入参数,让编译器自动识别,而我们只需要写一个模板,就可以衍生出无数种函数,这就是模板的优势。

那么我们现在来实现一下一开始swap函数的模板:

template <class T>
void Swap(T& left, T& right)
{T temp = left;left = right;right = temp;
}

不过要注意,模板只是一个蓝图,本身不是函数,当我们传入指定类型参数,其就会生成相应的函数。


显式实例化

再回到刚刚的Add函数模板。

template <class T>
T Add(T x, T y)
{return x + y;
}

如果我们传入两个不同类型的参数怎么办?
比如这样:

int a = 5;
double b = 3.0;
Add(a, b);

请问这个模板是转化为double类型好呢,还是转化为int类型好呢?
对编译器来说,这就是一个大问题了,如果转化错误了,编译器就要背黑锅。所以遇到这种情况,编译器不会为我们做决定,而是报错,必须由程序员指明要用哪一种类型的模板。

比如使用强制类型转换:

int a = 5;
double b = 3.0;
Add(a, (int)b);
Add((double)a, b);

上述代码,Add(a, (int)b);将b转化为了int,此时模板推演出int类型的函数;而Add((double)a, b);将a转化为了double类型,此时模板推演出double类型的参数。

此外,我们还可以使用显式实例化的方式:
显式实例化,就是在使用模板时,明确的告诉模板,要用什么类型。

语法:

函数名 <类型> (参数);

比如:

int a = 5;
double b = 3.0;
Add<int> (a, b);
Add<double> (a, b);

Add<int> (a, b);此代码就是推演出int类型的函数;而Add<double> (a, b);就是推演出double类型的函数。


模板参数缺省

在设置模板参数时,可以设置缺省值,在显式实例化时,对于没有指明的模板参数,会被推演为缺省值。

看到以下模板:

template <class T1, class T2>
void func(T1 a)
{T2 b = 5;cout << a / b;
}

这个模板中,函数func只有一个形参,而T2这个模板参数不在形参中,而是用于定义b这个变量了。此时T2是无法根据函数的实参推演出来的,必须显式实例化中指明。
比如这样:

func<int, int>(8);

此时T1int类型,T2int类型,执行整数除法8/5=1

接下来我们给模板参数缺省值试试:

template <class T1, class T2 = double>
void func(T1 a)
{T2 b = 5;cout << a / b;
}

此处我们给了T2一个缺省值double,也就是说我们不传入第二个模板参数,T2就会被推演为缺省值double

func<int>(8);
func(8);

对于func<int>(8);,我们只传了一个模板参数,此时T2就会被推演为缺省值double,变量b就是double类型了,此时执行小数除法8 / 5.0 = 1.6
对于func(8);,我们没有进行显式实例化,此时对于T1,由于我们传入了参数aint类型,此时T1被推演为int,而T2得到缺省值double,执行小数除法8 / 5.0 = 1.6


参数匹配规则

模板本身不是一个函数,所以同名的函数和模板是可以共存的。
比如这样:

template <class T>
T func(T x, T y)
{return x + y;
}int func(int x, int y)
{return x + y;
}

以上代码中我们创建了一个Add的模板,一个Addint类型函数。

那么我们调用函数时,会这么调用呢?

调用函数时,如果函数有现成的,完全匹配的函数,那么不会调用模板

如果我们这样调用函数:

Add(1, 2);

这个调用,两个参数都是int类型,而我们刚好写了一个两个参数都是int类型的函数,那么此次调用就不会调用模板,而是直接用我们写过的函数。

调用函数时,如果可以通过模板产生更加匹配的函数,那么会调用模板进行推演

如果我们这样调用函数:

Add(1.0, 2.0);

此时两个参数都是double类型,如果不存在模板的话,double就会被转化为int,然后调用int,int的函数。但是由于模板存在,可以推演出更加匹配从函数double,doouble类型。所以此次调用会调用模板。


类模板

类模板的特性与函数模板几乎一致,此处不额外讲解了,只讲解类模板的特殊的地方。

语法:

template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};

先简单为大家展示一个类模板:

template <class T>
class stack
{
public:stack(size_t capacity = 10):_pData(new T[capacity]),_size(0),_capacity(capacity){}private:T* _pData;size_t _size;size_t _capacity;
};

这就是一个stack类的模板,有了这个模板,我们的栈就可以存放intdouble等等的其他类型了。
我们的模板参数为T,由于类没有传参的概念,不能通过参数来推演类型,所以一般而言类的模板都是要显式实例化的

比如:

stack<int> s1;
stack<double> s2;

类名与类型

通过模板创建的类,其类名与类型也有所不同,接下来我们看看规则:

在一般的类中,类的类名和类型符号相同

比如stack的类,其类名为stack,类型也为stack

而在类模板中,不能单纯的将类名作为类型了,比如:

stack<int> s1;
stack<double> s2;

请问s1和s2的类型都是stack吗?
s1明明是用int推演的类,s2是用double推演的类,两者有很大的区别,如果都是stack类,后续如何区分?
所以我们用了其他规则来修饰这个类型符号,从而区分开同一个模板推演出来的不同类型。

类型 = 类名<模板参数>

对于stack<int> s1;其类名为stack,类型为stack<int>
对于stack<double> s2;其类名为stack,类型为stack<double>;


类成员的声明定义分离

当我们希望把一些类中的成员定义在类的外部时,那就需要声明和定义分离。

假设我们希望分离析构函数~stack
对于一般的类,我们会这样分离:

class stack
{
public:stack(size_t capacity = 10):_pData(new int[capacity]),_size(0),_capacity(capacity){}~stack();//声明private:int* _pData;size_t _size;size_t _capacity;
};stack::~stack()
{//函数体
}

首先要用类型::函数名来限定作用域,然后再开始定义函数。

所以我们的类模板也要类型::函数名来限定作用域。类模板的类型刚刚介绍过,就是stack<T>,所以函数的声明应该这样写:

stack<T>::~stack()
{//函数体
}

但是这还不是一个合法的声明。

对于类模板,当在类外定义函数时,要添加模板参数列表。

也就是说要这样:

template <class T>
stack<T>::~stack()
{//函数体
}

这才是一个模板类的成员函数声明。


非类型模板参数

我们的模板参数也可以不是一个类型,而是一个数值

对于指定类型的参数,我们称为类型形参,比如int,double。
对于一个数值的参数,我们称为非类型形参。

比如以下类模板:

template <class T, int N>
class Array
{
public:private:T _arr[N];
};

我们在模板参数列表中有一个类T,一个int类型的N,此时T就是类型形参,N就是非类型形参。

在定义类时,可以通过这个非类型参数,为这个类传入一些值:

Array<int, 10> a;
Array<double, 20> b;

对于a这个对象,我们在创建时,为T传入了intN传入了10。那么经过初始化,_arr就指向了10int的数组。

对于b这个对象,我们在创建时,为T传入了doubleN传入了20,那么经过初始化,_arr就指向了20double类型的数组。

注意:非类型模板参数必须是整型,bool,char类型的常量


模板特化

通常情况下,使用模板可以实现一些与类型无关的代码,但是对于一些特殊的类型,有可能会得到错误的结果。

比如以下代码:

template<class T>
bool Less(T x, T y)
{return x < y;
}int main()
{int* p1 = 5;int* p2 = 10;cout << Less(p1, p2) << endl;return 0;
}

这个函数模板中,我们用Less来比大小,我们此时传入了两个指针p1p2,原本的意图是通过指针来比较数字5和10的大小。但是当传入后,我们比较的是p1 < p2,也就是对两个指针比大小了,这不符合我们的预期。也就是说在面对指针的时候,我们需要特殊处理,这就需要模板特化了。

模板特化的功能就是:

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

其分为函数模板的特化与类模板的特化:


函数模板特化

我们先看到一个函数模板特化,再讲解语法:

//基础模板
template<class T>
bool Less(T x, T y)
{return x < y;
}//模板特化
template<>
bool Less<int*>(int* x, int* y)
{return *x < *y;
}

第一段代码是一般的函数模板,而第二段是对int进行了特化的版本,当我们传入参数类型为int时,就会调用这个特化版本,执行*x < *y,先解引用再比较。

那么这个模板特化有什么特点呢?
首先,我们将T特化为了int*,所以T不再是一个需要推演的参数了,此时将T从模板参数列表中抽离出来,改为int*放到函数名Less后面,用尖括号括起来,然后把函数参数中所有的T改为特化后的int*

模板特化要满足以下语法:

  1. 必须存在一个基础模板
  2. 对于特化版本,template后面的<>内部不写被特化的模板参数
  3. 对于特化版本,在函数名后跟上<>,内部指定特化类型
  4. 将特化前的模板参数改为特化后的具体类型

但是函数模板是一个没有必要的东西,因为相比于对模板进行特化,不如直接重载一个函数,模板特化在类模板中较为有用。

比如这样:

bool Less(int* x, int* y)
{return *x < *y;
}

可以达到一样的效果,而且无需繁杂的语法。

模板特化的主要用处体现在类模板特化上,函数并不推荐使用这个模板特化。


类模板特化

类模板特化的语法和刚才是一样的:
模板特化要满足以下语法:

  1. 必须存在一个基础模板
  2. 对于特化版本,template后面的<>内部不写被特化的模板参数
  3. 对于特化版本,在函数名后跟上<>,内部指定特化类型
  4. 将特化前的模板参数改为特化后的具体类型

类模板特化分为全特化和偏特化。


全特化

全特化是指将模板参数的所有参数都确定下来

比如以下案例:

//基础模板
template<class T1, class T2>
class Data
{
public:Data() {cout<<"Data<T1, T2>" <<endl;}	
private:T1 _d1;T2 _d2;
};//模板特化
template<>
class Data<int, char>
{
public:Data() {cout<<"Data<int, char>" <<endl;}
private:int _d1;char _d2;
};

此处我们将T1T2两个参数都设立了特化,这就叫做全特化。
只有模板参数第一个值为int,第二个参数为char,调用此类。

比如:

Data<int, char> d1;

这里的d1就是一个模板特化创造出来的类对象。


偏特化

偏特化是指并没有把模板参数确定下来,但是对满足特定条件的模板参数,执行特化版本

偏特化分为部分特化和限制特化:

部分特化

部分特化是只将一部分参数特化

比如以下案例:

//基础模板
template<class T1, class T2>
class Data
{
public:Data() {cout<<"Data<T1, T2>" <<endl;}	
private:T1 _d1;T2 _d2;
};//模板特化
template<class T1>
class Data<T1, char>
{
public:Data() {cout<<"Data<T1, char>" <<endl;}
private:T1 _d1;char _d2;
};

以上的第二段代码就是一个部分特化,其只特化了第二个模板参数为char,只有当第二个参数为char类型,不论第一个参数类型是什么,都会调用特化版本的类了。

由于T1没有被确定下来,仍然需要推演,所以第一行的模板参数列表保留T1

比如:

Data<int, char> d2;
Data<double, char> d3;

这里的d2d3都是通过模板特化创建出来的对象,因为它们满足第二个模板参数是char类型。


限制特化

限制特化是对参数进行条件限制,但是没有把参数类型确定下来

比如以下案例:

//基础模板
template<class T1, class T2>
class Data
{
public:Data() {cout<<"Data<T1, T2>" <<endl;}	
private:T1 _d1;T2 _d2;
};//模板特化
template<class T1, class T2>
class Data<T1*, T2*>
{
public:Data() {cout<<"Data<T1*, T2*>" <<endl;}
private:T1 _d1;T2 _d2;
};

此处<T1*, T2*>是限定:当T1T2为指针是,调用此模板特化。
也就是说,这个过程中,T1T2的类型是不确定的,任然需要推演,所以第一行的模板参数列表保留了T1T2

而这样对模板参数进行限制,就是限制特化了。


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

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

相关文章

StringBuffer和StringBuilder的区别,设计目的

StringBuffer和StringBuilder是Java编程语言中用于处理字符串的两个类&#xff0c;它们在功能上非常相似&#xff0c;都用于创建可变的字符串。然而&#xff0c;它们之间存在一些关键的区别&#xff0c;主要体现在线程安全性和性能上。这两个类的设计目的反映了不同的使用场景需…

Java中的main方法和可变参数

目录 分析main方法形参为String[] 那么实参到底是什么&#xff1f;可变参数实例 分析main方法 在Java中&#xff0c;main方法是程序的入口点。当你运行一个Java程序时&#xff0c;JVM&#xff08;Java虚拟机&#xff09;会寻找一个名为main的方法&#xff0c;并从这里开始执行…

html2canvas 截图功能使用 VUE

html2canvas 是一个 JavaScript 库&#xff0c;可以将网页内容转换为 Canvas 元素&#xff0c;并生成图像或 PDF 文件。使用 html2canvas&#xff0c;你可以在客户端将网页的内容截图&#xff0c;并将其作为图像或 PDF 文件保存或分享。 以下是一些 html2canvas 库的特点和用途…

LeetCode 0292.Nim 游戏:脑筋急转弯

【LetMeFly】292.Nim 游戏&#xff1a;脑筋急转弯 力扣题目链接&#xff1a;https://leetcode.cn/problems/nim-game/ 你和你的朋友&#xff0c;两个人一起玩 Nim 游戏&#xff1a; 桌子上有一堆石头。你们轮流进行自己的回合&#xff0c; 你作为先手 。每一回合&#xff0c…

【NTN 卫星通信】基于NTN的多3GPP连接应用场景

1 概述 同时聚合两条3GPP接入链路&#xff0c;其中一条为非地面网络&#xff0c;可以提供以下5G业务使能&#xff0c;尤其适用于带宽有限或接入链路不可靠的服务不足地区:   -扩展流动宽频   -超可靠的服务通信 如技术报告38.821所述&#xff0c;若干服务场景(例如在偏远地…

【算法题】91. 解码方法

题目 一条包含字母 A-Z 的消息通过以下映射进行了 编码 &#xff1a; A -> "1" B -> "2" ... Z -> "26" 要 解码 已编码的消息&#xff0c;所有数字必须基于上述映射的方法&#xff0c;反向映射回字母&#xff08;可能有多种方法&…

缓存组件Caffeine的使用

caffeine是一个高性能的缓存组件&#xff0c;在需要缓存数据&#xff0c;但数据量不算太大&#xff0c;不想引入redis的时候&#xff0c;caffeine就是一个不错的选择。可以把caffeine理解为一个简单的redis。 1、导入依赖 <!-- https://mvnrepository.com/artifact/com.git…

STM32F407 CAN参数配置 500Kbps

本篇CAN参数适用 芯片型号&#xff1a;STM32F407xx系统时钟&#xff1a;168MHz&#xff0c;CAN挂载总线APB1为42M波 特 率 &#xff1a;500Kpbs引脚使用&#xff1a;TX_PB9&#xff0c;RX_PB8&#xff1b;修改为PA11PA12后&#xff0c;参数不变。 步骤一、打勾开启CAN&#xf…

百面嵌入式专栏(面试题)网络编程面试题

沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇我们将介绍网络编程面试题 。 1、什么是IO多路复用 I/O多路复用的本质是使用select,poll或者epoll函数,挂起进程,当一个或者多个I/O事件发生之后,将控制返回给用户进程。以服务器编程为例,传统的多进程(多线程…

GaussDB新体验,新零售选品升级注入新思路【华为云GaussDB:与数据库同行的日子】

选品思维&#xff1a;低频VS高频 一个的商超&#xff0c;假设有50个左右的品类&#xff0c;每个品类下有2到10个不等的商品。然而如此庞大的商品&#xff0c;并非所有都是高频消费品。 结合自身日常的消费习惯&#xff0c;对于高频和低频的区分并不难。一般大型家电、高端礼盒…

HCIA--DHCP动态分配ip地址实验

要求&#xff1a; 1. pc1&#xff0c;pc2不能获取 250-254的地址 2. pc3固定获取172.16.1.3/24 pc4固定获取172.16.1.6/24 1. 在AR1上配接口ip、划分网段&#xff0c;创建地址池&#xff0c;开启dhcp: [Huawei]int g0/0/0 [Huawei-GigabitEthernet0/0/0]ip add 192.168.1.1 2…

FPGA开发

Quartus13.0使用 编译下载&#xff1a; 添加引脚&#xff1a; # ---------------- LED ---------------- # set_location_assignment PIN_K2 -to led_out[11] set_location_assignment PIN_J1 -to led_out[10] set_location_assignment PIN_J2 -to led_out[9] set_locatio…

C++实现鼠标点击和获取鼠标位置(编译环境visual studio 2022)

1环境说明 2获取鼠标位置的接口 void GetMouseCurPoint() {POINT mypoint;for (int i 0; i < 100; i){GetCursorPos(&mypoint);//获取鼠标当前所在位置printf("% ld, % ld \n", mypoint.x, mypoint.y);Sleep(1000);} } 3操作鼠标左键和右键的接口 void Mo…

Redis渗透SSRF的利用

Redis是什么&#xff1f; Redis是NoSQL数据库之一&#xff0c;它使用ANSI C编写的开源、包含多种数据结构、支持网络、基于内存、可选持久性的键值对存储数据库。默认端口是&#xff1a;6379 工具安装 下载地址&#xff1a; http://download.redis.io/redis-stable.tar.gz然…

pytorch的安装步骤

PyTorch是一个深度学习框架&#xff0c;下面是PyTorch的安装步骤&#xff1a; 安装Anaconda&#xff08;可选&#xff09;&#xff1a;Anaconda是一个用于数据科学的Python发行版本&#xff0c;它包含了很多常用的Python库。如果你已经安装了Anaconda&#xff0c;可以跳过这一步…

Web APIs 1 DOM操作

Web APIs 1 引入&#xff1a;const优先Web API 基本认知01 作用和分类02 什么是DOM03 DOM树04 DOM对象 获取DOM对象01 根据CSS选择器获取02 其他获取DOM元素方法 操作元素内容01 innerText 属性02 innerHTML 属性 操作元素属性操作元素的常用属性操作元素的样式属性操作表单元素…

【FFmpeg】ffplay 命令行参数 ① ( 设置播放分辨率 | 禁用 音频 / 视频 / 字幕 选项 )

文章目录 一、ffplay 命令行参数 - 设置播放分辨率1、强制设置通用播放分辨率 -x -y 参数2、命令行示例 - 正常播放视频3、命令行示例 - 强制设置播放分辨率4、设置 YUV 播放分辨率 -video_size 和 像素设置 -pixel_format5、全屏播放 -fs 参数 二、ffplay 命令行参数 - 禁用 音…

C++max函数的使用

在 C 中&#xff0c;max 函数用于找出两个数中的最大值。这个函数在 <algorithm> 头文件中定义&#xff0c;因此使用它之前需要包含这个头文件。max 函数可以用于基本数据类型&#xff08;如 int、float 等&#xff09;和用户自定义类型&#xff0c;只要这些类型支持比较…

【高质量精品】2024美赛A题22页word版成品论文+数据+多版本前三问代码及代码讲解+前四问思路模型等(后续会更新)

一定要点击文末的卡片&#xff0c;进入后&#xff0c;即可获取完整资料后续参考论文!! 整体分析:这个题目是一个典型的生态系统建模问题&#xff0c;涉及到动物种群的性比例变化、资源可用性、环境因素、生态系统相互作用等多个方面。这个题目的难点在于如何建立一个合理的数学…

6.函数表达式 - JS

函数表达式 function (someArgs) { someStatements } function name(someArgs) { someStatements } (someArgs) > { someStatements }函数表达式就是要&#xff0c;在一个表达式中定义一个函数&#xff1b;箭头函数也是一个简洁的函数表达式&#xff1b;执行完函数表达式&a…