C++里面的“百变怪”:模板

爱编程的小赵 2024-10-07 08:05:00 阅读 58

🌞0.前言

言C++之言,聊C++之识,以C++会友,共向远方。各位博友的各位你们好啊,这里是持续分享C++知识的小赵同学,今天要分享的C++知识是模板 ,在这一章,小赵将会向大家聊聊C++的模板知识 。✊

 相信大家对于精灵宝可梦里面的百变怪是相当的熟悉,它可以变成各种各样的精灵宝可梦。而我们今天要介绍的C++里面的一个主人公也是一个百变怪,它可以变成各种各样的类型,好了话不多说快速开始进入我们今天的知识闲聊。

🚈1. 泛型编程

引入:还记得我们上次是怎么解决两个变量之间的求和的吗?我们前面的博客说我们写求和函数只能针对某一种类型进行求和,可是如果是其他类型就没办法解决,于是我们引入了函数重载。可是引入函数重载真的可以解决我们的问题吗?

<code>int Sum(int a,int b)

{

return a + b;

}

double Sum(double a, double b)

{

return a + b;

}

long long int Sum(long long int a, long long int b)

{

return a + b;

}

.......

我们发现有的时候我们要处理的求和类型太多,我们根本没办法将他们一个一个初始化。(这里的例子其实不太好,比较函数和交换函数好,这里主要还是为了和前面文章有个连接)

而且我们发现这样一个一个写函数重载真的很累人,而且如果我这个时候用这个求和函数,还得去看我写的求和函数重载里面有木有我要的,没有我还要自己加,所以基于以上的种种问题和原因,

我们开始思考如何去让我们的代码简化。

我们是否可以引入一种东西像百变怪一样按照我们的需求自由变化类型。

 又或是也能够存在这样一个模具,通过给这个模具中填充不同材料(类型),来获得不同材料的铸件

(即生成具体类型的代码)。

由此我们的前人引入了泛型编程,让我们的代码更加简短,同时可以满足我们的不同需求。 

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

而其中最典型的就是我们的模板: 

 🚈2. 函数模板

🚝2.1 函数模板概念

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

🚝 2.2 函数模板格式

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

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

下面直接实际用下解决我们上面的问题:

<code>template<typename T>

T Sum( T left, T right)

{

return left+right;

}

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

🚝 2.3 函数模板的原理

 那么如何解决上面的问题呢?大家都知道,瓦特改良蒸汽机,人类开始了工业革命,解放了生产力。机器生 产淘汰掉了很多手工产品。本质是什么,重复的工作交给了机器去完成。有人给出了论调:懒人创造世界。

 

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器。(就像我们前面的for(auto ch:num)又或是auto都是把我们的事情交给了编译器。)

这里附上小赵一贯的代码解析图

( 真的很像百变怪(或者前面的模具),要它变什么就变什么。)

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

🚝 2.4 函数模板的实例化

 用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。

✈️2.4.1隐式实例化:让编译器根据实参推演模板参数的实际类型

<code>template<class T>

T Add(const T& left, const T& right)

{

return left + right;

}

int main()

{

int a1 = 10, a2 = 20;

double d1 = 10.0, d2 = 20.0;

Add(a1, a2);

Add(d1, d2);

/*

该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型

通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,

编译器无法确定此处到底该将T确定为int 或者 double类型而报错

注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅

Add(a1, d1);

*/

// 此时有两种处理方式:1. 用户自己来强制转化 2. 使用显式实例化

Add(a, (int)d);//强制类型转化

return 0;

}

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

int main(void)

{

int a = 10;

double b = 20.0;

// 显式实例化

Add<int>(a, b);

return 0;

}

我一个学C++的老朋友说这里的隐式类型转化就像是一个人指着东西告诉你,它要什么,而显示类型转化就是直接告诉你。感觉还是蛮形象的。(我另一个朋友说这就像女朋友,隐式类型就是让你猜她想要什么,显示就是直接告诉你。我感觉怪怪的)(我看不如百变怪,显示就是你告诉它变什么,隐式就是你指着一个精灵告诉它变什么。)

