C 语言函数中的形式参数和实际参数

目录汇总:C 语言零基础入门教程

复习一下形式参数实际参数之间的差异。形式参数(parameter)出现在函数定义中,它们以假名字来表示函数调用时需要提供的值;实际参数(argument)是出现在函数调用中的表达式。在形式参数实际参数的差异不是很重要的时候,有时会用参数表示两者中的任意一个。

在 C 语言中,实际参数是值传递的:调用函数时,计算出每个实际参数的值并且把它赋给相应的形式参数。在函数执行过程中,对形式参数的改变不会影响实际参数的值,这是因为形式参数中包含的是实际参数值的副本。从效果上来说,每个形式参数的行为好像是把变量初始化成与之匹配的实际参数的值。

实际参数的值传递既有利也有弊。因为形式参数的修改不会影响到相应的实际参数,所以可以把形式参数作为函数内的变量来使用,这样可以减少真正需要的变量数量。思考下面这个函数,此函数用来计算数 xn 次幂:

1
2
3
4
5
6
7
8
9
int power(int x, int n)
{
  int i, result = 1;

  for (i = 1; i <= n; i++)
    result = result * x;

  return result;
}

因为 n 只是原始指数的副本,所以可以在函数体内修改它,也就不需要使用变量 i 了:

1
2
3
4
5
6
7
8
9
int power(int x, int n)
{
  int result = 1;

  while (n--  > 0)
    result = result * x;

  return result;
}

可惜的是,C 语言对实际参数值传递的要求使它很难编写某些类型的函数。例如,假设我们需要一个函数,它把 double 型的值分解成整数部分和小数部分。因为函数无法返回两个数,所以可以尝试把两个变量传递给函数并且修改它们:

1
2
3
4
5
void decompose(double x, long int_part, double frac_part)
{
  int_part = (long) x;   /* drops the fractional part of x */
  frac_part = x  int_part;
}

假设采用下面的方法调用这个函数:

1
decompose(3.14159, i, d);

在调用开始时,程序把 3.14159 复制给 x,把 i 的值复制给 int_part,而且把 d 的值复制给 frac_part。然后,decompose 函数内的语句把 3 赋值给 int_part,把 .14159 赋值给 frac_part,接着函数返回。可惜的是,变量 i 和变量 d 不会因为赋值给 int_partfrac_part 而受到影响,所以它们在函数调用前后的值是完全一样的。

一、实际参数的转换

C 语言允许在实际参数的类型与形式参数的类型不匹配的情况下进行函数调用。管理如何转换实际参数的规则与编译器是否在调用前遇到函数的原型(或者函数的完整定义)有关。

  • 编译器在调用前遇到原型。就像使用赋值一样,每个实际参数的值被隐式地转换成相应形式参数的类型。例如,如果把 int 类型的实际参数传递给期望得到 double 类型数据的函数,那么实际参数会被自动转换成 double 类型。
  • 编译器在调用前没有遇到原型。编译器执行默认实参提升:(1) 把 float 类型的实际参数转换成 double 类型;(2) 执行整值提升,即把 char 类型和 short 类型的实际参数转换成 int 类型。(C99 实现了整数提升。)

注意

默认实参提升可能无法产生期望的结果。思考下面的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <stdio.h>

int main(void)
{
 double x = 3.0;
 printf("Square: %d\n", square(x));

 return 0;
}

int square(int n)
{
 return n * n;
}

在调用 square 函数时,编译器没有遇到原型,所以它不知道 square 函数期望 int 类型的实际参数。因此,编译器在变量 x 上执行了没有效果的、默认实参提升。因为 square 函数期望 int 类型的实际参数,却获得了 double 类型值,所以 square 函数将产生无效的结果。通过把 square 的实际参数强制转换为正确的类型,可以解决这个问题:

1
printf("Square: %d\n", square((int) x));

当然,更好的解决方案是在调用 square 前提供该函数的原型。在 C99 中,调用 square 之前不提供声明或定义是错误的。

二、数组型实际参数

数组经常被用作实际参数。当形式参数是一维数组时,可以(而且是通常情况下)不说明数组的长度:

1
2
3
4
int f(int a[])   /* no length specified */
{
  ...
}

实际参数可以是元素类型正确的任何一维数组。只有一个问题:f 函数如何知道数组是多长呢?可惜的是,C 语言没有为函数提供任何简便的方法来确定传递给它的数组的长度;如果函数需要,我们必须把长度作为额外的参数提供出来。

注意

虽然可以用运算符 sizeof 计算出数组变量的长度,但是它无法给出关于数组型形式参数的正确答案:

1
2
3
4
5
6
int f(int a[])
{
 int len = sizeof(a) / sizeof(a[0]);
   /*** WRONG: not the number of elements in a ***/
 ...
}

