在一个int数组上使用memcmp函数是否符合严格的规范?

26
以下程序是C语言的严格遵从程序吗?我对c90和c99感兴趣,但c11的答案也可以接受。
#include <stdio.h>
#include <string.h>

struct S { int array[2]; };

int main () {
    struct S a = { { 1, 2 } };
    struct S b;
    b = a;
    if (memcmp(b.array, a.array, sizeof(b.array)) == 0) {
        puts("ok");
    }
    return 0;
}

不同问题的答案评论中,Eric Postpischil坚持认为程序输出将根据平台而变化,主要是由于未初始化的填充位可能性。我认为结构体赋值将覆盖b中的所有位,使其与a中的位相同。但是,C99似乎没有提供这样的保证。来自第6.5.16.1节第2段:

简单赋值(=)中,右操作数的值被转换为赋值表达式的类型,并替换左操作数指定的对象中存储的值。

在复合类型的情况下,“转换”和“替换”的含义是什么?

最后,考虑相同的程序,只是ab的定义是全局的。那个程序将是一个严格符合规范的程序吗?

编辑:只想总结一些讨论材料,不添加我自己创作的答案。

  • 该程序不是严格符合标准的。由于赋值是按值而不是按表示方式进行的,因此b.array可能包含与a.array不同的位设置。
  • a不需要转换,因为它与b是相同类型,但替换是按成员逐个进行的。
  • 即使在ab的定义被全局化之后,在赋值后,b.array可能包含与a.array不同的位设置。(关于b中的填充字节的讨论很少,但发布的问题并不是关于结构比较的。c99缺乏有关静态存储器中填充如何初始化的说明,但c11明确指出它被零初始化。)
  • 顺便说一句,如果b是通过从a中使用memcpy初始化的,则有共识认为memcmp是定义良好的。

感谢所有参与讨论的人。


与填充无关,也不适用于问题中使用的1和2值,但仍然符合其精神,我发现没有任何阻止使用补码或反码实现将-0视为表示0的冗余方式以在赋值时进行规范化。 - AProgrammer
你在问题中提供的例子仍然不是一个好的例子。你并没有像你想象的那样比较struct,而只是比较了数组。因此,在你提供的这个例子中,仍然只有一个问题,即int是否具有填充位或所谓的负零。这些事情在现代架构上不会发生。你真的需要考虑一个真正有填充字节(而不是填充位)的实际结构,这样问题才变得相关。 - Jens Gustedt
@JensGustedt: 这个问题特别涉及到 memcmpint 数组,而这个 struct 被用于影响分配到 b 持有的数组中。 - jxh
@user315052,对于数组来说,使用memcpy是可以的。memcpymemcmp基于字节进行操作,这些字节被视为unsigned charunsigned char是唯一一个保证没有填充比特和所有表示具有不同值的数据类型。 - Jens Gustedt
Eric是Stack Overflow上少数几个在涉及C标准语言方面,你可以毫不犹豫地接受其意见的人之一。 - Stephen Canon
2个回答

4
在C99 §6.2.6中,
§6.2.6.1 总则
1 所有类型的表示形式都是未指定的,除非在本小节中另有规定。
[...]
4 [...] 两个具有相同对象表示(除了NaN)的值比较相等,但相等的值可能具有不同的对象表示。
6 将值存储在结构体或联合类型的对象中时,包括成员对象中,与任何填充字节对应的对象表示的字节取未指定的值。42) 42) 因此,例如,结构分配不需要复制任何填充位。
43) 当以类型T的对象方式访问x和y时,对象x和y可能具有相同的值,但在其他上下文中具有不同的值。特别地,如果为类型T定义了==,则x == y并不意味着memcmp(&x, &y, sizeof (T)) == 0。此外,x == y不一定意味着x和y具有相同的值;类型T的其他值的操作可能区分它们。
§6.2.6.2 整数类型
[...]
2 对于有符号整数类型,对象表示的位应分为三组:值位、填充位和符号位。可以没有填充位;[...]
[...]
5 任何填充位的值都是未指定的。[...]
在J.1 未指定行为中,
- 在结构体或联合中存储值时填充字节的值(6.2.6.1)。 - 整数表示中任何填充位的值(6.2.6.2)。
因此,在不影响值的情况下,a和b的表示中可能存在不同的位。这与其他答案得出的结论相同,但是我认为从标准中引用这些内容会提供良好的附加上下文。
如果进行memcpy,则memcmp将始终返回0,并且程序将严格符合规范。 memcpy复制了a的对象表示到b中。

