为什么在C中调用函数时允许传递的参数数量不足?

why it is allowed to pass insufficient number of parameters when calling a function in C?

我知道如果在 main() 函数之后定义函数,函数原型在 C++ 中是强制性的,但在 C 中它是可选的(但推荐)。我最近写了一个简单的程序,执行加法 2数字,但在传递参数时错误地使用了点运算符而不是逗号。

#include <stdio.h>
int main()
{
    printf("sum is: %d",add(15.30)); // oops, uses dot instead of comma
}

int add(int a,int b)
{
    return a+b;
}

在上面的程序中,如果add(int,int)函数定义在main()函数之前,那么程序肯定会编译失败。这是因为调用函数时传递的参数少于所需的参数。

但是,为什么上面的程序可以编译并且 运行 很好 - 给出一些大的垃圾值作为输出?是什么原因?使用函数原型设计是否更好,以便编译器检测类型不匹配以及与函数调用相关的任何其他错误?

这是未定义的行为吗?

why it is allowed to pass insufficient number of parameters when calling a function in C?

不是。

您的编译器没有 warn/error 并不意味着它是正确的。

如果 C 发现您调用了一个尚未声明的函数,它将生成一个隐式声明,采用任意数量的参数,并假设 return 为 int。在您的示例中,它正在创建 int add(); 的隐式声明,这当然是错误的。

在运行时,add 函数将在其堆栈上接收一个双精度值,并尝试以一种混乱且未定义的方式解释字节。

C99 改变了这个规则,禁止调用未声明的函数,但大多数编译器仍然允许你这样做,尽管有一个警告。

printf无关。

在 C 的早期,方法看起来像:

int add(a,b)
  int a;
  int b;
{
  return a+b;
}

给定 int x; 一条语句 add(3,5); 将生成如下代码:

push 5
push 3
call _add
store r0,_x

编译器不需要知道 任何 关于 add 方法的 return 类型(它可以猜测为 int ) 以生成上述代码;如果该方法最终存在,将在 link 时间被发现。提供比例程预期更多参数的代码会将这些值压入堆栈,它们将被忽略,直到代码稍后弹出它们。

如果代码传递的参数太少、参数类型错误或两者兼而有之,就会出现问题。如果代码传递的参数比例程预期的少,而且被调用的代码没有碰巧使用后面的参数,这通常不会有任何效果。如果代码 读取 未传递的参数,它们通常会读取 'garbage' 值(尽管不能保证读取这些变量的尝试不会产生其他副作用).在许多平台上,第一个未传递的参数是 return 地址,但并非总是如此。如果代码写入未传递的参数,通常会弄乱 return 堆栈并导致奇怪的愚蠢行为。

传递类型不正确的参数通常会导致被调用函数在错误的位置查找其参数;如果传递的类型小于预期的类型,则可能导致被调用方法访问它不拥有的堆栈变量(就像传递的参数太少的情况一样)。

ANSI C 规范了函数原型的使用;如果编译器在看到对 add 的调用之前看到像 int add(int a, int b) 这样的定义或声明,那么它将确保它传递两个 int 类型的参数,即使调用者提供了其他东西(例如,如果调用者传递 double,它的值将在调用前被强制键入 int)。尝试将太少的参数 传递给编译器已识别的方法 将导致错误。

在上面的代码中,对 add 的调用是在编译器对方法的预期内容有任何线索之前进行的。虽然较新的方言不允许这样的用法,但较旧的方言会让编译器默认假定 add 是一种方法,它将接受调用者提供的任何参数和 return int。在上面的例子中,被调用的代码可能期望将两个单词压入堆栈,但 double 常量可能会被压入两个或四个单词。因此,该方法可能会从 double 值 15.3.

的二进制表示中接收 ab 两个词

顺便说一句,如果一个函数的类型被隐式假定为 returning 不同于 int 的其他东西,即使是接受指定语法的编译器也几乎总是会发出尖叫声;如果这样的函数是使用旧式语法以外的任何其他方式定义的,许多人也会抱怨。虽然第一个 C 编译器总是将函数参数压入堆栈,但较新的编译器通常希望它们在寄存器中。因此,如果要声明

 /* Old-style declaration for function that accepts whatever it's given
    and returns double */

double add();

然后代码 add(12.3,4.56) 会将两个 double 值压入堆栈并调用 add 函数,但如果其中一个声明为:

double add(double a, double b);

然后在许多系统上,代码 add(12.3,4.56) 会在调用前用 12.3 加载浮点寄存器 0,用 4.56 加载浮点寄存器 1(不压入任何东西)。因此,这两种声明方式 兼容。在这样的系统上,尝试在不声明的情况下调用方法,然后在同一编译单元中使用新式语法声明它会产生编译错误。此外,一些这样的系统在接受寄存器参数的函数的名称前加上两个下划线,而不是一个下划线,所以如果方法是使用新式语法定义的,但在编译器没有看到原型的情况下调用,编译器会尝试调用 _add,即使该函数被调用 __add。这会导致 linker 拒绝该程序,而不是生成无法运行的可执行文件。