在C++中,什么是前置声明?

295

此链接中提到:

add.cpp:

int add(int x, int y)
{
    return x + y;
}

主函数.cpp:

#include <iostream>
 
int add(int x, int y); // forward declaration using function prototype
 
int main()
{
    using namespace std;
    cout << "The sum of 3 and 4 is " << add(3, 4) << endl;
    return 0;
}

我们使用前向声明,以便编译器在编译main.cpp时知道"add"是什么。如先前所述,为想要使用的每个位于另一个文件中的函数编写前向声明可能会很快变得乏味。

请解释一下"前向声明",如果在main函数中使用它会有什么问题吗?


2
“前向声明”实际上只是一个声明。请参见(本答案的结尾):https://dev59.com/V3M_5IYBdhLWcg3wXx1N#1410632 - sbi
8个回答

485

为什么在C++中需要前置声明

编译器希望确保您没有拼写错误或向函数传递了错误数量的参数。因此,它坚持要先看到“add”(或任何其他类型、类或函数)的声明,然后才能使用。

这实际上只是使编译器更好地验证代码并允许其清理松散的端点,以便它可以生成一个整洁的目标文件。如果不必预先声明某些内容,则编译器将生成一个对象文件,该文件必须包含有关可能是函数“add”的所有可能猜测的信息。链接器必须包含非常聪明的逻辑来尝试确定您实际想要调用哪个“add”,当“add”函数可能存在于连接器正在与使用add的对象文件结合以生成“dll”或“exe”的对象文件不同时。链接器可能会选择错误的“add”。假设您想使用“int add(int a, float b)”,但是不小心忘记了写,但链接器找到了已经存在的“int add(int a, int b)”,并认为那是正确的并代替使用了那个。您的代码将编译,但不会做您期望的事情。

因此,为了保持明确并避免猜测等情况,编译器坚持在使用之前声明所有内容。

声明和定义之间的区别

顺便说一下,了解声明和定义之间的区别很重要。声明只提供足够的代码以显示某些内容的外观,因此对于函数而言,这是返回类型、调用约定、方法名称、参数及其类型。但是不需要方法的代码。对于定义,您需要声明,然后还需要函数的代码。

如何使用前置声明可以显著缩短构建时间

您可以通过“#include”包含已经包含函数声明的头文件将函数的声明放入当前的“.cpp”或“.h”文件中。但是,这可能会减慢编译速度,特别是如果您将头文件“#include”到程序的“.h”文件而不是“.cpp”中,因为“#include”了您写的所有头文件的所有内容都会被“#include”。突然之间,编译器需要编译数页的代码,即使您只想使用一两个函数。为避免这种情况,您可以使用前置声明,在文件顶部仅键入该函数的声明。如果您只使用几个函数,则与始终将头文件“#include”相比,这确实可以加快编译时间。对于非常大的项目,差异可能会从一个小时或更长的编译时间缩短到几分钟。

打破两个定义都互相使用的循环引用

此外,前置声明可以帮助您打破循环。这是两个函数都尝试使用彼此的情况。当发生这种情况(这是完全有效的事情)时,您可能会“#include”一个头文件,但该头文件会尝试“#include”您当前正在编写的头文件......然后“#include”另一个头文件,该头文件“#include”您正在编写的另一个头文件。您陷入了一个鸡和蛋的局面,每个头文件都试图重新“#include”另一个头文件。为解决此问题,您可以在其中一个文件中前置声明所需的部分,并留下该文件的“#include”。#include "Wheel.h" // Include Wheel's definition so it can be used in Car. #include <vector> class Car { std::vector<Wheel> wheels; };

文件 Wheel.h

嗯...需要在此处声明 Car,因为 Wheel 有一个指向 Car 的指针,但是不能在这里包含 Car.h,否则会导致编译器错误。如果包含了 Car.h,那么就会尝试包含 Wheel.hWheel.h 又会包含 Car.h,这样就会无限循环,所以编译器会报错。解决方法是改为前置声明 Car

class Car;     // forward declaration

class Wheel
{
    Car* car;
};

如果Wheel类中有需要调用Car类方法的方法,那么这些方法可以在Wheel.cpp文件中定义,这样Wheel.cpp就可以包含Car.h而不会导致循环依赖。


