------------恢复内容开始------------

每一种现象背后都隐藏着一种本质,关键是要不要挖掘

前言:

 


 

函数重载的重要性还不清楚,但是你知道 C++中如何实现函数重载(虽然这篇文章是关于 C++中函数重载的实现,但我想其他语言也是如此)?这可分成以下两个问题

在声明/定义重载函数时如何解决命名冲突?撇开函数重载不谈, using是一种解决命名冲突的方法,还有许多其他方法可以解决命名冲突,在此不再赘述。2、当我们调用一个重载函数时,如何进行解析?也就是说,如何知道调用了哪个函数呢?

所有支持函数重载的语言都必须解决这两个问题!有了这两个问题,我们就可以开始讨论了。这篇论文是这样的:

1.例题介绍了(现象)函数重载(what)。为何函数重载(why)是必需的?编译器如何解决命名冲突?为什么函数重载不考虑返回值类型3、重载函数的调用与凌号匹配情况4、编译器如何解析重载函数调用?依据一组可供选择的函数名称确定最佳匹配函数5、总结

1.介绍实例(现象)

1.什么是函数重载(what)?

“函数重载”是指在同一个范围内,可以有一组具有相同函数名、不同列表参数的函数,这组函数叫做“重载函数”。重负载函数通常用于命名一组具有类似功能的函数,减少了函数名称的数量,避免了名称空间的污染,对程序的可读性有很大的帮助。

When two or more different declarations are specified for a single name in the same scope, that name is said to overloaded.  By extension, two declarations in the same scope that declare the same name but with different types are called overloaded declarations. Only function declarations can be overloaded; object and type declarations cannot be overloaded. ——摘自《ANSI C++ Standard. P290》

看看下面这个例子,体会一下:实现一个打印函数,它可以同时打印 int类型和字符串类型。用 C++,我们可以做到这些:

#include using namespace std; void print(int i) {        cout<<"print a integer :"<

在上述代码的实现中, print (string)或 print (string)都可以根据具体 print ()的参数调用。以上 print (12)将调用 print (int),而 print (" hello world")将调用 print (string),其结果如下所示:

 


 

(先用 g++ test. c编译,然后执行)1.2.为什么需要函数重载(why)?想象一下,如果没有像 C那样的函数重载机制,那么您必须这样做:

给 print函数取一个不同的名字,比如print_int,print_string。在这里,有两种情况,如果有很多种情况,则需要为实现相同功能的函数取许多名称,比如添加打印 long类型, char*,各种类型的数组,等等。这么做是有害的!

一个类有相同的类名,即:构造函数都具有相同的名称。在没有函数重载机制的情况下,实例化不同对象是相当麻烦的!运算符重载,本质上是函数重载,它极大地丰富了现有运算符的意义,使+可以用来连接字符串回忆。

 


 

接下来,我们将分析 C++如何实现函数重载机处理。如何编译来解决命名冲突呢?要想理解编译器是被放在文件中的,查看汇编编编编下的汇编,我们将上面的编译结果反编译,查看汇编代码(全文都是在 Linux下进行的实验, Window在这里面可以参考《一个简单的题目引发的思考》一文,旧版本中既用到 Linux下,这里就是汇编, Windows下就是汇编,两者之间有区别)。

我们执行命令objdump-d a. out> log. txt反汇编,然后重定向结果到 log. txt文件,然后对 log. txt文件进行分析。discovery函数 void print (int i)编类型已在编译 id print (string str)之后类型,个数,返回类型。看一下下面的映射参数列表: type、 type和 type。表格,看下图:

数字的类型,数字数量,与返回类型无关。但是,请查看下列映射:void print(int i)                    -->        _Z5printi void print(string str)        -->        _Z5printSsZ5代表返回值类型, print函数名, i代表整型 int, Ss代表 string字符串,也就是被映射为返回类型+函数名+参数列表。最终将通过_Z5printi,_Z5printSs在主函数内调用相应的函数:

