【问题标题】:Can I call a function taking a long parameter with an int argument?我可以调用带有 int 参数的长参数的函数吗?
【发布时间】:2021-05-04 09:02:21
【问题描述】:

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

extern long f(long x);

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

根据 C11 标准,在 6.5.2.2 §6 中:

如果函数定义的类型包含原型,并且 [...] 提升后的参数类型与参数类型不兼容,则行为未定义。

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

如果类型相同,则两种类型具有兼容的类型。

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

但是,在 6.5.2.2 §7 中:

如果表示被调用函数的表达式具有包含原型的类型,则参数将隐式转换为相应参数的类型,就像通过赋值一样,将每个参数的类型作为非限定版本其声明的类型。

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

当整数类型的值转换为_Bool以外的其他整数类型时,如果该值可以用新类型表示,则不变。

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

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

【问题讨论】:

  • 您希望f(42) 工作吗?
  • @4386427 42 的类型为 int。我想说:“是的,它应该工作!”但看完标准后,我就不确定了。
  • 第 6 段似乎主要是关于 默认参数提升,但是关于使用包含原型的类型定义的函数的部分似乎措辞不佳,因为它似乎暗示调用使用包含省略号的原型定义的函数会导致 UB。
  • 关键在于推广后。因此这不是 UB。
  • @Pierre 升级后,int 参数变为long,因为long 的排名更高。因此,在您的示例中没有不兼容或 UB。

标签: c type-conversion language-lawyer undefined-behavior function-call


【解决方案1】:

您提供了与您的代码 sn-p 无关的引号。根据同一节(6.5.2.2 函数调用)

2 如果表示被调用 函数的表达式的类型为 包括原型,参数的数量应与 参数数量。 每个参数都应该有一个类型,使得它的 可以将值分配给具有非限定版本的对象 对应参数的类型。

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

extern long f(long x);

还有这个任务

int argument;
long parameter;
parameter = argument

是正确的。

至于这句话

6 如果表示被调用函数的表达式的类型为 不包括原型,整数提升在 每个参数和具有浮点类型的参数都被提升为 双倍的。这些称为默认参数提升。如果 参数的数量不等于参数的数量, 行为未定义。如果函数是用一个类型定义的 包括一个原型,并且原型以省略号结尾 (, ...) 或提升后的参数类型不是 与参数的类型兼容,行为是 不明确的。如果函数定义的类型不 包括原型,以及提升后的参数类型 与升级后的参数不兼容, 行为未定义,但以下情况除外:

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

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

#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 类型中。所以行为是不确定的。 但是您最初的函数调用示例与此引用无关。

【讨论】:

  • 你引用的第 2 段和第 6 段似乎是对立的,不是吗?
  • Re“和传递的值不能存储在类型short”中:值无关;类型错误足以使行为未定义。例如,如果传递了 int 值 1,则 short 参数可能会由于字节顺序问题而得到错误的值(int 参数将四个字节推入堆栈,short 参数只查看两个,它们是高值字节,这也取代了其他参数/​​参数匹配)。因此,即使1intshort 中都可以表示,程序也会“行为不端”。
  • @EricPostpischil 我的意思是当参数具有例如有符号类型并且参数具有相应的无符号类型时的情况,反之亦然,因此值不能在参数中表示..跨度>
【解决方案2】:

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

所以“提升后的参数与参数的类型不兼容”适用于您没有原型的情况,应用默认参数提升(整数情况下的整数提升)以及类型不兼容的情况然后,有未定义的行为。

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

如果表示被调用函数的表达式具有包含原型的类型,参数会隐式转换为相应参数的类型,就像通过赋值一样,采用每个参数都是其声明类型的非限定版本。

然后我们去阅读关于“好像被分配”的说法,C17 6.5.16 强调我的:

左边的操作数是原子的、合格的或不合格的算术类型,右边是算术类型;

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

赋值表达式的类型是左操作数的类型 左值转换后。

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

int x;
long y;
y = x;

如果我们让标准让我们进一步了解这个快乐的追逐,接下来查看左值转换,C17 6.3.2.1:

...不具有数组类型的左值被转换为存储在指定对象中的值(不再是左值);这称为左值转换

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

当整数类型的值转换为_Bool以外的其他整数类型时,如果该值可以用新的类型表示,则保持不变。
否则,如果新类型是无符号的,则通过重复添加或转换值 比新类型可以表示的最大值减一 直到值在新类型的范围内。
否则,新类型是有符号的,值不能在其中表示;无论是 结果是实现定义的或引发了实现定义的信号。

long 始终可以保存int 的值,因此第一句是适用于这种情况的转换。

【讨论】:

  • 我对 6.5.2.2/6 感到困惑。如果我正确理解你,你是说这一段只是关于不包含原型的类型的函数。但是,在该段的后面,它说:“如果函数是用包含原型的类型定义的,......”。好像这一段讲的是没有原型的函数和有原型的函数,不是吗?
  • @Pierre 是的,它写得很糟糕。 “包括一个原型,原型以省略号 (, ...) 结尾提升后的参数类型”。这些“和”和“或”指的是什么并不明显。它仅表示带有省略号的原型吗?像这样阅读标准通常会有所帮助:§6“如果表示被调用函数的表达式的类型不包含原型 ... [此处无关文本] §7 如果表达式表示被调用函数的类型确实包含原型,[relevant text here] "
【解决方案3】:

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

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

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

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

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

【讨论】:

  • 请注意,自 C99 以来这是格式错误的,甚至在没有声明的情况下调用函数
  • @M.M,哪里禁止了? “附件一”仅提及警告。
  • “附件一”是非规范性的。 6.5.1 使f(x) 出现语法错误:未声明的标识符不是primary-expression,函数调用运算符的语法需要primary-expression。 C89 具有相同的文本,但在关于函数调用的部分中也有一个例外,即未声明的标识符的行为就像有隐式声明一样。该异常已在 C99 中删除
  • @M.M:标准确实允许在没有原型的情况下声明函数。我使用过一些 C 方言实现,它们对接受参数的原型函数和不接受参数的原型函数使用不同的命名和调用约定。虽然我认为该标准不会将此类处理视为符合标准,但如果项目需要链接使用旧式声明和定义编写的翻译单元,它将很有用。
  • @supercat 我指的是使用未声明的标识符,而不是非原型声明
【解决方案4】:

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-04-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-17
    • 2020-02-09
    • 1970-01-01
    相关资源
    最近更新 更多