【问题标题】:Why does/did C allow implicit function and typeless variable declarations?为什么/C 允许隐式函数和无类型变量声明?
【发布时间】:2012-08-03 19:41:53
【问题描述】:

为什么一种语言允许隐式声明函数和无类型变量是明智的?我知道 C 是旧的,但允许省略声明并默认为 int()(或在变量的情况下为 int)对我来说似乎并不那么理智,即使在当时也是如此。

那么,为什么最初引入它?它真的有用吗?它实际上(仍然)使用吗?

注意:我意识到现代编译器会给你警告(取决于你传递给它们的标志),你可以禁止这个特性。这不是问题!


例子:

int main() {
  static bar = 7; // defaults to "int bar"
  return foo(bar); // defaults to a "int foo()"
}

int foo(int i) {
  return i;
}

【问题讨论】:

  • 该功能已从语言中删除。不幸的是,许多编译器仍然默认接受它。
  • 这不是一个真正的问题吗?说真的,这是怎么回事?
  • 我同意,这是一个真实(而不是无趣)的问题。但我认为这是题外话和/或没有建设性。我们都只能猜测为什么。
  • 我认为这里更适合。等待它再次打开。
  • “我们都只能猜测为什么。” -- 熟悉历史或愿意做研究的人,做的远比猜想的好。

标签: c history language-design implicit-declaration


【解决方案1】:

值得深思。 (这不是答案;我们实际上知道答案——它允许向后兼容。)

人们应该先看看 COBOL 代码库或 f66 库,然后再说出为什么 30 年左右没有清理干净!

gcc 默认不会发出任何警告。

-Wallgcc -std=c99 吐出正确的东西

main.c:2: warning: type defaults to ‘int’ in declaration of ‘bar’
main.c:3: warning: implicit declaration of function ‘foo’

现代gcc 中内置的lint 功能正在显示其颜色。

有趣的是lint 的现代克隆,安全的lint——我的意思是splint——默认只给出一个警告。

main.c:3:10: Unrecognized identifier: foo
  Identifier used in code has not been declared. (Use -unrecog to inhibit
  warning)

llvm C 编译器 clang 还内置了一个静态分析器,如 gcc,默认情况下会发出两个警告。

main.c:2:10: warning: type specifier missing, defaults to 'int' [-Wimplicit-int]
  static bar = 7; // defaults to "int bar"
  ~~~~~~ ^
main.c:3:10: warning: implicit declaration of function 'foo' is invalid in C99
      [-Wimplicit-function-declaration]
  return foo(bar); // defaults to a "int foo()"
         ^

人们过去认为我们不需要向后兼容 80 年代的东西。必须清理或替换所有代码。但事实证明并非如此。许多生产代码停留在史前非标准时代。

编辑:

在发布我的答案之前,我没有查看其他答案。我可能误解了海报的意图。但问题是有一段时间你手动编译代码,并使用切换将二进制模式放入内存中。他们不需要“类型系统”。 Richie 和 Thompson 摆在面前的 PDP 机器也不是这样的:

不要看胡须,看“开关”,我听说它是​​用来引导机器的。

还可以看看他们在本文中是如何引导 UNIX 的。它来自 Unix 第 7 版手册。

http://wolfram.schneider.org/bsd/7thEdManVol2/setup/setup.html

问题的关键是他们不需要那么多软件层来管理具有 KB 大小内存的机器。 Knuth 的 MIX 有 4000 个单词。您不需要所有这些类型来对 MIX 计算机进行编程。在这样的机器中,您可以愉快地将整数与指针进行比较。

我认为他们为什么这样做是不言而喻的。所以我专注于还有多少需要清理

【讨论】:

  • “我们实际上知道答案,它允许向后兼容”——这只是一个答案,如果一个人得到的只是问题的标题而不是阅读和理解 OP 真正想知道的内容.
  • 我没有注意到他的编辑。我想我可以扩展我的帖子来反映它背后的问题。
  • 编辑与重点无关。显然您也没有注意到实际解决该问题的其他答案。
  • PDP-11 切换允许将二进制值输入内存;它们的用途与 EPROM 加载器相同。它具有拨动开关与问题或类型系统的价值完全无关。在添加类型安全之前和之后,我都用 C 语言对 PDP-11 进行了编程,并且类型安全具有它现在所具有的所有优点。内存量无关紧要:PDP-11 C 编译器和 UNIX 操作系统是从类型安全中受益匪浅的复杂软件。这种不回答反映了对类型、编程语言、软件系统和计算机的深刻误解。
【解决方案2】:

对于“为什么”的最佳解释可能来自here

在同类语言中,C 语言最有两个特点:数组和指针之间的关系,以及声明语法模仿表达式语法的方式。它们也是其最常受到批评的功能之一,并且经常成为初学者的绊脚石。在这两种情况下,历史事故或错误都加剧了他们的困难。其中最重要的是 C 编译器对类型错误的容忍度。 从上面的历史可以清楚地看出,C 是从无类型语言演变而来的。它并没有突然出现在它的早期用户和开发者眼中,是一种拥有自己规则的全新语言。相反,随着语言的发展,我们必须不断地调整现有程序,并为现有的代码体留出余地。 (后来,标准化 C 的 ANSI X3J11 委员会也会面临同样的问题。)