80489bc:      e8 73 ff ff ff          call  8048934 <_Z5printi>………80489f0:      e8 7a ff ff ff          call  804896f <_Z5printSs>

另外,我们还会编写一些重载函数,以验证猜测,例如:

void print(long l)          -->          _Z5printl void print(char str)      -->          _Z5printc很可能是int-> i,long-> l,char-> c,string-> Ss….基本上都是用首字母表示的,现在我们来看一下,如果一个函数的返回值类型对它的变名有影响,比如:

#include using namespace std; int max(int a,int b) {        return a>=b?a:b; } double max(double a,double b) {        return a>=b?a:b; } int main() {        cout<<"max int is: "

<int max (int a, int b)映射为_Z3maxii, double max (double a, double b)映射为_Z3maxdd,这证实了我的猜测,在 Z之后有各种返回类型的数字代码。更多细节的对应关系,比如,数字对应返回类型,哪个字符代表哪个重参数类型,就不再详细讨论了,因为这是一个与编译器相关的问题。

上面的研究都是基于 g++编译器的,如果使用 vs编译器,那么这种对应关系肯定与这个不同。但规则是相同的:“return+函数名+参数列表”。因为在映射机制中也有返回类型,所以这种不同的返回类型映射之后,函数名肯定会有所不同,但是为什么不在函数重载中考虑函数的返回类型呢?

—这是为了保持解析操作符或函数调用与上下文无关(与上下文无关),请看下面这个示例

