【C++】模板初阶
文章目录
- 一. 泛型编程
- 1.1 什么是模板
- 1.2 为什么要使用模板
- 二. 函数模板
- 2.1 函数模板概念
- 2.2 函数模板格式
- 2.3 函数模板的原理
- 2.4 函数模板的实例化
- 2.4.1 隐式实例化
- 2.4.2 显式实例化
- 2.5 模板参数的匹配原则
- 三. 类模板
- 3.1 类模板的定义格式
- 3.2 类模板的实例化
- 3.3 在类模板外部定义成员函数
- END
一. 泛型编程
之前我们要实现一个交换两个值的函数就需要根据参数的类型去一一实现不同的交换函数,这种方法叫作函数重载。
#include<iostream>
using namespace std;
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;
}
int main()
{
int a = 1, b = 3;
Swap(a, b);
cout << "a = " << a << " b = " << b << endl;
double d1 = 0.12, d2 = 0.21;
Swap(d1, d2);
cout << "d1 = " << d1 << " d2 = " << d2 << endl;
char c1 = 'a', c2 = 'b';
Swap(c1, c2);
cout << "c1 = " << c1 << " c2 = " << c2 << endl;
return 0;
}

使用函数重载虽然可以实现,但是有几个不好的地方:
1.重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数。
2.代码的可维护性比较低,一个出错可能所有的重载均出错。
那能否给编译器一个模板,让编译器根据不同的类型利用该模板来生成对应的代码呢?
答案是有的,下面来介绍一下C++模板
1.1 什么是模板
首先来介绍一下什么是泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是C++中一种对”泛型编程“的实现方式,同时模板也是泛型编程的基础。通过模板,你可以定义一个函数或者类,而不必在定义时指定具体的类型。只有当你使用这个函数或者类时,才需要指定具体的类型。模板分为两种:

1.2 为什么要使用模板
- 复用性:模板允许你在不同的类型上使用相同的代码,而无需重复编写类似的代码。
- 可读性:模板代码具有更高的抽象性,使得代码更易于理解和维护。
- 类型安全:模板可以在编译时进行类型检查,避免运行时的类型错误。
二. 函数模板
2.1 函数模板概念
函数模板是一种通用的函数框架,它使用模板参数来表示数据类型或其他参数,使得函数可以适应多种不同的数据类型,而无需为每种类型编写独立的函数定义。通过函数模板,能够以一种抽象的方式编写函数,在调用时根据实际传入的参数类型来自动生成特定类型的函数版本。
2.2 函数模板格式
template<typename T1, typename T2,…,typename Tn>
返回值类型 函数名(参数列表){ }
函数模板的一般语法如下:
template <typename T>
返回类型 函数名(参数列表) {
// 函数实现
}
其中:
- template < typename T>:声明模板的开头,typename表示模板参数是一个类型,T是模板参数的占位符。
- 返回类型:可以是模板参数T或其他类型。
- 参数列表:可以包含模板参数T或者其他类型。
案列:
#include<iostream>
using namespace std;
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
int main()
{
int a = 1, b = 3;
Swap(a, b);
cout << "a = " << a << " b = " << b << endl;
double d1 = 11.2, d2 = 12.1;
Swap(d1, d2);
cout << "d1 = " << d1 << " d2 = " << d2 << endl;
char c1 = 'a', c2 = 'b';
Swap(c1, c2);
cout << "c1 = " << c1 << " c2 = " << c2 << endl;
return 0;
}

