C89中有效的程序,在C99中无效

19

在 C99 中引入或删除的特性/语义是否会使一个在 C89 中写的良好定义程序变得:

  • 无效(即根据 C99 标准无法编译)
  • 编译,但具有不同的语义。

我目前的研究结果,关于明显无效的程序:

  • 隐式 int(C89 §3.5.2)
  • 隐式函数声明(C89 §3.3.2.2)
  • 不从期望返回值的函数中返回(C89 §3.6.6.4)
  • 使用新关键字作为标识符(例如 restrictinline 等)
  • 涉及 // 的黑客技巧,现在被视为注释。然而,在实际生产代码中几乎不会遇到。

细微的更改,使相同的代码具有不同的语义:

  • 整数除法已经被定义良好,例如 -3 / 2 现在必须向零截断(C99 §6.5.5/6),而不是实现定义(C89 §3.3.5/6)
  • strtod 在 C99 中获得了解析十六进制数字的能力,通过解析 0x0X

我错过了什么?


3
根据标题,这个问题实际上是关于C99中的“破坏性变更”的。它绝对不会太宽泛。 - AnT stands with Russia
12
@Olaf:有关语言历史的问题在这里并不是离题的,也不会以任何方式暗示提问者“错过了过去17年的C开发”。请注意,我的翻译尽可能保持忠实原文,同时使其更通俗易懂。 - AnT stands with Russia
3
你听说过所谓的“Linux”吗?如果你在内核开发中工作,你必须坚持使用C89(或带有GNU扩展的C89)。 - Leandros
7
ISO C90标准和ANSI C89标准描述的是完全相同的编程语言,ISO C90发布后,ANSI也正式采用了它。此后,ANSI也正式采用了1999年和2011年的ISO C标准。关于C89/C90和C99是否已过时,在ISO看来是严格正确的,但它们仍然是相关的,讨论它们是完全适当的。您可以忽略旧版标准,但没有必要告诉我们不应该提到它们。 - Keith Thompson
3
如果您在询问哪些更改会导致原本有效的C90代码在C99中无效(或者更糟糕的是,仍然有效但语义不同),我建议您更新您的问题以使其更加明确。"主要不兼容性"这个词比较模糊;"导致现有代码出错的更改"则不那么模糊。建议阅读C99标准的草案[N1256]的前言。同时您也可以看一下C11标准的草案[N1570]。请注意,以上只是我的建议,不涉及除翻译外的任何其他内容。 - Keith Thompson
显示剩余30条评论
2个回答

4

在C99发布之前,很多程序都被认为是符合C89标准的,但一些人坚持认为这些程序从未符合标准。C89包括一个规则,要求任何类型的对象只能使用该类型的指针、相关类型的指针或字符类型的指针访问。在C99发布之前,这个规则通常被解释为仅适用于“命名”对象(静态或自动持续时间的变量,可以直接通过名称访问),并且仅适用于在将该对象作为不同指针类型使用之前没有立即取地址的情况下。这种解释受到了许多因素的影响:

  1. 标准的一个明确目标是与现有编译器和程序相适应,虽然现有程序很少使用不同类型的指针访问离散命名变量,除非在使用之前立即取地址,但指针类型的其他用法却非常普遍。
  2. 标准的基本原理包括一个函数接收一个原始类型的指针,以某种方式写入另一个原始类型的全局变量,使得编译器没有特定的理由来预期别名。将全局变量保留在寄存器中显然是一种有用的优化,规则的陈述目的是允许这样的优化,在编译器没有理由预期别名发生的情况下进行。禁止像(int*)&foo=23;这样的构造对于此类优化毫无帮助,因为代码正在获取foo的地址并对其进行解引用,任何不是故意模糊不清的编译器都应该清楚地知道代码将修改foo
  3. 有许多种代码需要语义上能够将内存位作为各种类型使用,并且标准中没有任何规定旨在使程序员跳跃(例如使用memcpy)以实现可以在规则不存在的情况下轻松获得的语义,特别是考虑到使用memcpy将阻止编译器在指针访问期间保持全局变量在寄存器中(从而破坏规则最初的目的)。
  4. 如果结构类型VW有一个公共初始序列,U是包含两者的任何联合类型,并且p是标识U中的VV*,则(W*)(U*)p可以用于访问这些公共成员,并且等同于(W*)p。除非编译器能够证明p不可能是某个包含W的联合的成员的指针,否则必须允许(W*)p访问公共成员;更有帮助的是,无论U是否存在以及其存在的位置如何,都将此类公共成员访问视为合法。
  5. C89规则中没有明确说明分配存储区域的“类型”如何定义,或者不再需要的一种类型的存储区域如何重新用于保存另一种类型的东西。
  6. 跟踪分配给命名变量的寄存器比跟踪分配给其他指针例外更容易,对于希望通过指针最小化加载和存储次数的代码,通常会将东西复制到命名变量中并在那里处理。

