目录
一、泛型编程
泛型编程是一种基于模板机制的编程风格,它允许对数据类型进行抽象描述,并在编译时生成类型相关的代码。
想要实现一个通用的交换函数swap()
//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; } //...
可以看到,不同类型的swap函数结构相同,只有函数类型不同,虽然函数重载能够完成工作,但是仍有一些不太好的地方。
- 重复的写这样的函数会造成代码冗余,也会使代码编写者重复工作。
- 有几个类型就要写几次,当增加新类型时,用户还要相应的再写一个或几个函数。
- 代码容错率低,一个出错可能造成所有的重载都出错。
于是C++引入了基于模板机制的泛型编程,即给编译器一个模板,让编译器根据不同的类型利用该模板生成相应的代码。
泛型编程的核心思想是将算法和数据结构与具体的数据类型分离,即在实现算法和数据结构时不指定具体的数据类型,而是用类型参数来描述数据类型,在使用时通过具体的数据类型实例化模板,从而生成类型相关的代码。
总之,C++泛型编程是一种强大的编程技术,可以通过模板机制实现代码重用、拥有更高的灵活性和可扩展性,同时也是C++语言的重要特性之一。
C++中的泛型编程主要通过两种方式实现:函数模板和类模板。
二、函数模板
函数模板是一种通用的函数形式,允许定义一个通用的函数,在使用时可以根据参数类型自动推导出模板参数类型,并生成相应的函数实例。
2.1 函数模板格式
template <typename T>
返回类型 函数名(参数列表) {
// 函数体
}
其中,
- template 是定义模板的关键字。
- typename 是定义模板参数的关键字,告诉编译器后面的标识符是一个类型参数,typename 也可以写成class,不可以写成struct。
- T 是类型参数的名称,可以根据需要进行自定义。
- 返回类型 是函数的返回类型,可以是任意合法的C++类型。
- 函数名 是函数的名称,自定义命名。
- 参数列表 是函数的参数列表,可以包含任意数量和类型的参数。
- 模板参数可以有多个。例如:template <typename T1, typename T2>
template <typename T>
//template <class T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = tmp;
}
2.2 函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
2.2.1 隐式实例化
隐式实例化:编译器根据实参的类型推演模板参数的类型,并生成相应的函数实例。
void Test1()
{
int a = 10;
int b = 20;
double d1 = 30.3;
double d2 = 40.4;
char c1 = 'x';
char c2 = 'y';
Swap(a, b);
cout << "a = " << a << ", b = " << b << endl;
Swap(d1, d2);
cout << "d1 = " << d1 << ", d2 = " << d2 << endl;
Swap(c1, c2);
cout << "c1 = " << c1 << ", c2 = " << c2 << endl;
//Swap(a, d1);
}
注意:
Swap(a, d1);该语句不能通过编译。
因为在编译期间,当编译器看到该实例化时,需要推演其实参类型。通过实参a将T推演为int类型,通过实参d1将T推演为double类型,但模板参数列表中只有一个T, 编译器无法确定此处到底该将T确定为int 或者 double类型而报错。
在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅
此时有两种处理方式:1. 用户自己来强制类型转换(部分函数适用) 2. 使用显式实例化
强制类型转换(部分函数适用):
Swap(a, (int)d1);
此处不能编译通过,因为转换后是临时变量,具有常性,形参要用const修饰,但是修饰后又不能更改对应。
下面这个函数模板就可以使用强制类型转换:
template <typename T> T Add(const T& left, const T& right) { return left + right; }
void Test2() { int a = 10; double b = 11.0; cout << Add(a, (int)b) << endl;//强制类型转换 cout << Add<int>(a, b) << endl;//显式实例化 }
结果都是21。
2.2.2显式实例化
显式实例化:在函数名后的<>中指定模板参数的实际类型
例如上面Test2中的 cout << Add<int>(a, b) << endl; 就是显式实例化
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
三、类模板
类模板是一种通用的类形式,可以根据实参的数据类型生成对应的类实例。
3.1 类模板格式
template <typename T>
class 类名 {
private:
// 私有成员public:
// 公有成员
};
其中,
- template 是定义模板的关键字。
- typename 是定义模板参数的关键字,告诉编译器后面的标识符是一个类型参数,typename 也可以写成class,不可以写成struct。
- T 是类型参数的名称,可以根据需要进行自定义。
- 类名 是类的名称,自定义命名。
- 在类模板中,可以定义私有成员和公有成员,具体内容根据需要自由定义。
- 模板参数可以有多个。例如:template <typename T1, typename T2>
需要注意的是,函数模板和类模板的参数可以是类型参数、非类型参数或模板类型参数,根据实际需求进行选择。
template <typename T>
class Stack
{
public:
Stack(size_t cp = 4)
: _arr(new T[(cp == 0 ? 4 : cp)])
, _top(0)
, _capacity(cp == 0 ? 4 : cp)
{}
~Stack()
{
delete[] _arr;
_arr = nullptr;
_top = 0;
_capacity = 0;
}
void Inspect()
{
size_t cp = _capacity * 2;
if (_top == _capacity)
{
T* temp = new T[cp];
for (size_t i = 0; i < _top; i++)
{
temp[i] = _arr[i];
}
delete _arr;
_arr = temp;
_capacity = cp;
}
}
void Push(T val)
{
Inspect();
_arr[_top] = val;
_top++;
}
private:
T* _arr;
size_t _top;
size_t _capacity;
};
3.2 类模板的实例化
在实例化类模板时,将会根据模板参数推断出具体的类型,并生成相应的类实例。实例化后的类可以使用类模板中定义的成员函数和成员变量。
void Test3()
{
Stack<int> s1;//类模板的实例化
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
s1.Push(5);
}
四、模板实现的原理
4.1 模板的原理
模板的原理是基于编译期间的代码生成,在编译时进行模板的解析和实例化,将模板参数替换成实际的类型或值,并生成对应的函数或类。
当编译器遇到模板定义时,并不生成实际的函数或类代码,而是在调用或实例化时才会根据需要生成对应的代码。
编译器会根据模板的使用情况,在编译时进行两个阶段的处理:
- 模板定义实例化阶段:编译器会根据模板定义和使用情况,生成对应的函数或类模板实例的代码,这些代码在编译时并不会立即执行,而是生成待用的模板实例。
- 模板实例化调用阶段:在实际调用或实例化模板时,编译器根据具体的模板参数替换模板中的参数,并将模板实例代码插入到调用或实例化的位置,生成最终的可执行代码。
通过函数调用的地址可以看到,三个函数对应三个地址,说明模板已经被实例化。
4.2 模板参数的匹配原则
模板参数的匹配原则:
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模 板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
- 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。