C++模板从入门到入土

2024-02-22 17:52
文章标签 模板 c++ 入门 入土

本文主要是介绍C++模板从入门到入土,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1. 泛型编程  

如果我们需要实现一个不同类型的交换函数,如果是学的C语言,你要交换哪些类型,不同的类型就需要重新写一个来实现,所以这是很麻烦的,虽然可以cv一下,有了模板就可以减轻负担。

下面写一个适合所有类型的交换就可以这样写。

template<typename T>
void Swap(T& left, T& right)
{T temp = left;left = right;right = temp;
}
int main()
{int a1 = 10, a2 = 20;double d1 = 1.0, d2 = 2.2;swap(a1, a2);swap(d1, d2);return 0;
}

让我们先从文字上来了解什么是泛型编程,泛型指的是广泛类型的意思。

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

问题:我们其实如果用函数重载也能解决问题,但是为什么我们还是有模板这个东西呢?

1.重载的只是函数类型不同,代码相同的部分很多,代码复用率很高。
2.如果有一行代码是有问题的话,这些重载的代码都是需要修改的
那我们就可以给编译器一个例子,然后让编译器自己去生成,就像古代的磨具一样,我们再磨具上印出东西,然后就拿的这个磨具去印出相同的东西,这不是很方便的东西。

函数模板

1.函数模板的概念

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

2.函数模板的格式

template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表){}

我们可以拿Swap这个例子来模拟

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

1.template是关键字

2.typename是修饰后面T的关键字,也有class这个关键字,class这个关键字比较短,所以我们用这个比较多。
3.T1, T2, ..., Tn 表示的是函数名,可以理解为模板的名字,名字你可以自己取。

注意事项:函数模板不是一个函数,而是我们的编译器拿的这个函数模板去实例化出一个一个的函数来的,我们可以理解为函数的模板

函数模板的原理

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模 板就是将本来应该我们做的重复的事情交给了编译器

 

#include<iostream>using namespace std;
template<class T>
void Swap(T& x, T& y)
{T tmp(x);x = y;y = tmp;
}
int main()
{int x = 1;int  y = 2;double d1 = 2.2;double d2 = 3.3;cout << "交换前->" << x << " " << y << endl;cout << "交换前->" << d1 << " " << d2 << endl;Swap(x, y);Swap(d1, d2);cout << "交换后->" << x << " " << y << endl;cout << "交换后->" << d1 << " " << d2 << endl;return 0;
}

我们可以看到的是我们的数据也是成功的进行交换了,那我们来想想他的原理是什么呢,首先编译器是会根据函数模板生成不同的函数,他们的类型是不同的。而且他们的函数栈帧不是同一个。

 编译器是会根据这个函数模板去实例化不同的函数出来,所以在函数栈帧上调用的不是同一个函数栈帧,我们也可以来看汇编代码,看看call的地址是不是同一个地址。

所以可以看出我们不是调用的用一个函数。

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

那我们下面就来探讨编译器是怎么进行实例化的。

 

函数模板的实例化

 其实过程是很简单的,我们在编译阶段的时候,告诉我们的函数模板你要去根据类型进行实例化,然后因为T是函数模板的参数,所以如果我们传int过去的时候他就知道T是int,所以的T改成int去实例化出一个函数出来。

但是函数模板在实例化的过程中也是会出现问题的,比如我们可以来下面的这种情况,我们下一个简单的Add函数模板,然后在main函数里面进行相加计算出结果,我们可以来看看如果不是同一个类型的化会出现怎样的问题。

#include<iostream>using namespace std;
template<class T>
T Add(const T& x, const T& y)
{return x + y;
}int main()
{int x = 1;int  y = 2;double d1 = 2.2;double d2 = 3.3;int ret1 = Add(x, y);double ret2 = Add(d1, d2);cout << ret1 << " " << ret2;return 0;
}

首先这样的代码是没有问题的,但是如果我们是x+d1呢,我们来看看他的报错信息。、

 

如果是这样写的化报错信息就是这个样子的,所以我们需要怎么进行修改才行呢。

 显式实例化:在函数名后的<>中指定模板参数的实际类型

没错,我们是需要进行显示实例化的,但是我们应该如何进行显示实例化呢,规则很简单。

上面的Add就可以写成。

Add<int>(x,d1);

 我们的代码是可以运行的,但是会有这样的警告,其实是可以忽略的,因为我们本生就是不同类型的相加,肯定会产生强转的。

对于模板函数的使用,编译器需要根据传入的实参类型来推演,生成对应类型的函数以供调用。但是我们可以显示的去实例化,规则和Add是一样的道理。 

像第一个 Add<int>(a1, a2)  ,a2 是 double,它就要转换成 int 。

