当前位置: 首页 > news >正文

C++模板(初识)

一、泛型编程

我们平时写交换函数的时候,会这样写:

//交换两个int类型变量
void Swap(int& left, int& right)
{int temp = left;left = right;right = temp;
}
//交换两个double类型变量
void Swap(double& left, double& right)
{double temp = left;left = right;right = temp;
} 
//交换两个char类型变量
void Swap(char& left, char& right)
{char temp = left;left = right;right = temp;
}

这样写相比于C语言已经很方便了,因为C++支持函数重载和引用。

使用函数重载虽然可以实现,但是有几个不好的地方:

  1. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增
    加对应的函数。
  2. 代码的可维护性比较低,一个出错可能所有的重载均出错。

但这样写又会感觉怪怪的,它们的大体框架一摸一样,实现的功能也一摸一样,就是要交换的变量类型不同,有没有一种方式能解决这种问题?

这时候模板就应运而生了,编译器会根据不同的类型利用该模板来生成相应代码。有函数模板类模板两种。

泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。

二、函数模板

1、函数模板格式

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

函数模板格式:

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

返回值类型 函数名(参数列表){}

其中, T1,T2...是一种类型,具体个数自己设定。

对于上述的代码,我们可以写一个函数模板:

template<typename T>
void Swap( T& a, T& b)
{T temp = a;a = b;b = temp;
}

注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替
class)。

这样一个模板就可以解决上述出现的问题:

template<class T>
void Swap(T& a, T& b)
{T tmp = a;a = b;b = tmp;
}
int main()
{int i = 1, j = 2;cout << "Swap Before:" << i << "  " << j << endl;Swap(i, j); //调用时,T会是int类型cout << "Swap After:" << i << "  " << j << endl;double m = 1.1, n = 2.2;cout << "Swap Before:" << m << "  " << n << endl;Swap(m, n); //调用时,T会是double类型cout << "Swap After:" << m << "  " << n << endl;return 0;
}

2、函数模板原理

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。
所以其实模板就是将本来应该我们做的重复的事情交给了编译器。简单来说就是原本我需要写3个函数,现在我只写了1个模板,调用时,其实本质上还是调用了3个函数,只是那是编译器的工作,它会帮助我们来进行具体调用,我们只用写1个模板就行,减少了我们的工作量,增加了编译器的工作量而已,当然,编译器不会喊"累"的。

在调试过程中,它们Swap(i, j);Swap(m, n);都会走模板,但实质上它们走的不是同一个函数,我们通过汇编代码可以看出:

进而说明了是编译器帮助我们生成各自的函数。像下面这样:

在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用(调用的不是同一个函数),比如:当用double类型使用函数模板时,编译器通过对实参类型的推演, 将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。用函数模板生成对应的函数这一过程被称为模板的实例化。

T到底是什么类型是根据实参传递给形参是推导出来的。

上述举例中函数模板只有一个参数T:

template<class T>
void Swap(T& a, T& b)
{T tmp = a;a = b;b = tmp;
}
int main()
{int i = 1, j = 2;double m = 1.1, n = 2.2;Swap(i, j);//okSwap(m, n);//okSwap(i, m);//err,参数模板只有一个类型,传参时就不能传2个不同类型的变量//因为编译器不知道T到底是int类型还是double类型return 0;
}

函数模板也可以有多个参数:

template<class T1, class T2>
void Func(T1& x,T2& y)
{//...
}
int main()
{int i = 1;double j = 1.1;Func(i, j);  //这里就能判断出T1是int类型,T2是double类型return 0;
}

3、单参函数模板可能遇到的情况

单参函数模板若传两个不同类型的变量就会报错,因为编译器分不清函数模板中的参数类型到底是哪一个。

template<class T>
T Add(const T& a, const T& b)
{return a + b;
}int main()
{int a1 = 10, a2 = 20;double d1 = 10.1, d2 = 20.2;Add(a1, a2);  //这没问题,因为a1和a2的类型一样Add(d1, d2);  //这也没问题,d1和d2的类型一样Add(a1,d1);  //err,这里就会报错,因为编译器不知道T到底是int类型还是double类型return 0;
}

 那有什么办法可以解决这种问题呢?

(1)强制类型转换

将两个不同类型的变量,其中一个强制转换成另一个,这样编译器就可以推导出T的类型了,这个过程就是推导实例化。

	//推导实例化cout << Add(a1, (int)d1) << endl;cout << Add((double)a1, d1) << endl;

 (2)显示实例化

	//显示实例化cout << Add<int>(a1, d1) << endl;cout << Add<double>(a1, d1) << endl;

