• 【C++】了解模板--泛型编程基础


    前言

            hello~(︶.̮︶✽) 欢迎能够点进我的文章!本篇文章我会描述C++模板编程的一些基础内容以及做好泛型编程基础的学习,准备好了吗?我们一起学习吧~

    --------------------------------------------------------------------------------------------------------------------------------

    目录

    前言

    一、了解模板&&什么是泛型编程

    1.引入

    2.结论

    二、函数模板

    1.函数模板的格式和简单使用

    2.模板函数的原理以及实例化

    1实现原理

    2隐式实例化

    3显示实例化

    3.模板函数的参数匹配

    三、类模板


    一、了解模板&&什么是泛型编程

    1.引入

            模板,在类与对象的时候就已经提过,一种类实际上就是对一类事物的抽象化,是一张图纸。那么模板是不是有着类似的定义呢?

            首先,我们从交换函数入手:

    1. #include
    2. using namespace std;
    3. void Swap(int& a, int& b)
    4. {
    5. int temp = a;
    6. a = b;
    7. b = temp;
    8. }
    9. int main()
    10. {
    11. int a = 1;
    12. int b = 0;
    13. cout << a << " " << b << endl;
    14. Swap(a, b);
    15. cout << a << " " << b << endl;
    16. return 0;
    17. }

             此时是传int引用返回,以便实际交换main函数中的ab值。但是我们此时就可以发现类型已经固定死了,无法在进行改变。此时想要浮点类型进行转化的话要么改变函数名字,要么进行函数重载。

    1. void Swap(double& a, double& b)
    2. {
    3. double temp = a;
    4. a = b;
    5. b = temp;
    6. }

             那么我们此时就进行了一个函数重载,此时便就可以实现我们想要交换int类型的就交换int类型的,想交换double类型的就交换double类型:

             但是会发现,这样我们自己手动去实现,逻辑是相似的,但是我们还是得手动的去写,难免会不方便。针对于此,模板就自然的就出现了。

            有人肯定想问,typedef不是也可以解决这个问题吗?虽然代码可以不用逻辑重复实现,但是不能同时出现int类型交换和double类型交换呀,也就是说,每一次运行程序typedef就会固定某种类型去替换,这样的做法是不可取的哦~

            综合上面的说法,也就是说我们现在的需求就是:需要一个能够随时识别类型,在运行程序时自动替换类型,即动态变化的,此逻辑也就便是像一张图纸,实现方式一致,只不过看你的材料是什么了。

    2.结论

            针对于上面这一类差别不大的逻辑实现,但是具体类型会变化的编程,为泛型编程。

    泛型编程是一种编程风格,其中算法以尽可能抽象的方式编写,而不依赖于将在其上执行这些算法的数据形式。模板就是泛型编程的基础。

            那么现在就进行一种泛型编程--不干类似的事情、代码复用(仅仅是数据类型)template模板

            格式如下:

            template or template

    其中 template表示模板typenameclass任意一个使用代表模板类型,一个类型就代表一种类型。(比如int就是一种类型,char就是另一种)

    在此语句之后t或者c就可以统一代表一类数据类型了。

            这个模板就可以类比于古代技术:古人智慧--活字印刷术。

            模板的应用就有两种:函数模板类模板

    二、函数模板

    1.函数模板的格式和简单使用

            那么,我们现在以上面的Swap函数为例,看看如何实现一个函数模板吧:

    1. template<typename T>
    2. void Swap(T& x, T& y)
    3. {
    4. T temp = x;
    5. x = y;
    6. y = temp;
    7. }
    8. int main()
    9. {
    10. int a = 1, b = 0;
    11. double c = 1.1, d = 2.2;
    12. cout << a << " " << b << endl;
    13. cout << c << " " << d << endl;
    14. Swap(a, b);
    15. Swap(c, d);
    16. cout << a << " " << b << endl;
    17. cout << c << " " << d << endl;
    18. return 0;
    19. }

    基本实现格式:

    template

    函数实现(类型为T或者自行定义)

    注意:

            template是关键字,表示后面实现的就是模板,classtypename可以任选,并且同时出现也没有问题。

             可以发现,我们原本是要实现函数重载或者不同的函数才能实现如上的效果,现在只需要一个模板函数就可以。那么,现在提出一个问题:这两个函数调用调用的是同一个函数吗?--自然不是。

            这就要和此模板函数的实现原理来讲了:

    2.模板函数的原理以及实例化

    1实现原理

            正如我们上面所说,模板也就好比一个蓝图,然后编译器根据我们输入进来的材料,造成什么样子的函数。

            比如编译器就是那个工程师,我们给他输入黑色材料,就是黑色的房子,输入红色材料出来的就是红色的房子。

            那么我们简单实现的Swap函数模板,在编译器这里就变成了:

            编译器根据我们传入此函数模板的类型,自动推演和分析类型,然后实现类似宏替换的方式,将对应类型的函数实现。

            然后就可以解释之前所提出的那个问题了:调用的自然不是同一个函数哦~

            其实,Swap函数库里实现的有,可以直接调用即可:

    1. int main()
    2. {
    3. int a = 1, b = 0;
    4. double c = 1.1, d = 2.2;
    5. cout << a << " " << b << endl;
    6. cout << c << " " << d << endl;
    7. swap(a, b);
    8. swap(c, d);
    9. cout << a << " " << b << endl;
    10. cout << c << " " << d << endl;
    11. return 0;
    12. }

             底层也是实现类似的函数模板哦~

            那么,我们现在来具体谈一谈函数模板的实例化吧:

    2隐式实例化

            所谓的隐式实例化,就是我们原理所说的那样,编译器自动识别我们传入的类型,然后推导函数实现的类型

            比如如下我们实现一个加法程序:

    1. template<typename T>
    2. T Add(const T& x, const T& y)
    3. {
    4. return x + y;
    5. }
    6. int main()
    7. {
    8. cout << Add(1, 2) << endl;
    9. cout << Add(1.1, 1.2) << endl;
    10. return 0;
    11. }

            

            可以发现前两种的隐式实例化没有任何问题,那么如果我给上如下代码呢?

        cout << Add(1, 1.1) << endl;

             发生了错误,调用‘Add(int, double)’没有匹配的函数。

            没有匹配就说明编译器没有生成,既然没有生成,就说明在推导类型的时候就出错了。因为此时传入的就是int类型和double类型,因为匹配的是同一个模板类型T,所以编译器自己就混乱了。为了帮助这个笨笨的编译器,你决定如何做呢?

            1.我们手动转化,2.设置两个类型, 3.显示实例化

            手动转化很简单,我们随机挑选一个1或者1.1强转成另外一个类型即可:

        cout << Add((double)1, 1.1) << endl; 

      

            那么设置两个类型是如何呢?如果是一个我们自己实现的比如int类型的加法函数,能否执行上述的情况呢?

    1. template<typename T>
    2. T Add(const T& x, const T& y)
    3. {
    4. return x + y;
    5. }
    6. int Add(const int& x, const int& y)
    7. {
    8. return x + y;
    9. }
    10. int main()
    11. {
    12. cout << Add(1, 2) << endl;
    13. cout << Add(1.1, 1.2) << endl;
    14. // cout << Add((double)1, 1.1) << endl;
    15. cout << Add(1, 1.1) << endl;
    16. return 0;
    17. }

            

             别忘了当类型不同的时候,会发生隐式类型转化哦,生成的临时变量就是会储存转化成对应类型的变量,这里自然就是double转化为int,大类型转化为int会发生数据丢失。但是这里必须设置为const,首先是因为引用,引用就要遵循访问权限,即转化为临时变量后,具有常性,如果不带const就代表权限基本比临时变量高了,所以自然不行:

    1. int Add(const int& x, int& y)

            no known conversion for argument 2 from ‘double’ to ‘int&’:没有已知的参数 2 从“double”到“int&”的转换。

            那么,我们所谓的设置两个类型就是两个模板类型,实现上述的隐式转换功能:

    1. template<typename T1, typename T2>
    2. T1 Add(const T1& x, const T2& y)
    3. {
    4. return x + y;
    5. }
    6. int main()
    7. {
    8. cout << Add(1, 2) << endl;
    9. cout << Add(1.1, 1.2) << endl;
    10. // cout << Add((double)1, 1.1) << endl;
    11. cout << Add(1, 1.1) << endl;
    12. return 0;
    13. }

            此时发生的类型转化位于return,int + double 小的数据类型会转化为大的数据类型(int < double),即转化为double类型的和,但是出去的时候类型为T1,因为此时自动匹配的为int,所以发生截断。当然,T1也可以换成T2,此时就是double类型了:

    1. template<typename T1, typename T2>
    2. T2 Add(const T1& x, const T2& y)
    3. {
    4. return x + y;
    5. }

            其实,这种方法通过隐式转化也表达了如果模板参数只有一个的话就不能发生隐式类型转换,因为此时编译器考虑的是哪个给T这个模板类型,而不是哪个转化为哪个类型!

             12两种方法均已试过了,现在来看看第三种方法吧:

    3显示实例化

            显示实例化就是在编译器自动识别即推导传入类型之前,用户指定其构造成什么类型的模板函数。格式:

    模板函数名<类型> (传参);

            比如,上面的Add函数我们就可以显示进行实例化(我们用户指定编译器构造成哪种类型的函数)

    1. cout << Add<int>(1, 1.1) << endl;
    2. cout << Add<double>(1, 1.1) << endl;

            在后面,我们使用类模板需要使用到显示实例化(到时候细讲为何)

    3.模板函数的参数匹配

            经过上面的实例化后,我们知道,在编译器将模板类型转化为对应的类型时,就会构造出一个关于此类型的函数。那么,允许同时存在一个非模板函数吗?当类型配对的时候又调用哪个呢?

    1. #include
    2. using namespace std;
    3. int Add(const int& x, const int& y)
    4. {
    5. return x + y;
    6. }
    7. template<typename T>
    8. T Add(const T& x, const T& y)
    9. {
    10. return x + y;
    11. }
    12. int main()
    13. {
    14. cout << Add(1, 2) << endl;
    15. cout << Add(1.2, 1.4) << endl;
    16. cout << Add<int>(2, 3) << endl;
    17. return 0;
    18. }

            可以发现,是允许存在的,并且,在进行匹配时,将优先匹配自己实现的函数,即条件如果符合非模板函数,会优先调用非模板函数。 

            如下使用gdb调试器进行证明:

     

            当我们-g编译成debug版本后,进入gdb,然后给第15行打上断点,run进行运行到此处,然后逐语句(step)进入此函数可以查看此时调用的Add是谁:

     

            果然,进入的就是第6行,第6行也就是 我们自己实现的int类型的Add函数,是非模板函数,因为此时条件满足,会优先调用非模板函数。

            自然,当不存在自己实现的类型时,模板函数也可以调用的:

            如上,执行第16行时是double类型,因为自己没有实现,就进行隐式实例化了,编译器便就转化为double类型进行构建此函数。

            并且,模板函数也可以被实例化为和非模板函数同类型的存在。 

             那么,如果我们实在想用模板函数呢(在有自己实现的对应类型的情况下),那么就和我们的代码最后一个显示实例化一样了,此时我们调试自然就会发现调用的是模板函数所构造的函数哦:

             综上:

    注意,优先看是否实现参数对应的函数,然后才看模板函数
        --原则:有现成就行、没有就自己做   ---   模板函数同样符合重载相似的效果

     

    三、类模板

            既然对函数有模板,自然少不了类。C++也是面向对象的语言,自然离不开类。类是一个种类,那么一个类模板就相当于一个类家族一样,只是类型不同而已。而且不同的是,类实例化使用的是构造函数,与类型无多少挂钩,所以类模板需要显示实例化

            如下,简单实现一个栈,利用上类模板来确定其中的参数类型:

    1. #include
    2. #include
    3. #include
    4. using namespace std;
    5. template<class T>
    6. class Stack
    7. {
    8. public:
    9. Stack(int capacity = 0)//防止传进来负数,利用判断进行初始化,不使用初始化列表
    10. :_a(NULL)
    11. ,_top(0)
    12. ,_capacity(0)
    13. {
    14. if (capacity > 0)
    15. {
    16. _a = new T[capacity];
    17. _capacity = capacity;
    18. }
    19. }
    20. ~Stack()
    21. {
    22. delete[] _a;
    23. _a = NULL;
    24. _top = _capacity = 0;
    25. }
    26. void Push(const T& x);
    27. const T& Top()//传引用返回的话一定要注意加上const,否则外界可以随意修改私有成员了
    28. {
    29. assert(_top > 0);
    30. return _a[_top - 1];
    31. }
    32. void Pop()
    33. {
    34. assert(_top > 0);
    35. --_top;
    36. }
    37. private:
    38. T* _a;//利用数组存放
    39. size_t _top;//储存当前数据个数
    40. size_t _capacity;//当前的容量
    41. };
    42. template<class T>
    43. void Stack::Push(const T& x)
    44. {
    45. if (_top == _capacity)//没有内存或者满了
    46. {
    47. size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
    48. //手动扩展 c语言里面是使用realloc,此处c++使用new出一个大的空间,在将原有数据拷贝进去
    49. T* temp = new T[newcapacity];
    50. if (_a)//防止原本空的被拷贝进去了
    51. {
    52. memcpy(temp, _a, sizeof(T)*_top);
    53. delete[] _a;
    54. }
    55. _a = temp;
    56. _capacity = newcapacity;
    57. }
    58. _a[_top++] = x;
    59. }
    60. int main()
    61. {
    62. Stack<int> a;
    63. Stack<int> b(3);
    64. Stack<char> c(2);
    65. b.Push(1);
    66. b.Push(2);
    67. b.Push(3);
    68. b.Push(4);
    69. c.Push('a');
    70. c.Push('b');
    71. c.Push('c');
    72. return 0;
    73. }

            首先就可以发现,我们在进行类模板的实例化的时候,需要给其指定模板类型,此时才能真正算一个类,之前就是一个类模具而已。

            然后需要注意的是,通常类模板里面的模板函数不支持分离文件(即在不同的文件里进行实现,但是为了不想加入inline的话,在同一个文件内是支持的,如上)

    分离写的话,需要指定类域和模板类型:

    template

    类实现

    {

            .....

            模板函数
    };

    template//需要在写一遍,否则会报错哦~

    返回类型 类模板名模板函数名

    {

            //实现

            .....

    }

    注意:类模板不支持分离编译(两个文件实现),同一个文件分离可以,需要上面一样声明。

            此时调试均可发现初始化为对应的类型哦~

            模板初阶了解完毕!未完待续!(●'◡'●)

  • 相关阅读:
    “论软件系统架构评估”精选范文,软考高级论文,系统架构设计师论文
    linux安装达梦数据库8
    概率论的学习整理4:全概率公式
    金仓数据库 KingbaseES 插件ftutilx
    博客之QQ登录功能(二)
    查看和修改自己的git提交时的作者信息
    算法-矩阵置零
    解决android中 ExifInterface.setAttribute()无效问题
    【Linux kernel/cpufreq】framework ----初识
    Windows 下 MySQL 8.1 图形化界面安装、配置详解
  • 原文地址:https://blog.csdn.net/weixin_61508423/article/details/126242220