第二个 Add<double>(a1, a2),a1 是 int,它就要转换成 double。

这种地方就是类型不匹配的情况,编译器会尝试进行隐式类型转换。

像 double 和 int 这种相近的类型,是完全可以通过隐式类型转换的。

🔺 总结:

  • 函数模板你可以让它自己去推,但是推的时候不能自相矛盾。
  • 你也可以选择去显式实例化,去指定具体的类型。

 模板参数的匹配原则

场景:我们会写一个关于Add的函数模板和实现一个Add的函数,类型是int那他到底会配对那个呢。

#include<iostream>using namespace std;
template<class T>
T Add(const T& x, const T& y)
{return x + y;
}
int Add(int x, int y)
{return x + y;
}
int main()
{int x = 1;int  y = 2;double d1 = 2.2;double d2 = 3.3;int ret1 = Add(x, y);double ret2 = Add(d1, d2);Add<int>(x, d1);cout << ret1 << " " << ret2;return 0;
}

就是像这样的场景,那我们如果函数是Add(int ,int)的时候是调用哪个呢。

规则:有现成的就用现成的呗,我们函数模板进行实例化是要根据类型去实例化的,但是我们已经有一个关于它的函数了,这个函数是最适合你的,你还要去生成一个,都多余了。

 

所以我们就不会再去麻烦编译器去再生成一个函数来实现了。

总结:

① 一个非模板函数可以和一个同名的模板函数同时存在,

而且该函数模板还可以被实例化为这个非模板函数:

② 对于非模板函数和同名函数模板,如果其他条件都相同,

在调用时会优先调用非模板函数,而不会从该模板生成一个实例。

如果模板可以产生一个具有更好匹配的函数,那么将选择模板。

3. 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换

 

第三点解释一下,就是我们再根据模板生成的时候,只会根据你给的类型去生成,而不存在强转这些,除非是隐式类型转换,隐式类型转换是会存在强转的可能性的。

类模板

1 类模板的定义格式

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

 规则其实和函数模板是差不多的。

1.template是关键字

2.typename是修饰后面T的关键字,也有class这个关键字,class这个关键字比较短,所以我们用这个比较多。
3.T1, T2, ..., Tn 表示的是函数名,可以理解为模板的名字,名字你可以自己取。

这里要强调一下函数模板和类模板都是不支持分离声明和定义的,你可以再同一个文件里,但是不能在不同的文件进行声明和定义(指的是在一个.h进行声明,在一个.cpp进行定义)这个情况是会我们程序进行链接的时候出现找不到这个地址的现象,因为我们的模板函数是不知我们要实例化的类型是什么,所以就会出现最后链接的时候Call(没地址),所以就会链接错误,后面会深入的讲解。

继续回归我们对类模板的认识,首先是引出问题,我们没有类模板的栈是怎么写的。来看看吧。

class Stack {
public:Stack(int capacity = 4) : _top(0) , _capacity(capacity) {_arr = new int[capacity];}~Stack() {delete[] _arr;_arr = nullptr;_capacity = _top = 0;}
private:int* _arr;int _top;int _capacity;
};

这个栈是只能来存int,有人就会说,如果我们对int进行typedef不就行了,如果我想要其他类型的时候就只需要改类型就行了,但是这样就有了第二个问题,那就是如果我们需要的是一个存放int的栈,一个存放的是node* 节点的栈,或者一个日期的时候,那问题就很大了,每当我们需要这个类型的时候就需要ctrl c + v然后改一下类型这个操作其实很简单,也很快,但是最终结果就是造成代码相同的还是很多,这样和我们之前的函数模板是一样的问题,所以就有了我们的类模板,那我们来改造一下上面的代码吧。

template<class T>
class Stack {
public:Stack(int capacity = 4) : _top(0) , _capacity(capacity) {_arr = new T[capacity];}~Stack() {delete[] _arr;_arr = nullptr;_capacity = _top = 0;}
private:T* _arr;int _top;int _capacity;
};int main(void)
{Stack<int> st1;   // 存储intStack<double> st2;   // 存储doublereturn 0;
}

这样就可以解决了我们要存放int和double或者其他类型的问题了。

但是我们发现,类模板他好像不支持自动推出类型,

 它不像函数模板,不指定它也可以根据传入的实参去推出对应的类型的函数以供调用

这就是为什么我们需要在类模板后面根生类型,这里大家就要记住的是类模板必须要显示实例化的方法写,它不能像函数模板一样去推演类型。

类模板实例化

模板实例化在类模板名字后跟 < >,然后将实例化的类型放在 < > 中即可。

注意事项:

① Stack 不是具体的类,是编译器根据被实例化的类型生成具体类的模具。

