【C++】:模板初阶—函数模板|类模板
白乐天_ξ( ✿>◡❛) 2024-10-05 14:05:03 阅读 71
✨ Blog’s 主页: 白乐天_ξ( ✿>◡❛)
🌈 个人Motto:他强任他强,清风拂山岗!
💫 欢迎来到我的学习笔记!
本文参考博客:一同感受C++模版的所带来的魅力
一、泛型编程思想
首先我们来实现一个<code>swap交换函数。如果学过了C++的函数重载和引用的话,就可以写出swap函数不同参数类型的重载函数。(注意:C语言中不支持重名函数的!)
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
根据上面的代码,我们就可以感觉到,swap交换函数仅仅只是实现一个交换功能,却需要根据参数的类型写出几个很相似的函数,重复相同或者相近的代码。如果又增加了其他类型数据需要进行交换呢?继续重复相同的操作再去实现这个函数吗?
这样操作虽然可行,但是它存在几处缺陷:
重载的函数仅仅只是类型不同,代码复用吕比较低,只要有新的类型出现,就需要自己再次实现所需类型的相近函数。代码的可维护性比较低,一个出错可能所有的重载均出错。
那么既然重载函数的重复率比较高,能否只给出一个标准,然后让他们自己根据这个标准去实现所需要的东西呢?
这个标准就像做月饼用的模具一样:我们只需要放入材料,就可以利用模具做出形状相同的月饼。
后来C++就生成一个类似于模具的东西,将其交给编译器,让编译器根据这个模具自行生成所需代码。这就是泛型编程思想:编写与类型无关的通用代码,是代码复用的一种手段。模板,是泛型编程的基础。
二、函数模板–函数的模板
2.1 概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
通过函数模板,可以编写一种通用的函数定义,使其能够是用于多种数据类型,从而提高代码的服用行和灵活性。
2.2 格式
声明一个函数模板需要关键字<code>template,<>
内部是模板参数,可以使用class
或者typename
来进行类型的声明(不能用struct
)。然后开始使用模板参数Tn,
template<typename T1,typename T2,typename T3,……,typename Tn>
返回值类型 函数名(参数列表)
{ }
注意:这里的函数模板的参数和普通的函数参数不一样。函数模板参数定义的是<u>类型</u>
,而普通函数参数定义的是<u>对象</u>
。
返回值类型 函数名(参数列表){ }
在了解上面的函数模板后,我们就可以为Swap()
函数写一个通用的函数模板了。
//模板类型
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.1, d2 = 2.2;
cout << "a1:" << a1 << " a2:" << a2 << endl;
cout << "d1:" << d1 << " d2:" << d2 << endl;
Swap(a1, a2);
swap(d1, d2);
cout << "a1:" << a1 << " a2:" << a2 << endl;
cout << "d1:" << d1 << " d2:" << d2 << endl;
return 0;
}
运行结果:
调试就可以发现:<code>Swap函数的函数模板可以自己推导传入参数的类型。
那如果给是Swap
函数传入不同的参数类型呢?
//模板类型
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
template<class T1,classT2>
void func(const T1& x,const T2& y)//不同的类类型
{ }
int main()
{
int a1 = 10, a2 = 20;
double d1 = 1.1, d2 = 2.2;
//传入不同的类型:
Swap(a1, d1); //error: message : “void Swap(T &,T &)”: 模板 参数“T”不明确
// message : 可能是“double”或 “int”
//message: “void Swap(T&, T&)” : 无法从“double”推导出“T & ”的 模板 参数
func(a1, d1); //right
return 0;
}
message : “void Swap(T &,T &)”: 无法从“double”推导出“T &”的 模板参数
那为什么func()
函数传入不同的类型的参数却没有问题呢?
原因:传入<code>func()函数的参数并没有用来做编译器无法推导参数类型的操作。Swap()
函数是因为编译器无法推导出参数类型。(一个T类型,结果参数是两个类型) 其实在C++中早就已经定义好了swap()
这个函数,头文件<utility>
,我们可以直接使用。
2.3 原理
2.4 实例化
用不同类型的参数使用函数模板,称为函数模板的实例化。现在我们利用下面这个函数模板来理解实例化概念。
template<class T>//一个模板参数T
T Add(const T& left, const T& right)
{
return left + right;
}
2.4.1 隐式实例化
概念:
隐式实例化(推导实例化):让编译器根据实参推演模板参数的实际类型,再返回不同类型的数据。
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
//推导实例化:实参传递给形参,编译器推导出T的类型
cout << Add(a1, a2) << endl;
cout << Add(d1, d1) << endl;
return 0;
}
运行结果:
缺陷:传入参数类型不同,编译器左右为难、骑虎难下。
<code>int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.2;
cout << Add(a1, d1) << endl;
//message : “T Add(const T &,const T &)”: 无法从“double”推导出“const T &”的 模板 参数
return 0;
}
①d1
强制类型转换为int
类型,解决类型冲突问题。
cout << Add(a1, (int)d1) << endl;
②a1
强制类型转换为double
类型,解决类型冲突问题。
cout << Add((double)a1, d1) << endl;
③ 如果非要传入不同类型的参数,就应该用不同的模板参数重新定义一个函数模板。
template<class T1& left,class T2& right>
T1 Add(const T1& left,const T2& right)//两个模板参数
{
return left + right;
}
2.4.2 显示实例化
概念
显示实例化:在函数名后面紧跟<>
并在其中指定模板参数的实际类型。
//显示实例化:用指定类型来实例化
cout << Add<int>(a1, d1) << endl;
cout << Add<double>(a1, d1) << endl;
缺陷:其实在某些场景下面我们只能选择显示实例化。
形参部分不是模板参数,而是普通的自定义类型,返回值才是。此时我们无法通过传参来指定这个T
的类型,只有外部在调用这个模板时显示指定。
template<class T>
T* Alloc(int n)
{
return new T[n];
}
// 有些函数无法自动推,只能显示实例化
int main()
{
// 有些函数无法自动推,只能显示实例化
double* p1 = Alloc<double>(10);
float* p1 = Alloc<float>(20);
int* p2 = Alloc<int>(30);
return 0;
}
2.5 模板参数的匹配原则
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
//普通函数
int Add(int left, int right)
{
return left + right;
}
//函数模板
template<class T>
T Add(T left, T right)
{
return left + right;
}
int main()
{
Add(1, 2);//调用了普通传参的函数
Add<int>(1, 2);//调用了模板函数让其实生成对应的函数
return 0;
}
根据调试可以发现:普通函数和函数模板是可以共存的。在进行普通传参时调用的是普通函数;显示指定了类型时,就会调用函数模板生成对应的函数。
对于非模板函数和同名函数模板,如果其他条件相同,在调用时会优先调用非模板函数而不会从该模板产生一个实例。如果模板可以产生一个更好匹配的函数,那么将会选择模板。
// 普通函数
int Add(int left, int right)
{
return left + right;
}
// 函数模板
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
int main()
{
Add(1, 2);//调用普通函数,因为参数匹配
Add(1, 2.2);///调用函数模板,因为参数不匹配,普通函数不接收此参数。
//函数模板可以根据这个类型自动推导
return 0;
}
模板函数不允许自动类型转换,但是普通函数可以进行自动类型转换。
普通函数传入的参数可以发生隐式类型转换,又称【自动类型转换】,下面代码中浮点数自动类型转换为整型数。
// 普通函数,允许自动类型转换
void print(int value)
{
std::cout << "Integer: " << value << std::endl;
}
int main()
{
print(3);
print(3.14); // 在这里发生了隐式类型转换,浮点数自动类型转换为整型数
return 0;
}
对于函数模板来说是不能进行自动类型转换的。a和b是两个不同的类型,并没有发生自动类型转换,一个模板参数T就使得编译器无法进行自动推导。解决办法就是上面提到的隐式/显示类型转换
。第三种方法就是增加模板参数。
template <class T>
void print(T a, T b) {
cout << a << " " << b << endl;
}
int main()
{
int a = 1;
double b = 1.11;
print(a, b);//error---message : “void print(T,T)”: 模板 参数“T”不明确
return 0;
}
三、类模板–类的模板
3.1 类模板的定义格式
函数模板是加在函数上,类模板是加在类上。
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
类模板都是显示实例化。
template<class T>
class Stack
{
public:
Stack(int n = 4)//写构造
:_array(new T[n])//不用检查是否失败,失败了直接抛异常
,_size(0)
,_capacity(n)
{
}
~Stack()
{
delete[] _array;
_size = _capacity = 0;
_array = nullptr;
}
void Push(const T& x)//传引用传参,避免多余拷贝浪费资源,能用引用就尽量使用引用
{
//空间不够用:扩容
if (_size == _capacity)
{
//不能使用realloc,无构造
//C++无自动扩容的概念,扩容需要手动
T* temp = new T[_capacity * 2];//扩容两倍,size不变
memcpy(temp, _array, sizeof(T) * _size);//拷贝数据
delete[] _array;//delete一次即可:释放旧空间
_array = temp;//指向新空间
_capacity *= 2;
}
_array[_size++] = x;
}
//其他功能…………
private:
T* _array;
size_t _capacity;
size_t _size;
};
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;
}
我们在C语言中学习栈的时候就有一个typedef
的操作,功能类似于这里的模板。既然如此,那类模板的意义是什么呢?
C语言中的栈只能实现某一种类型,而C++可以一个栈存入int类型,另一个栈存入double类型。 就像在这里一样,实现的栈的结构基本都是一样的,不一样的只是存入栈的数据类型是不一样的。 如果在这里进行声明和定义的分离?
定义的函数模板只能给当前的函数或者当前的类使用,每个函数模板都需要定义自己的模板参数,按需定义。当进行声明和定义分离时,需要在函数前面单独声明类模板,并指定类域。下面就是类模板中的成员函数在类外面实现所需要变化成的模板函数。
<code>template<class T>
class Stack
{
public:
Stack(int n = 4)
:_array(new T[n])
, _size(0)
, _capacity(n)
{
}
~Stack()
{
delete[] _array;
_size = _capacity = 0;
_array = nullptr;
}
void Push(const T& x);
//其他功能……
private:
T* _array;
size_t _capacity;
size_t _size;
};
//指定类域不行,还需要声明一下类模板
template<class T>
void Stack<T>::Push(const T& x)//传引用传参,避免多余拷贝浪费资源,能用引用就尽量使用引用
{
//空间不够用:扩容
if (_size == _capacity)
{
T* temp = new T[_capacity * 2];
memcpy(temp, _array, sizeof(T) * _size);
delete[] _array;
_array = temp;
_capacity *= 2;
}
_array[_size++] = x;
}
如果我们将上面分离后新声明的T改为X,能否编译?
能的。T只是一个符号而已,我们实际上在调用这个函数时,并没有T或者X的概念。 注意:模板不支持声明和定义分离到两个不同文件中去。(即使可以,也相当繁琐。)
3.2 类模板的实例化
类模板实例化和函数模板实例化不同。类模板实例化需要在类模板名称后面跟<code><>。然后将实例化的类型放在<>
中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
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;
}
以上就是本文所要介绍的所有类容,感谢您的阅读!🌹
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。