C++ 中的前向声明
本文将解释前向声明并说明为什么它们对于 C++ 编译器是必需的,并附有代码示例。
这还将讨论使用前向声明的优点,突出声明和定义之间的区别,并展示如何使用前向声明来避免 C++ 文件的循环依赖错误。
C++ 中的前向声明
前向声明是函数语法的声明,即在程序中使用它之前,它的名称、返回类型、参数和参数的数据类型。
在定义函数之前,我们包含前向声明,让编译器知道函数是在程序中的某个地方定义的。在单独文件中使用的函数的前向声明是使用 #include
形成的,以拥有该文件。
C++ 中函数的前向声明
让我们看看前向声明是如何在代码片段中工作的。
#include <iostream>
using namespace std;
// forward declaration of sub2
int sub2(int A, int B);
int main() {
cout << "Difference: " << sub2(25, 10);
return 0;
}
int sub2(int A, int B) // Defining sub2 here
{
return A - B;
}
输出:
Difference: 15
在这里,我们有一个名为 sub2
的函数,它返回传递的两个 int 参数的差值。我们在主要部分之前声明 sub2
,然后在程序后面定义我们的函数。
在开始解释之前,有必要了解 C++ 中定义和声明之间的区别。
声明
:声明提供声明简单信息,例如函数的名称、参数及其数据类型、返回类型,即函数原型。定义
:定义提供函数声明的详细信息,并包括任务函数将执行的代码片段。
现在,让我们回到前向声明。为什么在上述程序中需要 sub2
的前向声明?
让我们用相同的代码来解释,这次不使用前向声明。
#include <iostream>
using namespace std;
int main() {
cout << "Difference: " << sub2(25, 10);
return 0;
}
int sub2(int A, int B) // Defining sub2 here
{
return A - B;
}
输出:
error: 'sub2' was not declared in this scope
6 | cout << "Difference: " << sub2(25, 10);
| ^~~~
上面的程序没有问题,但仍然显示函数 sub2
未声明的错误。这是因为 sub2
在第 6 行被调用,但直到第 10 行才被定义。
由于 C++ 是一种自顶向下的解析语言,它从顶部构造解析树,在使用函数之前需要提前知道函数。你无需在调用之前定义该函数,但你必须声明它。
你还可以在 main 函数之前定义一个函数,此处为 sub2
,以避免此类错误。但是,在具有多个相互调用或外部包含文件的函数的程序中,错误会持续存在,这就是我们使用前向声明的原因。
C++ 中类的前向声明
你还需要 C++ 中的类的前向声明。让我们展示一下。
#include <iostream>
using namespace std;
// Forward declaration of classes One and Two
class One;
class Two;
class One {
int y;
public:
void num(int a) // Getting input number
{
y = a;
}
friend int sub2(One, Two);
};
class Two {
int x;
public:
void num(int a) // Getting input number
{
x = a;
}
friend int sub2(One, Two);
};
int sub2(One a, Two b) // Subtraction of two numbers from both classes
{
int ans = a.y - b.x;
return ans;
}
int main() {
Two y;
One x;
x.num(25);
y.num(10);
cout << "Difference: " << sub2(x, y);
return 0;
}
输出:
Difference: 15
上面的代码片段包含类 One
和 Two
,它们都有一个 num
函数来获取一个值和一个 sub2
函数来减去两个数字。
对于上述程序,两个类的前向声明都是必要的,因为类 One
包含友元函数 sub2
,其参数中提到了类 Two
。
如果我们在上面的代码片段中删除类的前向声明,我们会收到一条错误消息:
15 | [Error] 'Two' has not been declared In function 'int sub2(One, Two)':
该错误表明编译器在程序中使用它们之前需要函数和类的前向声明。
为什么 C++ 编译器需要前向声明
前向声明是必要的,因为它可以帮助编译器确保三件事:
- 程序正确,没有记号拼写错误。
- 声明函数的参数是正确的。
- 声明的函数存在于程序中,定义如下。
如果你没有转发声明函数,编译器将创建一个附加的目标文件,其中包含关于你的函数可能是什么的各种猜测的信息。
并且链接器(将多个对象和类链接到单个可执行对象文件的程序)将存在链接问题,因为它可能具有同名但具有不同数据类型的参数的现有函数。
例如,假设你有一个函数 int sub2(int a, int b)
。如果没有前向声明,链接器可能会与另一个现有函数 int sub2(float a, float b)
混淆。
编译器使用 C++ 前向声明验证干净文件的代码。最好记住 C++ 在某些情况下可能会编译和运行这样的程序。
但是,它不会提供预期的输出。这就是编译器在代码中实现或使用它们之前需要前向声明的原因。
在 C++ 中使用前向声明的优点
前向声明有助于编译器更好地验证你的代码并帮助避免链接问题。但它也有帮助:
- 避免命名空间污染:前向声明有助于确保没有代码片段被放错位置并避免污染命名空间。
- 缩短编译时间:你可以通过包含头文件的方式将函数声明添加到你的 C++ 程序中,编译器会解析文件中提供的所有标记,这可能需要很长时间。但是,你可以避免这种冗长的处理,并为你打算使用的特定类使用前向声明,而不是整个
cpp
文件。
这可能不会影响较小的代码,但在更重要的项目中会派上用场,因为它可以最大限度地减少编译时间,从而降低时间复杂度。因此,你可以使用带有 .h
扩展名的特定类,而不是包含整个 C++ 文件。
- 避免名称冲突:如果存在具有匹配函数或类名的不同项目,前向声明还有助于确保程序中没有令牌或预处理器名称冲突。
- 打破循环依赖:类的前向声明可以解决循环引用问题,方法是在文件中声明所需的特定部分,并将文件头排除在外。
在 C++ 中使用前向声明避免循环依赖
两个相关或使用彼此功能的类创建循环关系。这称为循环或循环依赖。
假设你的程序中有两个类都需要使用另一个。在这种情况下,你将为一个添加一个头文件,但它会进一步尝试包含另一个循环依赖类的头文件,从而创建一个循环,其中每个头都尝试拥有另一个头文件。
让我们看看如何避免循环依赖:
#include <iostream>
#include <vector>
#include "Two.h" // Defining Two as it is used in One
class One {
std::vector<Two> two;
};
int main() { return 0; }
我们的程序使用 #include
包含另一个名为 Two.h
的文件,因为它在类 One
中使用。如上所述,包含一个 class.h
文件而不是整个其他程序会显着减少编译时间。
现在,看看 Two.h
的内容。
#include "One.h" // Defining One as it is used in Two
class Two {
One* a; // Two uses a pointer of One
};
Two.h
包含 Two
类,它使用 One
类的指针。但是由于两个文件都包含包含另一个文件的标头,因此你将陷入循环依赖中,两个文件不断相互调用。这可以通过使用前向声明而不是在 Two.h
中添加标题来避免:
#include <iostream>
class One;
class Two {
One* a; // Two uses a pointer of One
};
最好记住,函数的前向声明需要知道函数的名称和在定义函数时将使用的参数,并且需要复制默认参数的默认值。
因此,在程序中使用前向声明时必须注意。