【问题标题】:technical legality of incompatible pointer assignments不兼容指针赋值的技术合法性
【发布时间】:2016-08-18 03:29:52
【问题描述】:

C11 标准 ISO/IEC 9899:2011 (E) 在 §6.5.16.1/1 中规定了以下简单分配的约束:

应满足以下条件之一:

  • 左侧操作数具有原子、合格或不合格算术类型,右侧具有 算术类型;
  • 左操作数具有结构或联合的原子、合格或不合格版本 类型与权利的类型兼容;
  • 左操作数具有原子、限定或非限定指针类型,并且(考虑到 左操作数在左值转换后的类型)两个操作数都是 指向兼容类型的合格或非合格版本的指针,以及指向的类型 to by left 具有所有由 right 指向的类型的限定符;
  • 左操作数具有原子、限定或非限定指针类型,并且(考虑到 左操作数在左值转换后的类型)一个操作数是一个指针 指向一个对象类型,另一个是指向一个合格或不合格版本的指针 void,并且左边指向的类型具有指向的类型的所有限定符 靠右;
  • 左边的操作数是一个原子的、合格的或不合格的指针,右边是一个空指针 指针常量;或
  • 左侧操作数的类型为 atomic、合格或不合格 _Bool,右侧是指针。

我对双方都是指向不同于void不兼容 类型的指针的情况感兴趣。如果我理解正确,这至少应该调用 UB,因为它违反了这个约束。不兼容类型的一个示例应该是(根据 §6.2.7 和 §6.7.2)intdouble

因此下面的程序应该是违规的:

int main(void) {
  int a = 17;
  double* p;
  p = &a;
  (void)p;
}

gcc 和 clang 都对“-Wincompatible-pointer-types”发出警告,但不要中止编译(使用 -std=c11 -Wall -Wextra -pedantic 编译)。

同样,以下程序只会导致“-Wint-conversion”警告,而编译正常。

int main(void) {
  int a;
  double* p;
  p = a;
  (void)p;
}

来自 C++,我预计这些测试用例中的任何一个都需要强制转换才能编译。有什么理由可以说明这两个程序都符合标准吗?或者,即使通过显式使用 -std=c11 而不是 -std=gnu11 来禁用有趣的 GNU C 扩展,是否至少有重要的历史原因支持这种代码风格?

【问题讨论】:

    标签: c gcc clang language-lawyer c11


    【解决方案1】:

    有什么理由说明这两个程序都符合标准吗?

    这些程序不是“标准合法的”。它们包含违反约束的内容,并且您已经从标准中引用了正确的文本。

    编译器通过生成约束违规诊断来符合标准。该标准不要求在违反约束或其他错误程序的情况下中止编译。

    它并没有说太多,但唯一合理的结论是,由于包含违反约束的程序而生成的任何可执行文件都具有完全未定义的行为。 (不过,我看到人们试图以其他方式争论)。

    推测如下:C(和 C++)用于多种用途;有时人们希望他们的机器使用“高级汇编程序”,而不关心可移植性或标准。据推测,编译器供应商将默认设置设置为他们认为目标受众喜欢的设置。

    【讨论】:

    • 在编译失败的情况下,构建系统留下旧版本代码的情况并不少见。因此,在编译失败后尝试运行程序的效果可能是执行该程序先前成功编译的版本会执行的任何操作,这可能与当前版本尝试执行的操作有任何关系,也可能没有任何关系。因此,此类行为将超出标准对当前版本代码的任何规定的管辖范围。
    【解决方案2】:

    请求检查严格标准符合性并拒绝编译不符合标准的代码的编译器标志(gcc 和 clang)是-pedantic-errors

    $ gcc -std=c11 -pedantic-errors x.c
    x.c: In function ‘main’:
    x.c:3:15: error: initialization from incompatible pointer type [-Wincompatible-pointer-types]
       double* p = &a;
                   ^
    

    叮当声:

    $ clang -std=c11 -pedantic-errors x.c
    x.c:3:11: error: incompatible pointer types initializing 'double *' with an
          expression of type 'int *' [-Werror,-Wincompatible-pointer-types]
      double* p = &a;
              ^   ~~
    1 error generated.
    

    大部分(至少可以说)野外的典型 C 代码是不一致的,因此-pedantic-errors 会导致大多数 C 程序和库无法编译。

    【讨论】:

    • 不,似乎更多的是工具链的问题。在我的机器上 gcc 和 clang 都给出了诊断。所以不,-pendantic 不是要走的路。方法是更新平台。
    • GCC 显然总是为此发出警告,但为了阻止不正确的 C 代码编译,您需要 -pedantic-errors
    【解决方案3】:

    您的代码示例和您对标准的引用不匹配。例子是初始化,6.5.16讲赋值。

    令人困惑的是,匹配类型要求在约束部分 6.5.16 中用于赋值,但“仅”在语义部分 (6.7.9) 中用于初始化。所以编译器有“权利”不发出初始化诊断。

    在 C 中,违反约束只需要“诊断”,编译器可以继续编译,但不能保证生成的可执行文件是有效的。

    在我的平台上,一个 Debian 测试,两个编译器都给我一个没有任何选项的诊断,所以我猜你的安装一定是很旧和过时了。

    【讨论】:

    • 您当然是正确的,我(在撰写本文的过程中)将分配缩短为初始化。尽管如此,两个编译器的行为对于赋值也是一样的。
    • 另请注意 §6.7.9/11,它本质上说在这种情况下它的处理方式与分配相同:“标量的初始化程序应是单个表达式,可选地用大括号括起来. 对象的初始值是表达式的初始值(转换后);应用与简单赋值相同的类型约束和转换,将标量的类型作为其声明类型的非限定版本。"
    • 我自己很惊讶地发现标准中的状态在两者之间是不同的。有关问题的实际情况,请参阅我的编辑。
    • 作为记录,GCC 给出了两种不同的警告“指针来自没有强制转换的整数”,一种是在初始化期间,另一种是在赋值期间。
    【解决方案4】:

    不,是的


    这真的很简单。

    不,不符合标准。是的,重要历史原因。

    C 最初甚至没有演员表。作为一门系统编程语言,将其用作超强大的美化汇编程序不仅是合理的,而且是最佳实践,而且在当时是真正的“唯一实践”。

    一条关键信息应该能说明问题:实现或强制执行规范确实不是编译器的工作。实际上,从字面上看,规范只是一个建议。编译器的实际工作是编译所有曾经编写过的 C 代码,包括 pre-C11、pre-C99、pre-C89,甚至 pre-K&R。这就是为什么有这么多可选限制的原因。具有现代代码样式的项目会开启严格遵循的模式。

    标准中定义 C 的方式,实践中使用 C 的方式。所以你可以看到,编译器根本无法拒绝构建代码。

    几十年来,开发人员一直在转向可移植的、严格符合标准的代码,但当 C 首次出现时,它的使用有点像一个非常强大的汇编程序,而且是地址算术和类型双关语的开放季节。那时的程序大多是一次针对一种架构编写的。

    【讨论】:

    • 当然,gcc 的作者有一个完全不同的观点——编译器没有义务在适合不严格符合的代码时产生任何有用的行为,即使每个其他编译器相同的平台会产生一种有用的行为,而这在严格符合的代码中是无法实现的。
    猜你喜欢
    • 2019-08-09
    • 2021-04-07
    • 1970-01-01
    • 2021-06-07
    • 1970-01-01
    • 2012-03-13
    • 2017-03-26
    • 2014-12-14
    • 1970-01-01
    相关资源
    最近更新 更多