【C++】模板(初阶)--- 初步认识模板
作者主页:lightqjx
本文专栏:C++
目录
一、背景
二、函数模板
1. 定义格式
2. 模板函数的原理
3. 模板函数的实例化
(1)隐式实例化
(2)显示实例化
三、类模板
1. 定义格式
2. 类模板的实例化
3. 类中成员函数声明和定义的使用问题
前言
本文是对模板的一些初步认识,没有讲解得很深,但能够让人理解对模板的基本认识和运用,并初步掌握如何使用模板。
一、背景
在C语言中,我们常常会写一个交换函数,即如下代码:
void Swap(int* a, int* b)
{int temp = *a;*a = *b;*b = temp;
}
但是,这样的代码虽然可以解决交换的问题,但只能局限于某一种类型的交换,也就是说,比如上面这个代码只能对int类型的变量进行交换,如果要交换其他类型的变量,就需要改变变量重新写这个函数,如以下代码:
//交换int类型的变量
void Swap(int* a, int* b)
{int temp = *a;*a = *b;*b = temp;
}
//交换double类型的变量
void Swap(double* a, double* b)
{int temp = *a;*a = *b;*b = temp;
}
//交换char类型的变量
void Swap(char* a, char* b)
{int temp = *a;*a = *b;*b = temp;
}
//其他类型同理.....
如果需要交换的类型很多,那么就要写很多这种的函数了,非常繁琐。对于一些对不同类型变量的相同操作,是非常浪费时间的,因此在C++中,就引入了模板,来告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码。
模板常常与泛型编程有关,泛型编程是编写与类型无关的通用代码,来实现代码复用的一种手段。模板是泛型编程的基础。
模板分为函数模板和类模板两种主要形式。
二、函数模板
1. 定义格式
概念:
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
格式:
template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表)
{}
typename 是用来定义模板参数关键字,也可以使用 class (切记:不能使用struct代替class)。其中每次定义的模板参数的作用范围只有其后面紧跟的括号范围之中(每次要定义模板函数,都必须先定义模板参数)。
示例:
#include<iostream>
using namespace std;
template<typename T>//只有一个参数类型
void Swap(T& a, T& b)
{T temp = a;a = b;b = temp;
}template<typename T1, typename T2>//两个参数类型,多个参数依次类推即可
void Print(T1& a, T2& b)
{cout << a << " " << b << endl;
}
2. 模板函数的原理
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于其他类型也是如此。
所以,调用模板函数其实是调用模板函数推到出的函数,而不是直接调用模板本身。如图所示:
3. 模板函数的实例化
函数模板的实例化是指根据具体的类型参数,由编译器生成具体的函数代码的过程。函数模板本身只是一个通用的模板或蓝图,只有在实际调用时,编译器才会根据传入的实参类型,生成对应的特定类型的函数实现。
模板参数实例化分为:隐式实例化和显式实例化。
(1)隐式实例化
隐式实例化 是指 当调用函数模板时,如果未显式指定模板参数类型,编译器会根据传入的实参类型自动推导出模板参数,并生成对应的函数。
#include<iostream>
using namespace std;
template<typename T>//只有一个参数类型
void Swap(T& a, T& b)
{T temp = a;a = b;b = temp;
}
int main()
{int x = 10, y = 20;cout << x << " " << y << endl;Swap(x, y); // 自动根据实参推到出模板参数,并生成对应函数cout << x << " " << y << endl;return 0;
}
但是,如果只有一个模板参数,而传递的不止一个参数类型,若不进行处理编译器就会报错。
#include<iostream>
using namespace std;
template<typename T>
T Add(T& x, T& y)
{return x + y;
}
int main()
{int a1 = 10;double d1 = 10.0;cout << Add(a1, d1) << endl; // 错误return 0;
}
其原因如下:
在模板中,编译器一般不会进行类型转换操作。因为在编译期间,当编译器看到该实例化时,需要推演其实参类型通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,编译器无法确定此处到底该将T确定为int 或者 double类型而报错。
而它的解决办法有两种:1. 用户自己来强制转化; 2. 使用显式实例化(下面会讲)
对于用户自己来强制转化时需要注意临时变量的问题,因为临时变量具有常性,所以函数模板的参数需要加const。如以下代码所示:
#include<iostream>
using namespace std;
template<typename T>
T Add(const T& x, const T& y)
{return x + y;
}
int main()
{int a1 = 10;double d1 = 10.0;cout << Add(a1, (int)d1) << endl; // 用户自己来强制转化return 0;
}
(2)显示实例化
显示实例化 是指 在调用函数模板时,通过在函数名后的 < > 中指定模板参数的实际类型,就可以显式指定模板参数类型,编译器就会根据指定的类型生成对应的函数。
使用代码示例如下:
#include<iostream>
using namespace std;
template<typename T>
T Add(const T& x, const T& y)
{return x + y;
}
int main()
{int a1 = 10;double d1 = 10.0;cout << Add<int>(a1, d1) << endl; // 显示实例化return 0;
}
模板函数的匹配规则:
共存与实例化:非模板函数和同名函数模板可以同时存在,且模板还可实例化为该非模板函数。
调用优先级:在调用时,若非模板函数与模板函数均可行,优先调用非模板函数;模板仅在能生成更匹配函数时被选择。
类型转换限制:模板函数禁止自动类型转换,而普通函数允许。
三、类模板
1. 定义格式
类模板的使用机制和函数模板差不多,只是类模板的作用对象是类罢了。它的定义格式如下:
template<class T1, class T2, ..., class Tn> // 模板参数
class 类模板名
{// 类内成员定义
};
为什么要使用类模板呢?
类模板可以让一个类的结构进行多次使用,可以解决代码重复问题,从而提高代码的开发效率。比如对一个栈类,如果栈只能对一种数据进行存储,要想使用其他类型的栈,就需要重新写这个栈的代码了,不仅代码冗余重复,还很耗时间来写,所以若将栈类实现一个模板,这就可以大大提高代码开发效率了。
代码示例:
template<class T> // 定义一个模板参数
class Stack
{
public:Stack(size_t capacity = 3){_array = new T[capacity];_capacity = capacity;_size = 0;}// 其余操作......~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}private:T* _array; // 可以实现对不同类型存储的栈int _capacity;int _size;
};
2. 类模板的实例化
类模板实例化与函数模板实例化不同,因为类模板通常自己无法自动推导类型,所以一般了模板就只有显示实例化。
类模板实例化的方式是: 在类模板名字后跟<>,然后将实例化的类型放在<>中即可 。
注意:类模板名字不是真正的类,而实例化的结果才是真正的类。
如对于上面这个栈类的模板,其实例化如下:
int main()
{// 实例化出对不同类型存储的栈Stack<int> st1;Stack<double> st2;Stack<char> st3;return 0;
}
这里需要注意:
- 普通类,类名和类型是一样
- 类模板,类名和类型不一样
通过类模板实例化出的对象,它的类型是 类名<类型> 而不是类名。比如:
// Stack是类名,而Stack<int>才是类型
Stack<int> st1;
3. 类中成员函数声明和定义的使用问题
在 C++ 的类模板中,当函数声明和定义分离时,不仅需要再次定义模板参数,函数定义还需要同时指定类名(模板名)和模板参数,才能限定类域。
在类模板中,当声明和定义在一起时:
template<typename T>
class MyClass
{
public:// 声明和定义在一起void Init(int capacity = 4){_arr = new T[capacity];}private:T* _arr;
};
而在类模板中,当声明和定义分离时,其正确的写法如下:
template<typename T>
class MyClass
{
public:// 声明和定义分离void Init(int capacity = 4);private:T* _arr;
};template<typename T> // 需要再次定义上面的模板参数
void MyClass<T>::Init(int capacity) //同时指定类名(模板名)和模板参数
{_arr = new T[capacity];
}
由于模板参数的作用范围只有其后面的一个括号内,所以就需要重新再定义一下。还需要同时指定类名(模板名)和模板参数来确保函数定义与正确的类模板绑定
感谢各位观看!希望能多多支持!