17

为什么语言允许隐式声明函数和无类型变量是明智的?我知道 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;
}
4

5 回答 5

14

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

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

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

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

于 2012-08-06T20:12:44.453 回答
14

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

例如,

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

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

在此期间,尤其是在 1977 年左右,语言变化主要集中在考虑可移植性和类型安全性,以应对我们在将大量代码移至新的 Interdata 平台时预见和观察到的问题。当时的 C 仍然表现出其无类型起源的强烈迹象。例如,指针与早期语言手册或现存代码中的完整记忆索引几乎没有区别。字符指针和无符号整数的算术性质的相似性使得很难抗拒识别它们的诱惑。添加了无符号类型以使无符号算术可用,而不会与指针操作混淆。类似地,早期的语言允许整数和指针之间的赋值,但这种做法开始受到反对;发明了一种类型转换的符号(称为 Algol 68 示例中的“强制转换”)以更明确地指定类型转换。被 PL/I 的例子所迷惑,早期的 C 并没有将结构指针牢固地绑定到它们所指向的结构上,并且允许程序员编写指针->成员几乎不考虑指针的类型;这样的表达式被不加批判地视为对指针指定的内存区域的引用,而成员名称仅指定了偏移量和类型。并允许程序员编写指针->成员几乎不考虑指针的类型;这样的表达式被不加批判地视为对指针指定的内存区域的引用,而成员名称仅指定了偏移量和类型。并允许程序员编写指针->成员几乎不考虑指针的类型;这样的表达式被不加批判地视为对指针指定的内存区域的引用,而成员名称仅指定了偏移量和类型。

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

于 2012-08-06T20:26:34.140 回答
8

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

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

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

于 2012-08-06T20:27:08.987 回答
6

很久很久以前,在 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。同样,如果你通过 afloat它被提升为加倍。这些规则确保原型不是必需的,只要您获得正确的返回类型,并且只要您传递了正确数量和类型的参数。

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

// 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则引用指向的单元格的内容,或者作为表达式中的值,或者作为赋值的目标。

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

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

于 2012-08-06T20:26:55.620 回答
2

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

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

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

吐出正确的-Wall东西gcc -std=c99

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

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

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

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

llvmC 编译器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 机器也不是这样:

不要看胡须,看看我听说用于引导机器的“切换”。

K&R

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

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

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

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

于 2012-08-06T20:33:57.907 回答