这段代码的意思就是,直接显示说明T的类型,第一行T的类型是int,d1是double类型会走隐式类型转换(转换成int类型),第二行T的类型是double,a1是int类型也会走隐式类型转换(转换成doubel类型)。

(3)改为两个参数的函数模板(具体几个参数看题目要求)

template<class T1,class T2>
T1 Add(const T1& a, const T2& b)
{return a + b;
}

这样就不会有歧义了。

有时候必须用显示实例化,比如:

template<class T>
T* func(int n)
{return new T[n];
}int main()
{func(3);return 0;
}

这种情况,推导不出来T是什么类型。必须用显示实例化,说明T的类型。

double* p1 = func<double>(10);//这里必须显式实例化,T是double类型

4、函数模板和具体函数同时存在

如果函数模板和具体函数同时存在,会先调用哪一个?

template<class T>
T Add(const T& a, const T& b)
{return a + b;
}int Add(const int& a, const int& b)
{return (a + b) * 10;
}int main()
{int a1 = 10, a2 = 20;cout << Add(a1, a2) << endl;return 0;
}

运行结果:

编译器会优先调用具体的函数。俗话说的好,能省即省,我放着现成的不用为什么要走模板二次加工呢? 

三、类模板

1、类模板格式

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

这里的class也可以用typename,其它的和函数模板差不多。

我们先来写一个类模板:

