为啥我可以在 C 中调用一个函数而不声明它,但在 C++ 中却不能?
Posted
技术标签:
【中文标题】为啥我可以在 C 中调用一个函数而不声明它,但在 C++ 中却不能?【英文标题】:Why can I call a function in C without declaring it but not in C++?为什么我可以在 C 中调用一个函数而不声明它,但在 C++ 中却不能? 【发布时间】:2016-02-12 16:13:09 【问题描述】:在 C++ 中,在声明之前调用函数是编译器错误。但在 C 中,它可以编译。
#include<stdio.h>
int main()
foo(); // foo() is called before its declaration/definition
int foo()
printf("Hello");
return 0;
我试过了,知道它是正确的,但我不知道背后的原因。谁能解释一下编译过程是如何实际发生的,并且两种语言的不同之处。
【问题讨论】:
@NathanOliver 如果禁用将编译的警告,我不确定它是否会静默执行。 @NathanOliver 我认为您正在编译为 C++。无论如何,IIRC,这在 C89 中有效,但在较新的标准中无效。 @juanchopanza 是的,你是对的。 @iharob 你可以在 ANSI C 中。请不要说错话。 一个很好的理由是 C != C++ 作为语言 【参考方案1】:代码“编译”作为c 程序这一事实并不意味着您可以做到。编译器应该警告函数foo()
的隐式声明。
在这种特殊情况下,隐式声明将声明一个相同的foo()
,不会发生任何不好的事情。
但是假设以下,说这是
main.c
/* Don't include any header, why would you include it if you don't
need prototypes? */
int main(void)
printf("%d\n", foo()); // Use "%d" because the compiler will
// implicitly declare `foo()` as
//
// int foo()
//
// Using the "correct" specifier, would
// invoke undefined behavior "too".
return 0;
现在假设 foo()
在不同的编译单元中定义1foo.c 为
foo.c
double foo()
return 3.5;
它是否按预期工作?
您可以想象如果您使用 malloc()
而不包含 stdio.h 会发生什么,这与我在上面尝试解释的情况几乎相同。
这样做会引发未定义的行为2,因此在这种情况下,“Works”一词在可理解的意义上是不适用的。
这可以编译的原因是因为在很早以前它是被c 标准所允许的,即c89 标准。
c++ 标准从未允许这样做,因此如果在调用之前调用代码中没有原型(“声明”)的函数,则无法编译 c++ 程序.
现代c 编译器对此发出警告,因为可能很容易发生未定义的行为,并且由于忘记添加原型或包含适当的标头并不难,因此最好对于程序员来说,如果编译器可以对此发出警告而不是突然出现一个非常莫名其妙的错误。
1它不能在同一个文件中编译,因为它会被定义为不同的返回类型,因为它已经被隐式声明了子>
2从double
和int
是不同类型的事实开始,因此会有未定义的行为。
【讨论】:
由于问题是关于 C 与 C++ 的行为,您可以提到它在标准 C++ 中从未被允许。 “大概不会发生什么坏事” 是不正确的。这是一个确凿的事实,在 C 中不会发生任何不好的事情,因为允许隐式声明函数(尽管通常不是一个好主意) 答案很棒。正是我需要的适当洞察力。谢谢!【参考方案2】:在开发 C 语言时,函数名称就是您调用它所需的全部内容。将参数与函数参数匹配完全是程序员的事;编译器不在乎你是否将三个浮点数传递给只需要一个整数的东西。
然而,事实证明这很容易出错,因此后来的 C 语言迭代添加了函数原型作为(仍然是可选的)附加限制。在 C++ 中,这些限制更加严格:现在函数原型始终是强制性的。
我们可以推测原因,但部分原因是在 C++ 中,仅仅知道函数名已经不够了。可以有多个同名但参数不同的函数,编译器必须确定调用哪一个。它还必须弄清楚如何调用(直接调用还是虚拟调用?),甚至可能必须在模板函数的情况下生成代码。
鉴于所有这些,我认为让语言要求在调用函数时知道函数原型是有意义的。
【讨论】:
C++ 处理同名但参数不同的函数的正常方式称为name mangling。 Microsoft C 还对部分 windows 库使用重命名的名称,具体取决于调用约定(标准调用与快速调用),例如 __imp__CreateFileA@28,其中 imp 表示 DLL 导入(动态 DLL)与静态(包含在程序映像中)链接,A表示 Ascii(相对于 16 位 unicode 的 W),@28 表示用于参数的字节数, prototypes 是在 C89 中添加的(实际上是第一个标准),但它也规定它们对于某些功能是可选的(例如int foo()
)【参考方案3】:
最初,C 没有函数原型,C++ 也不存在。
如果你说
extern double atof();
这表示atof
是一个返回双精度的函数。 (没有提及它的论点。)
如果你说
double d = atof("1.3");
它会起作用的。如果你说
double d2 = atof(); /* whoops, forgot the argument to atof() */
编译器不会抱怨,但如果你尝试运行它会发生一些奇怪的事情。
在那些日子里,如果您想捕获与使用错误数量或类型的参数调用函数相关的错误,那是一个单独的程序 lint
的工作,而不是 C 编译器。
同样在那些日子里,如果你只是突然调用了一个编译器以前从未听说过的函数,像这样:
int i = atoi("42");
编译器基本上假装你之前说过
extern int atoi();
这就是所谓的隐式函数声明。每当编译器看到一个对它不知道名称的函数的调用时,编译器就会假定它是一个返回 int 的函数。
快进几年。 C++ 发明了我们今天所知道的函数原型。除此之外,它们还允许您声明函数期望的参数的数量和类型,而不仅仅是它的返回类型。
再过几年,C 可选地采用函数原型。你可以根据需要使用它们,但如果你不这样做,编译器仍然会对它看到的任何未知函数调用进行隐式声明。
快进几年更多年,到 C11。现在隐式 int 终于消失了。如果您在没有先声明的情况下调用函数,编译器必须提出投诉。
但即使在今天,您可能仍在使用 C11 之前的编译器,它仍然对隐式 int 感到满意。如果您忘记在调用函数之前声明函数,C11-complaint 编译器可能会选择仅发出警告(而不是编译终止错误)。并且符合 C11 的编译器可能会提供关闭这些警告的选项,并悄悄地接受隐式整数。 (例如,当我使用非常现代的 clang 时,我安排使用 -Wno-implicit-int
调用它,这意味着我不想要有关隐式 int 的警告,因为我仍然有很多旧的、我不需要的工作代码'不想重写。)
【讨论】:
【参考方案4】:为什么我可以在不声明的情况下调用 C 中的函数?
因为在 C 中,但在 C++ 中,没有原型的函数假定返回int
。
这是该函数的隐式声明。如果该假设成立(该函数稍后以int
的返回类型声明),则程序编译得很好。
如果这个假设被证明是错误的(例如,假设它返回一个int
,但实际上发现返回一个double
),那么你会得到一个编译器错误,两个函数不能有相同的姓名。 (例如int foo()
和double foo()
不能同时存在于同一个程序中)
请注意,所有这些都仅限 C。在 C++ 中,不允许使用隐式声明。即使它们是,错误消息也会有所不同,因为 C++ 具有函数重载。该错误会说函数的重载不能仅因返回类型而异。 (重载发生在参数列表中,而不是返回类型)
【讨论】:
请注意,这仅适用于 C89/C90。在 C99 中删除了隐式 int 仍然允许符合标准的实现接受依赖于隐式声明作为扩展的程序,前提是只要给定这样的程序的实现将输出至少一个诊断。请注意,无条件输出“警告:水是湿的!”的实现将满足后一个要求。以上是关于为啥我可以在 C 中调用一个函数而不声明它,但在 C++ 中却不能?的主要内容,如果未能解决你的问题,请参考以下文章