跳转至

C++模版(基础)

约 1817 个字 317 行代码 3 张图片 预计阅读时间 10 分钟

C++泛型编程思想

泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。

模板是泛型编程的基础。

虽然可以直接使用函数重载来解决不同类型的问题,但是使用函数重载会出现可能不好的地方

  1. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
  2. 代码的可维护性比较低,一个出错可能所有的重载均出错

C++模版

模版介绍

在C语言中,当需要交换两个变量的数据时需要考虑到不同类型,例如下面的代码

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>

//交换int类型数据
void swap_int(int* num1, int* num2)
{
    int tmp = *num1;
    *num1 = *num2;
    *num2 = tmp;
}

//交换double类型的数据
void swap_double(double* num1, double* num2)
{
    double tmp = *num1;
    *num1 = *num2;
    *num2 = tmp;
}

int main()
{
    int num1 = 1, num2 = 2;
    printf("num1=%d num2=%d\n", num1, num2);
    swap_int(&num1, &num2);
    printf("num1=%d num2=%d\n", num1, num2);
    double num3 = 4.1, num4 = 4.5;
    printf("num3=%.1f num4=%.1f\n", num3, num4);
    swap_double(&num3, &num4);
    printf("num3=%.1f num4=%.1f\n", num3, num4);

    return 0;
}
输出结果
num1=1 num2=2
num1=2 num2=1
num3=4.1 num4=4.5
num3=4.5 num4=4.1

在上面的C语言代码中,当需要交换int类型的数据时需要int类型交换函数,需要double类型的数据时需要double类型的交换函数,但是这两个函数除了类型不同以外其他代码都一样,增加了工作量,并且因为C语言不支持函数重载,所以两个交换函数的函数名不能相同

为了解决上面的问题,C++中提出了一种模版函数,如下面代码

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
using namespace std;

template<class T>
void Swap(T& num1, T& num2)
{
    T tmp = num1;
    num1 = num2;
    num2 = tmp;
}

int main()
{
    int num1 = 1, num2 = 2;
    printf("num1=%d num2=%d\n", num1, num2);
    Swap(num1, num2);
    printf("num1=%d num2=%d\n", num1, num2);
    double num3 = 4.1, num4 = 4.5;
    printf("num3=%.1f num4=%.1f\n", num3, num4);
    Swap(num3, num4);
    printf("num3=%.1f num4=%.1f\n", num3, num4);

    return 0;
}
输出结果
num1=1 num2=2
num1=2 num2=1
num3=4.1 num4=4.5
num3=4.5 num4=4.1

在上面的代码中,将Swap函数作为一种模版,当调用Swap函数时,根据传入的参数类型自动实例化函数从而完成函数执行

模版使用

函数模版

函数模版基础语法
C++
1
2
3
4
5
6
7
template<typename name1, typename name2, ...>
函数返回类型 函数名(形式参数)
{
    //函数体
}

//typename也可以用class代替,但是不可以用struct

在C++中,使用template关键字创建模版,使用<>包裹函数体中需要使用到类型,typename name1用于指代类型,在函数调用时自动匹配类型,默认不会隐式类型转换,模版下方正常写函数即可

Note

模版函数的下方也可以写其他普通函数

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
using namespace std;

//模版
template<class T>
void Swap(T& num1, T& num2)
{
    T tmp = num1;
    num1 = num2;
    num2 = tmp;
}

//普通函数
int add(const int num1, const int num2)
{
    return num1 + num2;
}

int main()
{
    int num1 = 1, num2 = 2;
    printf("num1=%d num2=%d\n", num1, num2);
    Swap(num1, num2);
    printf("num1=%d num2=%d\n", num1, num2);
    double num3 = 4.1, num4 = 4.5;
    printf("num3=%.1f num4=%.1f\n", num3, num4);
    Swap(num3, num4);
    printf("num3=%.1f num4=%.1f\n", num3, num4);

    cout << add(num1, num2) << endl;//可以正常使用

    return 0;
}
输出结果
num1=1 num2=2
num1=2 num2=1
num3=4.1 num4=4.5
num3=4.5 num4=4.1
3
函数模版原理

函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。所以其实模板就是将本来应该我们做的重复的事情交给了编译器

在函数调用的过程中,直接调试时不论是int类型还是double类型都会走到模版,但是进入反汇编可以看到当形参是int类型时,编译器会进入int类型的函数,同样double类型类似

所以,函数模版是告诉编译器应该生成何种类型的函数,如下图所示

在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用

函数模版实例化

用不同类型的参数使用函数模板时,称为函数模板的实例化。

模板参数实例化分为:隐式实例化和显式实例化

隐式实例化:让编译器根据实参类型自动推演出形式参数类型

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

template<class T>
void Swap(T& num1, T& num2)
{
    T tmp = num1;
    num1 = num2;
    num2 = tmp;
}

int main()
{
    int num1 = 1, num2 = 2;
    printf("num1=%d num2=%d\n", num1, num2);
    Swap(num1, num2);//自动推演出int类型
    printf("num1=%d num2=%d\n", num1, num2);

    return 0;
}
输出结果
num1=1 num2=2
num1=2 num2=1

但是,当模版参数类型种类个数与实参种类个数不匹配时,编译器将无法自动推演

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

template<class T>
void add(T& num1, T& num2)
{
    return num1 + num2;
}

int main()
{
    int num1 = 1;
    double num2 = 2.0;
    add(num1, num2);//无法自动推演

    return 0;
}
报错信息
没有与参数列表匹配的 函数模板 "Swap" 实例