//类模板
template<typename T>
class Stack
{
public:Stack(int n = 4):_array(new T[n]), _capacity(n), _size(0){}~Stack(){delete[] _array;_array = nullptr;_size = _capacity = 0;}void Push(const T& x){if (_size == _capacity){//手动扩容T* tmp = new T[_capacity * 2];memcpy(tmp, _array, sizeof(T) * _size);delete[] _array;_array = tmp;_capacity *= 2;}_array[_size++] = x;}private:T* _array;  //栈开辟空间的起始指针int _capacity;  //栈的容量int _size;  //栈中元素个数
};

 这个模板的主要功能是,模拟一个栈,写了一个Push成员函数用来向栈顶添加元素。栈中的成员类型是T。

我们要实现的是动态栈,即容量不够会自动扩容,自动扩容的机制需要我们自己写。

什么时候要判断容量是否满了?

这里其实就是Push时,像栈顶添加元素前要判断栈中容量是否能装下这个元素。

在C语言中有realloc函数支持扩容,但C++中可没有,我们可以手动写一个:

if (_size == _capacity)
{//手动扩容T* tmp = new T[_capacity * 2];  //扩为原来的2倍memcpy(tmp, _array, sizeof(T) * _size);  //拷贝原来的内容到新空间delete[] _array; //释放原有空间_array = tmp; //_array再次成为指向空间首位置的指针_capacity *= 2; //容量扩2倍
}_array[_size++] = x; //在栈顶添加新元素同时_size需要加1

进入Push,首先要判断是否需要扩容,扩容时开辟原来两倍的空间(没有定性,一般都是2倍),再用memcpy函数将原有的空间的内容拷贝到新开辟的空间,释放原有的空间。这就是大致过程。

2、类模板实例化

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的
类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。

int main()
{//类模板都是显示实例化Stack<int> st1;  //栈中的成员都是int类型st1.Push(1);st1.Push(2);st1.Push(3);Stack<double> st2; //栈中的成员都是double类型st2.Push(1.1);st2.Push(2.2);st2.Push(3.3);return 0;
}

为什么必须显示实例化呢?

因为如果不用显示实例化,模板中的T根本推导不出来是什么类型,那这个模板还有什么用处呢?

所以必须要显示实例化。

3、类模板中函数声明和定义分离

在同一文件下,类模板中,我们也可以让其中的函数的声明和定义分离:

//类模板
template<typename T>
class Stack
{
public:Stack(int n = 4):_array(new T[n]), _capacity(n), _size(0){}~Stack(){delete[] _array;_array = nullptr;_size = _capacity = 0;}//声明void Push(const T& x);private:T* _array;int _capacity;int _size;
};//定义
template<typename T>
void Stack<T>::Push(const T& x)
{if (_size == _capacity){//手动扩容T* tmp = new T[_capacity * 2];memcpy(tmp, _array, sizeof(T) * _size);delete[] _array;_array = tmp;_capacity *= 2;}_array[_size++] = x;
}

平时在类中,函数的声明和定义可不是这样写的?

void Stack::Push(const T& x)

但在模板中这样写是不行的,template<typename T>只管下面的一个类或函数,到了Push时就管不到了,所以Push中的T编译器是不认识的,所以我们应该这样写:void Stack<T>::Push(const T& x)。

模板中的函数的定义和分离尽量不要写到两个文件中,也可以理解为不能写到两个文件中。

四、总结

本篇到这里就结束了,主要写了模板的一些基本知识,希望对大家有所收获,祝大家天天开心!

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【Android】Material Design编写更好的UI
  • 火狐浏览器现在可以让你在不切换标签的情况下访问你最喜欢的AI聊天机器人 - 具体方法如下
  • 人工智能新能源数字化转型商业模式专家教授学者讲师培训师唐兴通谈新媒体营销大客户销售大模型创新思维短视频内容社私域数字经济人工智能
  • 【重构获得模式 Refactoring to Patterns】
  • Python知识点:Python研发中,如何使用JIRA进行项目管理
  • 传统CV算法——边缘检测算法Canny算法实战
  • 艾体宝洞察丨透过语义缓存,实现更快、更智能的LLM应用程序
  • ESD防静电监控系统助力电子制造行业转型升级
  • cesium加载在线3dtiles
  • PS插件DR5至臻高级版下载安装教程Photoshop美颜美白牙齿磨皮使用插件百度网盘分享
  • 2024Java基础总结+【Java数据结构】(1)
  • 【python】—— Python爬虫实战:爬取珠海市2011-2023年天气数据并保存为CSV文件
  • 初始MYSQL数据库(2)——创建、查询、更新、删除数据表的相关操作
  • Django + websocket 连不上
  • 深度学习(一)-感知机+神经网络+激活函数
  • [nginx文档翻译系列] 控制nginx
  • 【跃迁之路】【477天】刻意练习系列236(2018.05.28)
  • 4. 路由到控制器 - Laravel从零开始教程
  • Centos6.8 使用rpm安装mysql5.7
  • electron原来这么简单----打包你的react、VUE桌面应用程序
  • happypack两次报错的问题
  • java架构面试锦集:开源框架+并发+数据结构+大企必备面试题
  • JS变量作用域
  • Shell编程
  • yii2中session跨域名的问题
  • Zepto.js源码学习之二
  • 从零开始的webpack生活-0x009:FilesLoader装载文件
  • 使用docker-compose进行多节点部署
  • 视频flv转mp4最快的几种方法(就是不用格式工厂)
  • 限制Java线程池运行线程以及等待线程数量的策略
  • 项目管理碎碎念系列之一:干系人管理
  • 学习使用ExpressJS 4.0中的新Router
  • 用element的upload组件实现多图片上传和压缩
  • - 转 Ext2.0 form使用实例
  • 阿里云IoT边缘计算助力企业零改造实现远程运维 ...
  • 阿里云服务器如何修改远程端口?
  • ​DB-Engines 11月数据库排名:PostgreSQL坐稳同期涨幅榜冠军宝座
  • ​LeetCode解法汇总2696. 删除子串后的字符串最小长度
  • ​MySQL主从复制一致性检测
  • ​必胜客礼品卡回收多少钱,回收平台哪家好
  • # 学号 2017-2018-20172309 《程序设计与数据结构》实验三报告
  • #Js篇:单线程模式同步任务异步任务任务队列事件循环setTimeout() setInterval()
  • #我与Java虚拟机的故事#连载09:面试大厂逃不过的JVM
  • $.ajax,axios,fetch三种ajax请求的区别
  • (1)Map集合 (2)异常机制 (3)File类 (4)I/O流
  • (13)Latex:基于ΤΕΧ的自动排版系统——写论文必备
  • (173)FPGA约束:单周期时序分析或默认时序分析
  • (2.2w字)前端单元测试之Jest详解篇
  • (Bean工厂的后处理器入门)学习Spring的第七天
  • (php伪随机数生成)[GWCTF 2019]枯燥的抽奖
  • (ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_READ_ONLY)讲解
  • (二十五)admin-boot项目之集成消息队列Rabbitmq
  • (附源码)springboot社区居家养老互助服务管理平台 毕业设计 062027
  • (力扣题库)跳跃游戏II(c++)
  • (十八)三元表达式和列表解析