【问题标题】:Why do we need to include prototypes in source file in C?为什么我们需要在 C 的源文件中包含原型?
【发布时间】:2017-11-26 22:38:56
【问题描述】:

我试图理解为什么我们需要在源代码中包含函数原型。据我了解: 为了从多个源文件中获取一个可执行文件,需要将源文件转换为目标文件。目标文件可以毫无问题地相互引用:例如,主文件可以调用 foo 函数,该函数将从另一个源文件编译。 链接器负责解析所有源文件中对各种函数/符号的所有引用。

我成功地独立编译了这两个文件,然后生成了可执行文件。您会注意到 main.c 中没有#include "function.h"

函数.c

int foo() {
return 1;
}

main.c

int main() {
return foo();
}

使用的命令:

gcc mainc.c function.c -S
gcc main.o function.o -o exec

由于函数 foo 的隐式声明,我收到第一个命令的警告,但输出 exec 正在工作。所以我的问题是:

为什么我们需要包含函数原型?

【问题讨论】:

  • 因为标准要求它?
  • 你选择了一个乐观的例子。但是尝试使用float foo() { return 25.0; } 看看当结果被解释为整数时会发生什么。
  • 从 C99 开始,此代码将无法编译 - 不再允许隐式声明。
  • 主要是为了避免在原型之前的日子里出现的错误,因为预期的功能实现与消费者提供的功能不匹配。由于有可用的原型,代码更加安全。从 C99 开始,您需要在调用函数之前在范围内声明函数。这不是对原型的正式要求——非原型声明就足够了——但拥有原型有很多好处。忽略它们你就是个傻瓜。

标签: c include


【解决方案1】:

您的示例符合 C 在没有原型时查看函数的方式。

在 C89 及更早版本中,允许调用尚未声明的函数,在这种情况下,编译器假定为 int f(),因此它在您的示例中有效(并且链接器找到了函数名称,因此无需抱怨侧面)

但是如果你把返回值改成float,或者添加参数,编译器会生成错误的参数传递/返回值赋值代码,你会得到奇怪的结果。

(一个很好的例子是尝试调用返回 doublefloat 的数学函数而不包括 math.hNo math.h include - sqrt function return value?)。

这就是原型在这里的原因。指导编译器如何正确调用外部函数。

请注意,即使没有任何警告标志,gcc 也会对此发出警告(因此,在最低警告级别,对您的代码而言绝不是一个好兆头):

test.c:11:12: warning: implicit declaration of function 'foo' [-Wimplicit-function-declaration]
     return(foo());
            ^~~

【讨论】:

    【解决方案2】:

    编译器并不像我们想象的那么聪明,有时我们需要告诉编译器函数期望什么:例如参数,输出,返回类型...... 当您不对函数进行原型设计时,编译器会扣除您的函数期望但不准确的内容。 当您对函数进行原型设计时,您只需告诉编译器您的程序中可能有一个名为“foo”的函数,该函数采用 void 参数

    对不起,如果我犯了语法错误,但英语不是我的母语。 问候

    【讨论】:

      【解决方案3】:

      是的,当范围内没有函数原型时,可以调用函数。事实上,这就是 C 多年来的工作方式。所以从这个意义上说,函数原型不是必需的。

      然而,在没有函数原型的情况下,正确地调用外部函数给程序员带来了沉重的负担,以使所有的外部函数调用都完全正确。如果您使用错误的参数调用函数,或者函数返回类型不匹配。你会得到奇怪的错误结果,编译器通常不会警告你。 (曾几何时,lint 可能会警告您注意它们,但 lint 几乎消失得无影无踪。)

      函数原型是帮助程序员(和编译器)更可靠地匹配外部函数调用中的参数和返回类型的工具。随着时间的推移,它们已经从不存在(早期的 C 根本没有它们)变为可选,变为必需。

      它们现在是必需的,因为标准规定它们是。为什么标准需要它们?这并不是因为它们是编译器生成正确代码所必需的。 (再一次,历史表明早期的 C 编译器没有它们也能过得很好。)相反,标准需要它们,因为今天的共识是,获得一致可靠的程序比让 C 程序员成为牛仔并尝试去做更重要。一切都是自己。

      【讨论】:

        【解决方案4】:

        为什么要使用原型?有几个原因。 C 被设计为在源文件上一次性解析。这种选择的一个结果是必须在使用标识符之前定义标识符(即,在源文件中的行号小于)它被使用。如果不是,C 将假设标识符在标量上下文中使用时计算为 int 类型,或者在函数调用上下文中使用 function-returning-int。

        有时这种默认整数假设是可以的(例如,您的示例)。其他时候不是那么多(参见 Jean-François Fabre 的回答)。

        原型用于在源文件的早期声明函数的实际类型,而不必给出函数的完整定义。这对于给出其他源文件中定义的事物的定义很有用,但它对于解决单个源文件中的定义/使用依赖关系也很有用。例如,这对无意义的函数不能以所有函数在使用之前定义的方式排序。 bar 的早期原型解决了这个问题。

        float bar(float n);    
        
        float foo(float n)
        {
          if(n < 15.0f)
            return bar(n);
          return 3.0f * n + 1.0f;
        }
        
        float bar(float n)
        {
          if(n > 3.0f)
            return foo(n*2.0f);
          return n-1.0f;
        }
        

        【讨论】:

          【解决方案5】:

          正如我在评论中提到的,从 C99 开始,不再支持隐式 int 声明 - 所有函数必须在使用前显式声明。

          其中一部分是为了确保调用者和函数之间的二进制兼容性。如果调用者希望函数返回 32 位 int,但函数被定义为返回 64 位 double,那么您将遇到某种运行时问题(该值将被截断,或内存将被覆盖,或其他问题)。

          如果调用者和函数是在不同的源文件中定义的,那么编译器就无法知道它们是否兼容,除非你在调用者中有函数声明。如果它们是在同一个源文件中定义的,那么编译器可以将隐式函数声明与定义进行比较,如果它们不匹配,则发出诊断。

          与更现代的语言相比,C 的翻译模型相当原始,并且它依赖于您(程序员)为其提供确保最终产品正常工作所需的所有信息。这意味着包括外部函数的任何声明。

          在 80 年代和 90 年代初期,可笑很容易编写代码,编译时没有警告或错误,但由于函数声明和定义没有对齐,所以潜伏着数十个定时炸弹。强制执行显式函数声明是几十年痛苦经验的结果。

          【讨论】:

            猜你喜欢
            • 2015-09-09
            • 1970-01-01
            • 2017-06-02
            • 2015-02-10
            • 1970-01-01
            • 1970-01-01
            • 2018-10-09
            • 2015-08-17
            • 1970-01-01
            相关资源
            最近更新 更多