C函数调用中的默认参数提升

41

设置

我有关于在C语言中调用函数时默认参数提升的一些问题。下面是C99标准(pdf)第6.5.2.2节的第6、7和8段文字(已加粗并分为列表以便阅读):

第6段

  1. 如果表示被调用函数的表达式具有不包括原型的类型,则对每个参数执行整数提升,并将类型为float的参数提升为double类型。这些被称为默认参数提升
  2. 如果参数数量不等于参数数量,则行为未定义。
  3. 如果使用包括原型的类型来定义函数,并且原型以省略号结尾(, ...)或者提升后参数的类型与参数的类型不兼容,则行为未定义。
  4. 如果使用不包括原型的类型来定义函数,并且提升后的参数类型与提升后的参数类型不兼容,则行为未定义,除以下情况:
    • 一个提升类型是带符号整数类型,另一个提升类型是相应的无符号整数类型,并且该值可以用两种类型表示;
    • 两个类型都是指向限定或不限定版本字符类型或void的指针。

第7段

  1. 如果表示被调用函数的表达式具有包括原型的类型,则参数会被隐式转换,就像通过赋值一样,转换为相应参数的类型,将每个参数的类型视为其声明类型的未限定版本。
  2. 函数原型声明符中的省略号表示参数类型转换在最后一个声明的参数后停止。默认参数提升会执行到尾部参数上。

第8段

  1. 没有其他隐式转换执行;特别地,在不包括函数原型声明符的函数定义中,不比较参数的数量和类型与参数的数量和类型。

我知道的事情

  • 默认参数提升是将charshort提升为int/unsigned int,将float提升为double
  • 可变参数函数(如printf)的可选参数受默认参数提升的影响

记录一下,我对于函数原型的理解是:

void func(int a, char b, float c);  // Function prototype
void func(int a, char b, float c) { /* ... */ }  // Function definition

问题

我对这一切的理解非常困难。以下是我的一些问题:

  • 原型函数和非原型函数的行为真的有很大差异吗,例如在默认提升和隐式转换方面?
  • 默认参数提升是什么时候发生的?总是这样吗?还是只有特殊情况(如可变参数函数)?这是否取决于一个函数是否被原型化?
3个回答

42
  • 带有原型的函数的(非变参)参数将被转换为相应的类型,该类型可以是char、short或float。

  • 没有原型的函数和变参函数的参数会受到默认参数提升的影响。

如果您定义了一个带有原型的函数并在没有原型的情况下使用它,或者反之亦然,并且它具有char、short或float类型的参数,则在运行时可能会出现问题。如果推广的类型与读取参数列表时使用的类型不匹配,变参函数也会出现同样的问题。

示例1: 在定义具有原型的函数但在没有原型的情况下使用时出现问题。

definition.c

void f(char c)
{
   printf("%c", c);
}

use.c

void f();

int main()
{
   f('x');
}

由于传递了 int 而函数需要一个 char,因此可能会导致失败。

示例2: 在没有原型的情况下定义函数并在使用时使用原型会出现问题。

definition.c

void f(c)
   char c;
{
   printf("%c", c);
}

这种定义方式非常老式。

use.c

void f(char c);

int main()
{
   f('x');
}

在传递 char 类型参数时,由于期望的是 int 类型参数,函数可能会失败。

注意:您会发现标准库中的所有函数都具有默认提升而导致的类型,因此在添加原型时它们并没有引起问题。


当你说“如果你声明一个函数带有原型并且在没有原型的情况下使用它...”时,你是什么意思? - Andrew Keeton
哦,我在意思上用了“声明”,但实际应该是“定义”。已经修复并添加了一个示例。 - AProgrammer

42

赞同AProgrammer的回答——那些才是真正的好东西。

对于那些想知道为什么会这样的人:在1988年之前的“K&R” C经典语言中,没有函数原型的概念,而默认参数提升则是因为(a) 它们基本上是免费的,因为将一个字节放入寄存器与将一个字放入寄存器的成本相同,并且(b) 为了减少参数传递中潜在的错误。但第二个原因并没有完全实现,这就是为什么ANSI C引入函数原型是C语言有史以来最重要的单一变化的原因。

至于何时启动默认提升:默认参数提升恰好在期望的参数类型是未知的时使用,也就是说,当没有原型或者参数是可变参数时。


2
谢谢您澄清。回答“为什么”确实帮助我理解清楚这个问题。 - Andrew Keeton

19
你的困惑源于对术语的微小误解 - 声明和定义都可以包括原型(或者不包括):
void func(int a, char b, float c);

这是一个包含原型的函数声明。

void func(int a, char b, float c) { /* ... */ }

这是一个包含原型的函数定义。

"具有原型"和"没有原型"只是函数类型的属性,声明和定义都会引入函数类型。

因此,您可以有没有原型的声明:

void func();

或者你可以使用没有原型的定义(K&R C风格):

void func(a, b, c)
    int a;
    char b;
    float c;
{ /* ... */ }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接