• 【 C++ 】模板初阶 —— 函数模板、类模板


    目录

    1、泛型编程

    2、函数模板

            函数模板概念

            函数模板格式

            函数模板的原理

            函数模板的实例化

            模板参数的匹配原则

    3、类模板

            类模板的定义格式

            类模板的实例化

    4、补充

            函数模板一定是推演?类模板一定是指定?

            模板的分离编译


    1、泛型编程

    泛型编程:不再是针对某种类型,能适应广泛的类型

    • 如下的交换函数:
    1. //交换int类型
    2. void Swap(int& left, int& right)
    3. {
    4. int temp = left;
    5. left = right;
    6. right = temp;
    7. }
    8. //利用C++支持的函数重载交换double类型
    9. void Swap(double& left, double& right)
    10. {
    11. double temp = left;
    12. left = right;
    13. right = temp;
    14. }

    使用函数重载虽然可以实现不同类型的交换函数,但是有以下几个不好的地方:

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

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

    如果在C++中,也能够存在这样一个模具,通过给这个模具中填充不同材料(类型),来获得不同材
    料的铸件(即生成具体类型的代码),那将会节省许多头发。巧的是前人早已将树栽好,我们只需
    在此乘凉。

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

    模板分为如下两类:

    • 函数模板
    • 类模板

    2、函数模板

    函数模板概念

    函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。

    函数模板格式

    1. template<typename T1, typename T2,......,typename Tn>
    2. 返回值类型 函数名(参数列表)
    3. {
    4. //……
    5. }
    6. 注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)

    因此,交换函数就可以这样套用模板:

    1. template<typename T>//或者template<class T>
    2. void Swap(T& left, T& right)
    3. {
    4. T tmp = left;
    5. left = right;
    6. right = tmp;
    7. }

    调用情况如下:

    函数模板的原理

    问题:我上述交换函数调用过程中的Swap都是调用的同一个函数吗?

    当然不是,这里我三次Swap不是调用同一个函数,其实我Swap的时候根据不同的类型通过模板定制出专属你的类型的函数,然后再调用,这里可以通过反汇编观察到:

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

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

    • 补充:

    其实库里面有一个swap函数,因此我们也不需要自己写模板了:

    直接套用swap即可:

    1. int main()
    2. {
    3. int a = 1, b = 5;
    4. double c = 1.2, d = 6.66;
    5. swap(a, b);
    6. swap(c, d);
    7. }

    函数模板的实例化

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

    • 1、隐式实例化:让编译器根据实参推演模板参数的实际类型
    1. template<class T>
    2. T Add(const T& left, const T& right)
    3. {
    4. return left + right;
    5. }
    6. int main()
    7. {
    8. int a1 = 10, a2 = 20;
    9. double d1 = 10.0, d2 = 20.0;
    10. Add(a1, a2); //编译器推出T是int
    11. Add(d1, d2); //编译器推出T是double
    12. }

    但是我调用的时候如若这样就会出错:

    1. int main()
    2. {
    3. int a1 = 10, a2 = 20;
    4. double d1 = 10.0, d2 = 20.0;
    5. Add(a1, d1); //err 编译器推不出来
    6. /*
    7. 该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
    8. 通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有
    9. 一个T,编译器无法确定此处到底该将T确定为int 或者 double类型而报错
    10. 注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅
    11. */
    12. }

    编译器无法确定这里的T到底是int还是double。此时有两种处理方式:

    法一:用户自己来强制转化

    1. int main()
    2. {
    3. int a1 = 10, a2 = 20;
    4. double d1 = 10.0, d2 = 20.0;
    5. Add(a1, (int)d1); //强制类型转换。或者Add((double)a1, d1);
    6. }

    法二:使用显式实例化

    • 2、显示实例化:在函数名后的<>中指定模板参数的实际类型
    1. template<class T>
    2. T Add(const T& left, const T& right)
    3. {
    4. return left + right;
    5. }
    6. int main()
    7. {
    8. int a1 = 10, a2 = 20;
    9. double d1 = 10.0, d2 = 20.0;
    10. //显示实例化
    11. Add<int>(a1, d1); //double隐式类型转换成int
    12. Add<double>(a1, d2);
    13. }
    •  补充:模板支持多个模板参数
    1. template<class K, class V> //两个模板参数
    2. void Func(const K& key, const V& value)
    3. {
    4. cout << key << ":" << value << endl;
    5. }
    6. int main()
    7. {
    8. Func(1, 1); //K和V均int
    9. Func(1, 1.1);//K是int,V是double
    10. Func<int, char>(1, 'A'); //多个模板参数也可指定显示实例化不同类型
    11. }

    我也可以给模板参数附上缺省值,和函数里的缺省参数一样,要从右往左给缺省值

    同样我也可以全缺省,其实这里面很多都和函数里的确实参数类似,不过多赘述。 以后在模板进阶还会继续详解。

    模板参数的匹配原则

    • 原则1: 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
    1. //专门处理int的加法函数
    2. int Add(int left, int right)
    3. {
    4. return left + right;
    5. }
    6. //通用加法函数
    7. template<class T>
    8. T Add(T left, T right)
    9. {
    10. return left + right;
    11. }
    12. int main()
    13. {
    14. Add(1, 2); //会调用哪个Add函数?
    15. }

    首先,这俩Add可以同时存在,关键是我调用Add时调的是模板函数Add,还是专门的Add?

    通过反汇编得知,调用的是专属Add函数。得出结论:编译器在调用时,有现成的就调用现成的,没有就套用模板。当然,我们也有办法强制让编译器走模板函数,如下:

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

    3、类模板

    类模板的定义格式

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

    如下的栈示例:

    1. //typedef int STDataType; //C语言的做法
    2. template<class T>
    3. class Stack
    4. {
    5. public:
    6. Stack(int capacity = 10)
    7. {
    8. _a = new T[capacity];
    9. _capacity = capacity;
    10. _top = 0;
    11. }
    12. ~Stack()
    13. {
    14. delete[]_a;
    15. _capacity = _top = 0;
    16. }
    17. private:
    18. T* _a;
    19. int _top;
    20. int _capacity;
    21. };

    类模板的实例化

    类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的
    类型放在<>中即可
    ,类模板名字不是真正的类,而实例化的结果才是真正的类。

    1. int main()
    2. {
    3. Stack<int>st1; //int类型
    4. Stack<double>st2;//double类型
    5. }

    上述可以看出类模板是要显示实例化的,而我函数模板是不需要自己显示实例化的,编译器会自动帮我推演(并不是所有都会推演,下文会讲)


    4、补充

    函数模板一定是推演?类模板一定是指定?

    函数不一定都能推演,但是类模板一定要指定,假设有如下的函数模板:

    这里的模板就推不出T的类型。因此我们就要对其显示实例化。

    1. template<class T>
    2. T* func(int n)
    3. {
    4. return new T[n];
    5. }
    6. int main()
    7. {
    8. //函数模板显示实例化
    9. int* p1 = func<int>(10);
    10. double* p2 = func<double>(10);
    11. }

    因此如果函数模板不能自动推演,就要显示实例化,指定模板参数。

    模板的分离编译

    模板的声明和定义是可以分离的(前提是声明和定义在一个文件)。像下面这样就可以:

    1. //函数模板的声明
    2. template<typename T>
    3. void Swap(T& left, T& right);
    4. //类模板的声明
    5. template<class T>
    6. class Vector
    7. {
    8. public:
    9. Vector(size_t capacity = 10);
    10. private:
    11. T* _pData;
    12. size_t _size;
    13. size_t _capacity;
    14. };
    15. //函数模板的定义
    16. template<typename T> //定义的时候也要给模板参数
    17. void Swap(T& left, T& right)
    18. {
    19. T tmp = left;
    20. left = right;
    21. right = tmp;
    22. }
    23. //类模板的定义
    24. template<class T> //定义的时候也要给模板参数
    25. Vector<T>::Vector(size_t capacity)
    26. : _pData(new T[capacity])
    27. , _size(0)
    28. , _capacity(capacity)
    29. {}

    模板不支持声明和定义放到两个文件中的(xxx.h和xxx.cpp),会出现链接错误。

    为什么不支持声明和定义分别放到两个文件呢?

    这里跟实例化有关系。如下是我声明、定义、测试分别放置的文件:

    根据我们已有的经验,源文件在生成可执行程序的过程会经历预处理编译汇编链接这四大模块。我们画图演示器过程:

    我template.i在编译后生成对应的.s文件以及后续的.o文件其实都是空的,编译器下不了手,因为不知道T是啥,这也就导致符号表是空的,没有地址。而调用的地方就没问题,因为main函数里头已经实例化显示出了T的类型。随后就去符号表里找到对应函数的地址,但是找不到。所以链接就会出错

    那我非要声明和定义放两个文件,有何办法呢?

    • 解决办法1:在template.cpp中针对要使用的模板类型显示实例化指定

    你调用函数的地方有哪些类型,就要指定显示实例化哪些类型。加上了显示实例化,此时就能链接上了。 不过这种方法不实用,不推荐使用。

    • 解决办法2:将声明和定义放到一个文件 "xxx.hpp" 里面或者xxx.h其实也是可以的。推荐使用这种。

    模板的声明和定义一般是要放到一个文件中,有些地方就会命名成xxx.hpp,寓意就是头文件和定义实现内容合并一起,但并不一定是.hpp.h也是可以的。

    此时我头文件在预处理时在.cpp文件展开,根本不需要找函数地址,也不需要链接,因为既有声明又有定义。

  • 相关阅读:
    CMS getshell
    Vue实例生命周期
    pytorch冻结参数训练的坑
    分布式系统常见理论讲解
    Python语言学习:Python语言学习之面向对象编程OO(继承&封装&多态)/类&方法/装饰器的简介、案例应用之详细攻略
    java 版本企业招标投标管理系统源码+多个行业+tbms+及时准确+全程电子化
    vue3:3、项目目录和关键文件
    C++重载底层原理
    go 地址 生成唯一索引 --chatGPT
    一篇文章带你搞定Java封装
  • 原文地址:https://blog.csdn.net/bit_zyx/article/details/125063680