C++的进阶泛型编程学习(1):函数模板的基本概念和机制

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

回顾一下C++的面向对象特性有哪些?
在这里插入图片描述
在这里插入图片描述
我用这两个表来简要的总结了一下,接下来要接触的,是C++中更加复杂,更加进阶的内容

一、模板

1.1 模板的概念

C++的模板是一种通用编程工具,它允许在编写代码时定义通用的数据类型或函数,以便在不同的上下文中重复使用。模板可以用于类、函数和类成员函数。

1.1.1 形象的解释:模板就是通用的模具,目的是提高通用性

在这里插入图片描述
另外比如说PPT模板、实验报告模板等等。

1.1.1 模板的特点:

①模板不可以直接使用,他只是一个框架
②模板的通用不是万能的,只在一个或者几个方面发挥作用
所以C++里面的模板的作用也是大同小异

1.1.2 综述模板的作用

C++中的模板是一种通用编程工具,它允许在编写代码时定义通用的数据类型或函数,以便在不同的上下文中重复使用。模板的作用包括代码重用、泛型编程、类型安全、高性能代码生成以及提供容器和算法库等功能。通过使用模板,可以编写通用的代码,适应不同的数据类型,提高代码的灵活性和可重用性。模板在编译时进行类型检查,提供类型安全性,并通过编译时代码生成来提高性能。

1.2 模板的使用机制

C++提供两种模板机制:函数模板类模板

1.2.1 函数模板

函数模板是C++中的一种特殊函数,它可以用于定义通用的函数,可以在不同的数据类型上进行操作。函数模板通过使用模板参数来表示通用的数据类型,从而实现代码的重用和泛型编程。
即告诉编译器,我要声明一个暂时未知函数返回类型与未知形参的函数

函数模板的语法如下所示:

template <typename T>
返回类型 函数名(参数列表) {
   
    // 函数体
}

template:声明创建了一个模板
typename :表明其后面的符号是一种数据类型
T:一种通用的数据类型,通常为大写字母,可以替换

在上面的语法中,template 表示这是一个函数模板,并且T是一个模板参数,可以是任意类型。返回类型表示函数的返回类型,函数名是函数的名称,参数列表是函数的参数列表。

写一个实例:

template <typename T>
T maximum(T a, T b) {
   
    return (a > b) ? a : b;
}

int main() {
   
    int num1 = 10, num2 = 20;
    int maxNum = maximum(num1, num2);
    cout << "Maximum number is: " << maxNum << endl;

    double num3 = 3.14, num4 = 2.71;
    double maxDouble = maximum(num3, num4);
    cout << "Maximum double is: " << maxDouble << endl;

    return 0;
}

二、函数模板的深入学习及注意机制

2.1 函数模板的自动类型推导

在C++中,函数模板可以使用自动类型推导来推断模板参数的类型。通过使用auto关键字作为函数模板的参数类型,编译器可以根据函数调用时的实参类型来推导出模板参数的类型

以下是一个使用自动类型推导的函数模板示例:

template <typename T>
void print(T value) {
   
    std::cout << value << std::endl;
}

int main() {
   
    print(10);  // 推导为int类型
    print(3.14);  // 推导为double类型
    print("Hello");  // 推导为const char*类型

    return 0;
}

在上面的示例中,print函数模板使用了自动类型推导,它的模板参数类型使用了auto关键字。在main函数中,我们分别调用了print函数,并传递了不同类型的参数。编译器会根据实参的类型推导出模板参数的类型,并实例化相应的函数。

2.1.1 ①自动类型推导,必须使得推导出的数据类型T是一致的


需要注意的是,自动类型推导仅适用于函数模板的参数类型,而不适用于函数模板的返回类型。

2.2 typename为什么可以替换为class?

在C++中,typename和class在函数模板中都可以用来表示模板参数的类型。它们在函数模板中的使用是等效的,可以互相替换使用

使用typename关键字来表示模板参数的类型是C++标准的做法,特别是在模板的嵌套和依赖名称的情况下。例如,在模板内部使用嵌套类型时,需要使用typename来指示编译器该名称是一个类型。

