C++ 中的前向声明

Dhruvdeep Singh Saini 2023年10月12日
  1. C++ 中的前向声明
  2. C++ 中函数的前向声明
  3. C++ 中类的前向声明
  4. 为什么 C++ 编译器需要前向声明
  5. 在 C++ 中使用前向声明的优点
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++ 中定义和声明之间的区别。

  1. 声明:声明提供声明简单信息,例如函数的名称、参数及其数据类型、返回类型,即函数原型。
  2. 定义:定义提供函数声明的详细信息,并包括任务函数将执行的代码片段。

现在,让我们回到前向声明。为什么在上述程序中需要 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

上面的代码片段包含类 OneTwo,它们都有一个 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++ 中使用前向声明的优点

前向声明有助于编译器更好地验证你的代码并帮助避免链接问题。但它也有帮助:

  1. 避免命名空间污染:前向声明有助于确保没有代码片段被放错位置并避免污染命名空间。
  2. 缩短编译时间:你可以通过包含头文件的方式将函数声明添加到你的 C++ 程序中,编译器会解析文件中提供的所有标记,这可能需要很长时间。但是,你可以避免这种冗长的处理,并为你打算使用的特定类使用前向声明,而不是整个 cpp 文件。

这可能不会影响较小的代码,但在更重要的项目中会派上用场,因为它可以最大限度地减少编译时间,从而降低时间复杂度。因此,你可以使用带有 .h 扩展名的特定类,而不是包含整个 C++ 文件。

  1. 避免名称冲突:如果存在具有匹配函数或类名的不同项目,前向声明还有助于确保程序中没有令牌或预处理器名称冲突。
  2. 打破循环依赖:类的前向声明可以解决循环引用问题,方法是在文件中声明所需的特定部分,并将文件头排除在外。

在 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
};

最好记住,函数的前向声明需要知道函数的名称和在定义函数时将使用的参数,并且需要复制默认参数的默认值。

因此,在程序中使用前向声明时必须注意。