float sqrt(float); double sqrt(double); void f(double da, float fla) {      float fl=sqrt(da);//调用sqrt(double)      double d=sqrt(da);//调用sqrt(double)      fl=sqrt(fla);//调用sqrt(float)    d=sqrt(fla);//调用sqrt(float) } 

假如返回类型是在函数重载中考虑的,那么就不可能独立于上下文来决定调用哪个函数。到目前为止,看起来分析得很透彻,但是我们也漏掉了对函数重载的重要限定——作用域。前面介绍的所有函数重载都是全局函数,接下来我们将查看一个类,它调用 print函数与类的对象,并根据参数调用不同的函数:

#include using namespace std; class test{ public:        void print(int i)        {                cout<<"int"<接下来,我们将看到此时 print函数映射之后的函数名:void print(int i)                    -->            _ZN4test5printEivoid print(char c)              -->            _ZN4test5printEc请注意之前的N4test

我们很容易猜出应该代表范围,N4可能代表名称空间, test类名,等等。最精确的映射机制如下:范围+返回类型+函数名+参数列表对重载函数进行调用匹配既然重载函数的命名冲突问题已经得到解决,那么当定义完重载函数后,如何使用函数名调用它呢?

要估算哪一个重载函数是最合适的,需要按以下规则进行判断:完全匹配:不进行转换就进行参数匹配,或者只进行少量转换,例如数组名为指针,函数名为指针, T为 const T;提升匹配:

 


 

即整数提升(例如 bool为 int, char为 int, short为 int), float为 double使用标准转换匹配:例如 int为 double, double为 int, double为 double, double为 long double, Derived*为 Base*, T*为 void*, int为 unsigned int;使用用户定制匹配;使用省略号匹配:类似 printf中省略号参数若有多个匹配函数在最上层找到,调用就会被拒绝(因为模凌两可)。

查看以下示例:

void print (int); void print (const char*); void print (double); void print (long); void print (char); void h (char c, int i, short s, float f){print (c);//精确匹配,调用 print (char) print (i);//精确匹配,调用 print ('a');//精确匹配, print (49);//精确匹配,调用 print (int), print (0);//精确匹配,调用 print (double)*);

如下例所示,定义过少或过多的重载函数都可能导致模凌两可。

void f1(char); void f1(long); void f2(char*); void f2(int*); void k(int i) {        f1(i);//调用f1(char)? f1(long)?        f2(0);//调用f2(char*)?f2(int*)? } 

这个时候,侯编译器会报错,并把错误交给用户自己来处理:通过显示类型转换来调用,等等(比如f2 (static_cast< int*>(0)),当然,这样做很难看,并且您希望调用其他方法来进行转换)。以上示例只是一个参数,接下来我们将看到另外两个参数的情形:

int pow(int ,int); double pow(double,double); void g() {        double d=pow(2.0,2)//调用pow(int(2.0),2)? pow(2.0,double(2))? }

 编译器如何对重载函数调用进行解析?当编译器实现调用重载函数的解析机制时,肯定要先找到一些同名的候选函数,然后再从候选函数中找到最符合的,如果没有找到,就会报错。

 


 

以下是重载函数解析的一种方法:编译器用语法分析、 C++文法、符号表、抽象语法树等交互方式处理重载函数调用,交互图大致如下:它下面是这4个解析步骤完成的内容:获取函数名称,获取函数的所有参数表达式类型,由匹配文法中的函数调用;

语法分析程序查找重载函数,在符号表中进行重载解析,返回最佳函数语法分析程序创建抽象语法树,将符号表中存储的最佳函数绑定到抽象语法树下一步,我们将着重于解释重载解析,它满足前面“3,重载函数调用匹配”中所述的匹配顺序和匹配规则。

重函数的解析大致分为三个步骤:基于函数名确定候选函数集从候选函数集选择可用函数集从可用函数集确定最佳函数,或者由于模凌两可返回错误4.1、根据函数名确定一组候选函数取决于该函数在同一范围内的所有名称相同的函数,并且要求可见(如 private, protected, public, friend等)。

在函数重载的定义中,“相同范围”也是一种限定,如果不在某个范围内,则不算函数重载,如以下代码:void f (int); void g (){void f (double); f (1);//在此,应使用 f (double)代替 f (int)}也就是,内部范围的函数将隐藏外部范围的同名函数!相同派生类的成员函数对基类隐藏具有相同名称的函数。这样可以理解,对变量的访问也是如此,例如,用“::”限定一个函数中具有相同名称的变量访问全局。

对于候选函数集的查找,通常采用深度优化搜索算法:step1:从一个函数调用点开始查找,查找可以逐层作用域外部可见的候选者step2:如果前一步收集的不在用户自定义名称空间中,则使用 using机制引入的名称空间中的候选器,否则结束当收集候选函数时,如果调用函数的参数类型是非结构体类型,则该候选函数只包含调用点可见的函数;

 


 

如果调用函数的参数类型包括类类型对象、类类型指针、类类型引用或指向类成员的指针,则该候选函数为以下且:(1)在调用点可见的函数;(2)在定义此类类型的名称空间中或在定义此类的基类的名称空间中声明的函数;

(3)对此类或其基类使用友元函数;下一步,我们将通过一个简单的例子:

void f(); void f(int); void f(double, double = 314); names pace N {      void f(char3 ,char3); } classA{    public: operat or double() { } }; int main ( ) {    using names pace N; //using指示符    A a    f(a);    return 0; }

 按照上面的方法,因为参数是类类型向。收集候选器函数分为3个步骤:

(1)从函数调用所在的 main函数范围内开始查找函数 f的声明,但未找到结果。至主函数范围的外部范围查找,这个次序是类及其基类的友元函数。把他们放到一个候选集合中;

(2 named. sing)指示符所指称的 named N (char3,char3);

 


 

(3)考虑类别2的集合。一种是定义此类类型的名称空间,或者在定义该类的基类的名称空间中的一个函数字;第二个是该类或它的基类的友元函数。这种情况下,这2个类集合是空的。最后的候选集是上面列出的4个函数 f。4.2.确定可用功能可用函数表示:函数参数数目匹配且每个参数都有一个隐式转换序列。

点击了解更多资料,更有免费开源项目和课程等你观看哦!

 

------------恢复内容结束------------

内容来源于网络如有侵权请私信删除