}
};
intmain()
{
testt;
t.print
(1);
t.print('a');
return0;
}
我们现在再来看一下这时print函数映射之后的函数名:
voidprint(inti)-->_ZN4test5printEi
voidprint(charc)-->_ZN4test5printEc
注意前面的N4test,我们可以很容易猜到应该表示作用域,N4可能为命名空间、test类名等等。
这说明最准确的映射机制为:
作用域+返回类型+函数名+参数列表
3、重载函数的调用匹配
现在已经解决了重载函数命名冲突的问题,在定义完重载函数之后,用函数名调用的时候是如何去解析的?
为了估计哪个重载函数最适合,需要依次按照下列规则来判断:
精确匹配:
参数匹配而不做转换,或者只是做微不足道的转换,如数组名到指针、函数名到指向函数的指针、T到constT;
提升匹配:
即整数提升(如bool到int、char到int、short到int),float到double
使用标准转换匹配:
如int到double、double到int、double到longdouble、Derived*到Base*、T*到void*、int到unsignedint;
使用用户自定义匹配;
使用省略号匹配:
类似printf中省略号参数
如果在最高层有多个匹配函数找到,调用将被拒绝(因为有歧义、模凌两可)。
例子:
voidprint(int);
voidprint(constchar*);
voidprint(double);
voidprint(long);
voidprint(char);
voidh(charc,inti,shorts,floatf)
{
print(c);//精确匹配,调用print(char)
print(i);//精确匹配,调用print(int)
print(s);//整数提升,调用print(int)
print(f);//float到double的提升,调用print(double)
print('a');//精确匹配,调用print(char)
print(49);//精确匹配,调用print(int)
print(0);//精确匹配,调用print(int)
print("a");//精确匹配,调用print(constchar*)
}
定义太少或太多的重载函数,都有可能导致模凌两可,看下面的一个例子:
voidf1(char);
voidf1(long);
voidf2(char*);
voidf2(int*);
voidk(inti)
{
f1(i);//调用f1(char)?
f1(long)?
f2(0);//调用f2(char*)?
f2(int*)?
}
这时侯编译器就会报错,将错误抛给用户自己来处理:
通过显示类型转换来调用等等(如f2(static_cast(0),当然这样做很丑,而且你想调用别的方法时有用做转换)。
上面的例子只是一个参数的情况,下面我们再来看一个两个参数的情况:
intpow(int,int);
doublepow(double,double);
voidg()
{
doubled=pow(2.0,2)//调用pow(int(2.0),2)?
pow(2.0,double
(2))?
}
4、编译器是如何解析重载函数调用的?
编译器实现调用重载函数解析机制的时候,肯定是首先找出同名的一些候选函数,然后从候选函数中找出最符合的,如果找不到就报错。
下面介绍一种重载函数解析的方法:
编译器在对重载函数调用进行处理时,由语法分析、C++文法、符号表、抽象语法树交互处理,交互图大致如下:
这个四个解析步骤所做的事情大致如下:
由匹配文法中的函数调用,获取函数名;
获得函数各参数表达式类型;
语法分析器查找重载函数,符号表内部经过重载解析返回最佳的函数
语法分析器创建抽象语法树,将符号表中存储的最佳函数绑定到抽象语法树上
下面我们重点解释一下重载解析,重载解析要满足前面《3、重载函数的调用匹配》中介绍的匹配顺序和规则。
重载函数解析大致可以分为三步:
根据函数名确定候选函数集
从候选函数集中选择可用函数集合
从可用函数集中确定最佳函数,或由于模凌两可返回错误
4.1、根据函数名确定候选函数集
根据函数在同一作用域内所有同名的函数,并且要求是可见的(像private、protected、public、friend之类)。
“同一作用域”也是在函数重载的定义中的一个限定,如果不在一个作用域,不能算是函数重载,如下面的代码:
voidf(int);
voidg()
{
voidf(double);
f
(1);//这里调用的是f(double),而不是f(int)
}
即内层作用域的函数会隐藏外层的同名函数!
同样的派生类的成员函数会隐藏基类的同名函数。
这很好理解,变量的访问也是如此,如一个函数体内要访问全局的同名变量要用“:
:
”限定。
为了查找候选函数集,一般采用深度优选搜索算法:
step1:
从函数调用点开始查找,逐层作用域向外查找可见的候选函数
step2:
如果上一步收集的不在用户自定义命名空间中,则用到了using机制引入的命名空间中的候选函数,否则结束
在收集候选函数时,如果调用函数的实参类型为非结构体类型,候选函数仅包含调用点可见的函数;如果调用函数的实参类型包括类类型对象、类类型指针、类类型引用或指向类成员的指针,候选函数为下面集合的并:
(1)在调用点上可见的函数;
(2)在定义该类类型的名字空间或定义该类的基类的名字空间中声明的函数;
(3)该类或其基类的友元函数;
下面我们来看一个例子更直观:
voidf();
voidf(int);
voidf(double,double=314);
namespaceN
{
voidf(char3,char3);
}
classA
{
public:
operatordouble(){}
};
intmain()
{
usingnamespaceN;//using指示符
Aa;
f(a);
return0;
}
根据上述方法,由于实参是类类型的对象,候选函数的收集分为3步:
(1)从函数调用所在的main函数作用域内开始查找函数f的声明,结果未找到。
到main函数作用域的外层作用域查找,此时在全局作用域找到3个函数f的声明,将它们放入候选集合;
(2)到using指示符所指向的命名空间N中收集f(char3,char3);
(3)考虑2类集合。
其一为定义该类类型的名字空间或定义该类的基类的名字空间中声明的函数;其二为该类或其基类的友元函数。
本例中这2类集合为空。
最终候选集合为上述所列的4个函数f。
4.2、确定可用函数
可用的函数是指:
函数参数个数匹配并且每一个参数都有隐式转换序列。
(1)如果实参有m个参数,所有候选参数中,有且只有m个参数;
(2)所有候选参数中,参数个数不足m个,当前仅当参数列表中有省略号;
(3)所有候选参数中,参数个数超过m个,当前仅当第m+1个参数以后都有缺省值。
如果可用集合为空,函数调用会失败。
这些规则在前面的《3、重载函数的调用匹配》中就有所体现了。
4.3、确定最佳匹配函数
确定可用函数之后,对可用函数集中的每一个函数,如果调用函数的实参要调用它计算优先级,最后选出优先级最高的。
如对《3、重载函数的调用匹配》中介绍的匹配规则中按顺序分配权重,然后计算总的优先级,最后选出最优的函数。
5、总结
本文介绍了什么是函数重载、为什么需要函数重载、编译器如何解决函数重名问题、编译器如何解析重载函数的调用。
通过本文,我想大家对C++中的重载应该算是比较清楚了。
说明:
在介绍函数名映射机制是基于g++编译器,不同的编译器映射有些差别;编译器解析重载函数的调用,也只是所有编译器中的一种。
如果你对某个编译器感兴趣,请自己深入去研究。
最后我抛给大家两个问题:
1、在C++中加号+,即可用于两个int型之间的相加、也可以用于浮点数数之间的相加、字符串之间的连接,那+算不算是操作符重载呢?
换个场景C语言中加号+,即可用于两个int型之间的相加、也可以用于浮点数数之间的相加,那算不算操作符重载呢?
2、模板(template)的重载时怎么样的?
模板函数和普通函数构成的重载,调用时又是如何匹配的呢?
附录:
一种C++函数重载机制
这个机制是由张素琴等人提出并实现的,他们写了一个C++的编译系统COC++(开发在国产机上,UNIX操作系统环境下具有中国自己版权的C、C++和FORTRAN语言编译系统,这些编译系统分别满足了ISOC90、AT&T的C++85和ISOFORTRAN90标准)。
COC++中的函数重载处理过程主要包括两个子过程:
1、在函数声明时的处理过程中,编译系统建立函数声明原型链表,按照换名规则进行换名并在函数声明原型链表中记录函数换名后的名字(换名规则跟本文上面描述的差不多,只是那个int-》为哪个字符、char-》为哪个字符等等类似的差异)
图附1、过程1-建立函数链表(说明,函数名的编码格式为:
<原函数名>_<作用域换名><函数参数表编码>,这跟g++中的有点不一样)
2、在函数调用语句翻译过程中,访问符号表,查找相应函数声明原型链表,按照类型匹配原则,查找最优匹配函数节点,并输出换名后的名字下面给出两个子过程的算法建立函数声明原型链表算法流程如图附1,函数调用语句翻译算法流程如图附2。
图附2、过程2-重载函数调用,查找链表