下面的函数说明了一维数组型实际参数的用法。当给出具有 int 类型值的数组 a 时,sum_array 函数返回数组 a 中元素的和。因为 sum_array 函数需要知道数组 a 的长度,所以必须把长度作为第二个参数提供出来。

1
2
3
4
5
6
7
8
9
int sum_array(int a[], int n)
{
  int i, sum = 0;

  for (i = 0; i < n; i++)
    sum += a[i];

  return sum;
}

sum_array 函数的原型有下列形式:

1
int sum_array(int a[], int n);

通常情况下,如果愿意的话,则可以省略形式参数的名字:

1
int sum_array(int [], int);

在调用 sum_array 函数时,第一个参数是数组的名字,第二个参数是这个数组的长度。例如:

1
2
3
4
5
6
7
8
9
#define LEN 100

int main(void)
{
  int b[LEN], total;
  ...
  total = sum_array(b, LEN);
  ...
}

注意,在把数组名传递给函数时,不要在数组名的后边放置方括号:

1
total = sum_array(b[], LEN);    /*** WRONG ***/

一个关于数组型实际参数的重要论点:函数无法检测传入的数组长度的正确性。我们可以利用这一点来告诉函数,数组的长度比实际情况小。假设,虽然数组 b 有 100 个元素,但是实际仅存储了 50 个数。通过书写下列语句可以对数组的前 50 个元素进行求和:

1
total = sum_array(b, 50);   /* sums first 50 elements */

sum_array 函数将忽略另外 50 个元素。(事实上,sum_array 函数甚至不知道另外 50 个元素的存在!)

注意

注意不要告诉函数,数组型实际参数比实际情况大:

1
total = sum_array(b, 150);   /*** WRONG ***/

在这个例子中,sum_array 函数将超出数组的末尾,从而导致未定义的行为。

关于数组型实际参数的另一个重要论点是,函数可以改变数组型形式参数的元素,并且改变会在相应的实际参数中体现出来。例如,下面的函数通过在每个数组元素中存储 0 来修改数组:

1
2
3
4
5
6
7
void store_zeros(int a[], int n)
{
  int i;

  for (i = 0; i < n; i++)
    a[i] = 0;
}

函数调用

1
store_zeros(b, 100);

会在数组 b 的前 100 个元素中存储 0。数组型实际参数的元素可以修改,这似乎与 C 语言中实际参数的值传递相矛盾。事实上这并不矛盾,但现在没法解释。

如果形式参数是多维数组,声明参数时只能省略第一维的长度。例如,如果修改 sum_array 函数使得 a 是一个二维数组,我们可以不指出行的数量,但是必须指定列的数量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#define LEN 10

int sum_two_dimensional_array(int a[][LEN], int n)
{
  int i, j, sum = 0;

  for (i = 0; i < n; i++)
    for (j = 0; j < LEN; j++)
      sum += a[i][j];

  return sum;
}

不能传递具有任意列数的多维数组是很讨厌的。幸运的是,我们经常可以通过使用指针数组解决这种困难。C99 中的变长数组形式参数则提供了一种更好的解决方案。

三、变长数组形式参数

C99 增加了几个与数组型参数相关的特性。第一个是变长数组,这一特性允许我们用非常量表达式指定数组的长度。变长数组也可以作为参数。

考虑本节前面提到过的函数 sum_array,这里给出它的定义,省略了函数体部分:

1
2
3
4
int sum_array(int a[], int n)
{
  ...
}

这样的定义使得 n 和数组 a 的长度之间没有直接的联系。尽管函数体会将 n 看作数组 a 的长度,但是数组的实际长度有可能比 n 大(也可能比 n 小,这种情况下函数不能正确地运行)。

如果使用变长数组形式参数,我们可以显式地说明数组 a 的长度就是 n

1
2
3
4
int sum_array(int n, int a[n])
{
  ...
}

第一个参数(n)的值确定了第二个参数(a)的长度。注意,这里交换了形式参数的顺序,使用变长数组形式参数时参数的顺序很重要。

注意

下面的 sum_array 函数定义是非法的:

1
2
3
4
int sum_array(int a[n], int n)    /*** WRONG ***/
{
 ...
}

编译器会在遇到 int a[n] 时显示出错消息,因为此前它没有见过 n

对于新版本的 sum_array 函数,其函数原型有好几种写法。一种写法是使其看起来跟函数定义一样:

1
int sum_array(int n, int a[n]);      /* Version 1 */

另一种写法是用 *(星号)取代数组长度:

1
int sum_array(int n, int a[*]);      /* Version 2a */

使用 * 的理由如下。函数声明时,形式参数的名字是可选的。如果第一个参数定义被省略了,那么就没有办法说明数组 a 的长度是 n,而星号的使用则为我们提供了一个线索——数组的长度与形式参数列表中前面的参数相关:

