【浅谈】通过指针看看C函数的“传值调用”20210428

本文深入探讨了C语言中函数参数传递的原理,尤其是针对数组参数时的特殊行为。虽然C语言的函数参数传递通常为传值,但当数组作为参数时,实际传递的是数组首元素的地址,因此函数可以通过这个地址间接修改原数组。文章通过实例解析了整数加法和交换操作,阐述了传值调用和传址调用的区别,并指出数组参数实际上是一种特殊的传值调用形式。
摘要由CSDN通过智能技术生成

前言

对于初学者来说,当接触到由函数的参数传递带来的一些问题时,若是使用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函数的传值调用进行了简单探讨。本文涉及的思想对于之后的数据结构的知识也有很大的联系。

  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值