模板(二)

目录

非类型模板参数

引入

分类

使用typename的特殊情况

注意点 

模板特化

引入

介绍

函数模板特化 

使用

​编辑

优点

类模板特化 

全特化

偏特化

部分特化

特殊的特化 

使用

分离编译

介绍

问题代码示例 

代码

说明

预处理

编译

链接

类模板实例化原理

总结

解决方法 

显式实例化

声明和定义放在一个头文件


非类型模板参数

引入

一般我们使用模板都是想让这个类兼容更多的类型,但模板参数不止只有这个作用

分类

模板参数分为 类型形参 非类型形参
  • 类型形参 : 出现在模板参数列表中,跟在class或者typename之后的参数类型名称(这两个一般情况下可以随意使用,但在某些情况下只能使用typename)
  • 非类型形参 : 就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用(eg: 定义数组大小)

使用typename的特殊情况

  • 当我们需要使用一个类里的类型时, 就得使用::来指明该类型所属的类名
    模板(二)_第1张图片
  • 但在编译期间,编译器是不知道这个东西到底是一个类型,还是一个静态成员变量(根据语法来说,静态成员变量也可以被这样调用)
  • 如果这里是变量,我们用变量作为类型来创建变量,那语法就错误了
  • 而这里仅仅只是类名,并没有实例化,编译器无法通过代码判断是否合规(没有实例化就找不到对应的代码)
  • 所以为了安抚(?)编译器,需要在类名的前面 + typename,来告诉编译器,这里是一个类型,不要觉得有语法错误 ,具体等实例化后去类里找就行
  • 模板(二)_第2张图片

注意点 

  1. 浮点数、类对象以及字符串 " 是不允许作为非类型模板参数的,只能是整型(最初创造出来就是为了处理大小啥的)
  2. 非类型的模板参数必须在编译期就能确认结果,C++的模板机制要求 -- 编译器在编译期间就生成模板的实例化代码,而这些代码的生成需要基于已知的、在编译期就可以确定的参数值

模板特化

引入

之前在优先级队列中,我们有使用仿函数实现比较函数

当时如果传入参数是自定义类型时,需要我们另写一种比较函数,但也可以采用另外一种方法 -- 函数模板特化

介绍

特化是在原模板类的基础上,针对特殊类型 所进行特殊化的实现方式
模板特化分为 -- 函数模板特化 与 类模板特化

函数模板特化 

使用

模板(二)_第3张图片

 模板(二)_第4张图片

 在需要特化的函数名后 + <特化的类型>,这样这里就不需要T类型,但仍需要写tmplate<>

优点

模板(二)_第5张图片

按照原先的方法,直接给出对应函数,那么可能在传参的时候就费劲点,可能需要把所有参数都给出

但如果有函数特化,就不需要传入后两个参数,直接传类型就够了(因为此时函数名仍是当时给出的缺省参数)

类模板特化 

写法是一样的,只不过针对的对象不一样,一个是函数,一个是类

全特化

将类的所有模板参数都进行特化(都给出具体类型)

模板(二)_第6张图片

偏特化

部分特化

将模板参数类表中的一部分参数特化

模板(二)_第7张图片

特殊的特化 

可以将普通类型特化成该类型的指针/引用版本

模板(二)_第8张图片

模板(二)_第9张图片

使用

模板(二)_第10张图片

之前这样的类无法将自定义类型的指针传入进行比较(因为我们要的是比较对象),所以我们是另写了一个新类来完成该功能

模板(二)_第11张图片

但现在我们也可以使用特化的方式来实现

模板(二)_第12张图片

分离编译

介绍

分离编译是一种编程技术,允许将程序代码分割成多个文件,每个文件可以独立地编译成目标文件(.o文件),然后将这些目标文件链接在一起, 来创建最终的可执行程序

分离编译的核心思想 -- 是将代码逻辑分解为多个独立的模块,每个模块可以单独编译,从而减少了重新编译整个项目的需要 

问题代码示例 

一般来说,分离编译虽然很方便,但也很容易出现不少问题

代码

比如下面的三个文件

stack.h:

模板(二)_第13张图片

 stack.cpp:

模板(二)_第14张图片

main.cpp:

模板(二)_第15张图片

说明

预处理

在这三个文件形成可执行文件的过程中,首先是在两个cpp文件中,将.h文件展开... ,形成.i文件

编译

  • 这样stack.i中,有stack类的声明和定义,还有A类的func1定义 ; 但main.i中只有stack类的所有声明+部分定义 , 和A类的所有声明
  • 所以在编译阶段,只能确定 main.i中的size(),以及stack类的构造函数 的地址(这里只是生成了与地址相关的信息,但这些信息还没有映射到最终的绝对地址 , 编译阶段的地址是为后续阶段提供信息的一种中间状态)
  • 以及生成修饰后的函数名
  • 虽然没有确定所有函数的地址,但是编译是可以通过的,因为含有对应函数的声明

接下来生成.o文件

上面这三个阶段,对应生成地址的情况如下图:

模板(二)_第16张图片

链接

  • 接下来就是进行链接,链接是可以将func1的地址确定的,因为func1的定义有在stack.cpp中,俩文件相互一交流(拿着修饰后的函数去符号表找),就可以确定辽
  • 但素,这样的代码会报错,编译器说push在链接中出现问题
  • 原因是在main.s中,push修饰后的函数名适合int有关的(因为实例化传入的类型就是int),但是在stack.s中,没有人告诉他要实例化成什么,所以并没有实际生成代码,只是一个壳子,也就没有什么地址,自然在符号表中找不着

类模板实例化原理

  • 编译器在遇到类模板的使用时,会生成一个用于在实际需要时实例化代码的计划,这个计划称为模板的实例化
  • 这个计划会记录下特定模板参数,以及生成实际代码的需要
  • 在源文件中使用模板时,需要提供模板参数,并创建一个模板的实例 , 这样就会告诉编译器 我们想要使用特定类型来实例化模板
  • 当编译器在编译源文件时,会根据计划生成 特定类型的类代码,其中包含了特定类型的成员函数和数据成员

总结

所以,上面的情况之所以出现问题,就是因为类的具体代码不在当前编译单元中

编译器需要生成实际的代码,但如果实际的代码在其他源文件中,链接器就无法找到这些代码,从而导致链接错误

解决方法 

显式实例化

如果一定要把类的声明和定义放在两个文件,可以采用显式实例化模板的方式

模板(二)_第17张图片

但是坏处就是,一旦使用新的类型实例化模板,就得加代码,hin麻烦

声明和定义放在一个头文件

这样在包括头文件的源文件中,一旦源文件实例化了类,就能及时的在编译过程生成实际代码,然后可以链接成功

你可能感兴趣的:(c++,c++)