什么是C ++中的前向声明?


215

网址http//www.learncpp.com/cpp-tutorial/19-header-files/

提到以下内容:

add.cpp:

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

main.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;
}

我们使用了向前声明,使编译器会知道什么是“ add”是在编译时main.cpp。如前所述,为要在另一个文件中使用的每个函数编写前向声明会很快变得乏味。

您能否进一步解释“ 前瞻性声明 ”?如果我们在main()函数中使用它,那会是什么问题?


1
“向前声明”实际上只是一个声明。见(结束)这样的回答:stackoverflow.com/questions/1410563/...
SBI

Answers:


380

为什么在C ++中必须进行前向声明

编译器希望确保您没有犯拼写错误或没有将错误数量的参数传递给函数。因此,它坚持认为在使用“ add”(或任何其他类型,类或函数)之前,首先要看到一个声明。

这确实使编译器可以更好地验证代码,并整理松散的末端,从而可以生成外观整洁的目标文件。如果您不必转发声明的内容,则编译器将生成一个目标文件,该文件必须包含有关“添加”功能可能是什么的所有可能猜测的信息。链接器必须包含非常聪明的逻辑,才能尝试计算出您实际要调用的“添加”,当“添加”功能可能存在于不同的目标文件中时,链接器将与使用添加来生成的链接在一起dll或exe。链接器可能会添加错误的地址。假设您想使用int add(int a,float b),但无意间忘了编写它,但是链接器发现一个已经存在的int add(int a,诠释b)并认为这是正确的选择,而是改用了。您的代码可以编译,但是不会按预期执行。

因此,只是为了使事情保持明确,避免猜测,编译器坚持要求您在使用之前声明所有内容。

声明和定义之间的区别

顺便说一句,知道声明和定义之间的区别很重要。声明仅提供了足够的代码来显示外观,因此对于函数而言,这是返回类型,调用约定,方法名称,参数及其类型。但是该方法的代码不是必需的。对于定义,您需要声明,然后还需要函数的代码。

前向声明如何显着减少构建时间

您可以通过#includ包含已经包含该函数声明的标头,将函数的声明放入当前的.cpp或.h文件中。但是,这可能会减慢编译速度,尤其是在将#header包含在程序的.h而不是.cpp文件中的情况下,因为所有包含#h的内容都将最终包含在所有头文件中。您也为此写了#includes。突然,即使您只想使用一个或两个函数,编译器也会包含#include页和需要编译的代码页。为避免这种情况,您可以使用前向声明,而您自己可以在文件顶部键入函数的声明。如果只使用一些函数,则与始终包含头文件相比,这确实可以使编译更快。对于大型项目,

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

此外,前向声明可以帮助您打破循环。这是两个函数都试图互相使用的地方。发生这种情况时(这是完全正确的做法),您可以#include一个头文件,但是该头文件试图#include您当前正在编写的头文件....然后#include其他头文件,其中#包括您要编写的内容。您陷入困境,每个头文件都试图重新#include另一个。要解决此问题,您可以在其中一个文件中预先声明所需的零件,并将#include保留在该文件之外。

例如:

文件Car.h

#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.h,其中将包含Car.h,其中将包含Wheel.h,并且此操作将永远持续下去,因此编译器将引发错误。解决的方法是转发声明Car代替:

class Car;     // forward declaration

class Wheel
{
    Car* car;
};

如果Wheel类中的方法需要调用car方法,则可以在Wheel.cpp中定义这些方法,而Wheel.cpp现在可以包含Car.h而不会引起循环。


4
当函数对两个或多个类友好时,也需要进行前向声明
Barun 2014年

1
嗨,斯科特,关于构建时间的观点:您会说总是转发声明并根据需要在.cpp文件中包含标头是一种常见/最佳实践吗?通过阅读您的答案,看来应该是这样,但是我想知道是否有任何警告?
Zepee,2015年

5
@Zepee这是一个平衡。对于快速构建,我会说这是一种很好的做法,建议您尝试一下。但是,如果类型名称等仍在更改,则可能会花费一些精力和额外的代码行,这些代码行可能需要维护和更新(尽管工具在自动重命名方面变得更好)。因此需要权衡。我见过没有人打扰的代码库。如果您发现自己重复了相同的前向定义,则可以始终将它们放入单独的头文件中,并包括以下内容:stackoverflow.com/questions/4300696/what-is-the-iosfwd-header
Scott Langham

在头文件是指相互促进的声明是必需的:即stackoverflow.com/questions/396084/...
尼古拉斯·汉密尔顿

1
我可以看到,这使我团队中的其他开发人员成为代码库的真正坏人。如果您不需要在前声明中添加注释,// From Car.h那么可以创建一些毛茸茸的情况,试图保证在将来找到定义。
Dagrooms

25

编译器会查找当前转换单元中正在使用的每个符号是否已在当前单元中预先声明。在源文件的开头提供所有方法签名,而稍后提供定义只是一种样式问题。它的重要用途是当您使用指向某个类的指针作为另一个类的成员变量时。

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

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

因此,请尽可能在类中使用前向声明。如果您的程序仅具有函数(带有ho头文件),则在开始时提供原型只是样式问题。如果标头文件存在于具有仅具有功能的标头的普通程序中,则无论如何都会遇到这种情况。


12

由于C ++是自上而下进行解析的,因此编译器需要在使用前了解一些事情。因此,当您引用:

int add( int x, int y )

在主函数中,编译器需要知道它的存在。为了证明这一点,请尝试将其移至main函数下,然后会出现编译器错误。

因此,“ 前瞻性声明 ”就是它在罐头上所说的。它在使用前先声明一些东西。

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


12

C ++中的“ 前向声明 ” 一词仅用于类声明。请参阅此答案的(结尾),以了解为什么类的“前向声明”实际上只是一个带有奇特名称的简单类声明

换句话说,“向前”只是给该术语添加了镇流器,因为任何声明只要在使用声明了一些标识符就可以视为向前。

(关于什么是声明而不是定义,再次参见定义和声明之间有什么区别?


2

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

当时,编译器不担心实际实现的add,即它在哪里(或者如果有甚至一个),如果它编译。稍后调用链接程序时编译源文件之后才看到这一点。


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); 

1

一个关于以下内容的快速附录:通常,您将这些前向引用放入属于.c(pp)文件的头文件中,该文件在其中实现了功能/变量等。在您的示例中,它看起来像这样:add.h:

extern int add(int a,int b);

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

#包括 
#include“ add.h”

int main()
{
。
。
。


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

0

一个问题是,编译器不知道函数将传递哪种类型的值。假定int在这种情况下该函数返回一个,但这可以是正确的,也可以是错误的。另一个问题是,如果传递的是错误类型的值,则编译器将不知道函数需要哪种类型的参数,并且无法发出警告。有一些特殊的“促销”规则,当将浮点值传递给未声明的函数(编译器必须将其加宽以键入double)时,这些规则通常适用,而实际上并不是函数所期望的,从而导致难以发现错误在运行时。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.