【问题标题】:Why do functions need to be declared before they are used?为什么函数需要在使用前声明?
【发布时间】:2011-06-13 01:31:43
【问题描述】:

在阅读this question 的一些答案时,我开始想知道为什么编译器实际上确实在第一次遇到函数时需要知道它。在解析收集其中声明的所有符号的编译单元时添加一个额外的传递不是很简单,这样声明和使用它们的顺序就不再重要了吗?

有人可能会争辩说,在使用函数之前声明它们肯定是一种很好的风格,但我想知道,在 C++ 中这是强制性的还有其他原因吗?

编辑 - 举例说明: 假设您必须在头文件中内联定义函数。这两个函数相互调用(可能是递归树遍历,其中树的奇数层和偶数层的处理方式不同)。解决此问题的唯一方法是在另一个函数之前对其中一个函数进行前向声明。

一个更常见的例子(虽然是类,而不是函数)是具有private 构造函数和工厂的类的情况。工厂需要知道类才能创建它的实例,而类需要知道friend 声明的工厂。

如果这是过去的要求,为什么没有在某个时候删除?它不会破坏现有代码,对吗?

【问题讨论】:

  • 其实即使在实际的C++中,你也可以找到一个函数在声明之前可以使用的地方(严格来说,“之前”是关于程序源代码被读取的顺序)——一个班里面! (这可能是因为编译器在类声明后面放置了一个函数定义,正如这里有人指出的那样。) class A { public: static void foo(void) { bar(); } 私人: 静态无效栏(无效) { 返回; } }; int main() { A::foo();返回0; }

标签: c++ function-declaration


【解决方案1】:

主要原因将是使编译过程尽可能高效。如果您添加额外的通行证,您将同时增加时间和存储空间。请记住,C++ 是在四核处理器时代之前开发的 :)

