第17课-C++【模板进阶】

news2024/10/12 23:47:23

🌇前言

模板作为搭建STL的关键工具以及泛型编程思想的核心体现,对提高程序灵活性和推动高效迭代开发具有重要意义。除了基本的类型替换功能外,模板还具备如非类型模板参数、全特化、偏特化等高级操作。同时,模板声明与定义不能分离的问题也值得深入探讨。

🏙️正文

1、非类型模板参数

  • 1.1 使用方法
    • 以往模板参数多是用于匹配不同类型,如intdoubleDate等。但实际上模板参数还可匹配常量(非类型),用于确定如数组、位图等结构的大小。定义非类型模板参数时,不再使用classtypename,而是直接使用具体类型,如size_t。需注意,非类型模板参数必须为常量,且在编译阶段确定值。
    • 例如,利用非类型模板参数定义一个大小可自由调整的整型数组类:
    • cpp
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]; 
};

main函数中可以这样使用:

  • cpp
int main()
{
    arr<10> a1; 
    arr<20> a2; 
    arr<100> a3; 
    cout <<"a1 size():"<<a1.size() <<endl;
    cout <<"a2 size():"<<a2.size() <<endl;
    cout <<"a3 size():"<<a3.size() <<endl;
    return 0;
}

输出结果为:

a1 size():10
a2 size():20
a3 size():100

进一步地,如果再加入一个模板参数(类型),就可得到一个泛型且大小可自定义的数组:

  • cpp
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: 
    T _arr[N]; 
};
  • 使用示例:
  • cpp
int main()
{
    arr<int, 10> a1;
    arr<double, 20> a2;
    arr<char,100> a3;
    cout << typeid(a1).name() <<endl;
    cout<<typeid(a2).name()<<endl;
    cout<<typeid(a3).name()<<endl;
    return 0;
}
  • 非类型模板参数还支持缺省,例如:template <class T, size_t N = 10>

  • 1.2 类型要求

    • 非类型模板参数要求类型为整型家族,其他类型不符合标准。
    • 例如:
    • cpp
//整型家族(部分) 
template <class T, int N> 
class arr1 { /*……*/ }; 

template <class T, long N> 
class arr2 { /*……*/ }; 

template <class T, char N> 
class arr3 { /*……*/ };
  • 而使用其他家族类型作为非类型模板参数会引发报错,
  • 比如:
  • cpp
//浮点型,非标准 
template <class T, double N> 
class arr4 { /*……*/ };

会出现错误提示:浮点模板参数是非标准的(在 C++20 标准中或许有相关引入,但部分编译器可能仍不支持)。

  • 总结来说,非类型模板参数只能将整型家族类型作为参数,且必须为常量,在编译阶段确定结果。整型家族包括charshortboolintlonglong long等。

  • 1.3 实际例子:array

    • C++官网:array - C++ Reference (cplusplus.com)
    • C++11标准中,新容器array使用了非类型模板参数,是一个真正意义上的泛型数组,用于对标传统数组。
    • cpp
#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容器的一些功能,如迭代器和运算符重载等,主要改进是严格检查越界行为。不过在实际开发中,由于其对标传统数组且连初始化都没有,功能和实用性上不如vector,并且使用栈区空间存在栈溢出问题,所以使用相对较少。其严格检查越界行为是通过在进行下标相关操作前,对传入的下标进行合法性检验实现的,如assert(pos >= 0 && pos < N)

2、模板特化

  • 2.1 概念

通常模板可实现与类型无关的代码,但在某些场景中,泛型无法满足精准需求,会引发错误。例如使用日期类对象指针构建优先级队列时,若不编写对应的仿函数,比较结果会是未定义的。

例如:

如果传递的不是指针是正常的:

  如果传的是地址的话,地址每一次都不一样,不能满足需求;

模板特化就是在原模板基础上进行特殊化处理,创造出符合特定需求的 “特殊” 模板。

  • 2.2 函数模板特化

    • 函数模板也支持特化。例如下面的比较函数,如果不进行特化,对于字符串比较会出现错误结果:
    • cpp
template <class T> 
bool isEqual(T x, T y) 
{ 
    return x == y; 
} 

int main() 
{ 
    int x = 10; 
    int y = 20; 
    cout << "x == y: " << isEqual(x, y) << endl; 

    char str1[] = "Haha"; 
    char str2[] = "Haha"; 
    cout << "str1 == str2: " << isEqual(str1, str2) << endl; 
    return 0; 
}
  • 原因是字符串比较时,泛型比较的是地址而非内容。解决方法是利用模板特化为字符串比较构建特殊模板:
  • cpp
