文章目录
前言
对于初学者来说,当接触到由函数的参数传递带来的一些问题时,若是使用python等语法简洁的语言可能并无大碍,但若是使用C语言编程,由于指针这一概念的抽象性,经常会摸不着头脑。很感谢Kenneth Reek的《Pointers On C》一书,让我入门C语言时受益匪浅。为了让更多初学者在不用进行繁琐的阅读的前提下进一步理解C函数的传值调用并感受此书的魅力,我将书中的部分与自己的拙见整合到一起写成这篇文章。由于本人水平有限,文章中若存在错漏之处还请读者批评指正。
——Penguin
一、由数组参数引发的思考
C函数 的所有参数均以 “传值调用” 方式进行传递,这意味着函数将获得参数值的一份拷贝。这样,函数可以放心修改这个拷贝值,而不必担心修改调用程序实际传递给它的参数。
但是,如果被传递的参数是一个数组名,并且在函数中使用下标引用该数组的参数,那么在函数中对数组元素进行修改时,实际上修改的是调用程序中的数组元素。函数将访问调用程序的数组元素,数组并不会被复制。这个行为被称为 “传址调用” 。
数组参数的这种行为似乎与传值调用规则相悖,其足以引发我们思考其中的联系。
二、“传”到桥头自然“值”
1 “传值”与“传址”
1.1 整数加法
我们看到两个简单的加法函数
/*将传入的整数加7并返回*/
int add7_value(int num)
{
return (num + 7);
}
/*将传入的地址处的整数加7*/
void add7_address(int* num)
{
*num += 7;
}
此时我们可以轻松地讲出其中的原理。函数 add7_value 接收一个整型并 拷贝 (系统会自动分配一块内存以存放这份拷贝,拷贝名为num),接着在函数体内加 7,并返回。函数 add7_address 接收一个整型的地址并拷贝(同样会分配一块内存以存放,拷贝名 num,值得注意的是这里的 num 是一个地址),接着通过这个地址找到整型的“老家”(即使用单目操作符 * ),使它自增 7。
我们生动地将前后二者分别称之为传值调用和传址调用。
明白其中的原理后不难理解函数调用方式。
/*调用函数add7_value和add7_address*/
int main(void)
{
int v = 0;
int a = 0;
v = add7_value(v);
add7_value(&a);
}
1.2 整数交换
当我们希望交换调用程序所传递的两个参数的值。
/*交换调用程序中的两个整数(没有效果!)*/
void swap(int x, int y)
{
int temp;
temp = x;
x = y;
y = temp;
}
这个程序是无效的!因为它实际交换的是参数的拷贝,原先的参数值并未进行交换。为了访问调用程序的值,必须向函数传递指向所希望修改的变量的指针,接着函数必须对指针使用 间接访问 操作,修改需要修改的变量。下面的程序使用了这个技巧。
/*交换调用程序中的两个整数*/
void swap(int* x, int* y)
{
int temp;
temp = *x;
*x = *y;
*y = temp;
}
同样地,我们可以将二者用传值调用和传址调用区分开来。
2 数组参数的传值调用
2.1 恐怖故事
在上一节中我们回顾了简单的函数调用,按直观理解,有的人会提出:应该接收一份数组的拷贝!老实说这可真是个恐怖故事!
我们知道,函数一定会获得传入参数的一份拷贝。如果获得的是整个数组的拷贝,那么系统需要分配一块内存以存放这份拷贝,当数组非常大的时候,这个“拷贝”的过程会消耗大量的时间,尽管我们不考虑内存是否足够及内存分配是否成功等一系列问题。此外,我们调用一个操作数组的函数时,往往是想对这个数组的元素进行一些修改或运算,当计算机看着我们将原数组拷贝一份,再对这份拷贝里的元素进行修改或运算,最后重新赋值给原数组,如果计算机有腿的话一定要被吓跑啦!
2.2 地址的妙用
为了避免“恐怖故事”的发生,我们必须得想出一个办法。因为传入的数组参数的 长度 必须是 固定 的,这意味着这个数组在编译后拥有了一串 连续、固定 的地址,所以我们只需要把一个元素在数组中的相对位置及其地址告诉函数,就能使函数精确地操作数组的任意元素,那么我们把数组 首元素的地址 告诉函数真是再好不过了!值得注意的是这样能直接对原数组的元素进行操作,从而不用先操作数组的拷贝再赋值给原数组。愿此后再无“恐怖故事”。
2.3 作为函数参数的数组名
我们知道数组名的值就是其首元素的地址,即一个指向数组第一个元素的指针,所以很容易明白,当一个数组名作为参数传递给一个函数时,传递给函数的是一份 该指针的拷贝 。函数如果执行了下标引用,实际上是对这个指针执行间接访问操作,并且通过这种间接访问,函数可以访问和修改调用程序的数组元素。
现在可以解释C关于参数传递的“矛盾”之处。早先曾说过,所有传递给函数的参数都是通过传值方式进行的,但数组名参数的行为却仿佛是通过传址调用传递的。传址调用是通过传递一个元素的地址,即指向所需元素的指针,然后在函数中对该指针执行间接访问操作实现对数据的访问。作为参数的数组名是个指针,下标引用实际执行的就是间接访问。
那么数组的传值调用行为又表现在什么地方呢?传递给函数的是参数的一份副本(指向数组起始位置的指针的副本),所以函数可以自由地操作它的 指针形参 ,而不必担心会修改对应的作为实参的指针。这也与数组名是一个指针常量很好地相容,因为在其传入函数后被操作的是另一个指向同样位置的指针,从而不会修改数组名这个指针常量。
所以,此处并不存在矛盾:所有的参数都是通过传值方式传递的。所谓“传址”,其实也是一种更抽象的“传值”!
当然,如果传递了一个指向某个变量的指针,而函数对该指针执行了间接访问操作,那么函数就可以修改那个变量。尽管看上去并不明显,但数组名作为参数时所发生的正是这种情况。这个参数(指针)实际上是通过传值的方式传递的,函数得到的是该指针的一份拷贝,它可以被修改,但原始的参数并不受影响。
总结
至此,我通过数组这一简单、常见的聚合数据类型,结合指针的基本内容,对C函数的传值调用进行了简单探讨。本文涉及的思想对于之后的数据结构的知识也有很大的联系。