💡从泛型思想、模板特化到排序算法实战 ------ 基于现代C++深度剖析, 函数模板让算法脱离数据类型 ------ 真正实现 "Write once, use for any type".

✨ 一、为什么需要函数模板?
在C++中,如果需要实现功能相同但数据类型不同的函数(例如交换两个整数、两个浮点数,或者对不同类型数组排序),传统做法需要为每种类型重载函数,导致大量重复代码。函数模板允许编写通用的函数,编译器会根据实参类型自动生成对应的函数版本,实现了"一次编写,多类型使用",极大提高代码复用性与可维护性。
💡 核心思想:将数据类型参数化,使用 template <typename T> 或 template <class T> 定义类型占位符。
🔧 二、函数模板定义与基本语法
语法格式:
cpp
template <typename T>
T 函数名(T 参数1, T 参数2) {
// 函数体
}
基于示例代码中的交换模板和加法模板:
cpp
// 交换模板
template<typename T>
void swap_T(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
// 加法模板
template<class T>
T add_T(T a, T b) {
return a + b;
}
🔍 调用方式
- 自动类型推导:swap_T(a, b); 编译器根据实参推断 T 的类型。
- 显式指定类型:swap_T<int>(a, b); 或 add_T<int>(a, d); 适用于需要类型转换或避免二义性。
📌 关键限制:自动推导时,所有模板参数必须保持一致的类型。例如 swap_T(a, d)(一个int一个double)会导致推导失败。但显式指定类型时允许隐式转换(值传递场景)。
🧩 三、无参数的模板函数 & 值初始化安全性
当模板参数无法从函数参数推导时,必须显式指定类型 。示例中 test_T<int>() 使用了 C++11 的 统一初始化 {},防止未初始化变量和窄化转换。
cpp
template<typename T>
void test_T() {
T temp{}; // 值初始化,int为0,double为0.0,自定义类调用默认构造
T temp1{0}; // 安全
// T temp2{0.5}; // 编译错误:窄化转换(若T为int)
temp = 1.5; // 隐式转换,int会截断
}
✅ 最佳实践:在模板中尽量使用 T var{} 保证变量总是被初始化,提高泛型代码的鲁棒性。
🎯 四、函数模板特化 (Explicit Specialization)
通用模板无法满足某些特殊类型的需求时(例如自定义类 MyPoint 无法直接用 << 输出,或者需要特殊格式),可以为特定类型提供特化版本。编译器会优先匹配特化版本。
cpp
// 通用模板
template<typename T>
void printArr(T arr[], int len) { ... }
// 针对 int 类型的特化 (分隔符变为 " / ")
template<>
void printArr<int>(int arr[], int len) {
for (int i = 0; i < len; i++) cout << arr[i] << " / ";
}
// 针对 MyPoint 类型的特化 (优雅输出坐标)
template<>
void printArr(MyPoint arr[], int len) {
for (int i = 0; i < len; i++) {
cout << "(" << arr[i].getX() << "," << arr[i].getY() << ")";
}
}
调用时,若传入 MyPoint 数组,自动选择特化版本,实现定制行为。
⚖️ 五、普通函数 vs 函数模板 vs 模板特化 ------ 调用优先级
C++ 编译器选择调用哪个函数有一套明确的规则(优先级从高到低):
| 优先级 | 候选类型 | 示例 |
|---|---|---|
| 1️⃣ 最高 | 完全匹配的普通函数 | void printArr(int arr[], int len) |
| 2️⃣ | 模板特化 | template<> void printArr<int>(int arr[], int len) |
| 3️⃣ | 函数模板实例化 | template<typename T> void printArr(T arr[], int len) |
| 4️⃣ 最低 | 经过隐式转换的普通函数 | 例如 printArr(charArr, len) 不存在时可能会尝试转换 |
cpp
// 普通函数 (打印int数组使用逗号分隔)
void printArr(int arr[], int len) { ... }
// 调用时:
printArr(int_arr, num); // 匹配普通函数 (优先级最高)
printArr<>(int_arr, num); // 空模板列表强制调用函数模板版本 (跳过普通函数)
⚠️ 注意:普通函数和函数模板最好不要提供完全相同的重载,容易引发二义性。若希望总是使用模板,使用空尖括号 <> 强制指定。
🚧 六、类型转换 & 引用的特殊规则
基于代码中的 test1 可总结:
- 值传递:允许隐式类型转换(如 addInt(a, c) 将 char 提升为 int)。
- 引用传递 :swapInt(a, c) 错误!引用不能绑定到不同类型转换后的临时变量。
但是 const 引用 可以绑定到临时量(安全,只读)。 - 模板引用参数:同样不支持隐式转换,保持类型严格一致。例如 swap_T(a, d) 编译失败。
📖 解决方案:需要处理不同类型时,可显式指定模板参数,或利用重载/函数重载决议。
📊 七、算法模板实战:选择排序 & 冒泡排序 (泛型排序)
以下两个排序模板可以处理 任意支持比较运算符 < 的类型数组(int, double, char, 甚至自定义类若重载了运算符)。
📌 1.选择排序模板 (不稳定,每轮选择最大/最小交换)
cpp
template<typename T>
void mySelectionSort_T(T arr[], int len) {
for (int i = 0; i < len-1; i++) {
int max = i;
for (int j = i+1; j < len; j++)
if (arr[max] < arr[j]) max = j; // 降序
if (max != i) swap_T(arr[max], arr[i]);
}
}
📌 2.冒泡排序模板 (稳定,提前退出优化)
cpp
template<typename T>
void myBubbleSort_T(T arr[], int len) {
for (int i = 0; i < len-1; i++) {
bool swapped = false;
for (int j = 0; j < len-1-i; j++) {
if (arr[j] < arr[j+1]) { // 降序排列
swap_T(arr[j], arr[j+1]);
swapped = true;
}
}
if (!swapped) break;
}
}
在 test2 中对 "hello world" 字符数组进行降序排序,test3/test4 演示整型数组的排序过程,充分体现了模板对 char 和 int 的通用性。
🧪 八、完整测试模块说明
程序中的 test1~test5 覆盖了模板的所有关键特性,下面逐一说明:
- test1():演示普通函数 vs 模板交换、加法、自动推导失败案例、显式指定类型 + 隐式转换、无参模板必须显式实例化。
- test2() :字符数组选择排序 +
printArr重载测试,展示默认模板和自定义连接符模板的重载。 - test3() :整型数组选择排序,并演示
printArr<>(int_arr, num)强制调用模板版本。 - test4():冒泡排序模板测试,验证提前退出优化。
- test5():函数调用优先级测试 ------ 普通函数 printArr(int[],len) 被优先调用,特化版本处理 MyPoint,以及空模板参数强制使用模板。
💡 注意:test5 中通过 MyPoint point_arr[] 调用 printArr 会自动匹配模板特化版本,输出格式化为 (x,y)。
📌 九、易错点 & 最佳实践总结
✔️ 模板定义通常放在头文件中 ,因为编译器需要在使用时看到完整定义才能实例化。
✔️ 函数模板不支持默认模板参数 (C++11 后支持,但函数模板常用默认类型参数较少见,类模板更常见)。
✔️ 模板特化时 template<> 必须前置,且参数列表必须匹配原模板。
✔️ 避免过度特化,优先使用重载或 if constexpr (C++17)。
✔️ 如果希望模板只适用于具有某种特性的类型,可以结合 SFINAE 或 C++20 Concept。
💎 十、关键代码与模板
cpp
// 1. 泛型交换
template<typename T> void mySwap(T& a, T& b) { T t = a; a = b; b = t; }
// 2. 泛型打印数组(通用)
template<typename T>
void printArray(const T arr[], size_t len) {
for (size_t i = 0; i < len; ++i) cout << arr[i] << " ";
}
// 3. 针对int的特化
template<> void printArray(const int arr[], size_t len) {
for (size_t i = 0; i < len; ++i) cout << arr[i] << " | ";
}
// 4. 泛型排序算法框架(选择排序)
template<typename T>
void genericSelectionSort(T arr[], int n) {
for (int i = 0; i < n-1; ++i) {
int minIdx = i;
for (int j = i+1; j < n; ++j)
if (arr[j] < arr[minIdx]) minIdx = j;
if (minIdx != i) mySwap(arr[i], arr[minIdx]);
}
}
基于以上模板,可以轻松排序整数、浮点数、字符甚至自定义对象(需实现比较运算符或提供仿函数)。
Tips:仿函数 (也称为函数对象)是C++中一个行为类似函数 的对象 。它通过重载函数调用运算符operator()来实现,使得对象可以像函数一样被调用,这里就不多展开了。
cpp
// 传统比较函数
bool compareInts(int a, int b) {
return a < b; // 从小到大
}
// 仿函数比较器
class CompareInts {
public:
bool operator()(int a, int b) const {
return a < b; // 从小到大
}
};
📈 十一、模板的优势与运行时开销
函数模板在编译期进行实例化,为每个类型生成独立函数,因此没有运行时额外开销,代码效率和手写特定类型函数相同。同时又能保证类型安全,堪称C++泛型编程的基石。
🔥 函数模板使得代码极度精简,同时又保留了极高的灵活性。在项目中,尽量将通用算法抽象为模板,减少重复劳动。
📝 十二、思考与扩展
1.如何修改选择排序模板使其支持升序/降序通过函数参数控制?
可以从几个层面来修改,使其功能更灵活:
-
方案一:增加布尔参数 这是最直接的修改。在函数参数列表中添加一个
bool ascending(默认为true表示升序),然后在内部比较时根据这个参数决定是使用<(找最小值,用于升序)还是>(找最大值,用于降序)。cpptemplate<typename T> void mySelectionSort_T(T arr[], int len, bool ascending = true) { for (int i = 0; i < len-1; i++) { int targetIdx = i; // 目标位置应存放的元素索引 for (int j = i + 1; j < len; j++) { bool condition = ascending ? (arr[j] < arr[targetIdx]) : (arr[j] > arr[targetIdx]); if (condition) { targetIdx = j; } } if (targetIdx != i) { swap_T(arr[targetIdx], arr[i]); } } }使用示例 :
mySelectionSort_T(arr, len);// 默认升序mySelectionSort_T(arr, len, false);// 降序 -
方案二:增加枚举参数(增强可读性) 定义一个
enum SortOrder { ASCENDING, DESCENDING };,用枚举值代替布尔值,使调用意图更明确。cppmySelectionSort_T(arr, len, SortOrder::DESCENDING); -
方案三:增加仿函数参数(最灵活、最符合C++标准库风格,推荐) 这是最强大和通用的方法。在模板中增加一个比较器参数
Compare comp,默认值可以设为std::less<T>()(表示升序)。在内部比较时,不再直接使用<或>,而是调用比较器comp(arr[j], arr[targetIdx])。如果此调用返回true,则意味着arr[j]应该排在arr[targetIdx]之前 ,因此更新targetIdx。cpptemplate<typename T, typename Compare = std::less<T>> void mySelectionSort_T(T arr[], int len, Compare comp = Compare()) { for (int i = 0; i < len-1; i++) { int targetIdx = i; for (int j = i + 1; j < len; j++) { if (comp(arr[j], arr[targetIdx])) { // 使用比较器 targetIdx = j; } } if (targetIdx != i) { swap_T(arr[targetIdx], arr[i]); } } }使用示例:
-
mySelectionSort_T(arr, len);// 默认使用std::less,升序 -
mySelectionSort_T(arr, len, std::greater<int>());// 使用std::greater,降序 -
mySelectionSort_T(arr, len, [](int a, int b){ return a % 10 < b % 10; });// 使用lambda表达式作为比较器自定义比较规则(按个位数升序排序) -
mySelectionSort_T(arr, len, [](int a, int b){ return a % 10 > b % 10; });// 使用lambda表达式作为比较器自定义比较规则(按个位数降序排序)
tips:无论是 std::less、std::greater还是自定义的函数,在传递给像 std::sort或mySelectionSort_T这样的排序算法时,都遵循同一个严格的语义约定:
比较函数
comp(a, b)返回true的唯一含义 是:在算法所构建的最终有序序列中,元素a必须排在元素b的前面。
这个约定是排序算法能够工作的基础。算法并不关心 comp内部是实现了"小于"还是"大于",它只关心这个"前后顺序"的规则。
| 比较器 | 比较操作 | 返回 true的含义 |
在排序中产生的效果 |
|---|---|---|---|
**std::less<T>** |
a < b |
a(较小的) 应排在 b(较大的) 前面 |
升序 (小的在前,大的在后) |
**std::greater<T>** |
a > b |
a(较大的) 应排在 b(较小的) 前面 |
降序 (大的在前,小的在后) |
简单记忆 :std::less-> "小者在前" -> 升序。std::greater-> "大者在前" -> 降序。
2.如果希望模板只接受具有 operator< 的类型,应该怎么做?(C++20 Concept: template<typename T> requires std::totally_ordered<T>)
基于C++20的现代特性,我们可以这样实现:
std::totally_ordered<T> 是一个概念(Concept):它要求类型T必须支持<, <=, >, >=这四个完整的比较运算符。如果只需要operator<,使用std::totally_ordered是足够的,但可能约束"过强"。一个更精确的自定义概念可以是:
cpp
template<typename T>
concept HasLessThan = requires(const T& a, const T& b) {
{ a < b } -> std::convertible_to<bool>;
};
- template<typename T>:声明一个模板参数 T
- concept HasLessThan = :定义一个名为 HasLessThan的概念
- requires(const T& a, const T& b) :要求部分,定义需要满足的条件
- { a < b } -> std::convertible_to<bool>; :具体的要求:a < b必须是一个有效的表达式,表达式的结果必须能转换为 bool类型
这个概念的语义是 :类型 T必须支持 <运算符,并且这个运算符的结果可以被用作布尔值。
概念(Concept)与模板函数的区别
| 特性 | 概念(Concept) | 模板函数 |
|---|---|---|
| 本质 | 编译时类型约束 ,是谓词(返回true/false) | 编译时代码生成 ,是可执行代码 |
| 作用 | 检查类型是否满足条件,不产生可执行代码 | 为特定类型生成函数实例,产生可执行代码 |
| 返回值 | 编译时布尔值(类型是否满足条件) | 运行时返回值(函数执行结果) |
然后,在函数模板声明中使用它进行约束:
cpp
template <HasLessThan T> // 或者 template <std::totally_ordered T>
void mySelectionSort_T(T arr[], int len) {
// ... 函数体内部可以安全地使用 arr[j] < arr[targetIdx] ...
}
这样,当用户尝试用不支持operator<的类型实例化此模板时,编译器会在调用处给出清晰易懂的错误信息,而不是在函数体内部遇到<符号时才报出令人困惑的错误。这是C++20引入Concepts的主要优势之一:将接口约束检查提前,并改善错误信息。
3.尝试实现一个模板函数 findMax,返回数组中最大元素的索引。
这个函数可以看作是mySelectionSort_T函数中"寻找最大值"步骤的独立版本,实现起来非常直观:
cpp
/// 查找数组中最大元素的索引
/// @param arr 数组指针
/// @param len 数组长度
/// @return 最大元素的索引,如果数组为空(len<=0)则返回-1
template<typename T>
int findMax(T arr[], int len) {
if (len <= 0) return -1; // 处理边界情况
int maxIdx = 0; // 假设第一个元素最大
for (int i = 1; i < len; ++i) {
if (arr[maxIdx] < arr[i]) { // 如果发现更大的元素
maxIdx = i; // 更新最大元素索引
}
}
return maxIdx;
}
增强版本:可以像排序函数一样,为其增加一个比较器参数,使其更加通用,可以用于寻找"在某种比较规则下的极值"。
cpp
template<typename T, typename Compare = std::less<T>>
int findMax(T arr[], int len, Compare comp = Compare()) {
if (len <= 0) return -1;
int extremeIdx = 0;
for (int i = 1; i < len; ++i) {
if (comp(arr[extremeIdx], arr[i])) { // 如果当前"极值"排在 arr[i] 之前
extremeIdx = i; // 更新为新的"极值"
}
}
return extremeIdx;
}
// 使用:findMax(arr, len) 找最大, findMax(arr, len, std::greater<int>()) 找最小。
4.模板特化和函数重载有何区别?何时优先使用特化?
这是一个核心概念,代码已经同时包含了两种用法(printArr的特化和重载),正好可以用来对比。
核心区别:
| 特性 | 模板特化 | 函数重载 |
|---|---|---|
| 定义 | 为一个已有的主模板 的特定类型参数提供一个完全特殊的实现。 | 定义多个同名但参数列表不同 的独立函数。 |
| 关系 | 是主模板的"特例",语法上紧密绑定(需要template<>)。 |
函数之间是平等的"重载"关系,彼此独立。 |
| 匹配时机 | 在模板参数推导/确定之后发生。编译器先决定调用哪个模板,再检查是否有特化。 | 在重载决议阶段发生。编译器在所有候选函数(包括普通函数和可能推导出的模板)中选择最匹配的一个。 |
| 灵活性 | 主要是为特定类型定制行为,通常保持相同的函数签名和语义。 | 可以改变参数数量、类型,甚至语义(但通常不推荐改变语义)。 |
代码中的例子:
template<> void printArr<int>(int arr[], int len)是模板特化 。它是对主模板template<typename T> void printArr(T arr[], int len)在T=int时的特殊实现。void printArr(int arr[], int len)是一个普通函数重载 。它是一个独立的函数,与模板无关。当调用printArr(int_arr, num1)时,根据您提到的优先级规则(完全匹配的普通函数 > 模板),它会优先于模板被调用。
何时优先使用特化?
- 定制类模板行为时 :这是最常见场景。函数重载不能用于类,只能对类模板进行(全/偏)特化。例如,为
std::vector<bool>做特化优化。 - 希望行为变化对用户"透明"时 :当你想为某种类型提供特殊实现,但希望函数签名、名称和核心语义保持不变 ,对所有调用者来说,调用方式完全一样,只是内部效率或细节不同。例如,为
std::swap特化自定义类,使其交换更高效。 - 针对一类类型(通过偏特化,仅限类模板) :类模板支持偏特化(如
template<typename T> class Widget<T*>),可以为某一类类型(如所有指针)提供特殊实现,这是函数重载无法直接做到的。
总结建议 : 代码注释中"提供了函数模板,最好就不要提供普通函数,否则容易出现二义性"的观点非常重要。在函数层面,一个更通用的建议是:如果主模板的逻辑对大多数类型都有效,仅为少数类型需要特殊处理,且处理逻辑是"优化"或"微小调整",考虑使用特化。如果针对不同类型需要的是逻辑完全不同、甚至签名都不同的操作,那么应该使用函数重载。 在实际中,对于函数模板,使用"带约束的模板"或"标签分发"等技巧有时比特化更清晰。C++20的Concepts进一步增强了这种能力。
📖 附1:示例来自如下实际可运行的 .cpp 文件
cpp
#include <iostream>
/// <summary>
/// 表示平面坐标系中的点的类
/// </summary>
class MyPoint {
double x;
double y;
public:
MyPoint(double x, double y) { this->x = x; this->y = y; }
double getX()const { return x; }
double getY()const { return y; }
void show()const { std::cout << '(' << x << ',' << y << ')'; }
};
/// <summary>
/// 打印数组函数(模板函数)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="arr"></param>
/// <param name="len"></param>
template<typename T>
void printArr(T arr[], int len) {
for (int i = 0; i < len; i++) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}
/// <summary>
/// 打印整型数组函数(模板特化)
/// </summary>
/// <param name="arr"></param>
/// <param name="len"></param>
template<>
void printArr<int>(int arr[], int len) {
for (int i = 0; i < len; i++) {
std::cout << arr[i] << " / ";
}
std::cout << std::endl;
}
/// <summary>
/// 打印坐标点数组函数(模板特化)
/// 模板的通用性并不是万能的,比如两个数组直接赋值或者自定义数据类型操作,所以需要特化
/// 有参数可以自动推导为MyPoint类型,调用这个特化版本来打印坐标点数组
/// 也可以显式指定模板参数为MyPoint,void printArr<MyPoint>(MyPoint arr[], int len)
/// </summary>
/// <param name="arr"></param>
/// <param name="len"></param>
template<>
void printArr(MyPoint arr[], int len) {
for (int i = 0; i < len; i++) {
std::cout << "(" << arr[i].getX() << "," << arr[i].getY() << ")";
}
std::cout << std::endl;
}
/// <summary>
/// 打印数组函数,连接符自定义(模板函数)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="arr"></param>
/// <param name="len"></param>
/// <param name="hyphen"></param>
template<typename T>
void printArr(T arr[], int len, char hyphen) {
for (int i = 0; i < len; i++) {
std::cout << arr[i] << hyphen;
}
std::cout << std::endl;
}
/// <summary>
/// 打印数组函数,连接符固定为逗号(普通函数)
/// 如果函数模板和普通函数都可以实现,优先调用普通函数
/// 但是提供了函数模板,最好就不要提供普通函数,否则容易出现二义性
/// </summary>
/// <param name="arr"></param>
/// <param name="len"></param>
void printArr(int arr[], int len) {
for (int i = 0; i < len; i++) {
std::cout << arr[i] << ",";
}
std::cout << std::endl;
}
/// <summary>
/// 交换整型函数
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
void swapInt(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
/// <summary>
/// 整型加函数
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
int addInt(int a, int b) {
return a + b;
}
/// <summary>
/// 交换浮点型函数
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
void swapDouble(double& a, double& b) {
double temp = a;
a = b;
b = temp;
}
/// <summary>
/// 交换函数(模板函数)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="a"></param>
/// <param name="b"></param>
template<typename T>
void swap_T(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
/// <summary>
/// 加函数(模板函数)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
template<class T>
T add_T(T a, T b)
{
return a + b;
}
/// <summary>
/// 无参数模板函数测试
/// </summary>
/// <typeparam name="T"></typeparam>
template<typename T>
void test_T() {
//初始化T类型的变量,使用{}值初始化
//1. 安全性第一:防止未初始化变量
//2. 防止窄化转换(编译时错误)
//3. 统一性:一套语法适用于所有类型
//4. 区分函数声明和对象构造
//5. 清晰的语义表达
//6. 模板的通用性:适用更多类型(例如自定义类、指针、仅有 explicit构造函数的类型等)
T temp{};
std::cout << "测试:" << temp << std::endl;
T temp1{0};
std::cout << "测试:" << temp << std::endl;
//T temp2{0.5};//从"double"转换到"int"需要收缩转换
//T的类型由显示指定,而不是自动类型推导,所以T的类型不是浮点型,而是int类型
temp = 1.5;
std::cout << "测试:" << temp << std::endl;
}
/// <summary>
/// 测试函数
/// </summary>
void test1() {
std::cout << "==============================================================" << std::endl;
std::cout << "函数模板封装交换函数测试:" << std::endl;
int a = 10;
int b = 20;
char c = 'c';//'c' 对应 ASCII码 99
double d = 0.5;
std::cout << "交换前:a = " << a << ", b = " << b << ", c = " << c << ", d = " << d << std::endl;
std::cout << "=== 普通函数 ===" << std::endl;
//普通函数交换
swapInt(a, b);
std::cout << "交换后:a = " << a << ", b = " << b << std::endl;
//隐式转换允许不同类型的变量交换
//swapInt(a, c);// 引用传递 - 不允许隐式类型转换,引用应该对原变量的直接操作,而转换会产生临时变量
//但是const引用允许隐式转换,const引用是只读的,绑定到临时变量是安全的,因为不会修改它。
std::cout << a << "+ " <<c << "= " << addInt(a, c) << std::endl;// 值传递 - 允许隐式类型转换
std::cout << "=== 有参数版本(可以自动推导)===" << std::endl;
//模板函数交换
swap_T(a, b);
std::cout << "交换后:a = " << a << ", b = " << b << std::endl;
//显示指定类型
swap_T<int>(a, b);
std::cout << "交换后:a = " << a << ", b = " << b << std::endl;
//swap_T<int>(a, d);//引用传递同样不允许隐式类型转换
//隐式转换
std::cout << "相加:a = " << a << ", d = " << d << std::endl;
std::cout << a << "+ " << d << "= " << add_T<int>(a, d) << std::endl;//显式指定,可以隐式转换
//std::cout << a << "+ " << d << "= " << add_T(a, d) << std::endl;//自动推导,不可以隐式转换
//自动类型推导不允许不同类型的变量
// 推导过程:
//1. 看第一个参数a → T必须与a类型匹配 → T = int
//2. 看第二个参数d → T必须与d类型匹配 → T = double
//3. 矛盾!T不能既是int又是double
//4. 推导失败,编译错误
//swap_T(a, d);
std::cout << "=== 无参数版本(必须显式指定)===" << std::endl;
//必须显式指定类型
//test_T();
test_T<int>();
std::cout << "==============================================================" << std::endl;
}
/// <summary>
/// 选择排序函数(模板函数)
/// 不稳定排序,可能改变相同元素相对顺序,因为会远距离交换
/// 时间复杂度:始终O(n²)
/// 空间复杂度:O(1)原地排序
/// 比较次数:固定:n(n - 1) / 2
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="arr"></param>
/// <param name="len"></param>
template<typename T>
void mySelectionSort_T(T arr[],int len) {
// 外层循环:控制放置位置
for (int i = 0; i < len-1; i++) {
int max = i;
// 内层循环:从前往后在未排序部分寻找最大值
for (int j = i + 1; j < len; j++) {
if (arr[max] < arr[j]) {
max = j;
}
}
std::cout << "i = " << i << ", max = " << max << std::endl;
// 内层循环结束后,交换一次,每次都挑出最大值
if (max != i) {
swap_T(arr[max], arr[i]);
std::cout << "交换:" << std::endl;
printArr(arr, len);
}
}
}
/// <summary>
/// 冒泡排序函数(模板函数)
/// 稳定排序,不改变相同元素相对顺序,因为只会相邻交换
/// 时间复杂度:平均O(n²),最优O(n)
/// 空间复杂度:O(1)原地排序
/// 比较次数:平均n(n-1)/2,最优n-1
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="arr"></param>
/// <param name="len"></param>
template<typename T>
void myBubbleSort_T(T arr[], int len) {
// 外层循环:控制冒泡轮数
for (int i = 0; i < len-1; i++) {
bool swapped = false;//退出逻辑优化
// 内层循环:从后往前每轮减少比较范围执行相邻比较
for (int j = 0; j < len-1-i; j++) {
//每轮内循环:让一个小气泡上浮到水面(从大到小排序)
std::cout << "i = " << i << ", j = " << j << std::endl;
if (arr[j] < arr[j+1]) {
//立即交换
swap_T(arr[j], arr[j+1]);
swapped = true;
std::cout << "交换:" << std::endl;
printArr(arr, len);
}
}
if (!swapped) {
break;//如果没有交换,说明已经有序,提前退出
std::cout << "已经有序,提前退出" << std::endl;
}
}
}
/// <summary>
/// 测试函数
/// </summary>
void test2() {
std::cout << "==============================================================" << std::endl;
std::cout << "函数模板封装选择排序函数,char数组测试:"<< std::endl;
char char_arr[] = "hello world";//双引号括起的字符串会自动在末尾添加空字符'\0',所以长度12
int num = sizeof(char_arr) / sizeof(char);
std::cout << "输入数组元素数量:" << num << std::endl;
//函数模板重载测试
printArr(char_arr, num);
printArr(char_arr, num,'-');
mySelectionSort_T(char_arr, num);
printArr(char_arr, num);
std::cout << "==============================================================" << std::endl;
}
/// <summary>
/// 测试函数
/// </summary>
void test3() {
std::cout << "==============================================================" << std::endl;
std::cout << "函数模板封装选择排序函数,int数组测试:" << std::endl;
//int int_arr[] = {7,5,3,1,5,9,4,5,6,8,5,2 };
int int_arr[] = { 1,2,3,4 };
int num = sizeof(int_arr) / sizeof(int);
std::cout << "输入数组元素数量:" << num << std::endl;
printArr<>(int_arr, num);//空模板参数列表来强制调用函数模板
mySelectionSort_T(int_arr, num);
printArr(int_arr, num);
std::cout << "==============================================================" << std::endl;
}
/// <summary>
/// 测试函数
/// </summary>
void test4() {
std::cout << "==============================================================" << std::endl;
std::cout << "函数模板封装冒泡排序函数,int数组测试:" << std::endl;
//int int_arr[] = {7,5,3,1,5,9,4,5,6,8,5,2 };
int int_arr[] = { 1,2,3,4 };
int num = sizeof(int_arr) / sizeof(int);
std::cout << "输入数组元素数量:" << num << std::endl;
printArr(int_arr, num);
myBubbleSort_T(int_arr, num);
printArr(int_arr, num);
std::cout << "==============================================================" << std::endl;
}
/// <summary>
/// 测试函数
/// 函数调用优先级:
/// 1.完全匹配的普通函数(最高优先级)
/// 2.模板特化
/// 3.函数模板
/// 4.隐式转换的普通函数(最低优先级)
/// </summary>
void test5() {
std::cout << "==============================================================" << std::endl;
std::cout << "函数调用优先级测试:" << std::endl;
int int_arr[] = { 1,2,3,4 };
int num1 = sizeof(int_arr) / sizeof(int);
std::cout << "输入数组元素数量:" << num1 << std::endl;
printArr(int_arr, num1);
printArr<>(int_arr, num1);//空模板参数列表来强制调用函数模板
MyPoint point_arr[] = { MyPoint(1,2), MyPoint(3,4), MyPoint(5,6) };
int num2 = sizeof(point_arr) / sizeof(MyPoint);
std::cout << "输入数组元素数量:" << num2 << std::endl;
printArr(point_arr, num2);
std::cout << "==============================================================" << std::endl;
}
int main()
{
//调用测试函数
test1();
test2();
test3();
test4();
test5();
//等待用户输入
system("pause");
//返回0表示程序正常结束
return 0;
}
🎬 附2:程序运行输出
==============================================================
函数模板封装交换函数测试:
交换前:a = 10, b = 20, c = c, d = 0.5
=== 普通函数 ===
交换后:a = 20, b = 10
20+ c= 119
=== 有参数版本(可以自动推导)===
交换后:a = 10, b = 20
交换后:a = 20, b = 10
相加:a = 20, d = 0.5
20+ 0.5= 20
=== 无参数版本(必须显式指定)===
测试:0
测试:0
测试:1
==============================================================
==============================================================
函数模板封装选择排序函数,char数组测试:
输入数组元素数量:12
h e l l o w o r l d
h-e-l-l-o- -w-o-r-l-d--
i = 0, max = 6
交换:
w e l l o h o r l d
i = 1, max = 8
交换:
w r l l o h o e l d
i = 2, max = 4
交换:
w r o l l h o e l d
i = 3, max = 7
交换:
w r o o l h l e l d
i = 4, max = 4
i = 5, max = 7
交换:
w r o o l l h e l d
i = 6, max = 9
交换:
w r o o l l l e h d
i = 7, max = 9
交换:
w r o o l l l h e d
i = 8, max = 8
i = 9, max = 10
交换:
w r o o l l l h e d
i = 10, max = 10
w r o o l l l h e d
==============================================================
==============================================================
函数模板封装选择排序函数,int数组测试:
输入数组元素数量:4
1 / 2 / 3 / 4 /
i = 0, max = 3
交换:
4,2,3,1,
i = 1, max = 2
交换:
4,3,2,1,
i = 2, max = 2
4,3,2,1,
==============================================================
==============================================================
函数模板封装冒泡排序函数,int数组测试:
输入数组元素数量:4
1,2,3,4,
i = 0, j = 0
交换:
2,1,3,4,
i = 0, j = 1
交换:
2,3,1,4,
i = 0, j = 2
交换:
2,3,4,1,
i = 1, j = 0
交换:
3,2,4,1,
i = 1, j = 1
交换:
3,4,2,1,
i = 2, j = 0
交换:
4,3,2,1,
4,3,2,1,
==============================================================
==============================================================
函数调用优先级测试:
输入数组元素数量:4
1,2,3,4,
1 / 2 / 3 / 4 /
输入数组元素数量:3
(1,2)(3,4)(5,6)
==============================================================