在上面的代码中,函数模版中只有一种类型,但是实际调用函数传递的实际参数对应两种类型,此时因为类型不对应编译报错

第一种解决方式:添加额外种类的模版参数

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

template<class T, class R>
T add(T& num1, R& num2)
{
    return num1 + num2;
}

int main()
{
    int num1 = 1;
    double num2 = 2.0;
    cout << add(num1, num2) << endl;//当函数模版有两种参数时可以自动推演
    return 0;
}
输出结果
3

在上面的代码中,类型T被推演为int,类型R被推演为double,但是有个返回值问题,因为函数返回值只能为一种,所以存在精度丢失

第二种解决方式:对某一种类型进行强制转换

以强制转换int类型为例

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

template<class T>
T add(T num1, T num2)
{
    return num1 + num2;
}

int main()
{
    int num1 = 1;
    double num2 = 2.2;
    cout << add((double)num1, num2); << endl;//将int类型转换为double类型
    return 0;
}
输出结果
3.2

第三种解决方式:显式实例化

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

template<class T>
T add(T num1, T num2)
{
    return num1 + num2;
}

int main()
{
    int num1 = 1;
    double num2 = 2.2;
    cout << add<double>(num1, num2) << endl;//强制指定T为double类型此时会隐式转换
    return 0;
}
输出结果
3.2

对于显式实例化来说,如果此时类型依旧不匹配,编译器会尝试进行隐式类型转换,如果无法转换成功编译器将会报错

Note

注意,第二种方式和第三种方式都有强制性,指定的类型时何种类型函数模版就一定是何种类型,当需要使用同类型的引用时,要加上const修饰引用

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

template<class T>
T add(const T& num1, const T& num2)
{
    return num1 + num2;
}

int main()
{
    int num1 = 1;
    double num2 = 2.2;
    const double& ret = add((double)num1, num2);
    cout << ret << endl;//当函数模版有两种参数时可以自动推演
    return 0;
}
输出结果
3.2
模版参数匹配规则
  1. 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
  2. 对于非模板函数和同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
using namespace std;
//同名函数模版和非模版函数
//函数模版
template<class T, class R>
R add(T num1, R num2)
{
    return num1 + num2;
}

//单独处理整型加法
int add(int num1, int num2)
{
    return num1 + num2;
}

int main()
{
    int num1 = 1;
    int num2 = 2;
    cout << add(num1, num2) << endl;//此时编译器会调用单独处理整型加法的函数,而不是根据函数模版推演出新的int形参函数
    double num3 = 2.2;
    cout << add(num1, num3) << endl;//编译器直接推演出不需要强制转换的函数
    return 0;
}
输出结果
3
3.2

类模版

类模版基础语法
C++
1
2
3
4
5
template<typename name1, typename name2>
class 类名
{
    //类体
};

在C++中,使用template关键字创建模版,使用<>包裹类体体中需要使用到的类型,typename name1用于指代类型,在使用类时自动匹配数据类型,默认不会隐式类型转换,模版下方正常写类即可

Note

注意,使用模版类创建类对象时必须显式指定类型

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
using namespace std;

template<class T>
class SeqList
{    
private:
    T* _a;
    int _size;
    int _capacity = 4;
public:
    SeqList()
        :_a(nullptr)
    {
        _a = new T[_capacity];
    }

    ~SeqList()
    {
        delete[] _a;
        _size = _capacity = 0;
    }
};

int main()
{
    //类模版必须显式制定类型
    SeqList<int> s1;//存放int类型数据的顺序表
    SeqList<double> s2;//存放double类型的顺序表

    return 0;
}

Note

类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类 例如上面的代码中有两个类,一个是SeqList<int>,一个是SeqList<double>

如果声明和定义分开时,域作用限定符左侧的域名一定要带上模版参数列表

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//不指定具体类型
SeqList<T>::~SeqList()
{
    delete[] _a;
    _size = _capacity = 0;
}

//指定具体类型
SeqList<int>::~SeqList()
{
    delete[] _a;
    _size = _capacity = 0;
}

SeqList<double>::~SeqList()
{
    delete[] _a;
    _size = _capacity = 0;
}

Note

注意,类模版的声明和定义不能放在两个文件中(即声明在头文件,定义在源文件中),否则会出现链接错误

classtypename 在模板参数中的注意事项

在模板参数中,class关键字用于声明一个类型参数,这与类定义中的class关键字不同。这里的class并不意味着参数必须是一个类类型;它同样可以是任何类型,包括基本数据类型、指针、引用等。

typename关键字也用于模板参数列表中声明类型参数,但它主要用于依赖类型的情况,即模板内部依赖于模板参数的类型。typename的使用主要是为了解决编译器解析依赖类型名称时的歧义。

C++
1
2
3
4
5
template <typename T>
class Container {
    typedef typename T::iterator Iterator; // 使用typename指定依赖类型
    ...
};

classtypename 的区别

  1. 依赖类型typename用于指定依赖类型,而class不能用于这种情况。依赖类型是指依赖于模板参数的类型,如T::iterator

  2. 关键字用途class在模板参数中的用途仅限于声明类型参数,而typename除了这个用途外,还用于其他上下文,如指定依赖类型或模板模板参数中的类型。

  3. 语义清晰性:虽然二者可互换,但在某些情况下使用typename可以提高代码的语义清晰性,尤其是在处理依赖类型时

具体例子可以参考list模拟实现中的迭代器结构部分