【问题标题】:Why was mixing declarations and code forbidden up until C99?为什么在 C99 之前禁止混合声明和代码?
【发布时间】:2011-12-13 03:37:50
【问题描述】:

我最近成为了一门主要教授 C 的大学课程的助教。该课程在 C90 上标准化,主要是由于广泛的编译器支持。对于以前有 Java 经验的 C 新手来说,一个非常令人困惑的概念是变量声明和代码不得混合在一个块中(复合语句)的规则。

C99 终于解除了这个限制,但我想知道:有人知道它最初为什么存在吗?它是否简化了变量范围分析?它是否允许程序员指定在程序执行的哪个点堆栈应该为新变量增长?

如果完全没有任何目的,我认为语言设计者不会添加这样的限制。

【问题讨论】:

  • 这里有一些关于这个的讨论:stackoverflow.com/questions/6488503/… 我不太确定为什么会这样决定,但老实说,我有点喜欢在开头声明的所有变量范围。
  • 我知道这可能是一门高级主题,但我很想了解编译器如何优化东西,什么时候可以优化,什么时候不能,以及什么时候需要牢记在心让编译器优化 C 代码。程序员可能因为不懂编译器设计而不知道好的实践并产生缓慢的程序,但是关于编译自动优化的课程可能会起到很多作用。

标签: c c99 c89


【解决方案1】:

在最初的 C 语言中,可用的内存和 CPU 资源真的稀缺。所以它必须以最小的内存需求快速编译。

因此,C 语言被设计为只需要一个非常简单的编译器即可快速编译。这反过来又导致了“single-pass compiler”的概念:编译器读取源文件并尽快将所有内容翻译成汇编代码——通常是在读取源文件时。例如:当编译器读取全局变量的定义时,会立即发出相应的代码。

直到今天,这个特性在 C 中仍然可见:

  • C 需要对所有内容进行“前向声明”。多遍编译器可以自行预测和推断同一文件中函数的变量声明。
  • 这反过来又使*.h 文件成为必要。
  • 编译函数时,必须尽快计算堆栈帧的布局 - 否则编译器必须对函数体进行多次遍历。

现在没有一个严肃的 C 编译器仍然是“单遍”,因为许多重要的优化不能在一遍内完成。更多内容可以在Wikipedia 中找到。

标准体徘徊了很长一段时间,以放松关于函数体的“单程”点。我认为,其他事情更重要。

【讨论】:

  • “编译函数时,栈帧的布局必须尽快计算出来”——尽管限制是变量在的开头声明,不一定是函数的开始。因此,单程 C89 编译器必须在进入和退出时将堆栈指针移动到每个块,以利用该限制。在这种情况下,我认为每次在代码中间声明一个变量时它也可以移动堆栈指针(实际上,在该点开始一个额外的范围)并保持单次传递。
  • @SteveJessop 实际上我应该写“块”而不是“功能”,甚至可能是“范围”。但是不可能像你写的那样只移动 SP,因为临时结果推送到堆栈之前新变量无法从堆栈中弹出。那种堆栈和寄存器管理以及“单次通过”在当时根本是不可能的。
  • @A.H:那么,即使对于 C89,在这些约束下的单遍编译器也是不可能的。很明显,在 1989 年,编译器技术和能力可以记住中间结果在堆栈上的存储位置,而不仅仅是推送和弹出它们。鉴于 C89 打破了任何无法做到这一点的旧技术编译器,我真的不明白他们为什么保留限制,只是因为这是每个人都习惯的风格。
  • 实际上,想想看,如果在 C89 中,翻译限制在单个块中指定了 127 个块范围标识符,但只有 15 个嵌套级别的复合语句。因此,创建一组变量的集体负担被限制在每个块仅发生在一个点上是有道理的。我仍然声称这种负担包括移动堆栈指针,但负担是什么并不重要,只是标准要求仅支持少量嵌套块。
  • 即使经过这个解释,我也不知道在块开头定义变量的好处是什么。由于您可以在初始化程序中放置任意表达式,因此编译器仍然需要能够处理与声明混合的任意代码。
【解决方案2】:

之所以如此,是因为它一直都是这样做的,它使编写编译器更容易一些,而且没有人真正想过要这样做。随着时间的推移,人们意识到更重要的是让语言用户的生活更轻松,而不是编译器编写者。

如果完全没有任何目的,我认为语言设计者不会添加这样的限制。

不要假设语言设计者开始限制语言。像这样的限制通常是偶然和环境造成的。

【讨论】:

  • 可能是因为 Java 在 90 年代中期出现并允许它。还是 C++? (我不确定从什么时候开始允许。)
  • C++先介绍了。
【解决方案3】:

我想非优化编译器应该更容易以这种方式生成高效代码:

int a;
int b;
int c;
...

虽然声明了3个单独的变量,但堆栈指针可以一次递增,无需优化策略,如重新排序等。

比较一下:

int a;
foo();
int b;
bar();
int c;