使用class关键字来表示模板参数的类型是C++早期版本的做法,但在C++标准化过程中,为了更好地表达模板参数是类型的概念,引入了typename关键字。

因此,typename和class在函数模板中的使用是等效的,可以根据个人喜好和代码风格选择使用其中之一。但在一些特定的情况下,如模板的嵌套和依赖名称时,使用typename是必需的

2.3 函数模板的功能及使用时机

函数模板是一种通用的函数定义,可以用于处理多种不同类型的数据。它允许编写一次函数定义,然后根据需要在不同的上下文中使用不同的数据类型。

函数模板的主要功能是实现代码的重用和泛化。通过使用函数模板,可以编写一次通用的函数定义,然后在需要时根据具体的数据类型进行实例化。这样可以避免重复编写相似的代码,提高代码的可维护性和可读性

函数模板的使用时机包括但不限于以下情况:

处理不同类型的数据:当您需要编写一个函数来处理多种不同类型的数据时,可以使用函数模板。例如,您可以编写一个通用的排序函数,可以用于排序整数数组、浮点数数组或字符串数组。

泛化算法:当您需要编写一个通用的算法,可以适用于不同类型的数据结构时,可以使用函数模板。例如,您可以编写一个通用的搜索函数,可以在不同类型的容器中查找特定的元素。

类型安全的操作:函数模板可以提供类型安全的操作,因为它们在编译时进行类型检查。这意味着如果您在函数模板中使用了不兼容的数据类型,编译器将在编译时报错,而不是在运行时出现错误。

代码简化和减少重复:函数模板可以简化代码并减少重复。通过使用函数模板,您可以避免编写多个相似的函数来处理不同类型的数据,从而减少了代码量和维护成本。

三、函数模板具体案例——数组排序

3.1 目的:测试char数组和int数组的排序

如:定义char数组:“badcfe”,要求按照字母表的顺序进行从前往后的排序(即ASCII码数值大小),同时,定义int数组:2 5 8 7 4 1 6 3,要求从小到大排序。
使用函数模板进行泛化处理,提高重用性。

3.2 思路:

采用冒泡排序,每次比较相邻元素的大小,让较大的那个排后面,不断循环。
定义一个函数模板,传参的数组数据类型用模板代替。

template <typename T>

