📄 第13章 函数(二).htm
字号:
<P>1)int max(int a, int b);</P>
<P>2)float max(float a, int b);</P>
<P>3)double max(double a, double b);</P>
<P> </P>
<P>现在我这样调用:</P>
<P>int larger = max(1, 2);</P>
<P>被调用的将是第1)个函数。因为参数1,2是int类型。</P>
<P> </P>
<P>而:</P>
<P>double larger = max(1.0, 2);</P>
<P>被调用的将是第……注意了!是第3)个函数。为什么?</P>
<P>首先它不能是第1)个,因为虽然参数2是int类型,但1.0却不是int类型,如果匹配第1)函数,编译器认为会有丢失精度之危险。</P>
<P>然后,你可能忘了,一个带小数的常数,例如1.0,在编译器里,默认为比较保险的double类型(编译器总是害怕丢失精度)。</P>
<P> </P>
<P>最后,关于这两个规则,都是在同名的函数参数个数也相同的情况下需要考虑,如果参数个数不一样:</P>
<P>int max(int a, int b);</P>
<P>int max(int a, int b ,int c);</P>
<P>当然就没有什么好限制了,编译器不会傻到连两个和三个都区分不出,除非……</P>
<P> </P>
<P><B>实现函数重载的附加规则:</B>有时候你必须附加考虑参数的默认值对函数重载的影响。</P>
<P> </P>
<P>比如:</P>
<P>int max(int a, int b);</P>
<P>int max(int a, int b ,int c = 0);</P>
<P> </P>
<P>此例中,函数重载将失败,因为你在第二个max函数中设置了一个有默认值的参数,这将造成编译器对下面的代码到底调用了哪一个max感到迷惑。不要骂编译器笨,你自已说吧,该调用哪个?</P>
<P>int c = max(1, 2);</P>
<P> </P>
<P>没法断定。所以你应该理解、接受、牢记这条附加规则。</P>
<P> </P>
<P>事实上影响函数重载的还有其它规则,但我们学习这些就够了。</P>
<H4><A name=13.3.3>13.3.3</A> 参数默认值与函数重载的实例</H4>
<P><B>例五:</B>参数默认值、函数重载的实例</P>
<P> </P>
<P>有关默认值和函数重载的例子,前面都已讲得很多。这里的实例仅为了方便大家学习。请用CB打开下载的配套例子工程。所用的就是上面提到例子,希望大家自已动手分别写一个默认值和重载的例子。</P>
<P> </P>
<H3><A name=13.4>13.4</A> inline 函数</H3>
<P>从某种角度上讲,inline对程序影响几乎可以当成是一种编译选项(事实上它也可以由编译选项实现)。</P>
<H4><A name=13.4.1>13.4.1</A> 什么叫inline函数?</H4>
<P>inline(小心,不是online),翻译成“内联”或“内嵌”。意指:当编译器发现某段代码在调用一个内联函数时,它不是去调用该函数,而是将该函数的代码,整段插入到当前位置。</P>
<P>这样做的好处是省去了调用的过程,加快程序运行速度。(函数的调用过程,由于有前面所说的参数入栈等操作,所以总要多占用一些时间)。</P>
<P>这样做的不好处:由于每当代码调用到内联函数,就需要在调用处直接插入一段该函数的代码,所以程序的体积将增大。</P>
<P>拿生活现象比喻,就像电视坏了,通过电话找修理工来,你会嫌慢,于是干脆在家里养了一个修理工。这样当然是快了,不过,修理工住在你家可就要占地儿了。</P>
<P>(某勤奋好学之大款看到这段教程,沉思片刻,转头对床上的“二奶”说:</P>
<P>“终于明白你和街上‘鸡’的区别了”。</P>
<P>“什么区别?”</P>
<P>“你是内联型。”)</P>
<P> </P>
<P>内联函数并不是必须的,它只是为了提高速度而进行的一种修饰。要修饰一个函数为内联型,使用如下格式:</P>
<P> </P>
<P>inline 函数的声明或定义</P>
<P> </P>
<P>简单一句话,在函数声明或定义前加一个 inline 修饰符。</P>
<P> </P>
<P>inline int max(int a, int b)</P>
<P>{</P>
<P> return (a>b)? a : b;</P>
<P>}</P>
<P> </P>
<H4><A name=13.4.2>13.4.2</A> inline函数的规则</H4>
<P>规则一、一个函数可以自已调用自已,称为递归调用(后面讲到),含有递归调用的函数不能设置为inline;</P>
<P>规则二、使用了复杂流程控制语句:循环语句和switch语句,无法设置为inline;</P>
<P>规则三、由于inline增加体积的特性,所以建议inline函数内的代码应很短小。最好不超过5行。</P>
<P>规则四、inline仅做为一种“请求”,特定的情况下,编译器将不理会inline关键字,而强制让函数成为普通函数。出现这种情况,编译器会给出警告消息。</P>
<P>规则五、在你调用一个内联函数之前,这个函数一定要在之前有声明或已定义为inline,如果在前面声明为普通函数,而在调用代码后面才定义为一个inline函数,程序可以通过编译,但该函数没有实现inline。</P>
<P>比如下面代码片段:</P>
<P> </P>
<P>//函数一开始没有被声明为inline:</P>
<P>void foo();</P>
<P> </P>
<P>//然后就有代码调用它:</P>
<P>foo();</P>
<P> </P>
<P>//在调用后才有定义函数为inline:</P>
<P>inline void foo()</P>
<P>{</P>
<P> ......</P>
<P>}</P>
<P> </P>
<P>代码是的foo()函数最终没有实现inline;</P>
<P> </P>
<P>规则六、为了调试方便,在程序处于调试阶段时,所有内联函数都不被实现。</P>
<P> </P>
<P>最后是笔者的一点“建议”:如果你真的发觉你的程序跑得很慢了,99.9%的原因在于你不合理甚至是错误的设计,而和你用不用inline无关。所以,其实,inline根本不是本章的重点。</P>
<P> </P>
<P>所以,有关inline 还会带来的一些其它困扰,我决定先不说了。</P>
<P> </P>
<H3><A name=13.5>13.5</A> 函数的递归调用(选修)</H3>
<P>第4次从洗手间里走出来。在一周前拟写有关函数的章节时,我就将递归调用的内容放到了最后。</P>
<P> </P>
<P>函数递归调用很重要,但它确实不适于初学者在刚刚接触函数的时候学习。</P>
<H4><A name=13.5.1>13.5.1</A> 递归和递归的危险</H4>
<P>递归调用是解决某类特殊问题的好方法。但在现实生活中很难找到类似的比照。有一个广为流传的故事,倒是可以看出点“递归”的样子。</P>
<P>“从前有座山,山里有座庙,庙里有个老和尚,老和尚对小和尚说故事:从前有座山……”。</P>
<P>在讲述故事的过程中,又嵌套讲述了故事本身。这是上面那个故事的好玩之处。</P>
<P> </P>
<P>一个函数可以直接或间接地调用自已,这就叫做“递归调用”。</P>
<P>C,C++语言不允许在函数的内部定义一个子函数,即它无法从函数的结构上实现嵌套,而递归调用的实际上是一种嵌套调用的过程,所以C,C++并不是实现递归调用的最好语言。但只要我们合理运用,C,C++还是很容易实现递归调用这一语言特性。</P>
<P> </P>
<P>先看一个最直接的递归调用:</P>
<P> </P>
<P>有一函数F();</P>
<P> </P>
<P>void F()</P>
<P>{</P>
<P> F();</P>
<P>}</P>
<P> </P>
<P>这个函数和“老和尚讲故事”是否很象?在函数F()内,又调用了函数F()。</P>
<P>这样会造成什么结果?当然也和那个故事一样,没完没了。所以上面的代码是一段“必死”的程序。不信你把电脑上该存盘的存盘了,然后建个控制台工程,填入那段代码,在主函数main()里调用F()。看看结果会怎样?WinNT,2k,XP可能好点,98,ME就不好说了……反正我不负责。出于“燃烧自己,照亮别人”的理念,我在自已的XP+CB6上试了一把,下面是先后出现的两个报错框:</P>
<P> </P>
<P><IMG height=117 src="第13章 函数(二).files/ls13.h23.gif" width=551
border=0></P>
<P> </P>
<P>这是CB6的调试器“侦察”到有重大错误将要发生,提前出来的一个警告。我点OK,然后无厌无悔地再按下一次F9,程序出现真正的报错框:</P>
<P> </P>
<P><IMG height=114 src="第13章 函数(二).files/ls13.h15.gif" width=165
border=0></P>
<P> </P>
<P>这是程序抛出的一个异常,EStackOverflow这么看:E字母表示这是一个错误(Error),Stack正是我们前面讲函数调用过程的“栈”,Overflow意为“溢出”。整个
StasckOverflow 意思就:栈溢出啦!</P>
<P>“栈溢出”是什么意思你不懂?拿个杯子往里倒水,一直倒,直到杯子满了还倒,水就会从杯子里溢出了。栈是用来往里“压入”函数的参数或返回值的,当你无限次地,一层嵌套一层地调用函数时,栈内存空间就会不够用,于是发生“栈溢出”。</P>
<P>(必须解释一下,本例中,void
F()函数既没有返回值也没有参数,为什么还会发生栈溢出?事实上,调用函数时,需要压入栈中的,不仅仅是二者,还有某些寄存器的值,在术语称为“现场保护”。正因为C,C++使用了在调用时将一些关键数值“压入”栈,以后再“弹出”栈来实现函数调用,所以C,C++语言能够实现递归。)</P>
<P> </P>
<P>这就是我们学习递归函数时,第一个要学会的知识:</P>
<P> </P>
<P><B>逻辑上无法自动停止的递归调用,将引起程序死循环,并且,很快造成栈溢出。</B></P>
<P> </P>
<P>怎样才能让程序在逻辑上实现递归的自动停止呢?这除了要使用到我们前面辛辛苦苦学习的流程控制语句以后,还要掌握递归调用所引起的流程变化。</P>
<P> </P>
<H4><A name=13.5.2>13.5.2</A> 递归调用背后隐藏的循环流程</H4>
<P> </P>
<P>递归引起什么流程变化?前面的黑体字已经给出答案:“循环”。自已调用自已,当然就是一个循环,并且如果不辅于我们前面所学的if...语句来控制什么时候可以继续调用自身,什么时候必须结束,那么这个循环就一定是一个死循环。</P>
<P>如图:</P>
<P> </P>
<P><IMG height=188 src="第13章 函数(二).files/ls13.h18.gif" width=187
border=0></P>
<P> </P>
<P>递归调用还可间接形成:比如 A() 调用 B(); B() 又调用 A(); 虽然复杂点,但实质上仍是一个循环流程:</P>
<P> </P>
<P><IMG height=300 src="第13章 函数(二).files/ls13.h19.gif" width=220
border=0></P>
<P> </P>
<P>在这个循环之里,函数之间的调用都是系统实现,因此要想“打断”这个循环,我们只有一处“要害”可以下手:在调用会引起递归的函数之前,做一个条件分支判断,如果条件不成立,则不调用该函数。图中以红点表示。</P>
<P> </P>
<P>现在你明白了吗?一个合理的递归函数,一定是一个逻辑上类似于这样的函数定义:</P>
<P> </P>
<P>void F()</P>
<P>{</P>
<P> ……</P>
<P> if(……) //先判断某个条件是否成立</P>
<P> {</P>
<P> F(); //然后才调用自身</P>
<P> }</P>
<P> ……</P>
<P>}</P>
<P> </P>
<P>在武侠小说里,知道了敌人的“要害”,就几乎掌握了必胜的机会;然而,“递归调用”并不是我们的敌人。我们不是要“除掉”它,相反我们利用它。所以尽管我们知道了它的要害,事情还要解决。更重要的是要知道:什么时候该打断它的循环?什么时候让它继续循环?</P>
<P>这当然和具体要解决问题有关。所以这一项能力有赖于大家以后自已在解决问题不断成长。就像我们前面的讲的流程控制,就那么几章,但大家今后却要拿它们在程序里解决无数的问题。</P>
<P>(有些同学开始合上课本准备下课)程序的各种流程最终目的是要合适地处理数据,而中间数据的变化又将影响流程的走向。在函数的递归调用过程中,最最重要的数据变化,就是参数。因此,大多数递归函数,最终依靠参数的变化来决定是否继续。(另外一个依靠是改变函数外的变量)。</P>
<P>所以我们必要彻底明了参数在递归调用的过程中如何变化。</P>
<H4><A name=13.5.3>13.5.3</A> 参数在递归调用过程中的变化</H4>
<P>我们将通过一个模拟过程来观察参数的变化。</P>
<P> </P>
<P>这里是一个递归函数:</P>
<P> </P>
<P>void F(int a)</P>
<P>{</P>
<P> F(a+1);</P>
<P>}</P>
<P> </P>
<P>和前面例子有些重要区别,函数F()带了一个参数,并且,在函数体内调用自身时,我们<FONT
color=#ff0000>传给它当前参数加1的值,作为新的参数</FONT>。</P>
<P>红色部分的话你不能简单看过,要看懂。</P>
<P> </P>
<P>现在,假设我们在代码中以1为初始参数,第一次调用F():</P>
<P> </P>
<P>F(1);</P>
<P> </P>
<P>现在,参数是1,依照我们前面“参数传递过程”的知识,我们知道1被“压入”栈,如图:</P>
<P><IMG height=187 src="第13章 函数(二).files/ls13.h20.gif" width=271
border=0></P>
<P>F()被第1次调用后,马上它就调用了自身,但这时的参数是
a+1,a就是原参数值,为1,所以新参数值应为2。随着F函数的第二次调用,新参数值也被入栈:</P>
<P><IMG height=187 src="第13章 函数(二).files/ls13.h21.gif" width=322
border=0></P>
<P>再往下模拟过程一致。第三次调用F()时,参数变成3,依然被压入栈,然后是第四次……递归背后的循环在一次次地继续,而参数a则在一遍遍的循环中不断变化。</P>
<P>由于本函数仍然没有做结束递归调用的判断,所以最后的最后:栈溢出。</P>
<P> </P>
<P>要对这个函数加入结束递归调用的逻辑判断是非常容易的。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -