为什么“auto”关键字对 C 语言中的编译器编写器有用?

Why is the 'auto' keyword useful for compiler writers in C?

提问人:Chi_Iroh 提问时间:6/22/2023 最后编辑:Chi_Iroh 更新时间:7/22/2023 访问量:5516

问:

我目前正在阅读“Expert C Programming - Deep C Secrets”,刚刚遇到这个:

从不需要存储类说明符。它对编译器编写者来说最有意义 在符号表中输入一个条目 — 它说“此存储是在输入 块“(而不是在编译时静态分配,或在堆上动态分配)。 对于所有其他程序员来说几乎毫无意义,因为它只能在函数内部使用,但是 默认情况下,函数中的数据声明具有此属性。autoauto

我看到有人在这里问了同样的事情,但他们没有任何答案,评论中给出的链接只解释了为什么 C 中有这样一个关键字,继承自 B,以及与 C++11 或 C++11 之前的区别。

无论如何,我发帖是为了专注于说明关键字在编译器编写中以某种方式有用的部分,但是这个想法或与符号表的联系是什么?auto

我真的坚持这样一个事实,即在用 C 语言编写编译器时,我只询问潜在的用法(而不是编写 C 编译器)。

为了澄清一下,我之所以问这个问题,是因为我想知道是否有代码示例是合理的,因为作者说在编写编译器时会有。auto

这里的重点是我认为已经理解了(继承自 B,它是强制性的,但在 C 中毫无用处),但我无法想象任何使用它是有用的(或者至少不是无用的)的例子。auto

似乎真的没有任何理由使用 ,但是是否有任何旧的源代码或类似的东西与引用的语句相对应?auto

c 编译器构造 关键字 auto automatic-storage

评论

5赞 Weather Vane 6/22/2023
编译器发出警告并生成代码,例如读取未初始化的变量,或返回指向变量的指针,为此它需要知道它是静态的还是自动的。
5赞 tadman 6/22/2023
请记住,C 规范中有很多东西在当时看起来是个好主意,但后来被证明是无用的。C 和 C++ 标准团队之间有很多反馈,其中 C 可能是“哦,是的,意思是”弄清楚“确实更有意义,但哦,好吧。auto
6赞 pmacfarlane 6/22/2023
你引用的书似乎是 1994 年(或更早)的。所以是的,也许 29 年前是真的,但现在不是那么多了。
5赞 pmacfarlane 6/22/2023
旁白:我相信 C23 将重新定位,使其更像 C++。auto
3赞 Dave S 6/22/2023
“我无法想象任何使用它的例子是有用的”——我注意到这本书的作者声称它是有用的,但没有实际解释如何或为什么。也许他们弄错了,或者说一些 80 年代或 90 年代初的“愚蠢”编译器需要更多提示?

答:

39赞 chqrlie 6/22/2023 #1

据我所知,从 40+ 年的 C 编程,包括编译器工作,这个关键字在 C 中已经完全没用了 50 年。auto

为了回答您的确切问题,为什么 auto 关键字对 C 语言中的编译器编写器有用?它根本没有用;C 编译器编写器只需要将其解析为关键字,并将其语义实现为存储类说明符。

它似乎是 B 语言的遗留物,B 是 C 语言的前身,由 Ken Thompson 和 Dennis Ritchie 在 60 年代末和 70 年代初在贝尔实验室开发。我从未使用过 B,我怀疑 1984 年在 Inria 遇到的 Peter 也使用过。

在 C23 之前,只能用于为函数范围内的定义指定自动存储类。这是默认设置,因此是完全冗余的,只要指定了类型或其他限定符,就可以删除。没有任何需要它的情况,因此将其包含在 C 标准中仅植根于 C 语言的早期历史。autoautoauto

auto自 C++ 11 以来,一直在 C++ 中用于在变量定义中启用类型推断,无论是否具有自动存储,其中编译器从初始值设定项的类型中检测类型。

随着当前趋势推动 C 和 C++ 语言的通用子集的收敛,在 C23 中,以 C++ 语义为模型的这个关键字附加了新的语义,但受到更多限制:

6.7.1 存储类说明符

auto可能与所有其他产品一起出现,但typedef;

auto仅应出现在具有文件范围的标识符的声明说明符中,或者如果要从初始值设定项推断类型,则应与其他存储类说明符一起出现。

如果与另一个存储类说明符一起出现,或者如果它出现在文件作用域的声明中,则在确定链接的存储持续时间时将忽略它。然后,它仅指示可以推断声明的类型。auto

类型推断指定为:

6.7.9 类型推断

约束

1 推断其类型的声明应包含存储类说明符 。auto

描述

2 对于作为对象定义的这种声明,初始化声明者应具有以下形式之一

direct-declarator = 赋值表达式 direct-declarator = { 赋值表达式 } direct-declarator = { 赋值表达式
, }

声明的类型是左值、数组到指针或函数到指针转换之后的赋值表达式的类型,另外由限定符限定,并由声明说明符中出现的属性(如果有)进行修改。如果直接声明符不是表单标识符 attribute-specifier-sequenceopt(可能用平衡的括号对括起来),则行为未定义。

类型推断在 C++ 中非常有用,因为类型可能非常复杂,几乎不可能在变量定义中指定,尤其是在使用模板时。相反,在 C 语言中使用它可能会适得其反,降低代码的可读性并鼓励懒惰和容易出错的做法。将指针隐藏在 typedef 后面已经够糟糕了,现在您可以使用关键字完全隐藏它们。auto


最后,我记得在棘手的面试测试中看到它,要求候选人找出为什么这段代码无法编译:

#include <stdio.h>
#include <string.h>

int main(void) {
    char word[80];
    int auto = 0;
    while (scanf("%79s", word) == 1) {
        if (!strcmp(word, "car")
        ||  !strcmp(word, "auto")
        ||  !strcmp(word, "automobile"))
            auto++;
    }
    printf("cars: %d\n", auto);
    return 0;
}

评论

2赞 pmacfarlane 6/22/2023
在 C23 中,拥有 C++ 语义不是已经完成了吗?auto
3赞 Ted Lyngmo 6/22/2023
@pmacfarlane 它没有确切的C++语义,但它可以用来推断 C23 中的类型。auto x = foo();
7赞 chqrlie 6/22/2023
@TedLyngmo:恕我直言,试图将 C 收敛到它的远房表亲是一个可悲的举动。
4赞 Peter Cordes 6/23/2023
auto 在 C++ 中用于启用类型推断 - 作为记录,这是 C++11 中的新功能。在此之前的 C++ 中,就像在 C23 之前的 C 中一样,作为存储类说明符。godbolt.org/z/6WeGab6of(并仅使用 C++14 推断函数的返回类型。auto
6赞 Mark Ransom 6/24/2023
@pmacfarlane我们的工作场所刚刚建立了一个代码检查器,只要可以通过上下文推断类型,它就会坚持使用。我真的很不喜欢这个规则,因为有时显式类型是关于你正在使用的内容的有用文档。auto
19赞 user7860670 6/22/2023 #2

该关键字源自 B 语言,它实际上非常有用,并允许编译器将本地名称与非本地名称(用关键字标记):autoextrn

main()
{
    extrn printf;
    auto x;
    x = 25;
    printf('%d', x);
}

当 B 语言演变为 C 语言时,它保留了高度的向后兼容性。在 B 中,基本上只有一个“单元格”类型,因此在 C 中,他们引入了类型注释作为可选功能。在 C89 及之前,用于引入本地名称的相同目的:auto

main()
{
    extern printf();
    auto x; /* type is int by default */
    x = 42;
    printf("%d", x);
}

在线编译器

在语言重点转向强制类型安全之后,对说明符的需求完全消失了,因为类型注释的存在允许区分本地名称声明。auto

61赞 Chi_Iroh 6/22/2023 #3

作者回答:我刚刚给Van der Linden先生发了电子邮件,他是这样说的:

是的,我同意在堆栈溢出上回答的人。 我不确定,因为我从未使用过 B 语言,但在我看来,“auto”最终出现在 C 中是很有道理的,因为它在 B 中。

即使在 1980 年代我专业地使用 C 语言进行内核和编译器编程时,我也从未见过任何我记得使用“auto”的代码。

关键的一点是,auto 关键字不会添加任何额外的信息,因此是多余的和不必要的。把它带入C是一个错误!

我还要求解释一下他所说的编译器编写和符号表是什么意思。以下是他的回应:

假设您正在编写一个编译器,该编译器将 C 源代码转换为链接器对象(可以链接的目标文件)。

每当词法分析器(编译器的前端)找到构成用户定义符号的字符序列(可能是变量、函数名称、常量等)时,编译器就会将该名称存储在称为“符号表”的表中。它还将存储它所知道的关于符号的所有其他信息 - 如果它是一个变量,它将存储它的类型,如果是一个常量,它将存储值,如果是一个函数,它将注意到它可以被调用,等等。它还将存储名称的范围(已知此符号的代码行)。符号表是编译器的核心数据结构之一,其中一些被传递到目标文件中。对象文件需要知道外部代码对象可寻址的任何名称,以便链接器可以将名称的使用与存储该名称的对象相关联。

然后,当编译器遇到相同的名称时,编译器会在符号表中查看它是否已经知道该名称的所有信息。存储有关名称的有用项目之一是“编译器将为其分配存储的位置”。只要符号保持在范围内,就必须维护该存储。因此,符号表知道在运行时应该在哪里分配存储是很有用的。我举了 3 个例子,说明变量可能被存储的不同位置。“auto”关键字告诉编译器“这是一个变量,您应该将其存储在堆栈上,其作用域是声明它的函数”。

只是,编译器不需要被告知这一点,因为对于函数中声明的所有变量来说,这已经是正确的。 我希望这个解释是有道理的。

我想我完全误解了他的陈述,认为在用 C 编写编译器时,在处理符号表的代码中可能有一些用法,但似乎他的意思是没用的,但 C 编译器编写者必须处理它并理解它。 尽管如此,我还是请他确认我的错误,这确实是我的误解:autoauto

也许最好的思考方式是:

  1. “auto”在 C 中没有语义效果
  2. 我们认为它来自 B,但不确定。
  3. 它将信息传达给为 C 代码编写编译器的人。
  4. 但该信息是编译编写器拥有的其他信息的副本。
  5. 因此,编译器编写者可以记下任何一条信息来更新符号表
  6. 或者,他们可以检查两条信息是否一致,如果不是,则发出错误消息。

评论

3赞 Pablo H 6/23/2023
我认为这个想法是编译器在符号表中保留了存储类别(例如静态、堆栈等)。由于从源代码到编译器内部的映射很薄,关键字或多或少直接映射:映射到静态,映射到堆栈,可能映射到寄存器等(只是,auto 是隐式的,register 由编译器计算,关键字被忽略)。staticautoregister
7赞 Joshua 6/23/2023
还有更多。该关键字之所以存在,是因为最早的 C 编译器无法在没有它的情况下进行编译。另一轮语法提升本可以删除它,但它没有完成。只是正则表达式删除是行不通的,因为变量是在编译器源代码本身中声明的。你看,是隐含的。autoautoauto x;int
0赞 Chi_Iroh 6/23/2023
@Joshua 早多久?你是说 ANSI 之前的吗?如果是,今天有没有办法编译 pre-ANSI 代码?GCC 似乎是不可能的,因为在 std=c89 之前没有 std 标志。
2赞 Joshua 6/23/2023
@Chi_Iroh:早早。我们说的是原始的 PDP-11 Unix 编译器。这种奇怪的行为之所以存在,是因为编译器依赖于它。(您可以检查 pcc,看看它是否仍然没有。(AFAIK std=c89 可以成功编译所有 K&R C,因此不需要更早的选项。
0赞 Chi_Iroh 6/23/2023
好的,谢谢,我一定会看看 pcc 和其他旧编译器。
12赞 Matthieu M. 6/22/2023 #4

首先是 4 或 5 个存储类说明符之一:、、、、和 C11 开始。C 中的每个变量都有一个来自上述列表中的关联存储类说明符,如果未指定,则为默认值。autoautoregisterstaticextern_Thread_localauto

从用户的角度来看,由于是默认值,因此很少需要指定它,可以说这样做只是噪音 - 如果通常不使用说明符,其他说明符会更加突出。auto

然而,从编译器编写者的角度来看,由于每个变量都有一个存储类说明符,因此这个概念是最重要的,设身处地为他们着想,你可以想象某个地方存在一个枚举 4 个(或 5 个)不同的说明符,每个变量声明都附加了一个枚举值。autoenum

它出现在编译器中这一事实并不要求它出现在语言中,但它确实为它提供了一个参数:规律性。无论它是否直接暴露(或是否直接暴露),这个概念都存在,并且暴露它的成本很小,所以也可能,不是吗?

1 @BenVoigt提到,在类型由用户提供的宏中,它可能很有用,因为它可以防止用户指定另一个存储说明符,例如 static,因为编译器不会接受两个存储说明符。

评论

3赞 chqrlie 6/22/2023
有趣的论点,但还有其他概念的规律性需要更多的关键字:与全局符号的公共,与局部符号的动态,与局部定义的符号,与可修改的......使用冗余关键字实际上违背了 C 设计者的基本价值观之一:简单性。他们保留关键字可能是为了与最初用 B 编写的古代代码兼容,如 user7860670 的答案所示,其中是隐式的。staticstaticexternconstautoint
0赞 Matthieu M. 6/22/2023
@Bob__:我确实做到了。
1赞 Matthieu M. 6/22/2023
@chqrlie:存储类说明符专门用于“变量”,与函数相对立:你不适用于函数。因此,对于全局变量,我们谈论的是 vs(如果您想在标头中声明它),而对于局部范围的变量,则讨论 vs .因此,对于变量来说,它实际上是相当有规律的。至于(和),它是在ANSI C中引入的,在K&R版本中不存在,所以它的不同是相当正常的。registerstaticexternstaticautoregisterconstvolatile
6赞 Ben Voigt 6/23/2023
有一种情况是,它在当前版本的C程序(>C90和C23<)中很有用 - 如果涉及宏。 可能是 , 可能是 , 可能是 , 可能是 。 可能是,可能是,可能是......但可以肯定的是,事实并非如此,如果任何未来的程序员试图添加到 #define 中,您将得到编译错误,而不是静默地破坏代码。MAILBOX x;intcharvolatilestaticauto MAILBOX x;intcharvolatilestaticstaticMAILBOX
1赞 Matthieu M. 6/23/2023
@BenVoigt:确实不错;我已将答案从“从不”修改为“很少”,并提到了您的示例。
0赞 Mohammed AL MahmoOd 7/5/2023 #5

C 语言中的 auto 关键字对大多数程序员来说不是很有用。但是,它对编译器编写器很有用。

符号表是一种数据结构,编译器使用它来跟踪程序中的所有变量和函数。当编译器看到自动声明时,它知道变量将在堆栈上分配。这意味着编译器可以优化该变量的代码,例如避免将其存储在寄存器中。

例如,请考虑以下函数:

void soso(int x) {
  int y = x * 2;
  // The compiler could optimize this code if it knew that y was allocated on the stack.
  int z = y + 3;
}

如果编译器知道 y 是在堆栈上分配的,则可以避免将 y 存储在寄存器中。这将节省内存并提高函数的性能。

当然,auto 关键字并不总是提高编译器生成代码性能所必需的。但是,对于想要优化代码的编译器编写者来说,它可能是一个有用的工具。

以下是有关 auto 关键字的一些其他详细信息:

auto 关键字在 C 中不是必需的。编译器将自动假定在函数中声明的任何变量都已在堆栈上分配。 auto 关键字可用于在函数外部声明变量。但是,不建议这样做,因为它会使代码更难阅读和理解。 auto 关键字并非在所有 C 编译器中都可用。某些编译器可能仅在某些情况下支持它。

评论

0赞 Chi_Iroh 7/7/2023
寄存器不是比堆栈快吗?我可能是错的,但我相信编译器实际上会尽可能地将堆叠的变量移动到寄存器中以加快代码速度。