文档介绍:第18章更深入的理解——函数进阶
第10章中已经讨论了函数的基本知识,让读者对函数有了基本的认识,本章从更深层次帮助读者理解函数。从和函数关系最密切的调用和返回入手,函数的参数传递有传值,传指针2种方式,从类型的角度上看,参数不仅仅可以是系统内建的数据类型,还可以是数组、结构等。此外,递归编程机制,函数的作用域和可见域,变量的生存期、作用域和可见域等都是本章讨论的重点。
参数传递的副本机制
如果将函数比作剧本,那形参和实参的关系相当于角色和演员的关系,函数的参数传递有传值和传地址两种方式。传值调用时,在函数内对形参的改变都不会影响实参,要想在函数内对实参进行操作,必须采用传地址调用的方式。这是形象化的理解,从本质上说,这是由参数传递的副本机制决定的。
所谓副本机制,是指copy(拷贝)的思想,不论是传值调用还是传址调用,编译器都要为每个参数制作临时副本,或称拷贝,函数体中对参数的修改都是对副本的修改,下面具体分析之。
传值调用的副本
传值调用的情况相对简单,不论传递的参数如何,编译器都为这些参数制作临时副本,函数体中对参数的修改都是针对副本进行的,丝毫不会影响传来的参数,试通过下述一段示例,体会传值调用的副本机制。
传址调用的副本机制
相比传值调用,传址调用似乎要复杂一点,但只要知道,传址调用也是通过副本机制,便能很好地理解传址调用的机理,同样从一个形象的例子入手。
函数返回值的副本机制
如果要细分,函数返回也可以认为存在传值和传址两种方式。函数返回同样也是根据副本机制来处理的,首先来回顾下函数返回的流程:
当执行到return语句时,return的值被复制到某个内存单元或寄存器中,其地址是由编译器来维护的,程序员无法直接访问该地址,也就是说,在函数执行完毕,相关现场被撤销前,返回的值被复制保存到了某个地方,编译器访问该位置即可知道函数的返回值。该位置即可看成是函数中返回值的副本。
对函数返回取地址是不合法的,即假设存在如下函数:
int A(int b,int c);
不允许使用如下形式的语句:
&A(3,4);
return 局部变量为什么合法
函数返回的副本机制很好地解释了为什么return一个局部变量是合法的,来看一段简单的求和函数代码:
int sum(int a,int b) /*函数定义*/
{
int c=a+b; /*局部变量c*/
return c; /*返回*/
}
……
int d=sum(1,2); /*函数调用*/
来看语句“int d=sum(1,2);”,该语句先执行函数sum,sum函数执行完毕后将结果赋值给int型变量d,如果从字面上理解,是将c赋值给d,但实际上,在执行赋值操作时,由于函数sum已经执行完毕返回,函数中的局部变量c已被撤销,不存在了。实际上,在c被撤销前,函数已经为返回值c创建了副本,保存在特定的位置上,赋值操作是由该位置处的副本完成的,形象的示意如所示。
返回指针申请动态内存
下面来看一下如何通过返回指针在函数中动态申请内存,试比较下述与的异同:
代码‑ ess2
不要返回指向栈内存的指针
动态申请内存是在堆中完成的,而函数返回不会释放堆内存,但不要忘记,函数返回时,栈内存中的内容会被自动清除,因此,不要返回指向栈内存的指针。
请读者试着分析下述的问题所在:
返回指向只读存储区的指针
如果将中的GetMemory函数修改如下,会怎样?
char* GetMemory(void) /*定义函数GetMemory*/
{
char* p="Hello,C"; /*栈内存中开辟字符串*/
return p; /*返回局部指针*/
}
"Hello,C"作为常量字符串,位于程序的只读存储区(.rodata),此时,返回指向只读存储区的指针p并没有问题,但该指针只能用于输出,而不能用于输入改写。
函数与结构体
结构体可以看成一种数据组织方式,将很多不同类型的相关数据打包,构成一种新的类型,从这种意义上说,结构体变量完全可以当成是一种普通类型的变量来使用。结构体变量作函数参数时,也有传值和传址两种方式,函数返回亦是如此,既可以返回结构体变量,也可以返回指向非局部结构体变量的指针。