1
谢谢!所以,做完memcpy后进行memcmp应该是安全的,因为memcpy复制的是相同的位,对吧? - jxh
@user315052 是的,这是符合要求的;请参见答案中的详细信息。 - Geoff Reedy

0

我的观点是它严格符合规范。根据Eric Postpischil提到的4.5:

一个严格符合规范的程序应该只使用国际标准中指定的语言和库特性。它不应该产生依赖于任何未指定、未定义或实现定义行为的输出,并且不应超过任何最小实现限制。

所讨论的行为是memcmp的行为,这是明确定义的,没有任何未指定、未定义或实现定义的方面。它在不知道值、填充位或陷阱表示的情况下,对表示的原始位进行操作。因此,在这种特定情况下,memcmp结果(但不是功能)取决于存储在这些字节中的值的实现。

6.2.6.2中的脚注43:

当以类型T的对象x和y作为类型T的对象访问时,它们具有相同的有效类型T并且可能具有相同的值,但在其他上下文中可能具有不同的值。特别地,如果对于类型T定义了==,则x == y并不意味着memcmp(&x, &y,sizeof(T)) == 0。此外,x == y并不一定意味着x和y具有相同的值;类型T的其他值操作可能会区分它们。
编辑:
进一步思考后,由于以下原因,我不再确定是否严格符合标准:
它不应产生依赖于任何未指定行为的输出[...]。
显然,memcmp的结果取决于表示的未指定行为,从而满足此条款,即使memcmp本身的行为是明确定义的。该条款没有关于功能深度的规定,直到发生输出。
因此,它不是严格符合标准。
编辑2:

我不确定当使用memcpy复制结构体时,它是否会变得严格符合规范。根据Annex J的规定,在初始化a时会发生未指定的行为:

struct S a = { { 1, 2 } };

即使我们假设填充位不会改变,而且 memcpy 总是返回 0,它仍然使用填充位来获得结果。并且,它依赖于这样的假设,即填充位不会改变,但标准中并没有对此作出保证。
我们应该区分用于对齐的结构体中的填充字节和特定本机类型(如 int)中的填充位。虽然我们可以安全地假设填充字节不会改变,但这只是因为没有真正的理由,但同样的情况并非适用于填充位。标准提到了奇偶校验标志作为填充位的示例。这可能是实现的软件功能,但也可能是硬件功能。因此,可能会使用其他硬件标志用于填充位,包括在读访问时出于任何原因而改变的标志。
我们将难以找到这样一台异域机器和实现,但我看不到任何禁止这样做的地方。如果我错了,请纠正我。

@jxh:如果程序生成的字符序列不允许在不同平台上变化,那么任何严格符合规范的程序使用 rand() 都会相当困难。更有帮助的符合性定义应该是,程序必须在任何符合标准的平台上满足其要求;如果上述程序的要求是它必须打印“ok”或什么也不输出,但不能输出字符串“Fred”,则上述程序将严格符合标准的任何合理解释[尽管它可能超出了最小实现限制]。 - supercat
@jxh:我的情况也是这样。如果你的程序在任何符合标准的平台上可能产生未定义行为,那么符合标准的平台可以生成输出“Fred”的代码。即使你的程序唯一的要求是不输出“Fred”,如果你的程序调用了未定义行为,仍然可能存在符合标准的平台违反该要求。然而,由于它不调用UB,所有符合标准的实现必须生成遵守不输出“Fred”要求的代码。 - supercat
@supercat:您提出了使用rand()的概念,这不一定需要同一程序生成相同的输出。我在我的评论中解释说,如果我的示例程序严格符合规范,则应为所有兼容编译器生成相同的输出。此答案的原始版本已经表明,即使在不同平台上可能会生成不同的输出,该程序也是符合规范的。 - jxh
@jxh:你的程序源代码中没有说明它需要做什么。如果有人需要一个可能会打印“ok”或者可能不打印,但无论如何都不会打印“Fred”的程序,上面的源代码将是这些要求的严格合规实现。如果要求程序打印“ok”,上述代码将不是该要求的严格合规实现。 - supercat
@supercat:嗯...也许我没有清楚地表达自己。一个严格遵守规范的程序不应该依赖于除了明确定义的行为之外的任何东西(因此未定义和实现定义的行为都被排除在外)。我想知道我的示例程序是否符合这个标准。并且,我向回答者辩称,如果程序符合这个标准,它将始终在所有平台上产生相同的输出。 - jxh
显示剩余5条评论

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