【讨论】:

    【解决方案2】:

    我想到了两个原因:

    • 它使解析变得容易。无需额外通行证。
    • 它还定义了范围;符号/名称​​可用 只有在声明之后才可用。意思是,如果我声明一个全局变量int g_count;,这行后面的代码可以使用它,但行之前的代码不行!全局函数的参数相同。

    例如,考虑以下代码:

    void g(double)
    {
        cout << "void g(double)" << endl;
    }
    void f()
    {
        g(int());//this calls g(double) - because that is what is visible here
    }
    void g(int)
    {
        cout << "void g(int)" << endl;
    }
    int main()
    {
        f();
        g(int());//calls g(int) - because that is what is the best match!
    }
    

    输出:

    void g(双)
    void g(int)

    在 ideone 上查看输出:http://www.ideone.com/EsK4A

    【讨论】:

    • 如果你声明一个全局变量,你会得到你想要的。
    【解决方案3】:

    我猜是因为 C 已经很老了,而且在当时 C 被设计为高效编译是一个问题,因为 CPU 速度要慢得多。

    【讨论】:

      【解决方案4】:

      由于 C++ 是一种静态语言,编译器需要检查值的类型是否与函数参数中预期的类型兼容。当然,如果你不知道函数签名,你就不能做这种检查,从而违背了静态编译器的目的。但是,由于您在 C++ 中拥有银牌,我想您已经知道这一点。

      C++ 语言规范是正确的,因为设计人员不想强制使用多通道编译器,因为当时硬件不如今天可用的那么快。最后,我认为,如果 C++ 是在今天设计的,那么这种强制就会消失,但是到那时,我们就会有另一种语言 :-)。

      【讨论】:

        【解决方案5】:

        因为 C 和 C++ 是古老的语言。早期的编译器没有很多内存,所以这些语言被设计成编译器可以从上到下读取文件,而不必考虑文件作为一个整体

        【讨论】:

        • 老实说,我看不出这会如何显着减少内存使用量。在编译器到达文件末尾时,它将所有定义存储在内存中;就像在使用前不需要声明一样。一个更可能的原因是通过消除额外传递的需要来缩短编译时间。但是,这与 C 更相关。C++ 的类无论如何都需要额外的通过。
        【解决方案6】:

        您打算如何解决未声明的标识符,这些标识符在不同的翻译单元中定义

        C++ 没有模块概念,但是作为从 C 的继承而来的单独翻译。C++ 编译器将自行编译每个翻译单元,根本不了解其他翻译单元。 (除了 export 打破了这一点,遗憾的是,这可能是它从未起飞的原因。)
        头文件,这是您通常放置声明的地方在其他翻译单元中定义的标识符实际上只是将相同声明滑入不同翻译单元的一种非常笨拙的方法。它们不会让编译器知道还有其他翻译单元,其中定义了标识符。

        编辑是您的其他示例:
        由于所有文本包含而不是适当的模块概念,编译对于 C++ 来说已经花费了非常长的时间,因此需要另一个编译通道(编译已经被分成几个通道,并非所有通道都可以优化和合并,IIRC)会恶化已经很糟糕的问题了。在某些情况下,改变它可能会改变重载分辨率,从而破坏现有代码。

        请注意,C++ 确实需要额外的传递来解析类定义,因为在类定义中内联定义的成员函数会被解析,就好像它们是在类定义后面定义的一样。但是,这是在考虑 C with Classes 时决定的,因此没有现有的代码库可以破坏。

        【讨论】:

        • 并非所有语言都将编译拆分为多个翻译单元。以 Java(还有其他语言)为例,编译器可以在其中找到所有其他必需的文件,并解析它们以获取所有导出的实体。
        • @Sylvian:嗯,我以为这只有一个c++ 标签?无论如何,我不了解Java,但是具有 模块概念的C# 要求您声明 您的函数。您只需在一个模块中定义它们,在另一个模块中引用该模块,编译器将在编译另一个模块时获取标识符。
        • @sbi 我想说 C++ 可以设计成模块系统。这将允许在不需要前向声明的情况下将代码拆分为多个文件,并且不会暗示编译器会消耗更多资源(它可能比 .h / .cpp 分离更有效)。因此,拥有单独的编译单元不可能是强加前向声明的唯一原因。
        • @Sylvian:啊,我明白了。我已经尝试在我的编辑中解决这个问题。
        • 我不明白翻译单元/模块的问题如何限制声明和使用的顺序。您没有对问题给出明确的答案!在您的“答案”中,您根本不会讨论如果在使用某事物之后出现某事物的声明(仅在一个翻译单元)。
        【解决方案7】:

        C 编程语言的设计使编译器可以实现为one-pass compiler。在这样的编译器中,每个编译阶段只执行一次。在这样的编译器中,您不能引用稍后在源文件中定义的实体。

        此外,在 C 语言中,编译器一次只解释一个编译单元(通常是一个 .c 文件和所有包含的 .h 文件)。所以你需要一种机制来引用另一个编译单元中定义的函数。

        之所以做出允许一次性编译器并能够将项目拆分为小型编译单元的决定,是因为当时可用的内存和处理能力非常紧张。并且允许前向声明可以通过单个功能轻松解决问题。

        C++ 语言源自 C 语言并继承了它的特性(因为它希望尽可能与 C 兼容以简化过渡)。

        【讨论】:

          【解决方案8】:

          从历史上看,C89 允许您这样做。当编译器第一次看到一个函数的使用并且它没有预定义的原型时,它“创建”了一个与函数的使用相匹配的原型。

          当 C++ 决定向编译器添加严格的类型检查时,它决定现在需要原型。此外,C++ 继承了 C 的单遍编译,因此无法添加第二遍来解析所有符号。

          【讨论】:

          • 这是正确答案。甚至 C99 也删除了对函数隐式声明的支持,这是有充分理由的。很容易花几个小时想知道为什么程序崩溃只是发现您只是忘记包含头文件,因此编译器会默默地隐式声明具有错误原型的函数。
          • 单程编译?那么它是如何处理类的呢?
          【解决方案9】:

          不过,您有时可以在声明函数之前使用它(严格地说:“之前”是关于读取程序源代码的顺序)——在一个类中!:

          class A {
          public:
            static void foo(void) {
              bar();
            }
          private:
            static void bar(void) {
              return;
            }
          };
          
          int main() {
            A::foo();
            return 0;
          }
          

          (根据我的测试,将类更改为命名空间不起作用。)

          这可能是因为编译器实际上是在类声明之后将成员函数定义放在类内部,正如有人在答案中指出的那样。

          同样的方法可以应用于整个源文件:首先,删除除声明之外的所有内容,然后处理推迟的所有内容。 (两次编译器,或者足够大的内存来保存推迟的源代码。)

          哈哈!因此,他们认为整个源文件太大无法保存在内存中,但具有函数定义的单个类不会:他们可以允许整个类坐在内存中等待声明被过滤掉(或对类的源代码进行第二次传递)!

          【讨论】:

            【解决方案10】:

            即使在 C99 中也强制要求这样做的最大原因之一是(与 C89 相比,您可以有隐式声明的函数)是隐式声明非常容易出错。考虑以下代码:

            第一个文件:

            #include <stdio.h>
            void doSomething(double x, double y)
            {
                printf("%g %g\n",x,y);
            }
            

            第二个文件:

            int main()
            {
                doSomething(12345,67890);
                return 0;
            }
            

            这个程序是一个语法有效的* C89 程序。您可以使用 GCC 使用此命令对其进行编译(假设源文件名为 test.ctest0.c):

            gcc -std=c89 -pedantic-errors test.c test0.c -o test
            

            为什么它会打印一些奇怪的东西(至少在 linux-x86 和 linux-amd64 上)?你能一眼看出代码中的问题吗?现在尝试在命令行中将 c89 替换为 c99 — 编译器会立即通知您您的错误。

            与 C++ 相同。但在 C++ 中,实际上需要函数声明还有其他重要原因,它们在其他答案中进行了讨论。

            * 但有未定义的行为

            【讨论】:

              【解决方案11】:

              我记得在 Unix 和 Linux 中,你有 GlobalLocal。在您自己的环境中,本地适用于函数,但不适用于Global(system)。您必须声明函数Global

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2010-09-15
                • 1970-01-01
                • 1970-01-01
                • 2013-07-06
                • 2011-01-10
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多