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

最好記住,函式的前向宣告需要知道函式的名稱和在定義函式時將使用的引數,並且需要複製預設引數的預設值。

因此,在程式中使用前向宣告時必須注意。