在 C++ 编程中,代码复用是提高开发效率的关键。泛型编程作为实现代码复用的重要手段,让我们能够编写与类型无关的通用代码,而模板正是泛型编程的基础。本文将带你深入了解 C++ 中的模板,包括函数模板和类模板的概念、用法及原理。
一、泛型编程与模板
泛型编程的核心思想是编写与类型无关的通用代码,通过这种方式实现代码的复用。而模板则是泛型编程的技术基础,它允许我们定义一种通用的结构,在使用时再指定具体的类型。
模板主要分为两类:
- 函数模板:用于生成通用的函数
- 类模板:用于生成通用的类
二、函数模板
1. 函数模板概念
函数模板代表了一个函数家族,它与具体类型无关,在使用时会根据实参类型被参数化,从而生成该函数的特定类型版本。
2. 函数模板格式
函数模板的定义以template关键字开头,后跟模板参数列表,格式如下:
cpp
template<typename T1, typename T2, ..., typename Tn>
// 函数定义
其中,typename是定义模板参数的关键字,也可以使用class(注意:不能用struct代替class)。
示例:
cpp
template<class T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
template<typename T1, typename T2>
void func(const T1& x, const T2& y)
{
// 函数实现
}
在main函数中使用这些模板函数:
cpp
int main()
{
int i = 1, j = 2;
double m = 1.1, n = 2.2;
Swap(i, j); // 实例化为int类型的Swap函数
Swap(m, n); // 实例化为double类型的Swap函数
func(i, m); // 实例化为T1=int, T2=double的func函数
func(i, j); // 实例化为T1=int, T2=int的func函数
return 0;
}
3. 标准库中的 swap 函数
C++ 标准库中已经实现了swap函数,定义在<utility>头文件中(通常会被其他头文件间接包含),使用时直接调用即可,要求传入的两个参数类型相同:
cpp
int main()
{
int i = 1, j = 2;
double m = 1.1, n = 2.2;
swap(i, j); // 使用标准库的swap函数
return 0;
}
4. 函数模板的原理
函数模板本身并不是一个实际的函数,而是一个蓝图或模具。编译器会根据我们使用函数模板时提供的实参类型,自动生成对应类型的具体函数。这相当于将原本需要我们手动编写多个类型版本函数的工作,交给了编译器来完成。
5. 函数模板的实例化
当我们用不同类型的参数使用函数模板时,就称为函数模板的实例化。模板参数实例化分为两种:
(1)隐式实例化
编译器根据实参的类型自动推导出模板参数的类型:
cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.1, d2 = 20.0;
Add(a1, a2); // 隐式实例化为int类型的Add函数
Add(d1, d2); // 隐式实例化为double类型的Add函数
// 当实参类型不同时,需要进行强制类型转换
cout << Add(a1, (int)d1) << endl; // 将d1强制转换为int
cout << Add((double)a1, d1) << endl; // 将a1强制转换为double
return 0;
}
(2)显式实例化
我们直接指定模板参数的类型,格式为:函数名<类型>(实参)
cpp
int main()
{
int a1 = 10;
double d1 = 10.1;
cout << Add<int>(a1, d1) << endl; // 显式指定为int类型,d1会被隐式转换为int
return 0;
}
必须显式实例化的情况:当模板参数无法通过函数实参推导时,必须进行显式实例化。
cpp
template<class T>
T* func1(int n)
{
return new T[n];
}
int main()
{
// 无法从实参int推导出T的类型,必须显式实例化
double* p1 = func1<double>(2);
return 0;
}
6. 模板参数的匹配原则
- 一个非模板函数可以和一个同名的函数模板同时存在,并且函数模板可以被实例化为这个非模板函数。
- 在调用时,若非模板函数和模板实例化后的函数都能匹配,会优先调用非模板函数。但如果模板能产生一个更匹配的函数,则会选择模板。
cpp
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
// 非模板函数,与上面的函数模板同名
int Add(const int& x, const int& y)
{
return (x + y) * 10;
}
int main()
{
int a1 = 10, a2 = 20;
cout << Add(a1, a2) << endl; // 优先调用非模板函数,输出300
return 0;
}
- 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。
三、类模板
1. 类模板的定义格式
类模板的定义格式如下:
cpp
template <class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
2. 类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟上<>,并将实例化的类型放在<>中。类模板名字本身不是真正的类,实例化的结果才是真正的类。
cpp
// Stack是类模板名,Stack<int>、Stack<double>才是真正的类
Stack<int> st1; // 实例化为int类型的Stack类
Stack<double> st2; // 实例化为double类型的Stack类
3. 类模板示例
下面是一个栈(Stack)的类模板示例:
cpp
template<typename T>
class Stack
{
public:
Stack(int n = 4)
: _array(new T[n])
, _size(0)
, _capacity(n)
{}
~Stack()
{
delete[] _array;
_array = nullptr;
_size = _capacity = 0;
}
void Push(const T& data)
{
// 判断是否需要扩容
if (_size == _capacity)
{
T* tmp = new T[_capacity * 2]; // 扩容为原来的2倍
memcpy(tmp, _array, sizeof(T) * _size); // 拷贝数据
delete[] _array; // 释放旧空间
_array = tmp; // 指向新空间
_capacity *= 2;
}
_array[_size++] = data;
}
private:
T* _array; // 存储数据的数组
size_t _size; // 当前元素个数
size_t _capacity; // 容量
};
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;
}
4. 类模板的声明和定义分离
类模板的声明和定义有一些特殊要求:
- 类模板的声明和定义一般不建议分离到两个文件中(.h 和.cpp),通常放在同一个头文件中。
- 声明和定义处都要写完整的模板参数列表(
template<typename T>)。 - 定义成员函数时,需要通过**
类名<T>::**指明所属的模板类实例。
示例:
cpp
// 类模板声明
template<typename T>
class Stack
{
public:
Stack(int n = 4)
: _array(new T[n])
, _size(0)
, _capacity(n)
{}
~Stack()
{
delete[] _array;
_array = nullptr;
_size = _capacity = 0;
}
// 成员函数声明
void Push(const T& data);
private:
T* _array;
size_t _size;
size_t _capacity;
};
// 成员函数定义
template<typename T>
void Stack<T>::Push(const T& data)
{
if (_size == _capacity)
{
T* tmp = new T[_capacity * 2];
memcpy(tmp, _array, sizeof(T) * _size);
delete[] _array;
_array = tmp;
_capacity *= 2;
}
_array[_size++] = data;
}