• 【C++】模板:了解泛型编程


    本篇是C++模板学习的一些笔记

    1.了解泛型编程

    泛型编程,故如其名,是一个泛化的编程方式。其实现原理为程序员编写一个函数/类的代码示例,让编译器去填补出不同的函数实现

    就好比活字印刷术,可以灵活调整印刷的板块和内容,比只能固定印刷某一个内容的雕版印刷术效率更高,也让印刷术由此得到了更广泛的应用。

    image-20220627090604430

    在C++中,函数重载和模板的出现,让泛型编程得到了实际的应用。其中模板,就是类似活字印刷术一样的存在。

    2.函数模板

    八八了那么多没用的,让我们来看看函数模板的语法实现吧

    2.1简单示例

    下面是一个最简单的交换函数的例子,通过标明模板参数T,让编译器自动识别函数传参,并调用出不同的函数

    template<typename T>
    void Swap(T& left,T& right)
    {
        T temp = left;
        left = right;
        right = temp;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    其中,typename是定义模板的关键字,我们可以使用class来替代,但不能使用struct

    image-20220627091101814

    可以看到,编译器成功调用了Swap函数,交换了int类型和double类型

    2.2多个模板参数

    如果我们尝试把int和double同时传参给这个函数,会发生什么呢?

    image-20220627091355313

    编译器会报错,表示模板参数T不明确

    这时候我们有几种解决方法

    • 首先是将double强转为int(反过来亦可)

    image-20220627091752759

    你会发现还是不行,那是因为强转并不支持用double引用int。所以我们把函数传参中的引用去掉,即可正常调用这个函数(暂且不提传引用和传值的区别)

    image-20220627093341796

    • 使用多个模板参数

    和函数传参类似,我们也可以设置多个模板参数

    在下图中,我使用typeid关键字来打印模板参数T1和T2的类型。

    使用typeid需要包含头文件#include <typeinfo>

    image-20220627092844081

    可以看到,实际上函数在调用这个模板的时候,已经实例化了这个函数(即替换模板参数为正确参数类型)这时候在后台处理的时候,其实Show函数已经实例化为了下面这个样子

    void Show(int left, double right)
    {
        cout << typeid(left).name() << endl;
        cout << typeid(right).name() << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.3模板实例化

    上面的方式,是编译器自动帮我们实例化模板参数。在实际使用中,我们还可以自己指定实例化为什么类型

    • 利用强制类型转换
    • 使用<int>直接指定实例化为int类型

    image-20220627094408745

    使用第二种方式的时候,编译器会对另外一个不匹配的参数进行隐式类型转换。如果转换不成功,则会报错。

    另外注意的是,函数模板参数T同样可以用来作为返回值,但是不能通过返回值来推断参数T的类型。比如下面这个函数,我们在使用的时候就需要直接指定模板参数T,而不能写一个int* ptr=test(10)让编译器通过“返回值是int*接收的,所以函数模板参数T是int”来推断。

    template<typename T>
    T* test(int num)
    {
    	return new T[num];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 函数模板支持给予参数缺省值

    当一个参数不确定的时候,函数模板是支持给予缺省值的

    template<typename T=char>
    T* test(int num)
    {
    	return new T[num];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    比如这样,当我们没有直接指定的时候,编译器就会将T作为char类型,返回一个num大小的char(一个字节)的空间

    注意:当有多个模板参数时,缺省值需要从右往左给

    • 函数模板的传参也支持缺省值
    template<typename T1>
    void Add(T1 left, T1 right=10)
    {
        cout << "Add temp "<<typeid(left).name() << " " << typeid(right).name() << endl;
        cout << left + right << endl << endl;
    }
    int main()
    {
        int a=1;
        Add(a);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这种情况下,编译器会正确调用该函数模板

    image-20220627104218098

    2.4模板和普通函数同时存在

    以Add函数为例,在函数模板存在的同时,我们还可以单独写一个int类型的add函数。这都归功于函数重载的存在。

    同时,我们还可以使用<int>来指定函数模板重载为已存在的Add函数。因为本质上这两个函数是不同的,并不会冲突。

    image-20220627104304580

    函数在调用的时候,首先会去调用已经存在的函数。当参数和已存在的函数不匹配时,才会调用函数模板

    2.5函数模板不支持定义和声明分离

    一般情况下,我们都会在头文件中生命函数,在另外一个源文件中定义函数。

    但是模板是不支持这么做的!编译器会报错 链接错误

    error LNK2019:无法解析的外部符号……
    
    • 1

    所以我们需要将函数模板的声明和定义放在一个头文件中。在部分使用场景,会使用.hpp来表示这个头文件是包含了函数定义的(即.h和.cpp的集合体)。需要注意,这并不是一个硬性要求,你也可以直接使用.h,并将声明和定义放入其中。

    • 这是为什么呢?

    因为单独的.h声明会在源文件顶部展开,而此时函数模板正常推演参数,但编译器并没有找到函数的实现,即这是一个没有地址的函数。从而导致编译器找不到函数的地址,产生了符号表的链接错误

    • 有无解决办法?

    其实是有的,我们可以在模板函数定义的.cpp中对我们需要使用的函数进行显式实例化指定

    //头文件
    //声明
    template<typename T1>
    void Add(T1 left, T1 right);
    
    //源文件
    //定义
    template<typename T1>
    void Add(T1 left, T1 right)
    {
       cout << left + right << endl << endl;
    }
    //在源文件中显式实例化
    template
    void Add<int>(int left, int right);
    template
    void Add<double>(double left, double right);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    显式实例化需要对我们要用的所有函数进行实例化,比如你需要用double类型,只显示实例化了int类型是不行的,依旧会报错。

    这样感觉非常多余……对吧!所以还是老老实实把声明和定义放在同一个文件里面吧!

    3.类模板

    类模板的基本形式如下,这里作为一个小区分,我用class来当作模板参数名。实际上typename也是可以的

    template<class T1, class T2, ...>
    class 类模板名
    {
    // 类内成员定义
    };     
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3.1简单示例

    下面用一个非常简单的顺序表代码来演示一下类模板

    template<class T>
    class List
    {
    public:
        List(int capacity = 10)
            : _a(new T[capacity])
            , _size(0)
            , _capa(capacity)
        {}
     
        ~List();
    
        T& operator[](int pos)
        {
            assert(pos < _size);
            return _a[pos];
        }
    private:
        T* _a;
        int _size;
        int _capa;
    };
    
    //类模板中函数放在类外进行定义时,需要加模板参数列表
    template <class T>
    List<T>::~List()
    {
        delete[] _a;
        _size = _capa = 0;
    }
    
    • 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

    可以看到,通过显式实例化的方式,我们成功让这个类模板变成了两个不同类型的顺序表

    image-20220627102953666

    3.2成员函数声明和定义分离

    其中需要注意的是析构函数,声明和定义分离的时候(同一文件),在定义的时候也需要加上模板参数

    //类模板中函数放在类外进行定义时,需要加模板参数列表
    template <class T>
    List<T>::~List()
    {
        delete[] _a;
        _size = _capa = 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    个人觉得这样也非常麻烦,既然模板最好是声明和定义放在同一个文件,那还不如直接将类的成员函数直接定义到类内部。多省事!

    • 如果是声明和定义放在不同文件中,显式实例化方式如下
    template
    class List <int>;
    template
    class List <double>;
    
    • 1
    • 2
    • 3
    • 4

    需要什么类型的类,就得实例化这个类型。

    4.等待添加……

    模板还有更多值得学习的内容,待我先捣鼓一下,再回来更新这篇博客

  • 相关阅读:
    数据结构薄弱知识点
    [李宏毅老师深度学习视频] 类神经网络训练不起来的四大原因 【手写笔记】
    【Linux】Nignx及负载均衡&动静分离
    Trinitycore学习之在vscode查看远端服务器上源码配置
    Excel·VBA二维数组组合函数的应用实例
    我做的百度飞桨PaddleOCR .NET调用库
    智能井盖传感器:数智赋能让城市管理更智慧
    Hadoop 3.0.0 (单机版)安装与配置与基础使用
    i912900hx和i912900h差距
    儿童护眼灯什么光源好?亮度柔和的护眼台灯分享
  • 原文地址:https://blog.csdn.net/muxuen/article/details/125479821