C99增加了“有效类型”规则,这些规则明确适用于已分配的存储。有些人坚称这些规则只是C89中已经存在的规则的“澄清”,但出于以上原因,我认为这种观点站不住脚。声称编译器没有将别名规则应用于未命名对象的唯一原因是#5和#6,这是时髦的说法,但反对意见#1-#4同样重要(并且在C99和C89中仍然适用)。尽管如此,由于C99添加了有效类型规则,许多构造在C89规则的大多数常见解释下被视为合法的构造,现在显然是被禁止的。


在那些喧嚣且常常错误(或者只是纯粹错误)的声音中,sanity 的一瞥,看到了每一个转折点上类型转换和严格别名违规的问题。 - David C. Rankin
@DavidC.Rankin:我觉得人们如此无视标准的概念是多么奇怪,标准旨在为那些不能有效地提供与更常见平台相同的功能和保证的平台提供最低基线,以便允许这些平台用于运行不需要平台缺乏功能的C程序。我从未看到任何迹象表明它旨在废弃在常见平台上可行的常见做法,也没有暗示可以轻松支持这些做法的平台不应该这样做。 - supercat
@DavidC.Rankin:就个人而言,我认为对于 C 语言来说,正确的前进方式应该是有指令选择至少三种别名模式,这些模式应该比现在存在的任何模式都更好定义:精确别名,其中所有操作都像通过内存一样运行 [速度慢,但与使任何别名假设的代码兼容],1990 年代风格 [假定直接访问命名对象不会与使用外部指针类型访问的对象别名,但不对指针相互别名作任何假设],和严格 [这将是大多数情况下更严格的...... - supercat
@underscore_d:模式的一个巨大优势是,它们可以在不需要任何人承认自己“错误”的情况下添加。肯定存在某些类型的代码,能够更自由地为别名命名将会受益匪浅,而且肯定有一些优化类型,在许多情况下都非常有用,但编译器甚至不能在严格解释标准的情况下执行。 - supercat
@underscore_d:顺便提一下,自从上面写了那段话以来,我做了一些探索,发现大部分在godbolt上的“现代”编译器都会破坏严格符合规范的程序。据我观察,它们的执行模型不能识别改变对象有效类型的代码概念,而不在物理上读取和写入相关存储是不可能生成高效和符合标准的代码的,虽然编译器需要这种能力。 - supercat
显示剩余2条评论

-1
作为对比和比较的元素,git/git 代码库始终严格遵守 C89 规范,不使用 C99 初始化程序或新版 C 标准的特性。
这在 Git 2.23 (2019 年第三季度) 的 Git 编码指南 中有详细说明。
本答案说明了可能与 C89 兼容的后 C89 特性。

查看 commit cc0c429(2019年7月16日),由Junio C Hamano (gitster)提交。
(合并于commit fe9dc6b,由Junio C Hamano -- gitster -- 完成于2019年7月25日)

编码指南:详细说明C89后的规则

尽管我们一直坚持使用C89,但在试用了一些新的C语言特性后,发现没有人反对,因此我们在代码库中借鉴了一些方便的功能。

详细说明它们。

顺便说一下,扩展现有的变量声明规则,以更好地阅读新拼写的for循环规则。

编码指南现在包括

即使你的编译器支持了最新版本的 C 标准,也不应使用其中的特性。
以下是一些例外情况:
自 2012 年初以来(Git v1.7.9.2),我们一直在使用 e1327023ea ,其中包括一个 enum definition whose last element is followed by a comma。这可以像以逗号结尾的数组初始化程序一样用于减少添加新标识符时的补丁噪音。
自 2017 年中期以来(Git v2.15.0-rc0),我们已经为结构体使用了指定初始化程序(例如,“struct t v = { .val = 'a' };”)。有一些 C99 特性可能对我们的代码库很有用,但我们一直在犹豫是否使用它们,以避免与旧编译器不兼容。但实际上,我们并不知道人们今天是否还在使用 C99 之前的编译器。如果此补丁能够在几个版本中幸存而没有投诉,则我们可以更有信心地认为指定的初始化程序得到了我们用户群的广泛支持。这也表明其他 C99 特性可能得到了支持,但这并不是保证(例如,在 C99 存在之前,gcc 就拥有了指定的初始化程序)。
自 2017 年中期以来(Git v2.15.0-rc0),我们已经为数组使用了指定的初始化程序(例如,“int array[10] = { [5] = 2 }”)。这是另一个测试气球,以查看那些编译器不支持数组指定初始化程序的人是否有抱怨。这些曾经是被禁止的,但我们没有收到任何破坏报告,并且它们被认为是安全的。
变量必须在块的开头声明,在第一个语句之前(即 -Wdeclaration-after-statement)。
在 for 循环中声明变量 "for (int i = 0; i < 10; i++)" 在此代码库中仍然不允许。

1
我不明白这与问题有什么关系。 - M.M

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