9
当一个函数同时与两个或多个类有友元关系时,前向声明也是必要的。 - Barun
5
嗨,斯科特,关于你提到的构建时间问题:你认为在 .cpp 文件中总是根据需要前向声明和包含头文件是一种常见/最佳实践吗?从你的回答中看来似乎应该是这样,但我想知道是否有任何注意事项? - Zepee
9
@Zepee 这是一个平衡考量。对于快速构建,我认为这是一种好的实践方法,建议尝试一下。但是,这可能需要一些额外的努力和代码行数,如果类型名称等仍在更改,则需要维护和更新(尽管工具正在自动重命名东西方面变得越来越好)。因此存在权衡取舍。我见过一些代码库,没有人费心去做这个。如果你发现自己在重复相同的前向声明,你可以将它们放入单独的头文件中并进行包含,类似于:https://dev59.com/yG855IYBdhLWcg3wg0nC - Scott Langham
当头文件相互引用时,需要使用前向声明。例如:https://dev59.com/hnRC5IYBdhLWcg3wJNmf - Nicholas Hamilton
4
我可以看出这可能会让我团队上的其他开发者成为代码库中真正糟糕的使用者。如果你不要求在前向声明时加注释,比如 // From Car.h,那么你将来可能会遇到一些棘手的情况,保证会费力地寻找定义。 - Dagrooms
"Wheel.cpp现在可以包含Car.h而不会导致循环依赖。即使没有方法,.cpp也可以包含Car.h而不会出现循环依赖。Car.h并没有包含.cpp文件。" - Puddle

30

编译器会查看当前翻译单元中使用的每个符号是否已在当前单元中声明。在源文件的开头提供所有方法签名,而稍后提供定义只是一种风格问题。当你将一个类的指针用作另一个类的成员变量时,这种方式的使用非常重要。

//foo.h
class bar;    // This is useful
class foo
{
    bar* obj; // Pointer or even a reference.
};

// foo.cpp
#include "bar.h"
#include "foo.h"

因此,在类中尽可能使用前置声明。如果您的程序只有函数(没有头文件),则在开头提供原型只是风格问题。如果带有仅包含函数的头文件的标准程序存在,则无论如何都是这种情况。


15

因为 C++ 是自上而下解析的,编译器需要在使用之前了解有关事物的信息。因此,当你引用:

int add( int x, int y )

在主函数中,编译器需要知道它的存在。尝试将其移动到主函数下方,你会遇到编译错误。

因此,'Forward Declaration'就是字面意思,事先声明某些东西以备使用。

通常,您会将前向声明包含在头文件中,然后以与包含iostream相同的方式包含该头文件。


14
在C++中,“forward declaration”这个术语主要用于类声明。请参见此答案的(结尾部分)解释,为什么“前向声明”只是一个有趣的名称而已,实际上就是一个简单的类声明。
换句话说,“forward”这个词只是给术语增加了一些含义,因为任何声明都可以被视为前向声明,因为它在使用之前先声明了某个标识符。
关于声明定义的区别,请参见什么是定义和声明之间的区别?

3
当编译器看到add(3, 4)时,它需要知道这意味着什么。使用前向声明,您基本上告诉编译器add是一个接受两个int并返回一个int的函数。对于编译器来说,这是重要信息,因为它需要将4和5放在正确的表示形式上,并且需要知道add返回的类型。

此时,编译器不用担心add的实际实现,即它在哪里(或者是否存在)以及是否编译。这将在编译源文件后调用链接器时出现。

2

关于这个问题的一个小补充:通常情况下,你需要将那些前置引用放入一个头文件中,该头文件属于实现函数/变量等的.c(pp)文件。在你的例子中,它看起来像这样:

extern int add(int a, int b);

关键字extern表示该函数实际上是在外部文件(也可以是库等)中声明的。 你的main.c应该是这样的:

#include 
#include "add.h"
int main() { . . .

但是,我们不是只把声明放在头文件中吗?我认为这就是为什么函数在“add.cpp”中定义,并因此使用前向声明的原因?谢谢。 - Simplicity

1
int add(int x, int y); // forward declaration using function prototype

你能进一步解释一下“前向声明”吗?如果我们在main()函数中使用它会有什么问题吗?
这与#include"add.h"相同。如果你知道,预处理器会扩展你在#include指令中提到的文件,在你写#include指令的.cpp文件中。这意味着,如果你写了#include"add.h",你得到的是同样的东西,就好像你在做“前向声明”。
我假设add.h有这一行:
int add(int x, int y); 

0
一个问题是编译器不知道你的函数返回什么类型的值;在这种情况下,它假设函数返回一个int,但这可能正确也可能错误。另一个问题是编译器不知道你的函数需要哪种类型的参数,并且无法警告你是否传递了错误类型的值。当将浮点数值传递给未声明的函数时,会应用特殊的“提升”规则(编译器必须将它们扩展为double类型),这通常不是函数实际期望的结果,导致难以找到的运行时错误。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接