1
int sum_array(int, int [*]);        /* Version 2b */

另外,方括号为空也是合法的。在声明数组参数时我们经常这么做:

1
2
int sum_array(int n, int a[]);      /* Version 3a */
int sum_array(int, int []);         /* Version 3b */

但是让括号为空不是一个很好的选择,因为这样并没有说明 na 之间的关系。

一般来说,变长数组形式参数的长度可以是任意表达式。例如,假设我们要编写一个函数来连接两个数组 ab,要求先复制 a 的元素,再复制 b 的元素,把结果写入第三个数组 c

1
2
3
4
int concatenate(int m, int n, int a[m], int b[n], int c[m+n])
{
  ...
}

数组 c 的长度是 ab 的长度之和。这里用于指定数组 c 长度的表达式只用到了另外两个参数;但一般来说,该表达式可以使用函数外部的变量,甚至可以调用其他函数。

到目前为止,我们所举的例子都是一维变长数组形式参数,变长数组的好处还体现得不够充分。一维变长数组形式参数通过指定数组参数的长度使得函数的声明和定义更具描述性。但是,由于没有进行额外的错误检测,数组参数仍然有可能太长或太短。

如果变长数组参数是多维的,则更加实用。之前,我们尝试过写一个函数来实现二维数组中元素相加。原始的函数要求数组的列数固定。如果使用变长数组形式参数,则可以推广到任意列数的情况:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int sum_two_dimensional_array(int n, int m, int a[n][m])
{
  int i, j, sum = 0;

  for (i = 0; i < n; i++)
    for (j = 0; j < m; j++)
      sum += a[i][j];

  return sum;
}

这个函数的原型可以是以下几种:

1
2
3
4
int sum_two_dimensional_array(int n, int m, int a[n][m]);
int sum_two_dimensional_array(int n, int m, int a[*][*]);
int sum_two_dimensional_array(int n, int m, int a[][m]);
int sum_two_dimensional_array(int n, int m, int a[][*]);

四、在数组参数声明中使用 static

C99 允许在数组参数声明中使用关键字 static

在下面这个例子中,将 static 放在数字 3 之前,表明数组 a 的长度至少可以保证是 3:

1
2
3
4
int sum_array(int a[static 3], int n)
{
  ...
}

这样使用 static 不会对程序的行为有任何影响。static 的存在只不过是一个“提示”,C 编译器可以据此生成更快的指令来访问数组。(如果编译器知道数组总是具有某个最小值,那么它可以在函数调用时预先从内存中取出这些元素值,而不是在遇到函数内部需要用到这些元素的语句时才取出相应的值。)

最后,关于 static 还有一点值得注意:如果数组参数是多维的,static 仅可用于第一维(例如,指定二维数组的行数)。

五、复合字面量

再来看看 sum_array 函数。当调用 sum_array 函数时,第一个参数通常是(用于求和的)数组的名字。例如,可以这样调用 sum_array

1
2
int b[] = {3, 0, 3, 4, 1};
total = sum_array(b, 5);

这样写的唯一问题是需要把 b 作为一个变量声明,并在调用前进行初始化。如果 b 不作他用,这样做其实有点浪费。

在 C99 中,可以使用复合字面量来避免该问题,复合字面量是通过指定其包含的元素而创建的没有名字的数组。下面调用 sum_array 函数,第一个参数就是一个复合字面量:

1
total = sum_array((int []){3, 0, 3, 4, 1}, 5);

在这个例子中,复合字面量创建了一个由 5 个整数(3、0、3、4 和 1)组成的数组。这里没有对数组的长度做特别的说明,其长度是由复合字面量的元素个数决定的。当然也可以显式地指明长度,如 (int[4]){1, 9, 2, 1},这种方式等同于 (int[]){1, 9, 2, 1}

一般来说,复合字面量的格式如下:先在一对圆括号内给定类型名,随后是一个初始化器,用来指定初始值。因此,可以在复合字面量的初始化器中使用指示器(C 语言一维数组简介](https://www.developerastrid.com/c/introduction-to-c-language-one-dimensional-array/))一样,而且同样可以不提供完全的初始化(未初始化的元素默认被初始化为 0)。例如,复合字面量 (int[10]){8, 6} 有 10 个元素,前两个元素的值为 8 和 6,剩下的元素值为 0。

函数内部创建的复合字面量可以包含任意的表达式,不限于常量。例如:

1
total = sum_array((int []){2 * i, i + j, j * k}, 3);

其中 ijk 都是变量。复合字面量的这一特性极大地增加了其实用性。

复合字面量为左值(C 语言赋值运算符简介),所以其元素的值可以改变。如果要求其值为“只读”,可以在类型前加上 const,如 (const int []){5, 4}

请参阅

(完)

comments powered by Disqus