§4 函数重载和默认参数
“:( ……”我的msn上突然弹出了这么一张苦瓜脸,不用看也知道是谁了。
“师傅,过来帮帮忙好吗?”
“不是说过别叫我师傅啊!叫我weily就可以了。怎么了?又有什么问题啊?”
“讲不清楚,还是您……老人家……过来看一下吧 :p”young还故意把“老人家”这三个字与前后文分开,改成了黑体、三号字,还改了颜色,并且加粗了。
还好我只是刚拿起杯子,还没喝水,否则,我的键盘和显示器就要遭殃了。
“生气了?开个玩笑呀,你还是快点过来吧,weily :)”
“我的气量可没那么小,不过下次你在发这种话之前最好先看看我有没有在喝水或者吃什么东西,我怕噎着…… 好了,我马上过来!”按下enter之后,就端着我的茶杯,走了过去。
“怎么了啊?又有什么问题啦?”
“我这边有一些代码不能通过编译,老是报错,可我又不知道为什么错了。”
“嗯,上次我告诉过你,要学会看编译器报的错。”
“我看了,它说:‘ambiguous call to overloaded function’,也就是重载函数的调用有二义性。可是,我没看出有什么二义性啊!”young的样子看上去一脸的无辜和茫然,就像一个被错怪了的孩子。
“喔,原来是二义性问题啊,这个问题的确有时候比较让人头痛的,因为c++有这么一个哲学信仰:它相信潜在的模棱两可状态不是一种错误(注1)。先让我看看你的代码。”
“嗯,就是这里。”young把鼠标指向出错的那行,“我就是想不通,这么一个简单的函数调用怎么会出错。”
我看了看屏幕,只是一个函数调用,就像这样:
func( i );
“首先我问你,你这个func函数的原型是什么样子的?”
“函数原型?哦,就是函数的声明是吧?在这里。”她按了一下pageup键,然后将函数的声明指给我看。
void func( char );
void func( long );
“嗯,这样啊,那你的i这个变量是什么类型的?”
“是int的。”
“喔,这样就没问题了,我都知道了。”
“嗯?知道什么啦?快说给我听啊!”young那双充满好奇的眼睛盯着我。
“其实,这个是一个常常被人们所忽视的一个模棱两可的状态。你是不是认为,从int转为char,会丢失精度,而转为long不会,所以,你就认为这个函数调用,应该毫无疑问地调用func( long )这个函数?”
“是啊,因为我记得solmyr老大曾经说过,当编译器不知道转成那个类型好的时候,它会报二义性错误。但是在这里,int转为char和int转为long应该很明显的啊,转为char会丢失精度,而转为long应该更好啊。”
“的确,你说的基本上都是对的,而且过去我也是这么认为的。”
“过去?你也是?”
“是的。但是你只要仔细看看tc++pl,里面7.4.3那节就有这样的一个例子的,几乎和你的这段代码一模一样,bjarne在tc++pl里明确地指出,这是一个二义性的函数调用。不过,很遗憾的是,他也没有具体解释为什么这个调用是二义性的。”
“喔,那这个到底是怎么回事呢?”
“你听我慢慢说。首先,在重载解析的时候,编译器是应该按照c++里的一个规则进行函数选择的。一共有五种类型的匹配和转换。最高级别的,就是精确匹配。”
“嗯,这个我知道,就比如我这里如果有一个函数是void func( int )的,那这里的调用就不会有二义性,肯定调用这个以int作为参数的函数。”
“对。但是他还包括数祖名转换成相应的指针类型,函数名转换成相应的函数指针,以及非常量类型转换为对应的常量类型,这些都是很细微的转换,转换后的类型和转换前的是相当的。比如这样,”
void f(const char*);
char p[ ] = “non-const”;
f( p );
“这里就有2个转换,一个是将p这个char的数祖名转换为相对应的char*类型的指针,第二个就是从char*转换为const char*,是非常量转换为常量,这些都属于精确匹配。”
“噢,知道了。”
“第二种级别的,就是类型的提升。其实这个就牵涉到你提出的那个问题的。在类型的提升中,没有从int转换到long的这种提升的。c++里的这个设计,是继承自c的,它就是将运算对象在运算之前,转换到相应的‘自然’的大小。”
“‘自然’大小?这是什么?”
“这个在c里面是非常常见的,例如在c里,char都被转换为int进行处理的。关于char和int的关系,在c里,两者是等价的,可以说在c里没有char这种类型的值,任何char的值都是int的,例如常量’a’,在c里面,它的类型是int的,而不是char,所以很多c的代码里都是从char到int的隐式类型转换。当然,在c++里,还是对两者有所区分的,也就是更匹配的类型存在时,就用更匹配的类型。例如,”
void f( char );
void f( int );
f( ‘a’ );
“噢,我知道,这里就没有二义性,就会调用f( char )这个函数。”young似乎有所领悟了。
“对,但是这样呢?”
void f( int );
void f( unsigned char );
f( ‘a’ );
“这个……”young又陷入了困惑。
“答案就是调用f( int ),这里没有二义性。记住一点,就是那些比较小的整数类型,包括char,在可以保证原有类型精度的情况下,转换为int类型,否则,转换为unsigned int类型(注2)。这就是我前面说的那种‘自然’的转换。关于这种隐式类型转换,你可以去看看tc++pl的c.6,那里有详细地说明。”
“噢,我知道了。”
“其实这些问题的起因,就是c++继承了c里面的一条规则,就是不区分narrowing conversions和widening conversions,所以也就带来了是从int转换为char好,还是int转换为long好这种模棱两可的状态。如果区分了narrowing conversions和widening conversions,那么就要定义隐式的 widening conversions (例如从 int到long) 和显式的 narrowing conversions (例如从int到char就需要一个显式类型转换操作)。但是这种设计是不能接受的,因为像从int到char的这种隐式类型转换太普遍了,如果一定要变成显式类型转换的话,那么将大大的降低c++与c的兼容性,这将使得c++不容易被人们使用和接受,c++的一个设计原则就是不强迫人们去用一种方法完成所需的事情,它会提供选择,让人们可以用自己喜欢的方式,而且c++在最初设计的时候,就要做到原来c能做的事情,那么原来c的代码理所当然的应该能被c++接受。”
“所以c++不区分narrowing conversions和widening conversions,而编译器对于narrowing conversions给出的是警告,而不是错误。是这样子的吧?”
“是的,好的编译器会对这种可能丢失精度的转换给出警告。看来对于这个问题你已经理解了大部分内容了。那么我们再来看看整数和指针的关系。先问你个问题,就是0是什么?”
“0?是一个整数吧。”
“基本正确,更精确地说,0是一个整数字面常量(literal integer constant)。对于0这个整数字面常量,或者值为0的常量表达式,都可以被隐式的转化为任何类型的指针。所以,对于下面这段代码,是二义性的函数重载。”
void func( char* );
void func( int* );
func( 0 ); //二义性函数调用!
“嗯,原来如此,知道了。”
“那么关于函数的默认参数你知道多少?”
“默认参数?哦,这个我已经看过了,好像最关键的一点就是默认参数只能是形式参数列表中的最后的几个。”
“不是好象,是肯定!这点一定要记住。其实,默认参数也和重载有一定的关系。你看看下面这段代码有什么问题。”
void func( int i );
void func( int i, char ch = ‘a’);
int i;
func( i );
“喔!模棱两可,这个函数调用是二义性的!”young的样子大概不亚于当年哥伦布发现了新大陆。
“对!对于多参数的函数重载,编译器在决定调用哪个函数的时候,还是遵循最佳匹配的规则,也就是让尽量多的参数能够得到最好的匹配。前面说过的两条,再加上标准转换、用户自定义的转换和省略号在函数定义中的使用,就构成了五条匹配规则,越前面的级别越高,当然,后面这3条规则中,还牵涉到用户自定义的类的设计问题,这个以后等你学到类和继承的时候,我们再说。这些规则,在tc++pl的7.4中有描述,你应该去看一下的。”
“书啊?我在看。其实,今天我遇到的问题,就是我刚看了一点关于函数重载的东西,但还有一些没搞明白。昨天你不在,我就去问pisces,学函数重载的时候要注意点什么。结果,她就说,我给你一些代码,你去把里面的错误全都找出来,并且搞清楚这些错误的原因,那就能学好了。最后,还丢给我一句话,尽力避免模棱两可的状态!”
“那么刚才那段代码就是她给你的咯?”
“是啊,而且,我搞不定了,去问过她,她也没说出个所以然,就是叫我以后写代码的时候别写出这样的。”
原来又是pisces搞的鬼,自己搞不定,救扔给新来的young,然后再通过young来让我帮忙解决这个问题。如果我没猜错的话,她现在应该就在附近…… 我一转头,果然看到她就在边上扮鬼脸,而且,看样子就是有了收获就想溜走。
“pisces啊,你自己有问题就可以直接来问我或者趣闻solmyr啊,何必欺负人家新来的呢?”
“这个…… 我只是看她现在在研究这方面的东西嘛,而且我看她和你混得挺熟的,所以就……”这家伙还真好意思说。
“好了,这次不和你计较,下次可不能这样哦……”我话还没说完,pisces已经回到自己的位子上了,真是的!
咦?怎么一会儿young也不见了?我用左手的食指和中指推了推鼻梁上的眼镜,然后四处张望了一下。结果就看到young手上端着个杯子,向这边走过来。那个杯子不是我的吗?怎么会在她手里?
“师傅,请喝茶,这可是您老人家最喜欢的红茶,而且还加了蜂蜜的哦!”
红茶加蜂蜜?我没听错吧?我把杯子接了过来,果然闻到了熟悉的茶香,杯子拿在手里,有一种温暖的感觉。
“你怎么知道我这个习惯的?”我觉得有点好奇,她才来不久啊,怎么就知道我喜欢红茶加蜂蜜啊?像pisces来了这么久了,上次还不是给我弄了一杯咖啡。
“这个啊?保密!”young得意地笑着。
“嗯,不错!味道刚好!哎,不对哦!你前面叫我什么来着?我怎么又听到‘师’字,还听到‘老’字,你……”我才反应过来,又被她耍了……
注:1.这个说法来自scott meyers的《effective c++》中的条款26
2. tc++pl中c.6.1 promotions中有详细的规则描述