如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。  

🚝2.5 模板参数的匹配原则

 ✈️2.5.1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数

// 专门处理int的加法函数

int Add(int left, int right)

{

return left + right;

}

// 通用加法函数

template<class T>

T Add(T left, T right)

{

return left + right;

}

void Test()

{

Add(1, 2); // 与非模板函数匹配,编译器不需要特化

Add<int>(1, 2); // 调用编译器特化的Add版本

}

这里为什么int类型的用的非模板类型呢?因为编译器也懒。就好像是我们家里有一个刚好的能穿的又好看的衣服,还会跑去街上无缘无故多买一个吗?而且编译器人家实例化一个模板也是很累的人也不想动,所以有现成的还是先用现成的没有再去买。

✈️2.5.2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板

// 专门处理int的加法函数

int Add(int left, int right)

{

return left + right;

}

// 通用加法函数

template<class T1, class T2>

T1 Add(T1 left, T2 right)

{

return left + right;

}

void Test()

{

Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化

Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函

}

 这里很明显,现成的并不完全匹配当下的情况,而模板实例化出来的更加好(这里可以看出计算机的严谨性,不能偷的懒坚决不偷)。

✈️2.5.3模板函数不允许自动类型转换,但普通函数可以进行自动类型转换(隐式类型转换)。

 需要注意的是,我们刚才说如果有模板且我们的现成的不满足时候会使用模板去实例化一个,但如果我们实在没有呢?这就有两种情况,一种可隐式类型转化,一种不可以。

int Sum(int a, int b)

{

return a + b;

}

int main()

{

cout<<Sum(2, 2.3);//可以隐式类型转换

cout<<Sum(2, 'a');//可以隐式类型转换//这里'a'转化成ascll码

//Sum(2, a);//报错//这里的a被识别为变量,字符要加'',属于常见错误

cout << Sum(2, "ssdd");//报错

}

 这里的隐式类型转化和我们前面说的差不多就是会先生成一个临时变量,在赋值给前面的。(但是必须要类接近才可以,不接近不可以。)

大致就是这样一个过程 

 🚈3. 类模板

 有了上述的知识再理解类模板就不难了。

🚝3.1 类模板的定义格式 

<code>template<class T1, class T2, ..., class Tn>

class 类模板

{

// 类内成员定义

};

// 动态顺序表

// 注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具

template<class T>

class Vector

{

public :

Vector(size_t capacity = 10)

: _pData(new T[capacity])

, _size(0)

, _capacity(capacity)

{}

// 使用析构函数演示:在类中声明,在类外定义。

~Vector();

void PushBack(const T& data);

void PopBack();

// ...

size_t Size() {return _size;}

T& operator[](size_t pos)

{

assert(pos < _size);

return _pData[pos];

}

private:

T* _pData;

size_t _size;

size_t _capacity;

};

// 注意:类模板中函数放在类外进行定义时,需要加模板参数列表

template <class T>

Vector<T>::~Vector()

{

if(_pData)

delete[] _pData;

_size = _capacity = 0;

}

 这里不需要全看懂,后面我们手撕解剖这里的vector代码,大家只需要大概知道怎么去写类模板就行。

🚝3.2 类模板的实例化 

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

// Vector类名,Vector才是类型

Vectorint> s1; Vectordouble> s2;

也是一样看懂怎么实例化就行。  

💎4.结束语

其实模板的知识还没有全部聊完,因为后面的模板的一些东西其实也是为了解决大家在模板使用中出现的问题,而且主要偏向于类,所以我们会在手撕完STL后和大家继续聊模板剩下的知识。 

好了小赵今天的分享就到这里了,如果大家有什么不明白的地方可以在小赵的下方留言哦,同时如果小赵的博客中有什么地方不对也希望得到大家的指点,谢谢各位家人们的支持。你们的支持是小赵创作的动力,加油。

如果觉得文章对你有帮助的话,还请点赞,关注,收藏支持小赵,如有不足还请指点,方便小赵及时改正,感谢大家支持!!!



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。