C 语言函数:问与答

本文内容

问:一些 C 语言教程出现了采用了不同于“形式参数”和“实际参数”的术语,是否有标准术语?

答:正如对待 C 语言的许多其他概念一样,没有通用的术语标准,但是 C89 和 C99 标准采用形式参数实际参数。表 1 应该对翻译有帮助。

表 1  术语对照

本教程 其他教程
形式参数(形参) parameter formal argument、formal parameter
实际参数(实参) argument actual argument、actual parameter

请记住,在不会产生混乱的情况下,有时会故意模糊两个术语的差异,采用参数表示两者中的任意一个。

问:在程序的形式参数列表后边,我们遇见过把形式参数的类型用单独的声明进行说明的例子:

1
2
3
4
5
double average(a, b)
double a, b;
{
  return (a + b) / 2;
}

这种实践是合法的吗?

答:这种定义函数的方法来自经典 C,所以可能会在较早的程序中遇到这种方法。C89 和 C99 支持这种格式以便于可以继续编译旧的程序。然而,由于下面两个原因,本教程避免在新程序中采用此种方法。

首先,用经典 C 方法定义的函数不会经受和新格式函数一样程度的错误检查。当函数采用经典方法定义(并且没有给出原型)时,编译器不会检测调用函数的实际参数数量是否正确,也不会检测实际参数是否具有正确的类型。相反,编译器会执行默认实参提升(C 语言函数中的形式参数和实际参数)。

其次,C 标准提到经典格式是“逐渐消亡的”,这意味着不鼓励此种用法,并且这种格式最终可能会从 C 语言中消失。

问:一些编程语言允许过程和函数互相嵌套。C 语言是否允许函数定义嵌套呢?

答:不允许。C 语言不允许一个函数的定义出现在另一个函数体中。这个限制可以使编译器简单化。

问:为什么编译器允许函数名后面不跟着圆括号?

答:在后面某一章中会看到,编译器把后面不跟圆括号的函数名看作指向函数的指针。指向函数的指针有合法的应用,因此编译器不能自动假定函数名不带圆括号是错误的。语句

1
print_pun;

是合法的,因为编译器会把 print_pun 看作指针(并进一步看作表达式),从而使得上述语句被视为有效(虽然没有意义)的表达式语句(C 语言表达式语句)。

问:在函数调用 f(a, b) 中,编译器如何知道逗号是标点符号还是运算符呢?

答:函数调用中的实际参数不能是任意的表达式,而必须是标准文档中,位于赋值表达式前面的那些表达式,这些表达式不能用逗号作为运算符,除非逗号是在圆括号中的。换句话说,在函数调用 f(a, b) 中,逗号是标点符号;而在 f((a, b)) 中,逗号是运算符。

问:函数原型中形式参数的名字是否需要和后面函数定义中给出的名字相匹配?

答:不需要。一些程序员利用这一特性,在原型中给参数一个较长名字,然后在实际定义中使用较短的名字。或者,说法语的程序员可以在函数原型中使用英语名字,然后在函数定义中切换成更为熟悉的法语名字。

问:我始终不明白为什么要提供函数原型。只要把所有函数的定义放置在 main 函数的前面,不就没有问题了吗?

答:错。首先,你是假设只有 main 函数调用其他函数,当然这是不切实际的。实际上,某些函数会相互调用。如果把所有的函数定义放在 main 的前面,就必须仔细斟酌它们之间的顺序,因为调用未定义的函数可能会导致大问题。

然而,问题还不止这些。假设有两个函数相互调用(这可不是刻意找麻烦)。无论先定义哪个函数,都将导致对未定义的函数的调用。

但是,还有更麻烦的!一旦程序达到一定的规模,在一个文件中放置所有的函数是不可行的。当遇到这种情况时,就需要函数原型告诉编译器在其他文件中定义的函数。

问:我看到有的函数声明忽略了形式参数的全部信息:

1
double average();

这种做法是合法的吗?

答:是的。这种声明提示编译器 average 函数返回 double 类型的值,但不提供关于参数数量和类型的任何信息。(留下空的圆括号不意味着 average 函数没有参数。)

在经典 C 中,这是唯一允许的一种声明格式。我们采用的函数原型格式是包含参数信息的,这是 C89 的新特性。旧式的函数声明虽然还允许使用,但现在已逐渐废弃了。

问:为什么有的程序员在函数原型中故意省略参数名字?保留这些名字不是更方便吗?

答:省略原型中的参数名字通常是出于防御目的。如果恰好有一个宏的名字跟参数一样,预处理时参数的名字会被替换,从而导致相应的原型被破坏。这种情况在一个人编写的小程序中不太可能出现,但在很多人编写的大型应用程序中是可能出现的。

问:把函数的声明放在另一个函数体内是否合法?

答:合法。下面是一个示例:

1
2
3
4
5
int main(void)
{
  double average(double a, double b);
  ...
}

average 函数的这个声明只有在 main 函数体内是有效的。如果其他函数需要调用 average 函数,那么它们每一个都需要声明它。

这种做法的好处是便于阅读程序的人弄清楚函数间的调用关系。(在这个例子中,main 函数将调用 average 函数。)另外,如果几个函数需要调用同一个函数,那么可能是件麻烦事。最糟糕的情况是,在程序修改过程中试图添加或移除声明可能会很麻烦。基于这些原因,本教程将始终把函数声明放在函数体外。