void sort(T arr[], int len) {
   

    for (int i = 0; i < len - 1; i++) {
   
        for (int j = 0; j < len - i - 1; j++) {
   
            if (arr[j] > arr[j + 1]) {
   
                // 交换相邻元素
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }

}

3.3 全部代码

#include <iostream>
#include<string>

using namespace std;

template <typename T>

void sort(T arr[], int len) {
   

    for (int i = 0; i < len - 1; i++) {
   
        for (int j = 0; j < len - i - 1; j++) {
   
            if (arr[j] > arr[j + 1]) {
   
                // 交换相邻元素
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }

}


int main(void) {
   


    char chararr[] = {
    'b','a','d','c','f','e'};
    int charlen = sizeof(chararr) / sizeof(chararr[0]);
    sort(chararr, charlen);
    std::cout << "按字母表顺序排序后的char数组:";
    for (int i = 0; i < charlen; i++) {
   
        std::cout << chararr[i] << " ";
    }
    std::cout << std::endl;

    int intarr[] = {
    2, 5, 8, 7, 4, 1, 6, 3 };
    int intlen = sizeof(intarr) / sizeof(intarr[0]);
    sort(intarr, intlen);
    std::cout << "从小到大排序后的int数组:";
    for (int i = 0; i < intlen; i++) {
   
        std::cout << intarr[i] << " ";
    }
    std::cout << std::endl;


	system("pause");
	return 0;
}


四、普通函数与函数模板的区别

定义一个普通函数max,用于比较两个整数的大小并返回较大的数:

int max(int a, int b) {
   
    return (a > b) ? a : b;
}

接下来,我们定义一个函数模板templateMax,也用于比较两个数的大小并返回较大的数:

template<typename T>
T templateMax(T a, T b) {
   
  return (a > b) ? a : b;
}

现在,我们来比较普通函数和函数模板的区别。

(1)定义方式:

普通函数的定义是针对特定类型的,如上述的max函数是针对整数类型的定义。

函数模板的定义使用template来声明一个通用的模板函数,可以适用于多种类型。

(2)参数类型:

普通函数的参数类型是具体指定的,如上述的max函数的参数类型是int。

函数模板的参数类型是使用模板参数T来表示的,可以是通用的类型。

(3)重载:

普通函数可以通过函数重载来处理不同类型的参数,如可以定义一个max函数来处理浮点数类型的参数。

函数模板可以通过参数推导来自动匹配不同类型的参数,无需手动重载。例如,我们可以使用templateMax函数模板来比较浮点数、字符等不同类型的参数。

(4)代码生成:

普通函数在编译时会生成具体的函数代码,如上述的max函数在编译时会生成一个针对整数类型的函数。

函数模板在编译时不会生成具体的函数代码,只有在实例化时才会根据具体的类型生成对应的函数代码。例如,当我们使用templateMax函数模板比较整数时,编译器会根据实际情况生成一个针对整数类型的函数。

(5)使用方式:

普通函数可以直接调用,如max(3, 5)。

函数模板需要通过实例化来生成具体的函数,然后才能调用。例如,templateMax(3, 5)会生成一个针对整数类型的函数,并返回较大的数。

五、普通函数和函数模板的调用规则

调用规则如下:
1.如果函数模板和普通函数都可以实现,优先调用普通函数
2.可以通过空模板参数列表来强制调用函数模板
3.函数模板也可以发生重载
4.如果函数模板可以产生更好的匹配优先调用函数模板

接下来一个个解释:

5.1 如果函数模板和普通函数都可以实现,优先调用普通函数

如果函数模板和普通函数都可以实现某个功能,编译器会优先选择普通函数进行调用,而不是实例化函数模板。

这是因为普通函数的匹配更加具体,不需要进行模板参数的推导和实例化过程,所以在编译时会更加高效。而函数模板需要在实例化时根据具体的类型生成对应的函数代码,这个过程可能会增加编译时间和代码体积。

例如,假设我们有一个普通函数foo和一个函数模板templateFoo,它们都可以接受一个整数作为参数:

void foo(int x) {
   
    // 普通函数实现
     std::cout << "普通函数"<< endl;
}

template<typename T>
void foo(T x) {
   
    // 函数模板实现
    std::cout << "函数模板"<< endl;
}


int main(void) {
   

     foo(40);
	system("pause");
	return 0;
}

此时编译器会调用普通函数foo。
当我们调用templateFoo函数模板时,编译器会进行实例化,生成针对具体类型的函数代码:
当我们调用foo函数时,编译器会直接选择普通函数进行调用:

但是,如果没有对应的普通函数实现,或者我们明确指定要调用函数模板,编译器会选择函数模板进行实例化和调用。

那如何告诉编译器去调用函数模板呢?

5.2 可以通过空模板参数列表来强制调用函数模板

即:

foo<>(40);

添加这个符号,就是强制告诉编译器去调用函数模板。

5.3 函数模板也可以发生重载

函数模板也可以发生重载。函数模板的重载与普通函数的重载类似,可以根据参数类型和数量的不同来进行区分。

当存在多个函数模板时,编译器会根据函数调用的参数类型和数量来选择最匹配的函数模板进行实例化和调用。

例如,考虑以下两个函数模板:

template<typename T>
void foo(T x) {
   
    // 函数模板1
}

template<typename T>
void foo(T x, T y) {
   
    // 函数模板2
}

当我们调用foo函数时,编译器会根据参数类型和数量来选择最匹配的函数模板进行实例化和调用。

foo(42);        // 调用函数模板1,参数类型为int
foo(3.14, 2.71); // 调用函数模板2,参数类型为double

在这个例子中,根据参数类型和数量的不同,编译器可以正确地选择调用不同的函数模板。

因此,函数模板也可以发生重载,编译器会根据参数类型和数量来选择最匹配的函数模板进行实例化和调用。

5.4 如果函数模板可以产生更好的匹配优先调用函数模板

不解释了,跟5.1是一个道理

六、函数模板的局限性:不是万能的

6.1 问题提出:函数模板无法解决数组、结构体这类特殊传参的比较

对于这段代码:

template<typename T>
void foo(T a,T ,b) {
   
    a=b;
}

或者:

template<typename T>
void foo(T a,T ,b) {
   
    if(a>b){
   
    //
    }
}

传递参数如果是字符型或者整形还好,但是如果a和b是数组的话就不行了,无法运行。或者说a和b是我们用结构体自定义的一种数据类型,那也不行。

那如何解决这类问题呢?

6.2 解决办法

函数模板可以处理数组和结构体这类特殊传参的比较,但需要使用适当的比较操作符或自定义比较函数来实现。

对于数组的比较,你可以使用逐个比较数组元素的方式来判断它们是否相等。例如,考虑以下函数模板:

template<typename T, int size>
bool isEqual(T arr1[size], T arr2[size]) {
   
    for (int i = 0; i < size; i++) {
   
        if (arr1[i] != arr2[i]) {
   
            return false;
        }
    }
    return true;
}

在这个例子中,arr1和arr2是模板参数类型为T的数组,size是数组的大小。

当你调用isEqual函数模板时,它会逐个比较数组元素,并返回比较结果。

int myArray1[] = {
   1, 2, 3, 4, 5};
int myArray2[] = {
   1, 2, 3, 4, 5};
bool result = isEqual<int, 5>(myArray1, myArray2);

在这个例子中,myArray1和myArray2作为参数传递给isEqual函数模板时,它们会被自动转换为指向数组首元素的指针,并且编译器会推导出数组的类型和大小。

对于结构体的比较,你可以重载比较操作符或自定义比较函数来实现结构体的比较。例如,考虑以下结构体和函数模板:

struct Point {
   
    int x;
    int y;
};

template<typename T>
bool isEqual(T obj1, T obj2) {
   
    return obj1 == obj2;
}

在这个例子中,Point是一个结构体,isEqual函数模板使用了比较操作符==来比较结构体的相等性。

当你调用isEqual函数模板时,它会使用重载的比较操作符或自定义的比较函数来判断结构体是否相等。

Point p1 = {
   1, 2};
Point p2 = {
   1, 2};
bool result = isEqual<Point>(p1, p2);

在这个例子中,p1和p2作为参数传递给isEqual函数模板时,它们会被按值传递给函数,并使用重载的比较操作符==来比较结构体的相等性。

总结起来,函数模板可以处理数组和结构体这类特殊传参的比较,但需要使用适当的比较操作符或自定义比较函数来实现。对于数组的比较,你可以逐个比较数组元素;对于结构体的比较,你可以重载比较操作符或自定义比较函数。

七、学习模板并不是为了写模板,而是在STL(标准模板库)能够运用系统提供的模板

当然,模板分为函数模板和类模板,所以下一节讲学习类模板。

相关推荐

  1. C++模板编程编程强大工具

    2024-02-15 08:32:01       21 阅读
  2. C++ 模板编程详解

    2024-02-15 08:32:01       28 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-02-15 08:32:01       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-02-15 08:32:01       100 阅读
  3. 在Django里面运行非项目文件

    2024-02-15 08:32:01       82 阅读
  4. Python语言-面向对象

    2024-02-15 08:32:01       91 阅读

热门阅读

  1. 【数据结构】单调栈

    2024-02-15 08:32:01       68 阅读
  2. c++STL系列——(八)multiset

    2024-02-15 08:32:01       43 阅读
  3. Leetcode 392 判断子序列

    2024-02-15 08:32:01       46 阅读
  4. Windows安装DeepSpeed

    2024-02-15 08:32:01       57 阅读
  5. 什么是系统工程(字幕)27

    2024-02-15 08:32:01       35 阅读
  6. 2024.2.14作业

    2024-02-15 08:32:01       66 阅读
  7. 【docker 的常用命令——详细讲解】

    2024-02-15 08:32:01       45 阅读
  8. 甲辰年正月初五情人节

    2024-02-15 08:32:01       47 阅读
  9. AutoSAR(基础入门篇)9.1-协议数据单元PDU

    2024-02-15 08:32:01       52 阅读
  10. 如何学习机器学习和深度学习: 软件工程师指南

    2024-02-15 08:32:01       61 阅读