目录
一、 泛型编程
1.1 为什么需要泛型编程?
举个例子:
我们要实现一个通用的交换数据的函数,在C语言中我们只能通过不同名的函数实现,很麻烦,C++中我们可以通过函数重载实现,但是也有弊端:重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要手动添加对应的函数实现,同时代码的可维护性也随之降低!
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;
}
//其他类型
.........
那么是否有其他方法呢?我只要提供我需要的类型,编译器会自动实现对应版本的函数!
C++模板正是解决此问题的利器。
二、模板
2.1 概念
C++中的模板有两种主要类型:函数模板和类模板。
2.2 函数模板
2.2.1 概念
2.2.2 语法
函数模板的语法很简单,它由模板头和函数体组成:
其中,template <typename T>
声明了一个模板,T
是类型参数的占位符。在函数参数和返回类型中使用 T
,编译器会根据参数类型自动确定实际的数据类型。(typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class))。
2.2.3 示例
可以创建可以用于不同数据类型的通用函数,例如最大值函数:
2.2.4 模板实例化
隐式实例化
指的是在代码中调用函数模板时,编译器会根据传递的参数类型隐式地生成特定数据类型的函数实现。这是函数模板常见的实例化方式。
template <typename T>
T Max(T a, T b) {
return a > b ? a : b;
}
int main() {
int result = Max(5, 10); // 隐式实例化为 int Max(int a, int b)
return 0;
}
假设我们调用 Max(5, 10)
,编译器会执行以下步骤来进行函数模板实例化:
- 编译器看到调用
Max(5, 10)
,需要实例化函数模板Max
。 - 编译器分析参数
5
和10
,确定它们的类型为int
。 - 编译器将模板参数
T
替换为int
,生成如下的特定实现:
那么我这样调用呢?
int a = 2;
double b = 3.0;
Max(a, b);
问题本质
该语句不能通过编译,当编译器遇到多个不同类型的实参时,需要确定一个适合的模板参数类型。然而,在某些情况下,可能出现无法明确确定模板参数类型的情况,因为模板参数列表中只有一个模板参数,但实际实参可能是不同的类型。
类型推断和类型转换
在模板实例化过程中,编译器通常不会进行隐式的类型转换,因为这可能导致不明确的结果。例如,在您的例子中,编译器不知道应该将模板参数 T
推断为 int
还是 double
,因此无法进行正确的实例化。
注意:在模板中,编译器一般不会进行类型转换操作
此时有两种常用处理方式:
1. 用户强制转化
2. 显式实例化
强制转换
Max(a, (int)b);
//或者
Max((double)a, b);
显示实例化
Max<int>(a, b);
//或者
Max<double>(a, b);
如果类型不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错。
2.2.5 模板参数的匹配原则
当存在非模板函数和同名函数模板时,编译器在进行函数调用时会根据匹配规则选择合适的函数。具体情况如下:
我们来看一看例子
#include <iostream>
// 通用输出函数模板
template<class T>
void Print(T value) {
std::cout << value << std::endl;
}
// 重载的输出函数,专门处理 const char* 类型
void Print(const char* value) {
std::cout << "String: " << value << std::endl;
}
int main() {
Print(42); // 调用通用函数模板 Print<T>
Print("Hello"); // 调用重载函数 Print(const char*)
Print(3.14); // 调用通用函数模板 Print<T>
Print<const char*>("Hello"); //显示调用通用函数模板 Print<T>
return 0;
}
-
template<class T> void Print(T value)
是一个通用的输出函数模板,用于输出不同类型的数据。 -
void Print(const char* value)
是一个专门处理const char*
类型的输出函数重载。 -
在
main
函数中,调用Print(42)
会匹配到通用函数模板,因为类型匹配。 -
调用
Print("Hello")
会匹配到重载函数,因为const char*
更为特化。 -
调用
Print(3.14)
仍会匹配到通用函数模板。 -
显示调用模板,Print<const char*>("Hello")显然会调用函数模板。
再来一个例子
#include <iostream>
// 专门处理 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); // 调用非函数模板,与 int Add(int left, int right) 匹配
Add(1, 2.0); // 调用函数模板,生成更匹配的版本
}
-
int Add(int left, int right)
是一个专门处理int
类型的加法函数。 -
template<class T1, class T2> T1 Add(T1 left, T2 right)
是一个通用的加法函数模板,可以适用于不同类型的数据。 -
在调用
Add(1, 2)
时,编译器会选择调用非函数模板,因为它与实际参数类型完全匹配。 -
在调用
Add(1, 2.0)
时,编译器会选择调用函数模板。虽然非函数模板也可以匹配,但是函数模板可以生成更匹配的版本,编译器会根据实际参数生成更匹配的Add
函数。
2.3 类模板
2.3.1 概念
2.3.2 语法
类模板的语法类似于函数模板,但是应用于类的定义。以下是一个简单的类模板示例:
template <class T>
class MyContainer {
private:
T value;
public:
MyContainer(T val) : value(val) {}
T GetValue() {
return value;
}
};
2.3.3 示例
int main() {
MyContainer<int> intContainer(42);
MyContainer<double> doubleContainer(3.14);
std::cout << intContainer.GetValue() << std::endl; // 输出: 42
std::cout << doubleContainer.GetValue() << std::endl; // 输出: 3.14
return 0;
}
2.3.4 注意事项
-
成员函数定义: 类模板的成员函数通常也需要在模板类内定义,否则需要在定义外使用
template
关键字进行模板声明和实现。 -
模板参数推导: 在实例化类模板时,编译器可以自动推导模板参数的类型,也可以使用显式指定的方式进行实例化。
-
模板特化: 类模板也可以进行特化,针对特定类型提供自定义实现,类似于函数模板的模板特化。
-
实例化时代码生成: 类模板实例化时,编译器会根据实际的类型参数生成相应的类定义,从而创建特定类型的类。
2.3.5 解释注意事项
1. 成员函数定义:
在类模板中,成员函数可以在模板类内部定义,也可以在类外部使用 template
关键字进行模板声明和实现。
template <class T>
class MyContainer {
private:
T value;
public:
MyContainer(T val) : value(val) {}
T GetValue() {
return value;
}
};
// 在类外部定义成员函数模板
template <class T>
T MyContainer<T>::GetValue() {
return value * 2;
}
2. 模板参数推导:
编译器在实例化类模板时可以自动推导模板参数的类型,也可以使用显式指定的方式进行实例化。
int main() {
MyContainer intContainer(42);
MyContainer<double> doubleContainer(3.14);
std::cout << intContainer.GetValue() << std::endl; // 输出: 42
std::cout << doubleContainer.GetValue() << std::endl; // 输出: 3.14
return 0;
}
3. 模板特化:
类模板也可以进行特化,为特定类型提供自定义实现,类似于函数模板的模板特化。
// 类模板定义
template <class T>
class MyContainer {
private:
T value;
public:
MyContainer(T val) : value(val) {}
T GetValue() {
return value;
}
};
// 类模板的特化版本
template <>
class MyContainer<int> {
private:
int value;
public:
MyContainer(int val) : value(val) {}
int GetValue() {
return value * 2; // 自定义的实现
}
};
三、模板分文件编写(特殊)
示例:
假设我们有一个类模板 MyTemplate
,其中包含成员函数,我们希望将其分为头文件和源文件。
MyTemplate.h(头文件):
#ifndef MYTEMPLATE_H
#define MYTEMPLATE_H
template <typename T>
class MyTemplate {
public:
MyTemplate(T value);
void PrintValue();
private:
T data;
};
#endif
MyTemplate.cpp(源文件):
#include <iostream>
#include "MyTemplate.h"
template <typename T>
MyTemplate<T>::MyTemplate(T value) : data(value) {}
template <typename T>
void MyTemplate<T>::PrintValue() {
std::cout << "Value: " << data << std::endl;
}
// 显式实例化模板
template class MyTemplate<int>;
template class MyTemplate<double>;
main.cpp(主文件):
#include "MyTemplate.h"
int main() {
MyTemplate<int> intObj(42);
MyTemplate<double> doubleObj(3.14);
intObj.PrintValue();
doubleObj.PrintValue();
return 0;
}
在源文件 MyTemplate.cpp
中,我们提供了类模板的实现,包括构造函数和成员函数的定义。我们还在源文件中使用了显式实例化(template class MyTemplate<int>;
和 template class MyTemplate<double>;
),以确保在编译期间生成特定类型的模板实例。
最后,在主文件 main.cpp
中,我们只需要包含头文件 MyTemplate.h
并使用类模板。编译时,编译器会将类模板的实现部分从 MyTemplate.cpp
中提取出来并进行实例化。
虽然这种分文件编写模板的方式需要一些额外的步骤,但它确实可以使代码更有组织性和可维护性。
进一步解释
函数分文件编写亦是如此原理!!!
函数模板初阶到此结束!笔者会继续更新进阶模板教程!!