📄 第13章 函数(二).htm
字号:
<P>如:</P>
<P style="FONT-SIZE: 12pt">void F(int &a) //在形参a之前加一个 &</P>
<P>{</P>
<P> a = 10;</P>
<P>}</P>
<P> </P>
<P>笔者我更习惯于把 & 贴在参数的类型后面:</P>
<P style="FONT-SIZE: 12pt">void F(int& a) //把&贴在类型之后,也可以</P>
<P>{</P>
<P> a = 10;</P>
<P>}</P>
<P> </P>
<P>两种书写格式在作用上没有区别。</P>
<P> </P>
<P>现在让我们用一模一样的代码调用函数F:</P>
<P> </P>
<P><B>例三:</B>地址传递演示:</P>
<P>int main(int argc, char* argv[])</P>
<P>{</P>
<P> //初始化a值为0: </P>
<P> int a = 0;</P>
<P> </P>
<P> //调用函数F:</P>
<P> F(a);</P>
<P> </P>
<P> //输出a的结果:</P>
<P> cout << a << endl;</P>
<P>}</P>
<P> </P>
<P>输出结果,a的值真的被函数F()改为10了。</P>
<P><IMG height=57 src="第13章 函数(二).files/ls13.h8.gif" width=421
border=0></P>
<P> </P>
<P>通过这个例子我们发现,C++中,函数的参数采用“值”或“地址”传递,区别仅仅在于函数的定义中,参数是否加了&符号。在调用函数时,代码没有任何区别。如此产生一个不便之处:我们仅仅通过看调用处的代码,将不好确定某一函数中有哪些参数使用地址传递?我们不得不回头找这个函数的定义或声明。C++很多地方被反对它的人指责,这是其一。C#语言改进了这一点,要求在调用时也明确指明调用方式。</P>
<P>比如,假设有一函数:</P>
<P>int foo(int a ,int &b, int c);</P>
<P>……</P>
<P>在代码某处调用该函数:</P>
<P>int i,j,k;</P>
<P>int r = foo(i,j,k);</P>
<P> </P>
<P>如果你看不到前面函数的声明,那么你在读后面的代码时,可能比较难以想起其中的j是采用传址方式。</P>
<P> </P>
<P>当然,我们没有必要因此就放弃C++这门强大的语言。如果的确需要让阅读代码的人知道某个地方采用了地址传送,可以加上注释,也可以使用我们以后将学的指针作为参数来解决就是。</P>
<P> </P>
<P>关于地址传送方式的作用,及如何实现地址传送,我们已明白。剩下来需要弄明白的是,“地址传送”是如何实现的?</P>
<P> </P>
<P>首先,为什么叫“地址”传送?如果你完成了前面指出的复习任务。那么你应该明白了变量与地址的关系,这里我从根本上重述一次:</P>
<P> </P>
<P>程序中,数据存放在内存里;</P>
<P>内存按照一定规则被编号,这些号就称为内存地址,简称地址;</P>
<P>内存地址很长,所以高级语言实现了用变量代表内存地址;</P>
<P>所以,一个变量就是一个内存地址。</P>
<P> </P>
<P>因此,这里“地址传送”中的“地址”,指的就是变量的地址。那么参数(实参)是变量吗?</P>
<P> </P>
<P>参数可以是变量,也可以不是变量。我们先来说是的情况。比如前面的例子:</P>
<P> </P>
<P>……</P>
<P>int a = 0;</P>
<P>F(a); //正确地调用函数F:参数a是一个变量</P>
<P>……</P>
<P>如果面上面例子中,我们直接传给F函数0,可以吗?</P>
<P> </P>
<P>……</P>
<P>F(0); //错误地调用函数F:0是一个常数,不是变量。</P>
<P>……</P>
<P> </P>
<P>错误原因是:因为函数F()的参数采用“地址传递”方式,所以它需要得到参数的地址,而0是一个常数,无法得到它地址。</P>
<P>得出第一个结论:在<B>调用函数时,凡是采用“传址方式”的参数,不能将常数作为该参数。</B></P>
<P> </P>
<P>如果你在程序中违返了这一规定,不要紧,编译器不会放过这一错误。下面让我们来理解为什么传递变量地址可以起到让函数修改参数。这也好有一比,我们再来考一次“电脑操作知识”。</P>
<P> </P>
<P>以下是某用户在电脑上的操作过程,请仔细阅读,然后回答问题。</P>
<P> </P>
<P>操作一:用户在C盘上找一个文本文件;</P>
<P>操作二:用户使用鼠标右键拖动该文件到D盘,松开后,出现右键菜单,用户选择“在当前位置创建方式”,如图:</P>
<P><IMG height=87 src="第13章 函数(二).files/ls13.h9.gif" width=194
border=0></P>
<P>操作三:用户双击打开在D盘创建的该快捷方式,然后进行编辑,最后存盘。</P>
<P> </P>
<P>请问:C盘上的文件内容是否在该过程受到修改?</P>
<P> </P>
<P>答案:C盘的文件并改变了,因为D盘上的快捷方式,正是C盘上文件的一个“引用”,双击该快捷方式,正是打开了C盘的文件。</P>
<P> </P>
<P>“地址传递”类似于此,将地址传送给函数,函数对该地址的内容操作,相当于对实参本身的操作。</P>
<P> </P>
<P><IMG height=227 src="第13章 函数(二).files/ls13.h11.gif" width=334
border=0></P>
<P> </P>
<H4><A name=13.2.3>13.2.3</A> 参数的传递过程(选修)</H4>
<P>刚讲完“参数的传递方式”,又讲“参数的传递过程”,不禁让人有点发懵:方式和过程有何区别?中学时我对前桌的女生“有意思”,想给人家传递点信息,是往她家打个电话呢?还是来个“小纸条”?这就是“传递方式”的不同。我选择了后者。至于传递过程:刚开始时我把纸条裹在她的头发里,下课时假装关心地“喂,你的头发里掉了张纸……”。后来大家熟了,上课时我轻轻动一下她的后背,她就会不自在,然后在一个合适时机,自动把手别过来取走桌沿的纸条……这就是传递过程的不同吧?</P>
<P>(以上故事纯属虚构)</P>
<P> </P>
<P>程序是在内存里运行的。所以无论参数以哪一种方式传递,都是在内存中“传来传去”。在一个程序运行时,程序会专门为参数开辟一个内存空间,称为“栈”。栈所在内存空间位于整个程序所占内存的顶部(为了直观,我们将地址较小的内存画在示意图顶部,如果依照内存地址由下而上递增规则,则栈区应该在底部),如图:</P>
<P><IMG height=184 src="第13章 函数(二).files/ls13.h12.gif" width=198
border=0></P>
<P>当程序需要传递参数时,将一个个参数“压入”栈区内存的底部,然后,函数再从栈区一个个读出参数。</P>
<P>如果一个函数需要返回值,那么调用者首先需要在栈区留出一个大小正好可以存储返回值的内存空间,然后再执行参数的入栈操作。</P>
<P>假设有一函数:</P>
<P>int AddTwoNum(int n1, int n2);</P>
<P> </P>
<P>然后在代码某处调用:</P>
<P>....</P>
<P>int a = 1;</P>
<P>int b = 2;</P>
<P> </P>
<P><B>int c = AddTwoNum(a,b);</B></P>
<P> </P>
<P>当执行上面黑体部分,即调用函数的动作发生时,栈区出现下面的操作:</P>
<P> </P>
<P><IMG height=265 src="第13章 函数(二).files/ls13.h14.gif" width=395
border=0></P>
<P> </P>
<P>图中标明为返回值预留的空间大小是4个字节,当然不是每个函数都这个大小。它由函数返回值的数据类型决定,本函数AddTwoNum返回值是int类型,所以为4个字节。其它的a,b参数也是int类型,所以同样各占4字节大小的内存空间。</P>
<P> </P>
<P>至于参数是a还是b先入栈,这依编译器而定,大都数编译器采用“从右到左的次序”将参数一个个压入。所以本示意图,参数b被先“压”入在底部,然后才是a。</P>
<P> </P>
<P>这样就完成了参数的入栈过程。根据前面讲的不同“传递方式”,被实际压入栈的数据也就不同。</P>
<P>一、如果是“传值”,则栈中的a,b就是“复制品”,对二者的操作,仅仅是改变此处栈区的内存,和调用处的实参:a,b毫不关联:</P>
<P><IMG height=244 src="第13章 函数(二).files/ls13.h16.gif" width=395
border=0></P>
<P>二、而在“传址”方式时,编译器会将调用处的a,b的内存地址写入栈区,并且将函数中所有对该栈区内存的操作,都转向调用处a,b的内存地址。请看:</P>
<P><IMG height=281 src="第13章 函数(二).files/ls13.h17.gif" width=507
border=0></P>
<P> </P>
<P>看起来二的图比一要复杂得多。其实实质的区别并不多。</P>
<P>你看:</P>
<P> </P>
<P>实参a, 值为1, 内存地址为:00129980</P>
<P>实参b, 值为2, 内存地址为:00129984</P>
<P> </P>
<P>在一图中,传给函数的是a,b的值,即1,2;</P>
<P>在二图中,传给函数的是a,b的地址,即:00129980,00129984。</P>
<P> </P>
<P>这就是二者的本质区别。</P>
<P> </P>
<P>“参数的传递过程”说到最后,还是和“参数的传递方式”纠缠在一起。我个人认为,在刚开始学习C++时,并不需要--或者甚至就是最好不要--去太纠缠语言内部实现的机制,而重在于运用。下面我们就来举一个使用“传址”方式的例子。</P>
<P>题目是:写一函数,实现将两个整型变量的值互换。</P>
<P>比如有变量:int a = 1, b =2;我们要求将它作为所写函数的参数,执行函数后,a,b值互换。即a为2,b为1。</P>
<P> </P>
<P>交换两个变量的值,这也是一个经典题目。并且在实际运用中,使用得非常广泛。事实上很多算法都需要用到它。</P>
<P>幸好实现它也非常的简单和直观。典型的方法是使用“第三者”你可能感到不解:交换两个变量的值,就让这两个变量自个互换就得了,比如小明有个苹果,小光有个梨子,两人你给我给你就好了啊,要小兵来做什么?</P>
<P>呵,你看吧:</P>
<P>int a = 1, b = 2;</P>
<P> </P>
<P>//不要“第三者”的交换(失败)</P>
<P>a = b;</P>
<P>b = a;</P>
<P> </P>
<P>好好看看,好好想想吧。当执行交换的第一句:a =
b;时,看去工作得不错,a的值确实由1变成了2。然后再下去呢?等轮到b想要得到a的值时,a现在和b其实相等,都是2,结果b=a;后,b的值还是2.没变。</P>
<P> </P>
<P>只好让“第三者”插足了……反正程序没有婚姻法。</P>
<P> </P>
<P>int a = 1, b = 2;</P>
<P>int c ; //“第三者”</P>
<P> </P>
<P>//交换开始:</P>
<P>c = a;</P>
<P>a = b;</P>
<P>b = c;</P>
<P> </P>
<P>好了,代码你自已琢磨吧。下面把这些代码写入函数,我命名为Swap;</P>
<P> </P>
<P><B>例四:</B>两数交换。</P>
<P> </P>
<P>void Swap(int& a, int& b)</P>
<P>{</P>
<P> int c = a;</P>
<P> a = b;</P>
<P> b = c;</P>
<P>}</P>
<P> </P>
<P>int main(int argc, char* argv[])</P>
<P>{</P>
<P> int a, b;</P>
<P> cout << "请输入整数a:" ;</P>
<P> cin >> a;</P>
<P> </P>
<P> cout << "请输入整数b":";</P>
<P> cint >> b;</P>
<P> </P>
<P> cout << "交换之前,a = " << a << " b = "
<< b << endl;</P>
<P> </P>
<P> Swap(a,b);</P>
<P> cout << "交换之后,a = " << a << " b = "
<< b << endl;</P>
<P> </P>
<P> getch();
//getchar会自动“吃”到我们输入b以后的回车符,所以改为getch(),记得前面有#include <conio.h></P>
<P> </P>
<P> return 0;</P>
<P>}</P>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -