【问题标题】:Valid programs in C89, but not in C99C89 中的有效程序,但在 C99 中无效
【发布时间】:2016-08-10 17:24:36
【问题描述】:

在 C99 中是否有引入或删除的特性/语义可以使 C89 编写的程序定义明确

  • 无效(即不再编译,根据 C99 标准)
  • 正在编译,但语义不同。

到目前为止,我的发现是关于明显无效的程序:

  • 隐式 int (C89 §3.5.2)
  • 隐式函数声明(C89 §3.3.2.2)
  • 不从期望返回值的函数返回 (C89 §3.6.6.4)
  • 使用新关键字作为标识符(例如restrictinline 等)
  • 涉及// 的黑客攻击,现在被视为 cmets。但是,在生产代码中几乎从未遇到过。

细微的变化,使相同的代码具有不同的语义:

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

我错过了什么?

【问题讨论】:

  • 根据标题,问题实际上是关于 C99 中的重大更改。绝对不会太宽泛。
  • @Olaf:关于语言历史的问题在这里并不是题外话,也不以任何方式暗示问他们的人“错过了过去 17 年的 C 开发”。
  • @Olaf 听说过这个所谓的“Linux”吗?如果您在内核上进行开发,则必须坚持使用 C89(或带有 GNU 扩展的 C89)。
  • @Olaf:1990 年的 ISO C 标准描述的语言与 1989 年的 ANSI C 标准描述的语言完全相同,ANSI 在发布后正式采用了 ISO C90。 ANSI 在 1999 年和 2011 年 ISO C 标准发布后不久也正式采用了这些标准。至于 C89/C90 和 C99 已经过时,就 ISO 而言这是完全正确的,但它们仍然相关,讨论它们是完全合适的。您可以随意忽略该标准的旧版本,但无需告诉我们其他人我们不应该提及它们。
  • 如果您询问会使有效 C90 代码在 C99 中无效(或者更糟糕的是,仍然有效但语义不同)的更改,我建议更新您的问题以明确这一点。 “主要不兼容”一词含糊不清; “破坏现有代码的更改”并非如此。我建议阅读N1256 的前言,C99 标准的草案。您还应该看看N1570,C11 标准的草案。

标签: c language-lawyer c99 c89


【解决方案1】:

在 C99 发布之前,有很多程序在 C89 下被认为是有效的,但有些人坚持认为这些程序从未有效。 C89 包含一条规则,要求任何类型的对象只能使用该类型、相关类型或字符类型的指针来访问。在 C99 发布之前,该规则通常被解释为仅适用于“命名”对象(通过名称直接访问的静态或自动持续时间的变量),并且仅适用于所讨论的对象没有其地址的情况在它被用作不同的指针类型之前立即获取。这种解释的动机有很多:

  1. 标准的一个既定目标是适应现有编译器和程序正在做的事情,而现有程序很少使用不同类型的指针访问离散的命名变量其他与相比,如果变量的地址是在使用之前立即获取的,则指针类型双关的许多其他用法相当普遍。

  2. 该标准的基本原理包括作为其唯一示例的函数,该函数接收一种原始类型的指针以写入另一种原始类型的全局变量,编译器没有特别的理由期望别名.能够将全局变量保存在寄存器中显然是一种有用的优化,并且该规则的既定目的是在编译器没有理由预期会发生别名的情况下允许这种优化。像(int*)&foo=23; 这样的取缔结构对这种优化没有任何帮助,因为代码正在获取foo 的地址并取消引用它的事实应该让任何编译器都清楚地知道代码正在运行修改foo

  3. 有很多类型的代码在语义上需要能够将内存位用作各种类型的能力,而标准中没有任何内容表明这些规则旨在让程序员跳过箍(例如通过使用 memcpy)来实现在没有规则的情况下可以很容易地获得语义,特别是考虑到使用 memcpy 会阻止编译器在指针访问 时将全局变量保存在寄存器中(因此违背了最初编写规则的目的地点).

  4. 如果结构类型VW 具有共同的初始序列,则U 是包含两者的任意联合类型,p 是一个V*,它标识@ 中的V 987654330@,然后(W*)(U*)p 可用于访问那些普通成员,将等效于(W*)p。除非编译器可以显示p 不可能是指向包含W 的某个联合的成员的指针,否则需要允许(W*)p 访问公共成员;不管U 是否存在或存在于何处,简单地将这种常见的成员访问视为合法比寻找拒绝它的借口更有帮助。

  5. C89 规则中的任何内容都没有明确说明分配存储区域的“类型”是如何定义的,或者存储不再需要的某种类型的存储如何重新用于存储另一个。

  6. 跟踪分配给命名变量的寄存器比跟踪分配给其他指针异常的寄存器更容易,并且对通过指针最小化加载和存储次数感兴趣的代码通常会将内容复制到命名变量和在那里工作。

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

