• 【C++】模板初阶 -- 详解


    一、泛型编程

    C 语言不支持泛型编程,C++ 支持泛型编程。

    实现一个通用的交换函数: 

    1. void Swap(int& left, int& right)
    2. {
    3. int temp = left;
    4. left = right;
    5. right = temp;
    6. }
    7. void Swap(double& left, double& right)
    8. {
    9. double temp = left;
    10. left = right;
    11. right = temp;
    12. }
    13. void Swap(char& left, char& right)
    14. {
    15. char temp = left;
    16. left = right;
    17. right = temp;
    18. }
    使用函数重载虽然可以实现,但是有几个 不好 的地方
    1. 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数。
    2. 代码的可维护性比较低,一个出错可能所有的重载均出错。

    而且上述函数它们的逻辑相似,唯一不同的就是待交换元素的类型。

    能否告诉编译器一个模具,让编译器根据不同的类型利用该模具来生成对应的代码呢? 

    在 C++ 中,存在这样一个模具,叫做模板(template )通过给这个模具中填充不同材料(类型),来获得不同材料的铸件(即生成具体类型的代码),这会给我们节省很多时间。
    泛型编程 :编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础。


    二、函数模板(Function Template)

    1、概念

    函数模板不是一个实在的函数,编译器不能为其生成可执行代码。定义函数模板后只是一个对函数功能框架的描述,当它具体执行时,将根据传递的实参类型决定其功能。
    函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。

    2、函数模板格式

    1. template <typename T1, typename T2, ......, typename Tn>
    2. 返回值类型 函数名(参数列表)
    3. {
    4. 函数体
    5. }

    注意

    • 其中 template typename 都是关键字
    • typename 是用来定义模板参数关键字,也可以使用 class 关键字代替在这里 typename 和 class 没有区别(切记:不能使用 struct 代替 class)。 
    1. template <typename T> // T代表一个模板类型(虚拟类型),具体是什么类型,得实例化了才知道
    2. void Swap(T& left, T& right)
    3. {
    4. T temp = left;
    5. left = right;
    6. right = temp;
    7. }
    8. int main()
    9. {
    10. double d1 = 2.0;
    11. double d2 = 5.0;
    12. Swap(d1, d2);
    13. int i1 = 10;
    14. int i2 = 20;
    15. Swap(i1, i2);
    16. char a = '0';
    17. char b = '9';
    18. Swap(a, b);
    19. return 0;
    20. }
    上面三次调用是同一个函数模板吗?

    不是。


    3、函数模板的原理

    函数模板并不是一个实在的函数,而是一个 对函数功能框架的描述,是 编译器根据实参类型产生具体类型函数的模具
    其实模板就是将本来应该我们做的重复的事情交给了编译器, 提高了编程效率

    编译阶段经历如下过程:

    • 先进行模板推演,推演 T 的具体类型是什么。

    • 推演出 T 的具体类型后,再实例化生成具体的函数

    编译器编译 阶段,对于模板函数的使用, 编译器 需要根据传入的实参类型来 推演生成 对应类型的函数以供调用。比如:传入 double 类型实参来使用函数模板时 ,编译器通过对实参类型的推演,将 T 确定为 double 类型,然后产生一份专门处理 double 类型的代码,对于 int、char 类型等也是如此。

    4、函数模板的实例化

    不同类型的参数 使用函数模板时,称为 函数模板的实例化
    模板参数实例化分为:隐式实例化和显式实例化
    (1)隐式实例化

    让编译器根据实参推演模板参数的实际类型

    1. template <class T>
    2. T Add(const T& a, const T& b)
    3. {
    4. return a+ b;
    5. }
    6. int main()
    7. {
    8. int a = 10;
    9. double d = 10.0;
    10. cout << Add(a, d) << endl; // 编译失败,模板参数T不明确
    11. // 用强制类型转换生成的临时变量作为实参传递
    12. Add(a, (int)d);
    13. Add((double)a, d);
    14. return 0;
    15. }

    无法通过编译的原因:

    因为在编译期间,当编译器看到该实例化时,需要推演其实参类型。通过实参 a 将 T 推演为 int,通过实参 d 将 T 推演为 double 类型,但模板参数列表中只有一个 T,编译器无法确定此处到底该将 T 确定为 int 或者 double 类型而报错。

    注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅。


    (2)显式实例化

    函数名后的 <> 中指定模板参数的实际类型

    有些函数模板的参数列表中没有用模板参数,在函数体中才有用,所以无法通过实参来推演 T 的类型,只能显式实例化。  

    1. template <class T>
    2. T func(int x)
    3. {
    4. T a(x);
    5. return a;
    6. }
    7. int main()
    8. {
    9. // func(1); // error
    10. func <int>(1); // 指定模板参数的实际类型为int
    11. func <double>(1); // 指定模板参数的实际类型为double

    5、模板参数的匹配原则

    (1)一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。

    模板匹配原则:

    1. 有现成完全匹配的,就直接调用。
    2. 没有现成完全匹配的,调用模板实例化生成的。
    1. // 专门处理int的加法函数
    2. int Add(int a, int b)
    3. {
    4. return a + b;
    5. }
    6. // 通用加法函数
    7. template <class T>
    8. T Add(T a, T b)
    9. {
    10. return a + b;
    11. }
    12. int main()
    13. {
    14. cout << Add(1, 2) << endl; // 与非模板函数匹配,编译器不需要特化
    15. cout << Add <int>(1.1, 2.2) << endl; // 调用编译器特化的Add版本
    16. Add(1.1, 2.2); // 没有现成匹配的,优先选择编译器特化的Add(double, double)函数
    17. return 0;
    18. }

    (2)对于非模板函数同名函数模板,如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板。
    1. // 专门处理int的加法函数
    2. int Add(int a, int b)
    3. {
    4. return a + b;
    5. }
    6. // 通用加法函数
    7. template <class T1, class T2>
    8. T1 Add(T1 a, T2 b)
    9. {
    10. return a + b;
    11. }
    12. int main()
    13. {
    14. Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
    15. Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
    16. return 0;
    17. }

    (3)模板函数不允许自动类型转换,但普通函数可以进行自动类型转换。
    1. int Add(const int& a, const int& b)
    2. {
    3. return a + b;
    4. }
    5. int main()
    6. {
    7. int a = 10;
    8. double d = 10.0;
    9. Add(a, d); // 类型不匹配,编译器进行隐式类型转换
    10. return 0;
    11. }

    三、模板(Class template)

    1、概念

    类模板是对一批仅仅成员数据类型不同的类的抽象,程序员只要为这一批类所组成的整个类家族创建一个类模板,给出一套程序代码,就可以用来生成多种具体的类(这些类可以看作是类模板的实例),从而大大提高编程的效率。

    类模板的成员函数是按需实例化:只有当程序用到它时才进行实例化。

    当类模板的成员函数(包括普通成员函数、成员函数模板)被调用时(即程序中出现了对该成员函数 / 函数模板的调用代码时),编译器才会帮我们把这些函数的具体实现代码进行实例化。若模板类中的某函数在程序中从未被调用过,那么编译器就不会实例化(生成)该成员函数的具体代码。

    类模板成员函数的模板形参由调用该函数的对象的类型确定。对象的模板实参能够确定成员函数的模板形参。

    注意类模板中的成员函数都是函数模板。 


    2、类模板的定义格式

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

    (1)举例一 
    1. // 注意:Stack不是具体的类,是编译器根据被实例化的类型生成具体类的模具
    2. template <class T>
    3. class Stack
    4. {
    5. public:
    6. Stack(size_t capacity = 10)
    7. :_a(new T[capacity])
    8. ,_top(0)
    9. ,_capacity(capacity)
    10. {}
    11. ~Stack(); // 析构函数,在类中声明,类外定义
    12. // ...
    13. private:
    14. T* _a;
    15. size_t _top;
    16. size_t _capacity;
    17. };
    18. // 注意:类模板中函数放在类外进行定义时,需要加模板参数列表
    19. template <class T>
    20. Stack::~Stack()
    21. {
    22. if (_a)
    23. {
    24. delete[] _a;
    25. _a = nullptr;
    26. }
    27. _top = _capacity = 0;
    28. }
    29. int main()
    30. {
    31. // 类模板的使用都是显式实例化
    32. // Stack是类名,Stack才是类型
    33. Stack<int*> st1;
    34. Stack<int> st2;
    35. return 0;
    36. }

    (2)举例二 
    1. template <class T>
    2. class Vector
    3. {
    4. public :
    5. Stack(size_t capacity = 0)
    6. {
    7. if (capacity > 0)
    8. {
    9. _a = new T[capacity];
    10. _capacity = capacity;
    11. _top = 0;
    12. }
    13. }
    14. ~Stack()
    15. {
    16. delete[] _a;
    17. _a = nullptr;
    18. _capacity = _top = 0;
    19. }
    20. void Push(const T& x); // 在类里面声明,类外面定义
    21. void Pop()
    22. {
    23. assert(_top > 0);
    24. --_top;
    25. }
    26. bool Empty()
    27. {
    28. return _top == 0;
    29. }
    30. const T& Top()
    31. {
    32. assert(_top > 0);
    33. return _a[_top - 1];
    34. }
    35. private:
    36. T* _a = nullptr;
    37. size_t _top = 0;
    38. size_t _capacity = 0;
    39. };
    40. // 注意:类模板中函数放在类外进行定义时,需要加模板参数列表
    41. template <class T>
    42. void Stack::Push(const T& x)
    43. {
    44. if (_top == _capacity)
    45. {
    46. size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2; // 开新空间
    47. T* tmp = new T[newCapacity];
    48. if (_a)
    49. {
    50. memcpy(tmp, _a, sizeof(T)*_top); // 拷贝数据
    51. delete[] _a; // 释放旧空间
    52. }
    53. _a = tmp;
    54. _capacity = newCapacity;
    55. }
    56. _a[_top] = x;
    57. ++_top;
    58. }

    注意函数 / 类模板不支持分离编译。

    比如:声明放在 .h ,定义放在 .cpp。 在 .h 里实例化了,但在 Stack.cpp 里却没有实例化,而 test.cpp 去找的时候,只有声明,没有定义,会报链接错误如果声明和定义分离,需要将模板写在同一个文件里。


    2、类模板的实例化

    类模板实例化与函数模板实例化不同, 类模板实例化 需要在 类模板名字后跟 <> ,然后 实例化的类型放在 <> 中 即可,类模板名字不是真正的类,而实例化的结果才是真正的类
    1. Vector<int> s1;
    2. Vector<double> s2;
    3. // 注意:Vector不是具体的类,是编译器根据被实例化的类型生成具体类的模具

    Vector 是类名,Vector 才是类型。

    • 对于普通类,类名就是类型。

    • 对于类模板,类名不是类型,类名才是类型。

    注意:一个模板,如果没有实例化,编译器是不会去检查它内部的语法的。 

  • 相关阅读:
    【分享】集简云小程序识别名片到CRM流程搭建示例
    Django DRF JWT 认证
    Java基础(二十六):正则表达式
    满意度从50%到90%,客服系统是怎么做到的
    Git常用命令及解释
    JAVA——通过自定义注解实现每次程序启动时,自动扫描被注解的方法,获取其路径及访问该路径所需的权限并写入数据库
    在Linux上安装RStudio工具并实现本地远程访问【内网穿透】
    云原生Kubernetes:K8S安全机制
    【Python Web】Flask框架(一)快速开发网站
    【RocketMQ】消息的存储
  • 原文地址:https://blog.csdn.net/weixin_74531333/article/details/132938860