【C++模板进阶】C++ 模板进阶的拦路虎:模板特化和分离编译,该如何逐个突破?

【C++模板进阶】C++ 模板进阶的拦路虎:模板特化和分离编译,该如何逐个突破?
前言:在之前的文章中,我们介绍了模板的基础知识,包括函数模板和类模板的使用方法。本文将深入探讨模板的进阶内容,涵盖非类型模板参数、模板特化以及模板的分离编译等高级特性。
在这里插入图片描述
🌟 专注用图文结合拆解难点+代码落地知识,让技术学习从「难懂」变“一看就会”!
🏠 个人主页MSTcheng · ZEEKLOG
💻 代码仓库MSTcheng · Gitee📚 精选专栏 :📖 :《C语言》🧩 :《数据结构》💡 :《C++由浅入深》💬 座右铭 :“路虽远行则将至,事虽难做则必成!”

文章目录

一、非类型模板参数

1.1模板参数的分类

首先要知道模板参数分为类型模板参数和非类型模板参数,在前面的文章中我们介绍了类型模板参数 例如:
template<classT>classDate{public:voidprint(){ cout << _year <<"-"<< _month <<"-"<< _day;}private: T _year=2025; T _month=11; T _day=20;};intmain(){ Date<int> d; d.print();return0;}
类型形参即:出现在模板参数列表中,跟在class或者typename之类的参数类型名称。

🔺T类型的私有成员在该日期类实例化对象的时候就实例化出了具体类型,比如上面示例中的int。所以类型模板参数是在实例化的时候才确定类型的

1.2非类型模板参数的认识

那什么是非类型模板参数呢?🤔

非类型模板参数(Non-Type Template Parameters)是C++模板中允许使用的非类型值作为参数,即在编译时确定的常量表达式。与类型模板参数(如typename T)不同,非类型模板参数可以是整型、枚举、指针、引用或std::nullptr_t等具体值

下面举个例子:

#defineN10//静态的栈template<classT>classstack{public:stack():_a(nullptr),_top(0),_capacity(0){}private: T* _a[N];int _top;int _capacity;};intmain(){ stack<int> s1;//可以控制N为10 创建一个空间为10的静态栈 stack<int> s2;//也可以控制N为100创建一个空间为100的静态栈return0;}
我们可以通过宏定义来控制静态栈的空间大小,但这种方法存在明显局限——所有栈实例只能使用相同的预设大小,无法实现不同栈拥有不同容量(例如一个栈10个元素,另一个栈100个元素这时小的那个栈就会浪费90个空间)。为解决这个问题,可以通过引入非类型模板参数来灵活控制各个栈的独立容量
//使用整型做非模板参数//非模板参数:使用一个正数说常量作为类模板的一个参数 //使用整型来做类模板的参数 这里的N为10是缺省值 传了值就用传的那个值 没传就用缺省值 template<classT=int, size_t N =10>classstack{private: T* _a[N];int _top;int _capacity;};intmain(){ stack<int> s1;//10个空间 stack<int,100> s2;//100个空间 stack<int,1000> s3;//1000个空间 cout <<sizeof(s1)<< endl; cout <<sizeof(s2)<< endl; cout <<sizeof(s3)<< endl;return0;}
这段代码利用非类型模板参数N来动态调整栈空间大小。 相比宏定义,模板参数提供了更大的灵活性,使每个栈实例都能拥有独立的存储空间。这种设计既确保了空间分配的确定性,又有效避免了资源浪费问题。

🔺注意:
1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
2. 非类型的模板参数必须在编译期就能确认结果。

1.3array容器

在我们之前学过的容器中,如string、vector、stack和queue,都很少使用非类型模板参数。而array容器则是个例外,它采用了非类型模板参数。既然提到了这个概念,我们不妨来了解一下它。
在这里插入图片描述


