很久很久以前,在 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
被提升为int
,y
被提升为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
则引用指向的单元格的内容,或者作为表达式中的值,或者作为赋值的目标。
由于尺寸限制而省略了原型的理论的证据:
在开发过程中,他不断地与内存限制作斗争:每种语言的添加都会使编译器膨胀,使其几乎无法适应,但每次利用该功能进行的重写都会减小其大小。