注意: typename是用来定义模板参数的关键字,也可以使用class(切记:不能使用struct代替class)
2.3 函数模板的原理
函数模板的原理是通过在编译时生成特定类型的函数来实现的。函数模板本身并不是一个可以直接调用的函数,而是一个用于创建函数的模板。当调用函数模板时,编译器会根据传入的参数类型实例化模板,生成一个具体的函数版本。
#include<iostream>
using namespace std;
template<typename T>
void Swap(T& left, T& right)
{
T temp = left;
left = right;
right = temp;
}
int main()
{
int a = 1, b = 3;
double d1 = 11.2, d2 = 12.1;
char c1 = 'a', c2 = 'b';
Swap(a, b);
Swap(d1, d2);
Swap(c1, c2);
return 0;
}
从图中可以看出编译器根据传入参数的类型生成不同的函数。
在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
2.4 函数模板的实例化
用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
2.4.1 隐式实例化
隐式实例化:让编译器根据实参推演出模板参数的实际类型。
#include<iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 3, a2 = 5;
double d1 = 10.1, d2 = 12.2;
cout << Add(a1, a2) << endl; //将模板参数T推演为int类型
cout << Add(d1, d2) << endl; //将模板参数T推演为double类型
return 0;
}
上述例子调用了两个Add函数中,传入的两个实参类型都是相同的,如果换成不同的会发生什么呢?
该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演出实参类型,通过实参a1将T推演为int类型,通过实参d1将T推演为double类型,但模板参数列表中只有一个T,编译器无法确定此处到底该将T确定为int还是double类型而报错。
注意: 在模板中,编译器一般不会进行类型转换操作,因为一旦转化出现问题,编译器就需要背黑锅。所以上述例子中,编译器只能将a1推演为int类型,将d1推演为double类型,不能因为模板参数列表中只有一个T就将d1转化为int类型或将a1转化为double类型。
要解决这个问题有两种方法:
- 用户自己来强制转化。
- 使用显式实例化。
#include<iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 3, a2 = 5;
double d1 = 10.1, d2 = 12.2;
Add(a1, int(d1)); //强制类型转换
Add<int>(a1, d1); //模板参数显式实例化,下面会讲到
return 0;
}
2.4.2 显式实例化
在函数名后的<>中指定模板参数的实际类型。
#include<iostream>
using namespace std;
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 3, a2 = 5;
double d1 = 10.1, d2 = 12.2;
//显式实例化
cout << Add<int>(a1, d1) << endl;
cout << Add<int>(d1, d2) << endl;
cout << Add<double>(a2, d2) << endl;
cout << Add<double>(a1, a2) << endl;
return 0;
}
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功则编译器就会报错。
显式实例化声明语句的语法格式:
template 函数返回值类型 函数名<实例化的类型>(参数列表);
例如:
template int Add<int>(const int& left, const int& right);
注意:这是个声明语句。
2.5 模板参数的匹配原则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。
#include<iostream>
using namespace std;
// 专门处理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版本
}
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数,那么将选择模板。
#include<iostream>
using namespace std;
// 专门处理int的加法函数
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.0); //模板函数可以生成更加匹配的版本,编译器根据实参类型生成更加匹配的Add函数
return 0;
}
通过反汇编可以印证以上的观点:
- 模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。
- 函数模板中的每一个类型参数在函数参数列表中必须至少使用一次。
例如:下面函数模板的声明是不正确的,函数模板中声明了两个模板参数T1和T2,但在使用时却只使用了T1,没有使用T2。
- 模板参数名在同一模板参数列表中只能使用一次,但可在多个不同函数模板的定义或声明中重复使用。
template<class T,class T> //错误,在同一函数模板中重复定义模板参数
#include<iostream>
using namespace std;
template<class T>
void Test1(T t)
{
cout << t << endl;
}
template<class T> //在不同函数模板中可以使用相同的模板参数名
void Test2(T t)
{
cout << t << endl;
}
int main()
{
Test1(3);
Test2(5);
return 0;
}
- 模板的声明和定义所使用的模板参数名可以不相同。
#include<iostream>
using namespace std;
//函数模板的声明
template<class T>
void Test(T t);
//函数模板的定义
template<class U>
void Test(U t)
{
cout << t << endl;
}
int main()
{
Test(10);
return 0;
}
- 函数模板如果有多个模板参数,则每个模板参数前都必须使用关键字typename或class修饰。
template<typename T,class U> //两个关键字可以混用
void Test1(T t, U u){}
template<typename T,U> //错误,每个模板参数前都要有关键字typename或class修饰
void Test2(T t,U u){}
三. 类模板
类模板也是模板的一种。类模板允许程序根据不同的类型参数生成不同的类。
3.1 类模板的定义格式
类模板的声明通常使用template关键字,后跟模板类型参数列表。
template<class T1, class T2, ..., class Tn>
class 类模板名
{
// 类内成员定义
};
例如:
template<class T>
class A {
public:
T a; //成员变量
T b; //成员变量
T Add(T a, T b); //成员函数的声明
};
由于类模板包含类型参数,因此也称为参数化类,如果说类是对象的抽象,对象是 类的实例,则类模板是类的抽象,类是类模板的实例。
3.2 类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
A<int> a; //int类型
A<double> b; //double类型
说明:
#include<iostream>
using namespace std;
template<class T>
class A {
public:
T a; //成员变量
T b; //成员变量
T Add(T a, T b)
{
return a + b;
}
};
int main()
{
A<int> a;
a.Add(1, 1.2);
//此处不报错,因为在创建对象a时已经指定T是int类型,
//当Add()函数模板实例化时也会实例化出一个int类型的函数,它会自动将double类型的1.2转换为int类型的1。
return 0;
}
- 当类模板有两个模板形参时,实例化类模板时,类型之间要用逗号分隔开。
#include<iostream>
using namespace std;
template<class T1,class T2>
class A {
public:
T1 a; //成员变量
T2 b; //成员变量
T1 Add(T1 a, T2 b)
{
return a + b;
}
};
int main()
{
A<int, int> a; //创建模板类的一个对象a
A<int, double> b; //创建模板类的一个对象b
return 0;
}
- 使用new操作符创建对象的方式实例化。
A<int, int>* a = new A<int, int>;
A<int, double>* b = new A<int, double>;
注意:赋值号两边都要指明具体的数据类型,且要保持一致。
- 模板的声明或定义只能在全局、命名空间或类范围内进行,不能在局部、函数 范围内进行,比如不能在main()函数中声明或定义一个模板。