系统编程语言不一定需要类型;你在乱搞字节和单词,而不是浮点数、整数、结构和字符串。类型系统是零零散散地移植到它上面的,而不是从一开始就成为语言的一部分。随着 C 从主要是一种系统编程语言转变为一种通用编程语言,它在处理类型方面变得更加严格。但是,即使范式来来去去,遗留代码是永远的。仍然有很多代码依赖于隐含的int,标准委员会不愿意破坏任何有效的东西。这就是为什么花了将近 30 年才摆脱它的原因。

【讨论】:

    【解决方案3】:

    参见 Dennis Ritchie 的“C 语言的发展”:http://cm.bell-labs.com/who/dmr/chist.html

    例如,

    与在 B的创建,BCPL的核心语义内容——它的类型结构 和表达式评估规则——保持不变。两种语言都是 无类型,或者更确切地说只有一个数据类型,“单词”或“单元格”,一个 固定长度的位模式。这些语言的记忆由一个 此类单元格的线性数组,以及单元格内容的含义 取决于应用的操作。 + 运算符,例如,简单地 使用机器的整数加法指令将其操作数相加,并且 其他算术运算同样不知道实际 它们的操作数的含义。因为内存是一个线性数组,它是 可以将单元格中的值解释为该数组中的索引, BCPL 为此提供了一个操作员。在原来的 语言它被拼写为 rv,后来是 !,而 B 使用一元 *。 因此,如果 p 是包含以下索引的单元格(或地址,或 指向)另一个单元格,*p 指的是指向的内容 单元格,作为表达式中的值或作为目标 任务。

    这种无类型性在 C 中一直存在,直到作者开始将其移植到具有不同字长的机器上:

    这一时期的语言变化,尤其是 1977 年左右,主要集中在可移植性和类型安全的考虑上, 为了解决我们预见和观察到的问题 将大量代码移至新的 Interdata 平台。猫 那个时候仍然表现出其无类型起源的强烈迹象。 例如,指针与整体内存几乎没有区别 早期语言手册或现存代码中的索引;的相似性 字符指针和无符号整数的算术性质 让人难以抗拒认出他们的诱惑。未签名的 添加了类型以使无符号算术无需 将其与指针操作混淆。同样,早期的语言 宽恕整数和指针之间的赋值,但这种做法 开始气馁;类型转换的符号(称为 'casts' 来自 Algol 68 的例子)被发明来指定类型 更明确的转换。被PL/I的例子迷住了,早期的C 没有将结构指针牢固地绑定到它们指向的结构 到,并允许程序员编写指针-> 成员几乎没有 关于指针的类型;采取了这样的表达方式 不加批判地作为对由 指针,而成员名只指定了一个偏移量和一个类型。

    编程语言随着编程实践的变化而发展。在现代 C 和现代编程环境中,许多程序员从未编写过汇编语言,整数和指针可互换的概念似乎几乎是深不可测和不合理的。

    【讨论】:

    • SRB 建议 DMR 在 C 中添加 void 作为类型。“它保存了返回值的指令加载寄存器”加上当 SRB 到达时“C 类型就像 PL/1 即从您喜欢的任何基数的偏移量”。 C 更改为 Algol68 类型,其他一些“A68isms”最终以 C... [注意:Algol68 具有强类型]
    • @NevilleDNZ 还有他受算法影响的外壳 (research.swtch.com/shmacro)。有趣的是 Algol60 -> CPL -> BCPL -> B -> C 和 {Algol60, CPL} ->ALGOL68 -> C. Strachey 的 CPL 是一门非常复杂的语言,可惜失去了许多优秀的特性一路走来(在其他地方重新出现,例如在 Haskell 和其他函数式语言中)。
    • 我发现 C 中的“switch” stmts 类似于 PL/I 的“switch”,我以为仅此而已。我惊讶地发现 C 以前有“PL/I 的类型偏移量来自任何基础”。有趣的是 C(一种被设计成很小的语言)如何设法包含来自这么多语言的影响。当然 BCPL 值得一看。
    • @NevilleDNZ CPL 比 BCPL 多得多……它是由这个人设计的:en.wikipedia.org/wiki/Christopher_Strachey
    【解决方案4】:

    很久很久以前,在 K&R 之前的 ANSI 时代,函数看起来与今天完全不同。

    add_numbers(x, y)
    {
        return x + y;
    }
    
    int ansi_add_numbers(int x, int y); // modern, ANSI C
    

    当你调用像add_numbers 这样的函数时,调用约定有一个重要区别:调用函数时所有类型都被“提升”。所以如果你这样做:

    // no prototype for add_numbers
    short x = 3;
    short y = 5;
    short z = add_numbers(x, y);
    

    发生的情况是x 被提升为inty 被提升为int,并且默认情况下假定返回类型为int。同样,如果您传递float,它会被提升为加倍。这些规则确保原型不是必需的,只要您获得正确的返回类型,并且只要您传递了正确数量和类型的参数。

    注意原型的语法是不同的:

    // K&R style function
    // number of parameters is UNKNOWN, but fixed
    // return type is known (int is default)
    add_numbers();
    
    // ANSI style function
    // number of parameters is known, types are fixed
    // return type is known
    int ansi_add_numbers(int x, int y);
    

    过去的一个常见做法是在大多数情况下避免使用头文件,而只是将原型直接粘贴到您的代码中:

    void *malloc();
    
    char *buf = malloc(1024);
    if (!buf) abort();
    

    如今,头文件在 C 中被认为是必要的邪恶,但正如现代 C 衍生品(Java、C# 等)已经摆脱了头文件一样,老前辈们也不太喜欢使用头文件。

    类型安全

    根据我对 C 之前时代的了解,并不总是有很多静态类型系统。一切都是int,包括指针。在这种古老的语言中,函数原型的唯一意义就是捕捉数量错误。

    因此,如果我们假设首先将函数添加到语言中,然后再添加静态类型系统,则该理论解释了为什么原型是可选的。这个理论还解释了为什么数组在用作函数参数时会衰减为指针——因为在这个 proto-C 中,数组只不过是指针,它会自动初始化为指向堆栈上的某个空间。例如,可能会出现以下情况:

    function()
    {
        auto x[7];
        x += 1;
    }
    

    引用

    关于无类型:

    两种语言 [B 和 BCPL] 都是无类型的,或者更确切地说,只有一种数据类型,即“单词”或“单元格”,一种固定长度的位模式。

    关于整数和指针的等价性:

    因此,如果p 是一个包含另一个单元格的索引(或地址或指针)的单元格,则*p 引用指向的单元格的内容,或者作为表达式中的值,或者作为任务的目标。

    原型由于尺寸限制而被省略的理论证据:

    在开发过程中,他不断地与内存限制作斗争:每种语言的添加都会使编译器膨胀,使其几乎无法适应,但每次利用该功能进行的重写都会减小其大小。

    【讨论】:

    • 最初,您可能写过:add_numbers(x,y){return x+y;}(该转录中没有不必要的空格;在源文件中使用 4 行代码)。
    • char *malloc();void * 也是后来添加的(就像普通的 void 一样),它在 C89 标准之前才进入该语言。太可怕了,不是吗?
    • @JonathanLeffler:令人着迷。这些天我认为void * 是理所当然的,但它曾经是char * 是有道理的,因为现在标准规定void *char * 必须具有相同的表示。
    【解决方案5】:

    这是通常的故事 - 歇斯底里的葡萄干(又名“历史原因”)。

    一开始,运行 C 的大型计算机(DEC PDP-11)有 64 KiB 的数据和代码(后来每个 64 KiB)。您可以使编译器运行的复杂程度受到限制。实际上,有人怀疑您是否可以使用 C 等高级语言编写 O/S,而不需要使用汇编程序。所以,有大小限制。此外,我们谈论的是很久以前,在 1970 年代初到中期。总体而言,计算不像现在那样成熟(特别是编译器不太被理解)。此外,衍生出 C 的语言(B 和 BCPL)是无类型的。这些都是因素。

    从那时起,语言已经发展(谢天谢地)。正如在 cmets 和被否决的答案中广泛指出的那样,在严格的 C99 中,用于变量的隐式 int 和隐式函数声明都已过时。然而,大多数编译器仍然承认旧语法并允许使用它,或多或少地警告,以保持向后兼容性,以便旧源代码继续像往常一样编译和运行。 C89 在很大程度上标准化了语言,疣 (gets()) 等等。这是使 C89 标准可接受的必要条件。

    仍然存在使用旧符号的旧代码 — 我花了很多时间研究一个古老的代码库(最古老的部分大约是 1982 年),它仍然没有完全转换为到处的原型(这很烦人我非常强烈,但是在拥有数百万行代码的代码库上,一个人只能做这么多)。其中很少有变量为“隐式int”;有太多地方没有在使用前声明函数,还有一些地方函数的返回类型仍然隐式int。如果您不必处理这些乱七八糟的事情,请感谢那些在您之前走过的人。

    【讨论】:

    • 如果包含这两个功能,编译器不会变得更大吗?或者这些天在 C 中没有“正确的”函数声明?
    • 不要忘记原型符号在 C89 标准发布之前没有正式添加到 C 中(并且是从证明它是有益的 C++ 改编的)。这增加了 C 编译器的复杂性。在此之前,您可能已经写过:main(){ static bar = 7; return foo(bar); } foo(i) { return i; } 没有原型和标头所呈现的 namby-pamby molly-coddling 等等。
    • 啊,是的,大小限制……不仅是编译代码的大小,还有源文件的大小。
    • 我明白了,有道理。不知道。
    • 我想说 BCPL 的无类型(B 也是无类型的吗?)可能是最大的原因。绝对值得一提的是,“隐式 int”已被 C99 标准从语言中删除; static bar = 7; 现在是语法错误。
    猜你喜欢
    • 2012-03-28
    • 2018-08-21
    • 2015-09-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-10
    • 1970-01-01
    相关资源
    最近更新 更多