【讨论】:

  • 在大声且经常被错误(或完全错误)的声音中一窥理智,动辄看到类型双关和严格混叠违规...
  • @DavidC.Rankin:我觉得奇怪的是,人们竟然如此忽视标准旨在为无法有效提供相同功能和保证作为更普通的平台,以便允许使用这些平台运行不需要平台缺乏的功能的C程序。我没有看到任何迹象表明它打算弃用可以在普通平台上运行的普通做法,也没有建议可以轻松支持此类做法的平台不应该这样做。
  • @DavidC.Rankin:就我个人而言,我认为 C 语言的正确前进方式是在至少三种别名模式中进行选择,这些模式比目前存在的任何模式都定义得更好:精确别名, 一切都表现得好像所有操作都通过内存 [缓慢,但与任何对别名做出任何假设的代码兼容],1990 年代风格 [假设直接访问的命名对象不会为使用外来指针类型访问的事物设置别名,但会使没有关于指针相互别名的假设],并且严格[这将更加严格......
  • ...比 C99 但是 将包含内部函数以通知编译器可能会产生别名]。后者将允许比在 C99 规则下可能实现的更好的语义更有效的优化。例如,给定void hey(float *fp, int *ip) {*fp =12.3f; *ip=6;},它将允许对两个存储进行重新排序,而不需要对 *fp 和 *ip 进行重新排序——这在 C99 规则下通常是不可能的,因为即使执行也会被明确定义下次读取时提供的指针别名是 int 类型。
  • @supercat 非常有趣的建议,即不同的模式应该标准化,而不是最低公分母。或者可以肯定的是,当他们添加restrict 时,他们可以免费添加norestrictvolatile 的超载。更多的选择 == 更多的赢家,当然。 C 和 C++ 永远不会逃脱它们的低级起源,他们也不应该在 imo 中,所以如果他们对某些事情不那么严厉会更好......尤其是当人们开始假设某种行为时,基于实现/解释,这突然在技术上变得无效。啊!
【解决方案2】:

作为对比和比较的元素,git/git 代码库仍然严格遵守 C89,并且不使用 C99 初始化程序或来自较新 C 标准的功能。
这在 Git Coding Guidelines 的 Git 2.23(2019 年第三季度)中有详细说明。

这个答案说明了可能与 C89 兼容的 C89 后特性。

参见Junio C Hamano (gitster)commit cc0c429(2019 年 7 月 16 日)。
(由 Junio C Hamano -- gitster -- 合并于 commit fe9dc6b,2019 年 7 月 25 日)

CodingGuidelines:阐明 C89 后的规则

尽管我们一直坚持使用 C89,但我们在代码库中从最近的 C 语言中借用了一些方便的功能,在天气气球中尝试这些功能后发现没有人尖叫。

把它们拼出来。

同时,将现有的变量声明规则稍微扩展为 使用 for 循环的新拼写规则更好地阅读。

coding guidelines now include:

您不应该使用较新的 C 标准的功能,即使您的编译器了解它们。

本指南有一些例外情况:

  • 自 2012 年初使用 e1327023ea (Git v1.7.9.2) 以来,我们一直在使用 enum definition whose last element is followed by a comma
    这与以逗号结尾的数组初始化程序一样,可用于在末尾添加新标识符时减少补丁噪声。

  • 自 2017 年年中使用 cbc0f81d (Git v2.15.0-rc0) 以来,我们一直在使用指定 结构的初始化器(例如“struct t v = { .val = 'a' };”)
    在我们的代码库中使用某些 C99 功能可能会很好,但我们犹豫是否这样做,以避免破坏与旧编译器的兼容性。
    但我们实际上不知道这些天人们是否甚至在使用 C99 之前的编译器。
    如果这个补丁可以在几个版本中无怨无悔地存活下来,那么我们可以更有信心指定初始化程序得到我们的用户群的广泛支持。
    这也表明可能支持其他 C99 功能,但不能保证(例如,gcc 在 C99 存在之前已指定初始化程序)。

  • 自 2017 年年中使用 512f41cf (Git v2.15.0-rc0) 以来,我们一直在为数组使用指定的初始值设定项(例如“int array[10] = { [5] = 2 }”)。
    这是另一个测试气球,看看我们是否会收到人们的投诉 其编译器不支持数组的指定初始化程序。
    这些过去是被禁止的,但我们没有听到任何破损报告,并且它们被认为是安全的。

  • 变量必须在块的开头声明,在第一条语句之前(即-Wdeclaration-after-statement)。

  • 在此代码库中仍然不允许在 for 循环“for (int i = 0; i < 10; i++)”中声明变量。

【讨论】:

  • 我看不出这与问题有什么关系
猜你喜欢
  • 1970-01-01
  • 2018-08-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-05-13
  • 2017-10-19
  • 1970-01-01
相关资源
最近更新 更多