3.3 在类模板外部定义成员函数
类中的成员函数既可以在类中定义,也可以在类外定义。类模板中的成员函数同样 可以在类模板内定义,也可以在类模板外定义,只是在类外定义成员函数时需要带上模板头:template<模板参数表>。
在类模板外部定义成员函数的方法:
template<模板形参表>
函数返回类型 类名<模板形参名>::函数名(参数列表){}
例如:
#include<iostream>
using namespace std;
template<class T>
class A {
public:
T a;
T b;
T Add(T a, T b);
};
//在类模板外部定义成员函数
template<class T>
T A<T>::Add(T a, T b)
{
return a + b;
}
int main()
{
A<int> a;
cout << a.Add(1, 2);
return 0;
}
这里使用类模板简单实现一下栈这个数据结构。
#include<iostream>
using namespace std;
//类模板
template<class T>
class Stack {
public:
Stack(size_t capacity = 4)
{
_array = new T[capacity];
_capacity = capacity;
_size = 0;
}
~Stack()
{
delete[] _array;
_array = nullptr;
_capacity = _size = 0;
}
void Push(const T& data);
void Pop();
bool empty()
{
return _size == 0;
}
private:
T* _array;
size_t _capacity;
size_t _size;
};
template<class T>
void Stack<T>::Push(const T& data)
{
if (_size >= _capacity)
{
T* tmp = (T*)realloc(_array, sizeof(T) * _capacity * 2);
if (tmp == nullptr) {
perror("扩容失败");
exit(1);
}
_array = tmp;
_capacity *= 2;
}
_array[_size++] = data;
}
template<class T>
void Stack<T>::Pop()
{
if (_size)
{
_size--;
}
}
int main()
{
// Stack是类名,Stack<int>才是类型
Stack<int> st1; //int
Stack<double> st2; //double
return 0;
}
注意:类模板在实例化时,带有模板形参的成员函数并不随着自动被实例化,只有当它被调用或取地址时,才被实例化。
下面来调试一下:
通过实例化类模板来创建对象st1。
通过调试,编译器调用了构造函数来进行初始化st1,但是没有实例化带有模板参数的成员函数。
随后开始实例化类模板来创建对象st2。
END
对以上内容有异议或者需要补充的,欢迎大家来讨论!