//函数模板特殊,专为char*服务 
template <> 
bool isEqual<char*>(char* x, char* y) 
{ 
    return strcmp(x, y) == 0; 
}
  • 2.3 类模板特化
    • 类模板特化可解决大部分特殊问题,分为全特化和偏特化。
    • 2.3.1 全特化
      • 全特化是将所有模板参数特化为具体类型,全特化后的模板在调用时会优先被选择。例如:
      • cpp
//原模板 
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 偏特化
    • 偏特化是将泛型范围进一步限制,可以限制为某种类型的指针或具体类型。
    • 例如:
    • cpp
//原模板---两个模板参数 
template <class T1, class T2> 
class Test 
{ 
public: 
    Test() 
    { 
        cout << "class Test" << endl; 
    } 
} ; 

//偏特化之一:限制为某种类型 
template <class T> 
class Test<T, 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; 
}

偏特化在泛型思想和特殊情况之间做了折中处理,在进行偏特化前需要存在基本的泛型模板,且要注意与全特化区分。

3、模板的分离编译问题

  • 3.1 失败原因
    • 当模板声明与定义分离后,在链接时无法在符号表中找到目标地址进行跳转,从而导致链接错误。
    • 例如,当模板声明与定义写在同一个文件中时,如Test.hmain.cpp
      Test.h文件内容:

cpp

#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文件内容:
  • cpp
#include <iostream> 
#include "Test.h" 

using namespace std; 

int main() 
{ 
    add(1, 2); 
    return 0; 
}

可以正常运行。但当声明与定义分离时,编译器无法确定函数原型,无法生成函数,也就无法获得函数地址,在符号表中进行函数链接时必然失败。

  • 3.2 解决方法
    • 解决方法有两种:
      • 在函数定义时进行模板特化,编译时生成地址以进行链接,但如果类型较多,不推荐这种方法,因为需要特化很多份。
      • 例如:
      • cpp
//定义 
//解决方法一:模板特化(不推荐,如果类型多的话,需要特化很多份) 
template <> 
int add(const int x, const int y) 
{ 
    return x + y; 
}
  • 模板的声明和定义不要分离,直接写在同一个文件中。例如:
  • cpp
//定义 
//解决方法二:声明和定义写在同一个文件中 
template <class T> 
T add(const T x, const T y) 
{ 
    return x + y; 
}

这也是为什么涉及模板的类,其中的函数声明和定义通常写在同一个文件(.h)中的原因,如STL库中的代码。为了区分,也可将头文件后缀改为.hpp,如Boost库中的一些命名方式。


4.模板中必须使用typename关键字而非class的场景

一、模板中的依赖名称

当在模板中使用一个依赖于模板参数的名称,且这个名称可能是一个类型也可能是一个值时,编译器无法确定它到底是不是一个类型。

  • 例如:
  • cpp
template<typename T>
void func(T t) {
    T::iterator it; // 这里编译器不知道 T::iterator 是不是一个类型
//T::iterator 可能是T类中的静态变量,也有可能是一个类(类部类)
}

在这种情况下,如果T::iterator确实是一个类型,就必须使用typename来明确告知编译器:

  • cpp
template<typename T>
void func(T t) {
    typename T::iterator it; // 使用 typename 明确表示这是一个类型
}

二、避免二义性

使用typename可以避免与class关键字可能引起的二义性。当使用class时,编译器可能会将其理解为一个类名的定义,而不是表示一个类型。

  • 例如:cpp
template<typename T>
class MyClass {
public:
    void doSomething() {
        typename T::SomeType st; // 使用 typename 明确类型
        // 如果这里使用 class,可能会被误解为类的定义而不是类型
    }
};

总之,在 C++ 模板中,为了明确表示一个依赖于模板参数的名称是一个类型,应该使用typename而不是class,以避免编译器的不确定性和二义性。


5、模板小结

  • 模板是STL的基础支撑,具有诸多优点。
    • 它复用了代码,节省资源,推动了更快的迭代开发,是C++标准模板库(STL)产生的基础。
    • 增强了代码的灵活性。
  • 同时也存在一些缺点。
    • 模板会导致代码膨胀,增加编译时间。
    • 出现模板编译错误时,错误信息复杂,不易定位。

