2000年至今,由于以Loki、MPL(Boost)等程序库为代表的产生式编程和模板元编程的出现,C++出现了发展历史上又一个新的高峰,这些新技术的出现以及和原有技术的融合,使C++已经成为当今主流程序设计语言中最复杂的一员-维基百科C++词条

丰富的程序库以及本身的语言特性,使得C++在开发效率和运行效率上有着不错平衡,但这同时也增加了学习C++所带来的时间成本,C++疑问集此系列用于记录笔者学习C++路上遇到的问题,对部分易于混淆的知识点加以总结与汇总,以便于读者对C++有更深入的了解。

静态多态与动态多态的区别?

  • 编译时多态:通过重载函数或者函数模板实现-静态联编
  • 运行时多态:通过虚函数实现-动态联编

函数模板相关问题

函数模板:泛型编程思想,通过建立一个通用函数,其函数类型和形参类型可以不用具体指定,调用函数时,系统会根据实参的类型,取代函数模板的类型参数,得到实例化的函数,注意在这之前函数模板未实例化,编译器没有为其生成可执行代码。编译器对模板都是进行二次编译,当编译器第一次遇到模板时进行一次普通的编译,当调用函数模板时进行第二次编译。(与此功能类似的还有类模板)

1
2
3
4
5
6
7
8
9
template <typename T>	//C++98之前未添加关键字typename,需要使用class来定义类型
void function(T &a,T &b){
/*函数实现*/
}
......
int a,b;
double c,d;
function(a,b); //生成void function(int &a,int &b)
function(a,d); // template parameter 'T' is ambiguous

隐式实例化implicit instantiation:

例子中调用function(a,b),使得编译器为function函数模板进行模板实参推演,从而生成一个参数类型为int的实例。

作用:类型让编译器进行判断,好处就是方便可以偷懒。

显式实例化explicit instantiation:

(语法格式:template 函数返回类型 函数名 <实例化的类型> (函数形参表); );
例如template void function(int &a,int &b)将function模板实例化为int类型;

实例化后的模板,编译器为其生成了可执行代码,这样就可以将其打包做成库供他人使用,最常见的例子就是STL标准库。

作用:提高编译效率,使用模板前,编译器根据显式实例化指定的类型生成模板实例,接下来调用相同参数类型的模板就无需重新生成该类型的代码,一般制作库需要用到显式实例化。(显式实例化声明所在的文件必须存在函数模板的定义)

显式特化/显式具体化explicit specialization:

(语法格式:template <> 函数返回类型 函数名 <实例化的类型> (函数形参表) {函数体});
例如template <> void function(int &a,int &b){/函数实现/}将function模板特化为int类型的特化版本;

特化与实例化最大的不同就是可以对模板函数进行全面的定制,可以增减成员以及成员函数,而显式实例化编译器会自动帮我们完成函数实现部分,因此显式实例化一般只需要声明即可。

作用:遇到特殊的参数类型(例如结构体),不能使用泛型编程则需要用到显式特化,则需要自己手动来编写模板的函数实现。(特化和实例化功能上是共通的,是从编程习惯上将两者区分开,需要自己定制的函数实现一般就使用显示特化来编写,而显式实例化一般只需要声明而不需要手动编写函数实现)

备注:

  1. 类模板可以进行偏特化,而函数模板只可以进行全特化,函数模板可以使用重载来实现偏特化相同的功能,但需要注意重载带来的二义性问题。
  2. 模板的声明和定义不能在局部范围或者函数内被执行。
  3. 一个程序不允许同一模板实参集的同一模板既有显式特化又有实例化。
  4. 类模板必须指定模板类型,因为编译器无法根据返回值推导其类型。
  5. 编译器的问题,模板的声明和实现一般都是需要我们写在.hpp文件中。

程序函数调用顺序优先级(高到低)

普通函数(包括隐式转换(char类型转为int))> 模板函数实例化\具体化 > 普通模板(注意实例化和具体化两者同时存在时,不同编译其对其rank的控制是不同的,所以编程中一般禁止出现同类型模板的实例化和具体化)

模板函数调用顺序优先级(高到低):

参数完全匹配>隐式转换(char转为int,float转为double)> *普通转换(int转为char,double转为float)> *用户自定义转换(未亲自实现,来源https://blog.csdn.net/qq_42128241/article/details/81632910

c++算术表达式隐式转换顺序:

  • char -> int -> long -> double
  • float -> double

存在疑问:显式具体化与普通函数重载的选择?

个人理解,这两者在功能上的表现基本是一致,为何选择模板函数的显式具体化,个人认为一些库函数都是用模板来编写的,在一些需要将模板进行重载的情况下,就需要使用显式具体化来进行重载,而不是将函数进行重载,举个不恰当的例子,就好像一直用筷子吃饭,送进口中这个操作也可以用叉子完成,但既然拿的是筷子继续使用筷子送入口中。

存在疑问:为何需要显式具体化/实例化,要的不就是惰性编程?

模板只有定义的时候,编译器并不会为其生成可执行的二进制代码,这也意味编译器进行连接步骤的时候是寻找不到我们要的模板函数的,所以就需要我们将模板进行实例化/具体化在模板定义的同个文件中,让编译器将我们的模板进行实例化生成可执行代码,这样制作出来的库才可以供给他人使用。

c++STL准模板库中常见的组件

序列容器

模板 含义 优点 缺点
list<T> 创建一个表示存储T类型对象的双向链表的类 - 使用的是非连续的内存空间进行存储,提高了内存的利用率 - 可在内部方便的进行插入和删除操作
- 可在两端进行push、pop操作
- 不能进行内部的随机访问,访问内容必须全遍历操作,即不支持[ ]操作符和vector.at()
- 相对于verctor占用内存多
vector<T> 创建一个表示存储T类型对象的动态数组的类 - 不需要指定内存大小的连续存储,可以对数组进行动态操作
- 随机访问方便,即支持[ ]操作符和vector.at()
- 相对于list节省了内存空间
- 由于时连续存储,只能对尾部进行操作,对插入和删除效率低
- 只可以在vector的尾部进行pust、pop操作,不允许在头部进行
- 动态添加的数据大小超过默认分配大小,需要对整个vector进行内存分配、拷贝和释放
deque<T> 创建一个表示存储T类型对象的双端队列的类 - 整合了list和vector的优点,一般面对插入和删除都在头部和尾部的进行时,我们趋向于选择deque - 占用内存大

容器适配器-基于deque<T>容器实现

模板 含义 运行机制
queue<T> 创建一个表示存储T类型对象的队列的类 默认情况下基于deque<T>容器实现先进先出机制,queue不提供便利功能,也不提供迭代器,只能尾部增加元素,头部移除元素
stack<T> 创建一个表示存储T类型对象的堆栈的类 默认情况下基于deque<T>容器实现后进先出机制,增删改查只能在尾节点进行

关联容器-基于高效的红黑树实现

模板 含义 特性
set<T> 创建一个表示T类型对象的set容器的类 - 所有元素的只有key没有value,value就是key不允许出现键值重复
- 所有的元素都会被自动排序 - 不能通过迭代器来改变set的值,因为set的值就是键
map<T> 创建一个表示T类型对象的map容器的类 - 所有元素都是key+value存在
- 不允许键重复所有元素是通过键进行自动排序的
- map的键是不能修改的,但是其键对应的值是可以修改的

才学疏浅,欢迎评论指导

评论