模板(初阶)
1. 泛型编程
如何实现一个通用的交换函数呢?
函数重载
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;
}
1. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增 加对应的函数2. 代码的可维护性比较低,一个出错可能所有的重载均出错
那能否告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码呢?
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。
2. 函数模板
2.1 函数模板概念
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。
2.1 函数模板格式
template<typename T1, typename T2, ......, typename Tn>
返回值类型 函数名(参数列表) {}
typename
是用来定义模板参数的关键字,也可以使用class
,但不能使用struct
代替class
。
typename
和class
都用于指定模板参数的类型,但在定义模板时,这两个关键字没有实质性的差别。两者是等效的,只是使用习惯问题。
2.3 函数模板的原理
- 这个过程称为“模板实例化”,即模板根据具体的类型生成对应的函数版本。
- 编译器在实例化模板时会进行类型推导(如果没有显式提供模板参数),这使得模板函数能够根据实际的类型自动生成代码,减少了代码重复。
- 模板实例化会在编译时发生,所以我们通常在调用模板函数时就能得到针对特定类型的代码。
2.4 函数模板的实例化
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main()
{
int a1 = 10, a2 = 20;
double d1 = 10.0, d2 = 20.0;
// 隐式实例化,编译器根据传入的参数类型自动推导模板参数T
Add(a1, a2); // T推导为int
Add(d1, d2); // T推导为double
/*
该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有
一个T,编译器无法确定此处到底该将T确定为int还是double类型而报错。
注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要
背黑锅。
*/
// Add(a1, d1); // 错误,不能隐式转换不同类型的参数
// 此时有两种处理方式:
// 1. 用户自己来强制转化
Add(a1, static_cast<int>(d1)); // 手动强制转换d1为int类型
// 2. 使用显式实例化
// Add<int>(a1, d1); // 显式指定模板参数为int
return 0;
}
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int main(void)
{
int a = 10;
double b = 20.0;
// 显式实例化,将模板参数指定为int类型
Add<int>(a, b); // 会将double类型的b转换为int类型,导致精度丢失
return 0;
}
隐式实例化:当我们调用模板函数时,编译器根据传入的实参类型自动推导出模板参数的类型。例如,
Add(a1, a2)
会将T
推导为int
,Add(d1, d2)
会将T
推导为double
。如果传入的实参类型不一致,编译器将无法推导出唯一的模板参数类型,从而导致编译错误。强制转换:为了避免类型不一致的问题,用户可以通过类型转换(如
static_cast<int>(d1)
)来确保模板参数类型一致。显式实例化:我们也可以在调用模板时显式指定模板参数类型,例如
Add<int>(a1, d1)
,这样可以明确告诉编译器该模板参数应该是int
类型
2.5 模板参数的匹配原则
// 专门处理 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); // 显式指定模板类型为 int,调用模板版本 Add<int>
}
// 专门处理 int 类型的加法函数
int Add(int left, int right)
{
return left + right;
}
// 通用加法函数模板,支持不同类型的参数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
return left + right;
}
void Test()
{
Add(1, 2); // 与非模板函数类型完全匹配,编译器调用非模板版本
Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参类型推导生成 Add<int, double>
}
3. 类模板
类模板是 C++ 中的一种模板类型,允许你编写可以操作多种数据类型的类。通过类模板,你可以定义一个类的模板,在实例化时指定具体的数据类型,从而实现代码的复用。类模板允许我们将数据类型作为参数传递,使得同一个类可以操作不同的数据类型。
基本语法:
template <typename T>
class MyClass {
private:
T data; // 用模板类型 T 定义成员变量
public:
MyClass(T val) : data(val) {} // 构造函数使用模板类型 T
T getData() {
return data; // 返回模板类型 T 的成员
}
void setData(T val) {
data = val; // 设置模板类型 T 的成员
}
};
template <typename T>
:声明模板类的语法。T
是一个占位符,可以代表任何数据类型。在类定义中,T
被当作一个数据类型来使用。MyClass
是类模板的名字。- 类中的成员(例如
data
)使用模板类型T
,这意味着data
的类型在实例化时由具体类型来决定。
实例化类模板:
你可以通过给模板指定具体的类型来实例化类模板。例如:
MyClass<int> intObject(10); // 实例化为 MyClass<int>
MyClass<double> doubleObject(3.14); // 实例化为 MyClass<double>
// Stack是类名,Stack<int>才是类型
Stack<int> st1; // int
Stack<double> st2; // double
或者来个完整点的例子
#include <iostream>
using namespace std;
template <typename T> // 类模板声明
class Box {
private:
T value;
public:
Box(T val) : value(val) {} // 构造函数初始化模板类型的成员
T getValue() const {
return value;
}
void setValue(T val) {
value = val;
}
};
int main() {
Box<int> intBox(10); // 创建一个存储 int 类型的 Box
Box<double> doubleBox(3.14); // 创建一个存储 double 类型的 Box
cout << "intBox value: " << intBox.getValue() << endl;
cout << "doubleBox value: " << doubleBox.getValue() << endl;
intBox.setValue(20); // 设置新的值
doubleBox.setValue(6.28);
cout << "Updated intBox value: " << intBox.getValue() << endl;
cout << "Updated doubleBox value: " << doubleBox.getValue() << endl;
return 0;
}
模板类型:
Box<int>
是将int
类型传递给模板类Box
,所以value
变量类型为int
。Box<double>
是将double
类型传递给模板类Box
,所以value
变量类型为double
。灵活性:通过类模板,你可以编写一个类,处理不同的数据类型,而不需要为每种类型写重复的代码。比如,你不需要为
int
和double
分别写不同的Box
类。
类模板的特点:
-
类型参数:类模板通过
template <typename T>
或template <class T>
定义类型参数T
,用于表示类中成员的类型。T
是占位符,可以在实例化时替换为任何数据类型。 -
实例化:类模板是不能直接实例化的,你必须指定一个具体的类型来实例化模板。例如:
Box<int>
、Box<double>
。 -
多个类型参数:类模板可以有多个类型参数。例如:
template <typename T1, typename T2> class Pair { private: T1 first; T2 second; public: Pair(T1 a, T2 b) : first(a), second(b) {} void print() { cout << "First: " << first << ", Second: " << second << endl; } };
你可以通过
Pair<int, double>
来实例化Pair
类,int
和double
是两个不同的类型参数。 -
成员函数模板:类模板中的成员函数也可以是模板的。即成员函数的返回类型和参数类型都可以是模板类型。例如:
template <typename T> class MyClass { private: T data; public: MyClass(T val) : data(val) {} template <typename U> U multiply(U other) { return static_cast<U>(data) * other; } };
-
默认类型参数:你还可以为类模板提供默认类型参数,例如:
template <typename T = int> // 默认类型为 int class Box { private: T value; public: Box(T val) : value(val) {} T getValue() { return value; } };
如果没有显式传递类型,编译器会使用默认类型
int
。