Menu

C 语言函数参数:值传递、指针和数组

C 语言的一切都是值传递:函数拿到的是副本,从来不是原件。这在实践中意味着什么,如何用指针模拟引用传递,为什么数组的表现不同,以及如何返回多个结果。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

形参和实参

这两个词经常混用,但值得区分清楚:

  • 形参(parameter)是函数定义中的变量:int square(int n) 中的 int n。
  • 实参(argument)是调用时提供的值:square(4) 中的 4。

调用函数时,会把形参创建为全新的局部变量,并把实参复制进去。这个复制就是本页的全部主题。

C 语言永远是值传递

这是一条规则,C 语言没有例外:函数接收的是实参的副本。给形参赋值只会改变副本。

value 仍然是 42。函数拿到的是一个叫 x 的副本,把副本设成了零,函数返回时副本就消失了。把形参改名为 value 也不会有任何变化:名字并不会把它们联系起来,它们是两个不同的变量。

每种类型都是如此:int、double、char、指针,甚至整个结构体(逐个字段复制)。如果你学过 Python、Java 或 JavaScript,要注意这些语言按值传递的是对象的引用,所以在函数内部修改对象,外部是能看到的。C 语言没有这种机制,它复制的是对象本身。

值传递是一种特性,而不是限制。函数不会意外破坏调用者的数据,这让代码更容易推理。

经典的失败案例:swap

教科书上的演示是一个试图交换两个值的函数:

逻辑是正确的,它确实交换了 a 和 b。但 a 和 b 是副本,所以函数一返回,所做的工作就全白费了。

用指针模拟引用传递

要让函数修改调用者的变量,就把变量的地址而不是值传给它。这个地址本身也是按值传递的(指针被复制了),但副本仍然指向原来的对象,所以通过它写入就能修改调用者的内存。

整个思路由三处语法支撑:

  • 形参列表中的 int *a 把 a 声明为“指向 int 的指针”。
  • 调用处的 &x 取得 x 的地址。
  • 函数内部的 *a 表示“a 所指向的那个 int”,可以读取,也可以赋值。

这就是 C 程序员所说的“引用传递”,但值得说得精确一点:它仍然是值传递,被复制的是指针。给 a 本身赋值(a = NULL;)只会改变副本;给 *a 赋值才会改变调用者的变量。指针一页完整讲解了这个模型。

代价是,调用处不再能直接看出一个变量是否可能被修改,这正是为什么必须写 &。swap(x, y) 无法通过编译,你必须写 swap(&x, &y),这个 & 就是一个可见的标记,表明这次调用可能会修改 x。

返回多个结果

return 只能产生一个值,所以额外的输出要通过指针参数传出。常见的形式是用返回值表示状态码,真正的结果通过指针写入:

注意调用失败时,q 和 r 保持原来的值:函数在写入任何东西之前就返回了。这是调用者必须遵守的约定,也是为什么要先检查状态,再使用输出。

另一种做法是返回一个同时包含两个字段的结构体,这样就完全不需要指针;当这些值本来就属于一个整体时,这通常是更好的设计。

数组不一样

数组不遵循复制规则,这几乎会绊倒每一个人。当你传递数组时,它会退化为指向首元素的指针,所以函数拿到的是地址,而不是数据的副本。

调用者的数组确实被修改了,而且哪里都没有 &,因为在这里 data 本身就是一个地址。由此有三个结论:

在形参列表中,int a[] 和 int *a 的含义完全相同。 就连 int a[100] 也一样,大小会被忽略。写成 int a[] 能表明意图,但不要把它理解为“数组被复制了”。

函数内部的 sizeof 是错的。 这就是那个 bug:

在 main 中,sizeof(data) 是 20 字节,算出的长度是 5。在函数内部,sizeof(a) 是指针的大小,在 64 位系统上是 8,所以“长度”是 2。数组的大小在那里根本无从得知。一定要把长度作为单独的参数传入。 现代编译器在使用 -Wall 时会对此发出警告。

不打算写入时就用 const。 const int a[] 表明函数只读取数据,编译器会强制保证这一点,读代码的人也可以放心:

int sum(const int a[], int n);        /* promises not to modify the array */

字符串也是数组,所以同样适用:void greet(const char *name) 是只读取字符串而不修改它的函数的标准签名。

开销怎么样?

按值传递一个大结构体,每次调用都会复制它的每一个字节。对于只有几个字段的结构体,这没问题,而且常常比指针带来的间接访问更快。对于大结构体,就传指针,如果函数只读取,再加上 const:

struct Config { char name[64]; int flags[32]; double weights[128]; };

void applyConfig(const struct Config *cfg);   /* no 512-byte copy per call */

const struct Config *cfg 表示“一个指向 Config 的指针,我不会修改它”,这样既有指针的效率,又有副本的安全性。

常见错误

  • 以为普通形参能改变调用者的变量。 做不到,要传指针。
  • 调用处忘了写 &。 当 swap 需要指针时写 swap(x, y) 是类型错误,编译器能发现,但前提是作用域内有函数原型。
  • 函数内部忘了写 *。 对两个指针形参写 a = b; 交换的是局部副本,而不是值。你想要的是 *a = *b;。
  • 对数组形参使用 sizeof。 永远是错的。传入长度。
  • 返回指向局部变量的指针。 函数返回时局部变量就消亡了,调用者拿到的是悬空指针,这是未定义行为。改为按值返回、写入调用者提供的缓冲区,或者用 malloc 分配。
  • 不检查指针参数是否为 NULL。 一个对传入的任何东西都直接解引用的函数,遇到空指针参数就会崩溃。

常见问题

C 语言有引用传递吗?

没有。C 语言只有一种方式:值传递。函数接收到的永远是每个实参的副本,所以给形参赋值永远不会影响调用者的变量。看起来像引用传递的做法,其实是按值传递一个指针:指针被复制了,但它仍然指向调用者的对象,所以函数可以通过指针修改它。

C 语言中怎么把数组传给函数?

把形参写成 int a[] 或 int *a(两者含义相同),并把长度作为单独的参数传入:int sum(const int a[], int n)。数组不会被复制;函数收到的是指向首元素的指针,所以对 a[i] 的修改,调用者都能看到。

为什么在 C 语言中对数组形参用 sizeof 不行?

因为这个形参实际上是指针。函数内部的 sizeof(a) 得到的是指针的大小(通常是 8 字节),而不是数组的大小,所以 sizeof(a)/sizeof(a[0]) 得出的是 2 之类的数,而不是元素个数。一定要显式传入长度。

C 语言函数怎么返回多个值?

为额外的结果传入指针,并通过指针写入:int divide(int a, int b, int *quotient, int *remainder) 返回一个表示成功与否的标志,并填好两个输出。另一种做法是返回一个把这些值打包在一起的 结构体。

Coddy 编程语言插图

和 Coddy 一起学编程

开始