问:如果几个函数具有相同的返回类型,能否把它们的声明合并?例如,既然 print_pun 函数和 print_count 函数都具有 void 型的返回类型,那么下面的声明合法吗?

1
void print_pun(void), print_count(int n);

答:合法。事实上,C 语言甚至允许把函数声明和变量声明合并在一起:

1
double x, y, average(double a, double b);

但是,此种方式的合并声明通常不是个好方法,它可能会使程序显得有点混乱。

问:如果指定一维数组型形式参数的长度,会发生什么?

答:编译器会忽略长度值。思考下面的例子:

1
double inner_product(double v[3], double w[3]);

除了注明 inner_product 函数的参数应该是长度为 3 的数组以外,指定长度并不会带来什么其他好处。编译器不会检查参数实际上的长度是否为 3,所以不会增加安全性。事实上,这种做法会产生误导,因为这种写法暗示只能把长度为 3 的数组传递给 inner_product 函数,但实际上可以传递任意长度的数组。

问:为什么可以留着数组中第一维的参数不进行说明,但是其他维数必须说明呢?

答:首先,需要知道 C 语言是如何传递数组的。在把数组传递给函数时,是把指向数组第一个元素的指针给了函数。

其次,需要知道取下标运算符是如何工作的。假设 a 是要传给函数的一维数组。在书写语句

1
a[i] = 0;

时,编译器计算出 a[i] 的地址,方法是用 i 乘以每个元素的大小,并把乘积加到数组 a 表示的地址(传递给函数的指针)上。这个计算过程没有依靠数组 a 的长度,这说明了为什么可以在定义函数时忽略数组长度。

那么多维数组怎么样呢?回顾一下就知道,C 语言是按照行主序存储数组的,即首先存储第 0 行的元素,然后是第 1 行的元素,以此类推。假设 a 是二维数组型的形式参数,并有语句

1
a[i][j] = 0;

编译器产生指令执行如下:(1) 用 i 乘以数组 a 中每行的大小;(2) 把乘积的结果加到数组 a 表示的地址上;(3) 用 j 乘以数组 a 中每个元素的大小;(4) 把乘积的结果加到第二步计算出的地址上。为了产生这些指令,编译器必须知道 a 数组中每一行的大小,行的大小由列数决定。底线:程序员必须声明数组 a 拥有的列的数量。

问:为什么一些程序员把 return 语句中的表达式用圆括号括起来?

答:Kernighan 和 Ritchie 写的《C 程序设计语言》第 1 版的示例中一直在 return 语句中有圆括号,尽管有时是不必要的。不少程序员(和后续书的作者)也采用了这种习惯。因为这种写法不是必需的,而且对可读性没有任何帮助,所以本教程不使用这些圆括号。(Kernighan 和 Ritchie 显然也同意这一点,在《C 程序设计语言》第 2 版中,return 语句就没有圆括号了。)

问:非 void 函数试图执行不带表达式的 return 语句时会发生什么?

答:这依赖于 C 语言的版本。在 C89 中,执行不带表达式的非 void 语句会导致未定义的行为(但只限于程序试图使用函数返回值的情况)。在 C99 中,这样的语句是不合法的,编译器会报错。

问:如何通过测试 main 的返回值来判断程序是否正常终止?

答:这依赖于使用的操作系统。许多操作系统允许在“批处理文件”或“Shell 脚本”内测试 main 的返回值,这类文件包含可以运行几个程序的命令。例如,Windows 批处理文件中的

1
if errorlevel 1 命令

会导致在上一个程序终止时的状态码大于等于 1 时执行命令

在 UNIX 系统中,每种 Shell 都有自己测试状态码的方法。在 Bourne Shell 中,变量 $? 包含上一个程序的运行状态。C Shell 也有类似的变量,但是名字是 $status

问:在编译 main 函数时,为什么编译器会产生“control reaches end of non-void function”这样的警告?

答:尽管 main 函数有 int 作为返回类型,但编译器已经注意到 main 函数没有 return 语句。在 main 的末尾放置语句

1
return 0;

将保证编译顺利通过。顺便说一下,即使编译器不反对没有 return 语句,这也是一种好习惯。

用 C99 编译器编译程序时,这一警告不会出现。在 C99 中,main 函数的最后可以不返回值,标准规定在这种情况下 main 自动返回 0。

问:对于前一个问题,为什么不把 main 函数的返回类型定义为 void 呢?

答:虽然这种做法非常普遍,但是根据 C89 标准,这是非法的。即使它不是非法的,这种做法也不好,因为它假设没有人会测试程序终止时的状态。

C99 允许为 main 声明“由实现定义的行为”(返回类型可以不是 int 型,也可以不是标准规定的参数),从而使得这样的行为合法化了。但是,这样的用法是不可移植的,所以最好还是把 main 的返回类型声明为 int

问:如果函数 f1 调用函数 f2,而函数 f2 又调用了函数 f1,这样合法吗?

答:是合法的。这是一种间接递归的形式,即函数 f1 的一次调用导致了另一次调用。(但是必须确保函数 f1 和函数 f2 最终都可以终止!)

请参阅

(完)

comments powered by Disqus

本文内容