在这里插入图片描述
该容器提供的接口与vector类似,迭代器也采用原生指针实现,因此不再赘述接口的使用方法。我们将重点探讨array与传统C语言数组的主要区别:
//---------------------静态数组 与array的区别#include<array>intmain(){//array容器 array<int,10> a1;//静态数组int a2[10]={0};//静态数组的越界访问  a2[0]=2; a2[9]=100;//访问这两个越界的地方没有报错 说明静态数组越界访问不会报错 cout << a2[10]<< endl; cout << a2[15]<< endl;//将越界处的数据修改会报错 说明静态数组越界 写(修改)会报错检查不出来//a2[11] = 23;//a2[15] = 24;for(auto e : a2){ cout << e <<" ";} cout << endl;//越界写入数据 会报错 a1[10]=2; cout << a1[10]<< endl;//越界访问读取数据 也会报错 由此得出array比静态数组 检查的更严格 静态数组只是抽查 cout << a1[15]<< endl;return0;}

🔺需要注意的是,array之所以能够检测数组越界读写,关键在于其重载的operator[]函数内部实现了对传入下标的边界检查机制

二、模板的特化

2.1什么是特化?

模板特化是C++中针对泛型编程的一种机制,允许为特定类型或条件提供定制化的模板实现。当通用模板无法满足某些特殊类型的需求时,可以通过特化来优化或改变其行为。相当于是模板的特殊处理。

2.2什么场景下使用特化?

下面请看例子:

