類別互相使用

類別互相使用 (mutual reference, circular relationship) 是一個在 C/C++ 程式裡常常遇到的狀況

範例一

--- A.h ---

class A
{
public:
   int fun(B& b);
private:
    B* ptrB;
};

--- B.h ---

class B
{
public:
   int fun();
private:
   A a;
};

或是 範例二

--- A.h ---

class A
{
public:
   int fun(B& b);
private:
    B* ptrB;
};

--- B.h ---

class B
{
public:
   int fun();
private:
   A* a;
};

或是 範例三

--- A.h ---

class A
{
public:
   int fun(B b);
private:
    B* ptrB;
};

--- B.h ---

class B
{
public:
   int fun();
private:
   A a;
};

注意下面這種狀況不可能發生

--- A.h ---

class A
{
public:
   int fun();
private:
    B b;
};

--- B.h ---

class B
{
public:
   int fun();
private:
   A a;
};

該如何使用 #include 呢以範例一來說, 直覺的作法當然就
A.h 裡面 #include "B.h", 然後
B.h 裡面 #include "A.h",
然後編譯器就不管怎樣都給你錯誤訊息了!!!

--- A.h ---
#pragma once
#include "B.h"
class A
{
public:
   int fun(B& b);
private:
    B* ptrB;
};

--- B.h ---
#pragma once
#include "A.h"
class B
{
public:
   int fun();
private:
   A a;
};

在你知道怎麼解決之前, 先弄清楚到底發生了什麼事??? 某一個單一狀況的解決方案並不重要, 重要的是為什麼, 以後發生類似的狀況你才能夠解決

原因

C/C++ 的編譯器在編譯每一個 .c/.cpp 的檔案時, 都只由第一列順序看到最後一列, 看一遍 (one pass) 而已, 所以所有程式裡定義的變數, 型態, 函式 名稱在使用之前一定要先宣告(declaration)或事先定義(definition) 過, 編譯器在看到使用的敘述時才知道如何檢查用法是否正確, 如何幫你轉換資料的型態

先宣告/定義, 再使用

以範例一為例, 讓我們看看上面互相 #include 的解決方法會造成什麼錯誤: C/C++ 的程式編譯的時候是用 .c/.cpp 檔案為單位的, 所以我們先看 A.cpp

--- A.cpp ---
#include "A.h"

int A::fun(B& b)
{
    ptrB->fun();
    b.fun();
}

前處理器會把 A.h 引入, 得到

--- A.cpp ---
#include "B.h"
class A
{
public:
   int fun(B& b);
private:
    B* ptrB;
};

int A::fun(B& b)
{
    ptrB->fun();
    b.fun();
}

然後前處理器再把 B.h 引入, 得到

--- A.cpp ---
#include "A.h"
class B
{
public:
   int fun();
private:
   A a;  // vc2010 error C2146: 語法錯誤 : 遺漏 ';' (在識別項 'a' 之前)
// error C4430: 遺漏型別規範 - 假設為 int。注意: C++ 不支援 default-int
}; class A { public: int fun(B& b); private: B* ptrB; }; int A::fun(B& b) { ptrB->fun(); b.fun(); }

這個時候前處理器看到 #include "A.h", 但是因為 #pragma once 的關係, 不再繼續引入 A.h 了, 你也看到類別 B 會在類別 A 之前定義, 所以在編譯器看到類別 B 的定義裡面的 A a; 這個敘述的時候就不行了....
發現 A 沒有定義過

編譯器編譯 B.cpp 時會看到

--- B.cpp ---
#include "B.h"

int B::fun()
{
    a.fun();
}

前處理器會把 B.h 引入, 得到

--- B.cpp ---
#include "A.h"
class B
{
public:
   int fun();
private:
   A a;
};

int B::fun()
{
    a.fun();
}

前然前處理器後再把 A.h 引入, 得到

--- B.cpp ---
#include "B.h"
class A
{
public:
   int fun(B& b); // error C2061: 語法錯誤 : 識別項 'B'
private: B* ptrB; // error C2143: 語法錯誤 : 遺漏 ';' (在 '*' 之前)
// error C4430: 遺漏型別規範 - 假設為 int。注意: C++ 不支援 default-int }; class B { public: int fun(); private: A a; }; int B::fun() { a.fun(); }

這個時候前處理器看到 #include "B.h", 但是因為 #pragma once 的關係, 不再繼續引入 B.h 了, 你看到類別 A 會在類別 B 之前定義, 所以在編譯器看到類別 A 的定義裡面的 B &b; 這個敘述的時候就不行了....
發現 B 沒有定義過

所以上面這兩種狀況都會導致編譯錯誤

該怎麼解決??

範例一和範例二是可以很快解決的, 範例三的話需要把它轉成範例一才可以, 下面的藍字部份是範例一的修改方法

--- A.h ---
class B; // forward declaration 前向宣告, 跟編譯器說 B 是個類別
class A
{
public:
   int fun(B& b);
private:
    B* ptrB;
};

--- A.cpp ---
#include "A.h"
#include "B.h" // 需要引入完整的類別 B 的定義, 處理接下來 ptrB->fun() 敘述時,
              // 編譯器才知道 fun() 是 B 的成員函式
int A::fun(B& b)
{
    ptrB->fun();
    b.fun();
}

--- B.h ---
#include "A.h"
class B
{
public:
   int fun();
private:
   A a;
};

--- B.cpp ---
#include "B.h"

int B::fun()
{
    a.fun();
}

再一次檢查 A.cpp 和 B.cpp 分別的編譯

是不是所有需要使用到的識別字都符合「先定義再使用」的原則

先看 A.cpp

--- A.cpp ---
#include "A.h"
#include "B.h"

int A::fun(B& b)
{
    ptrB->fun();
    b.fun();
}

前處理器會把 A.h 引入, 得到

--- A.cpp ---
class B;
class A
{
public:
   int fun(B& b);
private:
    B* ptrB;
};

#include "B.h"

int A::fun(B& b)
{
    ptrB->fun();
    b.fun();
}

然後前處理器再把 B.h 引入, 得到

--- A.cpp ---
class B;

class A
{
public:
   int fun(B& b);
private:
    B* ptrB;
};

class B
{
public:
   int fun();
private:
   A a;
}; int A::fun(B& b) { ptrB->fun(); b.fun(); }

這個時候編譯器可以完全正確運作, 用到 B&, B* 前編譯器先知道 B 是一個類別, 用到 A a; 之前需要有完整的 A 的定義

編譯器編譯 B.cpp 時會看到

--- B.cpp ---
#include "B.h"

int B::fun()
{
    a.fun();
}

前處理器會把 B.h 引入, 得到

--- B.cpp ---
#include "A.h"
class B
{
public:
   int fun();
private:
   A a;
};

int B::fun()
{
    a.fun();
}

前然前處理器後再把 A.h 引入, 得到

--- B.cpp ---
class B;

class A
{
public:
   int fun(B& b);
private:
    B* ptrB;
};

class B
{
public:
   int fun();
private:
   A a;
};

int B::fun()
{
    a.fun();
}

這個時候編譯器可以完全正確運作, 用到 B&, B* 前編譯器先知道 B 是一個類別, 用到 A a; 之前需要有完整的類別 A 的定義

C++ 物件導向程式設計課程 首頁

製作日期: 04/21/2016 by 丁培毅 (Pei-yih Ting)
E-mail: pyting@mail.ntou.edu.tw TEL: 02 24622192x6615
海洋大學 電機資訊學院 資訊工程學系 Lagoon