🌆总结

本文详细介绍了 C++ 模板进阶的相关内容,包括非类型模板参数、模板特化以及模板的分离编译问题。非类型模板参数可用于确定结构大小,有特定的使用方法和类型要求;模板特化分为函数模板特化和类模板特化,可解决泛型无法满足的特殊需求;模板声明与定义分离会导致链接错误,可通过特化或不分离来解决。总之,模板虽有优缺点,但合理使用可使代码更灵活、更优雅。后续还会继续探索C++进阶内容,如继承、多态、高阶二叉树等知识点。

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

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

相关文章

聚类分析 | AP近邻传播聚类算法

目录 效果一览基本介绍程序设计参考资料 效果一览 基本介绍 AP近邻传播聚类算法 AP&#xff08;Affinity Propagation&#xff09;近邻传播聚类算法是一种基于数据点之间的相似度矩阵来进行聚类的算法。该算法不需要事先设定聚类簇的个数&#xff0c;而是通过在数据点之间传播…

JavaScript 第7章:字符串处理

第7章&#xff1a;字符串处理 在 JavaScript 中&#xff0c;字符串是一个非常常用的数据类型&#xff0c;用于表示文本信息。JavaScript 提供了许多内置的方法来处理字符串&#xff0c;包括操作、搜索、替换和格式化等。 一、字符串操作方法 1. charAt charAt(index) 方法返…

Java面向对象编程--高级

目录 一、static关键字 1.1 静态变量 1.2 静态内存解析 1.3 static的应用与练习 二、单例设计模式 2.1 单例模式 2.2 如何实现单例模式 三、代码块 3.1 详解 3.2 练习&#xff0c;测试 四、final关键字 五、抽象类与抽象方法 5.1 abstract 5.2 练习 六、接口 6.…

基于机器视觉的水果品质检测研究进展

摘 要&#xff1a;水果品质检测关系到水果的包装运输贮藏和销售的效果和收益。传统的外观品质检测主要是利用分级机械&#xff0c;其存在很多不足之处&#xff0c;因此提出了利用机器视觉进行无损检测的技术。利用机器视觉技术主要是检测水果的大小、形状、颜色和表面缺陷四个…

106. 从中序与后序遍历序列构造二叉树【 力扣(LeetCode) 】

文章目录 零、LeetCode 原题一、题目描述二、测试用例三、解题思路四、参考代码 零、LeetCode 原题 106. 从中序与后序遍历序列构造二叉树 一、题目描述 给定两个整数数组 inorder 和 postorder &#xff0c;其中 inorder 是二叉树的中序遍历&#xff0c; postorder 是同一棵…

Static修饰不同对象

目录 Static修饰局部变量 Static修饰全局变量 Static修饰函数 Static修饰成员 Static修饰成员变量 Static修饰成员函数 Static修饰成员的特性&#xff1a; 静态成员变量和静态成员函数的使用案例&#xff1a; 案例1&#xff1a;求1 2 3...n 案例2&#xff1a;单例模…

【学术会议投稿链接】React前端框架:构建现代Web应用的强大工具

【即将截稿】第五届经济管理与大数据应用国际学术会议&#xff08;ICEMBDA 2024&#xff09;_艾思科蓝_学术一站式服务平台 更多学术会议请看&#xff1a;https://ais.cn/u/nuyAF3 目录 引言 一、React简介 二、React的核心概念 1. 组件化 2. 虚拟DOM&#xff08;Virtua…

LOID:有效提升遮挡条件下的车道检测精度

1.论文信息 论文标题&#xff1a;LOID: Lane Occlusion Inpainting and Detection for Enhanced Autonomous Driving Systems 作者&#xff1a;Aayush Agrawal, Ashmitha Jaysi Sivakumar, Ibrahim Kaif∗, Chayan Banerjee† 作者单位&#xff1a;印度马德拉斯印度理工学院&…

数学建模算法与应用 第12章 现代优化算法

目录 12.1 粒子群优化算法 Matlab代码示例&#xff1a;粒子群优化算法求解函数最小值 12.2 遗传算法 Matlab代码示例&#xff1a;遗传算法求解函数最小值 12.3 蚁群算法 Matlab代码示例&#xff1a;蚁群算法求解旅行商问题 12.4 Matlab 遗传算法工具 使用遗传算法工具箱…

java的LinkedList