② Stack 是类名,Stack<int> 才是类型:

我们上面说过类模板不能在两个文件里声明和定义分离,但是没说不能在同一个文件了,但是在同一个文件里有些讲究,我们得来探究一下。

就继续拿我们栈来说话。

#include<iostream>using namespace std;
template<class T>
class Stack {
public:Stack(int capacity = 4): _top(0), _capacity(capacity) {_arr = new T[capacity];}// 这里我们让析构函数放在类外定义void Push(const T& x);~Stack();
private:T* _arr;int _top;int _capacity;
};/* 类外 */void Stack::Push(const T& x) {//::::
}

 如果我们是这样写的化就是会存在一些小的问题,编译器是不认识外面的这个T,那我们要改的话是需要在下面函数上加上模板的参数的,

 

template<class T>
class Stack {
public:Stack(T capacity = 4): _top(0), _capacity(capacity) {_arr = new T[capacity];}// 这里我们让析构函数放在类外定义void Push(const T& x);~Stack();
private:T* _arr;int _top;int _capacity;
};/* 类外 */
template<class T>
void Stack<T>::Push(const T& x) {//::::
}

虽然是能编译通过,但是链接的时候又是会存在问题的,所以我的建议就是大家声明和定义都放在类模板里,多一事不如少一事。

对于这个需要记住的是----------> Stack 是类名,不是类型,Stack<T> 才是类型! 

初阶模板就分享到这里了,我们后面还有进阶的模板,今天的分享就到这里了,下次再见了~

 

这篇关于C++模板从入门到入土的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/736039

相关文章

C++中unordered_set哈希集合的实现

《C++中unordered_set哈希集合的实现》std::unordered_set是C++标准库中的无序关联容器,基于哈希表实现,具有元素唯一性和无序性特点,本文就来详细的介绍一下unorder... 目录一、概述二、头文件与命名空间三、常用方法与示例1. 构造与析构2. 迭代器与遍历3. 容量相关4

C++中悬垂引用(Dangling Reference) 的实现

《C++中悬垂引用(DanglingReference)的实现》C++中的悬垂引用指引用绑定的对象被销毁后引用仍存在的情况,会导致访问无效内存,下面就来详细的介绍一下产生的原因以及如何避免,感兴趣... 目录悬垂引用的产生原因1. 引用绑定到局部变量,变量超出作用域后销毁2. 引用绑定到动态分配的对象,对象

使用Java填充Word模板的操作指南

《使用Java填充Word模板的操作指南》本文介绍了Java填充Word模板的实现方法,包括文本、列表和复选框的填充,首先通过Word域功能设置模板变量,然后使用poi-tl、aspose-words... 目录前言一、设置word模板普通字段列表字段复选框二、代码1. 引入POM2. 模板放入项目3.代码

C++读写word文档(.docx)DuckX库的使用详解

《C++读写word文档(.docx)DuckX库的使用详解》DuckX是C++库,用于创建/编辑.docx文件,支持读取文档、添加段落/片段、编辑表格,解决中文乱码需更改编码方案,进阶功能含文本替换... 目录一、基本用法1. 读取文档3. 添加段落4. 添加片段3. 编辑表格二、进阶用法1. 文本替换2

C++中处理文本数据char与string的终极对比指南

《C++中处理文本数据char与string的终极对比指南》在C++编程中char和string是两种用于处理字符数据的类型,但它们在使用方式和功能上有显著的不同,:本文主要介绍C++中处理文本数... 目录1. 基本定义与本质2. 内存管理3. 操作与功能4. 性能特点5. 使用场景6. 相互转换核心区别

Python进行word模板内容替换的实现示例

《Python进行word模板内容替换的实现示例》本文介绍了使用Python自动化处理Word模板文档的常用方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友... 目录技术背景与需求场景核心工具库介绍1.获取你的word模板内容2.正常文本内容的替换3.表格内容的

C++右移运算符的一个小坑及解决

《C++右移运算符的一个小坑及解决》文章指出右移运算符处理负数时左侧补1导致死循环,与除法行为不同,强调需注意补码机制以正确统计二进制1的个数... 目录我遇到了这么一个www.chinasem.cn函数由此可以看到也很好理解总结我遇到了这么一个函数template<typename T>unsigned

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

从入门到精通详解Python虚拟环境完全指南

《从入门到精通详解Python虚拟环境完全指南》Python虚拟环境是一个独立的Python运行环境,它允许你为不同的项目创建隔离的Python环境,下面小编就来和大家详细介绍一下吧... 目录什么是python虚拟环境一、使用venv创建和管理虚拟环境1.1 创建虚拟环境1.2 激活虚拟环境1.3 验证虚

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象