在:http://www.learncpp.com/cpp-tutorial/19-header-files/
提到以下内容:
添加.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()
函数中使用它会出现什么问题?
为什么在 C++ 中需要前向声明
编译器希望确保您没有犯拼写错误或将错误数量的参数传递给函数。因此,它坚持在使用它之前首先看到“add”(或任何其他类型、类或函数)的声明。
这实际上只是允许编译器更好地验证代码并允许它整理松散的末端,以便它可以生成一个看起来整洁的目标文件。如果您不必转发声明内容,编译器将生成一个目标文件,该文件必须包含有关函数 add
可能是什么的所有可能猜测的信息。并且链接器必须包含非常聪明的逻辑来尝试找出您实际打算调用的 add
,当 add
函数可能存在于不同的目标文件中时,链接器将与使用 add 生成的目标文件相连接dll
或 exe
。链接器可能会得到错误的 add
。假设您想使用 int add(int a, float b)
,但不小心忘记了编写它,但链接器发现了一个已经存在的 int add(int a, int b)
,并认为这是正确的并使用了它。您的代码会编译,但不会按照您的预期进行。
因此,为了保持明确并避免猜测等,编译器坚持要求您在使用之前声明所有内容。
声明与定义的区别
顺便说一句,了解声明和定义之间的区别很重要。声明只提供了足够的代码来显示某些东西的样子,因此对于函数,这是返回类型、调用约定、方法名称、参数及其类型。但是,不需要该方法的代码。对于定义,您需要声明,然后还需要函数的代码。
前向声明如何显着减少构建时间
通过#includ'ing 已包含函数声明的标头,您可以将函数声明放入当前的 .cpp
或 .h
文件中。但是,这可能会减慢您的编译速度,尤其是如果您将标头 #include
放入程序的 .h
而不是 .cpp
时,因为您编写的所有 #includes .h
最终都会 #include'也为您编写#includes 的所有标题添加。突然间,编译器有#included 页面和需要编译的代码页面,即使您只想使用一两个函数也是如此。为避免这种情况,您可以使用前向声明并在文件顶部自己键入函数的声明。如果您只使用几个函数,与总是#include 标头相比,这确实可以使您的编译更快。对于非常大的项目,差异可能是一个小时或更长时间的编译时间缩短到几分钟。
中断两个定义都相互使用的循环引用
此外,前向声明可以帮助您打破循环。这是两个函数都试图相互使用的地方。发生这种情况时(这是完全有效的做法),您可能会#include
一个头文件,但该头文件会尝试 #include
您当前正在编写的头文件...然后#includes 另一个标头,其中 #include 包含您正在编写的标头。您陷入了先有鸡还是先有蛋的困境,每个头文件都试图重新#include 另一个。要解决此问题,您可以在其中一个文件中前向声明您需要的部分,并将#include 保留在该文件之外。
例如:
文件车.h
#include "Wheel.h" // Include Wheel's definition so it can be used in Car.
#include <vector>
class Car
{
std::vector<Wheel> wheels;
};
文件轮.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
而不会导致循环。
编译器查找当前翻译单元中使用的每个符号是否在当前单元中先前声明过。在源文件的开头提供所有方法签名,而稍后提供定义,这只是风格问题。它的重要用途是当您使用指向类的指针作为另一个类的成员变量时。
//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 头文件),那么在开始时提供原型只是风格问题。如果头文件存在于具有仅具有函数的头的普通程序中,无论如何都会出现这种情况。
因为 C++ 是自上而下解析的,所以编译器在使用它们之前需要了解它们。所以,当你参考:
int add( int x, int y )
在 main 函数中,编译器需要知道它的存在。为了证明这一点,试着把它移到主函数下面,你会得到一个编译器错误。
因此,“前向声明”就是它在锡罐上所说的。它在使用之前声明了一些东西。
通常,您会在头文件中包含前向声明,然后以与包含 iostream 相同的方式包含该头文件。
C++ 中的术语“前向声明”主要仅用于类声明。请参阅(结尾)this answer,了解为什么类的“前向声明”实际上只是一个简单的类声明,并带有一个花哨的名称。
换句话说,“转发”只是为该术语添加了镇流器,因为任何声明都可以被视为转发,只要它在使用之前声明了某个标识符。
(关于什么是声明而不是定义,再次参见What is the difference between a definition and a declaration?)
当编译器看到 add(3, 4)
时,它需要知道这意味着什么。通过前向声明,您基本上可以告诉编译器 add
是一个接受两个 int 并返回一个 int 的函数。这对编译器来说很重要,因为它需要将 4 和 5 以正确的表示形式放入堆栈,并且需要知道 add 返回的东西是什么类型。
那时,编译器并不担心add
的实际 实现,即它在哪里(或者是否有 甚至一个)以及它是否编译。稍后会在调用链接器时编译源文件之后看到这一点。
int add(int x, int y); // forward declaration using function prototype
您能否进一步解释“前向声明”?如果我们在 main() 函数中使用它会出现什么问题?
与 #include"add.h"
相同。如果您知道,预处理器会在您编写 #include
指令的 .cpp 文件中扩展您在 #include
中提到的文件。这意味着,如果你写 #include"add.h"
,你会得到同样的东西,就好像你在做“前向声明”。
我假设 add.h
有这一行:
int add(int x, int y);
关于以下内容的一个快速附录:通常,您将这些前向引用放入属于 .c(pp) 文件的头文件中,其中实现了函数/变量等。在您的示例中,它看起来像这样:add.h:
extern int add(int a, int b);
关键字 extern 表明该函数实际上是在外部文件中声明的(也可以是库等)。你的 main.c 看起来像这样:
#include #include "add.h" int main() { . . .
一个问题是,编译器不知道你的函数传递了哪种值;假设在这种情况下该函数返回一个 int
,但这可能是正确的,也可能是错误的。另一个问题是,编译器不知道您的函数需要哪种类型的参数,并且如果您传递的值类型错误,则无法警告您。有一些特殊的“提升”规则适用于将浮点值传递给未声明的函数(编译器必须将它们扩展为 double 类型),这通常不是函数实际期望的,导致很难找到错误在运行时。
不定期副业成功案例分享
// From Car.h
,那么您可以创建一些麻烦的情况,试图在未来找到一个定义,保证。