java的LinkedList 什么是LinkedListLinkedList的模拟实现LinkedList的使用ArrayList和LinkedList的区别 什么是LinkedList LinkedList的官方文档 LinkedList的底层是双向链表结构&#xff0c;由于链表没有将元素存储在连续的空间中&#xff0c;元素存储在单独的结点中&#xf…

一维数组的引用

#define SIZE 5 int main(void) { int i 0; int arr[SIZE] { 86,85,85,896,45 };//同理五个数据只是偶然&#xff0c;可能会更多 //输入 for (i 0;i < SIZE;i) { printf("请输入你的第%d个值&#xff1a;",i1); scanf_s(&…

【机器学习】逻辑回归|分类问题评估|混淆矩阵|ROC曲线|AUC指标 介绍及案例代码实现

文章目录 逻辑回归逻辑回归简介逻辑回归的数学基础逻辑回归原理概念损失函数 逻辑回归API函数和案例案例癌症分类预测 分类问题评估混淆矩阵分类评估方法 - 精确率 召回率 F1ROC曲线 AUC指标案例AUC 计算的API分类评估报告api 电信客户流失预测案例 逻辑回归 逻辑回归简介 ​…

python爬虫 - 进阶正则表达式

&#x1f308;个人主页&#xff1a;https://blog.csdn.net/2401_86688088?typeblog &#x1f525; 系列专栏&#xff1a;https://blog.csdn.net/2401_86688088/category_12797772.html 目录 前言 一、匹配中文 &#xff08;一&#xff09;匹配单个中文字符 &#xff08;二…

【网易云音乐】--源代码分享

最近写了一个网易云音乐的音乐实现部分&#xff0c;是通过JavaScript和jQuery实现的&#xff0c;具体效果大家可以参照下面的视频 源代码分享 - git地址: 网易云音乐源代码 下面将着重讲解一下音乐实现部分 视频有点模糊&#xff0c;不好意思&#xff0c;在b站上添加视频的时候…

【Oracle DB故障分享】分享一次由于SGA设置太小导致的DP备份失败

List item 今天给客户做Oracle例行数据库健康巡检&#xff0c;过程中检出一些备份异常&#xff0c;分享如下。 排查问题&#xff1a; 打开DP备份软件&#xff0c;随即弹出如下提示&#xff1a; 登录DP&#xff0c;查看备份情况&#xff1a;发现从10/6开始&#xff0c;DP备份…

ESP32—C3实现DS18B20(温度传感器)检测温度(Arduino IED )

1源代码&#xff08;DS18B20&#xff09; #include <OneWire.h> // 引入OneWire库&#xff0c;用于与单总线设备通信 #include <DallasTemperature.h> // 引入DallasTemperature库&#xff0c;用于读取DS18B20温度传感器数据// 定义连接到DS18B20数据引脚的GPIO编…

Vue入门-指令修饰符-@keyup.enter

指令修饰符&#xff1a; 通过"."指明一些指令后缀&#xff0c;不同后缀封装了不同的处理操作 ->简化代码 ①按键修饰符 keyup.enter ->键盘回车监听 ".enter"if(e.keyenter){} //".enter"用来简化代码 demo&#xff1a; <!DOCTYPE…

Ubuntu系统可以使用WIFI上网,而插网线有线网不能上网,网卡驱动未安装问题解决

文章目录 问题分析解决结果 问题 linux ubuntn系统下可以正常连WiFi上网&#xff0c;但是不能插网线上网。 分析 首先要排除是否为硬件问题&#xff0c;我在windows下是可以正常使用网线的&#xff0c;所以排除硬件的问题。 查看网卡是否被检测(wifi有说明网卡是有检测的) …

有了WPF后Winform还有活路吗?

近年来&#xff0c;随着技术的不断发展&#xff0c;Windows Presentation Foundation&#xff08;WPF&#xff09;和Windows Forms&#xff08;WinForms&#xff09;这两种技术在开发桌面应用程序方面一直备受关注。虽然WPF以其强大的功能和灵活性吸引了众多开发者&#xff0c;…

【iOS】YYModel的初步学习

YYModel的初步学习 文章目录 YYModel的初步学习前言与JSONModel对比YYModel的优势如何使用YYModel最简单的Model形式容器类属性白名单和黑名单Model的嵌套 小结 前言 随着时代的发展&#xff0c;iOS解析JSON数据的第三方库越来越多&#xff0c;原先的JSONModel的性能上的问题逐…