要增加堆栈指针一次,这需要一种优化,虽然不是很高级。

此外,作为一个风格问题,第一种方法通过能够在一个地方查看所有局部变量并最终将它们作为一个整体进行检查,从而鼓励采用更规范的编码方式(难怪 Pascal 也强制执行此操作)。这在代码和数据之间提供了更清晰的分离。

【讨论】:

  • 第二个例子需要什么优化?当然,您必须等到块的末尾才能知道分配所有变量所需的堆栈指针的总增量,但这并不难。此外,如果我没记错的话,您可以使用一种称为反向修补的技术:您为堆栈中变量的总大小生成一个占位符,然后在完成编译块时设置正确的值。
  • +1:我认为风格的原因也很重要,你提到的很好。事实上,当这个限制从 C 中移除时,我有点失望。:-)
  • 实际上,我认为将 var 声明为尽可能接近第一次使用,以尽量减少它们的范围,这在风格上是优越的。但这是一个不同的(有争议的)问题......
  • @sleske 这不仅仅是为了最小化它们的范围:在许多情况下,它是创建变量const 的唯一合理方法(例如,当值取决于在新变量初始化之前执行了一条语句时)。如果你的所有变量都在块的开头声明,你就没有机会跟踪应该是常量的意外变化。
【解决方案4】:

要求变量声明出现在复合语句的开头并不影响 C89 的表达能力。使用中间块声明可以合法地做的任何事情都可以通过在声明之前添加一个左大括号并将封闭块的右大括号加倍来完成。虽然这样的要求有时可能会使源代码变得杂乱无章,带有额外的左大括号和右大括号,但这样的大括号不会只是噪音——它们会标记变量范围的开始和结束

考虑以下两个代码示例:

{ do_something_1(); { 整数富; foo = something1(); if (foo) do_something_1(foo); } { 国际酒吧; bar = something2(); if (bar) do_something_2(bar); } { 诠释博兹; boz = something3(); if (boz) do_something_3(boz); } }

{ do_something_1(); 整数富; foo = something1(); if (foo) do_something_1(foo); 国际酒吧; bar = something2(); if (bar) do_something_2(bar); 诠释博兹; boz = something3(); if (boz) do_something_3(boz); }

从运行时的角度来看,大多数现代编译器可能不会关心foo 在执行do_something3() 期间是否在语法范围内,因为它可以确定它在该语句之前持有的任何值都不会是后使用。另一方面,鼓励程序员以在没有优化编译器的情况下生成次优代码的方式编写声明并不是一个吸引人的概念。

此外,虽然处理涉及混合变量声明的简单情况并不困难(即使是 1970 年代的编译器也可以做到,如果作者想要允许这样的构造),如果包含混合声明的块也包含任何gotocase 标签。 C 语言的创建者可能认为允许混合变量声明和其他语句会使标准过于复杂,以至于不值得。

【讨论】:

    【解决方案5】:

    在 C 青年时代,当 Dennis Ritchie 工作时,计算机(例如PDP-11)的内存非常有限(例如 64K 字),编译器必须很小,所以它必须非常优化几件事,非常简单。并且在那个时候(我在 1986-89 年代在 Sun-4/110 上用 C 编码),声明 register 变量对编译器非常有用。

    今天的编译器要复杂得多。例如,最新版本的 GCC (4.6) 有超过 5 或 1000 万行源代码(取决于您如何衡量它),并进行了大量优化,而在第一个 C 编译器出现时并不存在。

    而今天的处理器也非常不同(你不能认为今天的机器就像 1980 年代的机器,但速度要快数千倍,内存和磁盘要多数千倍)。今天,内存层次结构非常重要:缓存未命中是处理器最常做的事情(等待来自 RAM 的数据)。但是在 1980 年代,对内存的访问几乎与执行单个机器指令一样快(或按当前标准慢)。这在今天是完全错误的:要读取您的 RAM 模块,您的处理器可能需要等待数百纳秒,而对于 L1 缓存中的数据,它每纳秒可以执行多条指令。

    因此,不要将 C 视为非常接近硬件的语言:这在 1980 年代是正确的,但在今天是错误的。

    【讨论】:

    • 实际上 C 仍然非常接近硬件。事实上,编译器的复杂性(比如你指出的 GCC)是为了确保 C 转换为尽可能接近目标硬件优化的东西。
    • 您在这里所说的内容可能很有趣,但这与OP的问题基本无关。
    • @AhmedMasud:即使是Haskell也被编译成二进制,但这并不意味着它是一种裸机语言。
    • 您也可以说“不要将汇编视为非常接近硬件的语言”。 :)
    【解决方案6】:

    哦,但是您可以(在某种程度上)混合声明和代码,但声明新变量仅限于块的开头。例如以下是有效的C89代码:

    void f()
    {
      int a;
      do_something();
      {
        int b = do_something_else();
      }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-10-28
      • 1970-01-01
      • 1970-01-01
      • 2011-03-02
      • 2011-02-23
      相关资源
      最近更新 更多