我可以调用带有 long 参数和 int 参数的函数吗?

Can I call a function taking a long parameter with an int argument?

这段代码是未定义的行为吗?

extern long f(long x);

long g(int x)
{
    return f(x);
}

根据C11标准,在6.5.2.2 §6:

If the function is defined with a type that includes a prototype, and [...] the types of the arguments after promotion are not compatible with the types of the parameters, the behavior is undefined.

在上面的例子中,函数 f 定义了一个包含原型的类型,参数 x 的类型是 int,而参数的类型 xlong。根据 6.2.7 §1:

Two types have compatible type if their types are the same.

因此,longint 不兼容,所以行为未定义,对吗?

但是,在 6.5.2.2 §7 中:

If the expression that denotes the called function has a type that does include a prototype, the arguments are implicitly converted, as if by assignment, to the types of the corresponding parameters, taking the type of each parameter to be the unqualified version of its declared type.

如果我对这一段的理解是正确的,这意味着 int 类型的参数 x 在调用函数时被隐式转换为 long 。根据 6.3.1.3 §1:

When a value with integer type is converted to another integer type other than _Bool, if the value can be represented by the new type, it is unchanged.

由于 int 的排名低于 long,因此每个 int 变量都可以由一个 long 变量表示。因此,参数 x 可以转换为 long。因此,这不是未定义的行为。

该标准的哪种解释是正确的?我的代码是否未定义行为?

您提供了与您的代码片段无关的引用。根据同节(6.5.2.2 函数调用)

2 If the expression that denotes the called function has a type that includes a prototype, the number of arguments shall agree with the number of parameters. Each argument shall have a type such that its value may be assigned to an object with the unqualified version of the type of its corresponding parameter.

函数f有一个在调用表达式中可见的原型

extern long f(long x);

和这个作业

int argument;
long parameter;
parameter = argument

正确。

至于这句话

6 If the expression that denotes the called function has a type that does not include a prototype, the integer promotions are performed on each argument, and arguments that have type float are promoted to double. These are called the default argument promotions. If the number of arguments does not equal the number of parameters, the behavior is undefined. If the function is defined with a type that includes a prototype, and either the prototype ends with an ellipsis (, ...) or the types of the arguments after promotion are not compatible with the types of the parameters, the behavior is undefined. If the function is defined with a type that does not include a prototype, and the types of the arguments after promotion are not compatible with those of the parameters after promotion, the behavior is undefined, except for the following cases:

那么就是下面的意思了。函数调用表达式看不到函数原型。因此执行默认参数提升。但是在其他地方,该函数是用函数原型定义的,并且提升的参数与函数参数不兼容。在这种情况下,您将有未定义的行为。

这是一个演示程序,具有与函数调用相关的未定义行为。编译器可以发出错误消息。

#include <stdio.h>

void f();

int main(void) 
{
    short x = 10;
    
    f( x );
    
    return 0;
}

void f( char *s )
{
    printf( "s = %s\n", s );
}

#include <stdio.h>
#include <limits.h>

void f();

int main(void) 
{
    unsigned int x = UINT_MAX;
    
    f( x );
    
    return 0;
}

void f( int x )
{
    printf( "x = %hd\n", x );
}

例如在上一个程序中调用表达式

中的参数x
f( x );

提升为 unsigned int 类型。但是根据函数定义,该函数需要一个 signed int 类型的参数,并且传递的值不能存储在 signed int 类型中。所以行为是未定义的。 但是您原来的函数调用示例与此引用无关。

代码正确。 IMO,第一种解释不适用。

实际上是指调用一个没有原型的函数定义有原型:

long g(int x)
{
    return f(x);
}

// other translation unit
long f(long x) {
    return 0;
}

仅当使用与 long 兼容的类型的单个参数调用 f 时才定义代码。

“提升后的参数”部分令人困惑,它指的是默认参数提升 在同一段落的前面定义。这在这里不适用,因为这些规则仅在没有原型或我们有可变函数时使用。

因此“升级后的参数与参数类型不兼容”适用于您没有原型的情况,应用默认参数升级(整数情况下的整数升级)并且如果类型是那么不兼容,有未定义的行为。

但是既然你一个原型,忘记默认参数提升,而是继续阅读下一部分,C17 6.5.2.2/7 强调我的:

If the expression that denotes the called function has a type that does include a prototype, the arguments are implicitly converted, as if by assignment, to the types of the corresponding parameters, taking the type of each parameter to be the unqualified version of its declared type.

那我们去看看“as if by assignment”的说法,C17 6.5.16 重点我的:

the left operand has atomic, qualified, or unqualified arithmetic type, and the right has arithmetic type;

intlong都是算术类型(并且没有限定符),这是一种有效的赋值形式。在同一章的下方:

The type of an assignment expression is the type the left operand would have after lvalue conversion.

所以基本上传递参数的代码相当于简单的赋值:

int x;
long y;
y = x;

如果我们让标准让我们在这个快乐的追逐中走得更远,接下来查找左值转换,C17 6.3.2.1:

...an lvalue that does not have array type is converted to the value stored in the designated object (and is no longer an lvalue); this is called lvalue conversion.

然后是整数类型的实际转换,C17 6.3.1.3:

When a value with integer type is converted to another integer type other than _Bool, if the value can be represented by the new type, it is unchanged.
Otherwise, if the new type is unsigned, the value is converted by repeatedly adding or subtracting one more than the maximum value that can be represented in the new type until the value is in the range of the new type.
Otherwise, the new type is signed and the value cannot be represented in it; either the result is implementation-defined or an implementation-defined signal is raised.

一个long总是可以保存一个int的值,所以第一句就是适用于这种情况的转换。

在编译单元独立处理的典型 C 实现中,链接器组合单独编译的目标文件并执行地址重定位而不尝试其他优化,会有一个规范,今天通常称为应用程序二进制接口,其中描述了 where/how 调用函数的代码应该存储参数,并且 where/how 函数应该期望找到其调用者存储的参数。

如果函数尝试以与其调用者存储参数的方式不一致的方式检索参数,则结果可能没有意义。另一方面,许多平台 ABI 根据存储格式而不是 C 数据类型来描述行为。因此,例如一个 32 位 ARM 实现,其中 intlong 都是 32 位数据类型,一个期望 int 的函数将以与期望 longint32_t。因此,如果一个编译单元使用 int 类型而另一个使用 long 类型,则独立处理不同编译单元中的代码的 ARM 实现无需关心,前提是两种类型具有相同的表示形式。

一般来说,在可行的情况下,应该使 parameter/argument 类型匹配,即使是在不关心此类事情的实现上也是如此,因为这将使人们更容易阅读代码并了解它在做什么.然而,在某些情况下,一个人可能有不同的编译单元,这些编译单元期望被赋予指向函数的指针,这些函数的参数具有不同类型并具有匹配的表示形式。在这种情况下,如果使用的是单独处理编译单元的实现,则可以将单个函数的地址传递给两个编译单元中的代码。不幸的是,程序员无法指示何时需要以与 ABI 一致的方式处理函数调用,而不考虑标准是否会定义其行为,并且一些激进的优化器不会尝试有意义地处理构造其行为将由 ABI 而不是标准定义。