template<classT>boolLess(const T& left,const T& right){return left < right;}intmain(){int a =1, b =2; cout <<Less(a,b)<<" "<<endl; cout <<Less(2,3)<< endl;int* pa =&a;int* pb =&b; cout <<Less(pb, pa)<< endl;double* p1 =newdouble(2.2);double* p2 =newdouble(1.1); cout <<Less(p1, p2)<< endl;//这里明明p1指向的内容比p2大 //传值过去判断应该为假才对 但是输出结果是1//所以就要对函数模板进行特殊化 即生成一种专门应对这种情况的模板 string* p3 =newstring("111"); string* p4 =newstring("222"); cout <<Less(p3, p4)<< endl;//把p3和p4传递过去后 模板就实例化了一个string*的类型//而left和right拿到的是p3和p4的地址 所以比较的就是地址 而不是内容return0;}
🤨从上述代码可见,Less函数模板虽然能处理大多数情况但在比较指针类型时存在局限:它比较的是指针本身而非指针指向的内容。 因此,我们需要对模板进行特化处理来解决这个问题。

对于上面函数模板的特化版本如下:

//特化template<>//template<>表示这是一个显式特化bool Less<double*>(double*const& left,double*const& right){return*left <*right;}

注意:

🔺特化的模板需要显示实例化例如:Less<double*>表明对模板类或模板函数Less进行特化,特化的类型是double*(即指向double的指针)。
🔺特化后的模板参数不能写成const double* & ! 因为主模板是const T&修饰的是形参本身不能被修改,如果T=double*就边成const double* &修饰的是指针指向的内部不能被修改而不是指针本身不能被修改!所以特化模板和主模板参数不匹配!会报错!
📢最初的模板的参数一定要与特化后函数的参数对应匹配 !!!

在这里插入图片描述
const T* p1 ——>const在*的左边修饰指向的内容 内容不能改 *p1(解引用)不能修改 T const* p2——>const在*的左边修饰指向的内容 内容不能改 *p2(解引用)不能修改 *const p3 ——>const在*的右边修饰指针本身 本身的指向不能改 p3(指针本身)不能修改 

如果涉及较多的指针内容的比较我们也可写成一个通过模板——>前提:要有主模板

template<classT>boolLess( T*const& left, T*const& right){return*left <*right;}

2.3特化的种类

模板的特化分为两种:全特化、偏特化。

1、全特化

全特化👉指参数列表中所有的参数都确定!

//---------------------------------全特化-------------------------------//主类模板template<classT1,classT2>classData{public:Data(){ cout <<"Data<T1,T2>"<< endl;}private: T1 _d1; T2 _d2;};//特化类模板template<>classData<int,char>{public:Data(){ cout <<"Data(int,char)"<< endl;}private:int _d1;char _d2;};intmain(){ Data<int,char> d1;//调用特化的模板 Data<int,int>d2;//调用主模板 cout <<typeid(d1).name()<< endl; cout <<typeid(d2).name()<< endl;return0;}
在这里插入图片描述


2、偏特化

偏特化👉就是指定部分参数。

//------------------------------偏特化-------------------------------template<classT1,classT2>classData{public:Data(){ cout <<"Data<T1,T2>"<< endl;}voidf1(){};};// 偏特化// 特化部分参数template<classT1>classData<T1,char>{public:Data(){ cout <<"Data<T1, char>"<< endl;}voidf1(){};};// 对参数进一步限制template<classT1,classT2>classData<T1*, T2*>{public:Data(){ cout <<"Data<T1*, T2*>"<< endl;}voidf1(){ T1 x1; cout <<typeid(x1).name()<< endl; T1* x2; cout <<typeid(x2).name()<< endl;}};intmain(){ Data<int,int> d1;//调用主模板 d1.f1(); Data<int,char> d2;//调用偏特化模板 d2.f1(); Data<char,char> d3;//调用偏特化模板 Data<char*,char*> d4; Data<int*,char*> d5; Data<double*,double*> d6; d4.f1(); Data<double&,double&> d7; Data<double*,double&> d8;return0;}
在这里插入图片描述

这里可能会有人有疑问:d4中使用一个T定义了一个x1,使用T*定义了一个x2使用typeid打印出来的为什么一个是char一个是char*?

因为T1 x1声明了一个非指针变量,其类型为T1(即指针T1*所指向的底层类型)。
T1* x2声明了一个指针变量,其类型为T1*(即原始的模板参数类型)。

举个例子:
若实例化Data<int*, double*>:

T1被推导为intT2被推导为double
T1 x1;中的x1类型为int
T1* x2;中的x2类型为int*

三、模板的分离编译

3.1什么是模板的分离编译?

模板分离编译👉指将模板的声明和实现分别放在不同的文件中(通常是头文件.h和源文件.cpp),类似于普通函数的声明与实现分离。这种设计初衷是为了提高代码的可维护性和编译效率。

3.2模板的分离编译

假设有下面两个函数,他们的声明和定义均分别放在.h文件(声明)和.cpp文件(定义)中:

<在Func.h文件中>#include<iostream>usingnamespace std;//模板函数的声明template<classT>voidFuncT(const T& x);//普通函数的声明voidFuncF();
<在Func.cpp文件中>#include"Func.h"//模板函数的定义template<classT>voidFuncT(const T& x){ cout <<"模板函数:FuncT(const T& x)"<< endl;}//普通函数的定义voidFuncF(){ cout <<"普通函数:FuncF()"<< endl;}
<在test.cpp文件中调用这两个函数>#include"Func.h"intmain(){//函数模板调用FuncT(1);//call FuncT(?)找不到FuncT的地址//普通函数调用FuncF();//call FuncF(普通函数的地址)return0;}
在这里插入图片描述


在这里插入图片描述


为什么会报链接错误呢?这也是我们在前面STL各种容器的模拟实现中强调类模板声明和定义最好不要分离(分文件)的原因!这其实是没有实例化造成的,下面画个图让大家更加直观的理解:

在这里插入图片描述

3.3链接错误的解决方案

上面我们已经分析了,编译器报链接错误是由于函数模板没有实例化造成的,那么要解决该问题我们就要从实例化入手!

方法一:在func.cpp文件中显示实例化

//模板函数的定义template<classT>voidFuncT(const T& x){ cout <<"模板函数:FuncT(const T& x)"<< endl;}//显示实例化template<>voidFuncT(constint& x){ cout <<"模板函数:FuncT(const T& x)"<< endl;}
这种实例化方式有较为明显的局限性——>每实例化一种类型就要人为的去显示实例化,既增加了工作量也增加了代码的冗余度所以这种方法并不推荐。

方法二:将声明和定义放在同一个文件例如“xxx.h”文件或"xxx.hpp"文件

<在.h文件中>template<classT>voidFuncT(const T& x){ cout <<"void FuncT(const T& x)"<< endl;}//类模板也类似template<classT>classStack{public://类里面定义voidPush(const T& x);};//类外面定义template<classT>voidStack<T>::Push(const T& x){ cout <<"void Push(const T& x)"<< endl;}
这种方法明显优于第一种既减少了工作量,也减少了代码冗余度。推荐使用这种方法!

四、模板总结

💡【优点】

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

💡【缺陷】

  1. 模板会导致代码膨胀问题,也会导致编译时间变长。
  2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误。

Read more

计算机毕设java实体店管理系统 基于Java的实体店铺智能管理平台设计与实现 Java环境下实体店综合管理系统开发与应用

计算机毕设java实体店管理系统 基于Java的实体店铺智能管理平台设计与实现 Java环境下实体店综合管理系统开发与应用

计算机毕设java实体店管理系统mz6v49 (配套有源码 程序 mysql数据库 论文) 本套源码可以在文本联xi,先看具体系统功能演示视频领取,可分享源码参考。 随着互联网技术的飞速发展,传统的实体店管理模式已经难以满足现代商业的需求。越来越多的商家开始寻求更加高效、便捷的管理方式,以提升运营效率和客户满意度。在这种背景下,开发一套基于Java的实体店管理系统显得尤为必要。该系统旨在通过现代化的技术手段,实现实体店管理的数字化转型,帮助商家更好地管理店铺运营的各个环节。 在系统开发过程中,我们深入分析了实体店管理的实际需求,结合Java语言的强大功能和B/S架构的优势,设计并实现了一套功能完备的管理系统。系统的核心功能包括: * 系统首页:展示店铺的基本信息和运营概况,为用户提供直观的店铺概览。 * 个人中心:允许用户管理个人信息,如修改密码、更新联系方式等。 * 会员用户管理:支持对会员信息的增删改查,方便商家维护会员关系。 * 商品管理:实现商品信息的录入、编辑、删除和查询,帮助商家高效管理库存。 * 商品分类管理:对商品进行分类管理,便于用户快速查找所需商

By Ne0inhk
【Java 开发日记】设计一个支持万人同时抢购商品的秒杀系统?

【Java 开发日记】设计一个支持万人同时抢购商品的秒杀系统?

目录 一、系统架构设计 1. 分层架构 2. 具体组件 二、核心问题解决方案 1. 超卖问题 解决方案一:Redis原子操作 解决方案二:数据库乐观锁 解决方案三:预扣库存 2. 高并发请求处理 2.1 流量削峰 2.2 分层过滤 3. 系统性能优化 3.1 缓存策略 3.2 读多写少优化 4. 详细实现方案 4.1 秒杀流程 4.2 库存同步方案 三、高可用保障 1. 限流降级策略 2. 熔断降级 四、监控与告警 1.

By Ne0inhk

Java最新面试题库——精选100道(含精简答案),收藏这篇就够了

JavaEE面试题整理 * 一、Java基础篇 * 二、JVM篇 * 三、Tomcat篇 * 四、MyBatis篇 * 五、Spring篇 * 六、SpringMVC面试题整理 * 七、Redis篇 * 八、Mongodb篇 * 九、MQ篇 * 十、Shiro篇 * 十一、搜索引擎篇 * 十二、Nginx篇 * 十三、SpringBoot篇 * 十四、Dubbo篇 一、Java基础篇 1、JAVA中的几种基本数据类型是什么,各自占用多少字节? 浮点类型:float(4字节)、double(8个字) 整数类型:byte(1字节)、short(2字节)、int(4字节)、long(8字节) 字符类型:char(

By Ne0inhk
JavaScript模式——策略模式:告别if-else地狱,2个案例让你“策略”在握

JavaScript模式——策略模式:告别if-else地狱,2个案例让你“策略”在握

1. 策略模式是什么鬼? 简单说,策略模式就是帮你把一堆“算法”或者“规则”分别打包,让它们可以随时互换。就像你去旅行,可以选择飞机、火车、自驾,目的地一样,但方式随便换。这种模式的核心思想就是把“做什么”和“怎么做”分开。 比如,你要计算年终奖,绩效S和绩效A的算法不一样,但它们都是“计算奖金”这件事。策略模式就让你把这些算法一个个封装好,想用哪个就用哪个。 2. 策略模式长啥样? 策略模式通常有两部分: * 策略对象:负责具体干活,比如不同的奖金算法、不同的动画缓动公式。 * 环境对象:负责接收命令,然后把活派给某个策略。 听起来抽象?没关系,下面两个例子保你一看就懂。 3. 案例一:年终奖怎么算? 3.1 新手写法:if-else堆成山 刚入行的程序员可能会这么写: var

By Ne0inhk