【C++】兩個類的相互引用

Yngz_Miao發表於2021-01-03

有時候在設計資料結構的時候,可能會遇到兩個類需要相互引用的情形。比如類A有型別為B的成員,而類B又有型別為A的成員。

那麼這種情形下,兩個類的設計上需要注意什麼呢?

本文例項原始碼github地址https://github.com/yngzMiao/yngzmiao-blogs/tree/master/2021Q1/20210103


同一檔案

嘗試方案

將A和B的定義都放在一個檔案中,例如:

#include <iostream>

class A {
  public:
    A() {
      aa_ = 'A';
    }
    char aa_;
    B b_;
};

class B {
  public:
    B() {
      bb_ = 'B';
    }
    char bb_;
    A a_;
};

int main() {

  return 0;
}

編譯這一段程式碼,編譯出錯:

yngzmiao@yngzmiao-virtual-machine:~/test$ g++ main.cpp -o main --std=c++11
main.cpp:9:5: error: ‘B’ does not name a type
     B b_;
     ^

編譯的報錯提示,B不是一個資料型別。可能你會想,會不會前置宣告一下就可以了?即將程式碼修改為:

#include <iostream>

class B;

class A {
  public:
    A() {
      aa_ = 'A';
    }
    char aa_;
    B b_;
};

class B {
  public:
    B() {
      bb_ = 'B';
    }
    char bb_;
    A a_;
};

int main() {

  return 0;
}

編譯這一段程式碼,編譯仍然出錯:

yngzmiao@yngzmiao-virtual-machine:~/test$ g++ main.cpp -o main --std=c++11
main.cpp:11:7: error: field ‘b_’ has incomplete type
     B b_;
       ^

編譯時出現"field has incomplete type",通常的錯誤原因為:類或結構體的前向宣告只能用來定義指標物件或引用,因為編譯到這裡時還沒有發現定義,不知道該類或者結構的內部成員,沒有辦法具體的構造一個物件,所以會報錯。

解決辦法:將類成員改成指標就好了。程式中使用incomplete type實現前置宣告,有助與實現資料型別細節的隱藏。

按照這個辦法來進行修改,將b_的型別由B的物件修改成指向型別B的指標:

#include <iostream>

class B;

class A {
  public:
    A() {
      aa_ = 'A';
    }
    char aa_;
    B *b_;
};

class B {
  public:
    B() {
      bb_ = 'B';
    }
    char bb_;
    A a_;
};

int main() {
  A tmp1;
  std::cout << tmp1.aa_ << " " << tmp1.b_->bb_ << std::endl;
  B tmp2;
  std::cout << tmp2.bb_ << " " << tmp2.a_.aa_ << std::endl;

  return 0;
}

編譯這一段程式碼,編譯順利,沒有問題。執行這段程式碼:

yngzmiao@yngzmiao-virtual-machine:~/test$ g++ main.cpp -o main --std=c++11
yngzmiao@yngzmiao-virtual-machine:~/test$ ./main 
A H
B A

又有問題了,tmp1.b_->bb_的列印結果為H。這個問題很容易檢查出問題:在類A的定義中,定義了指向型別B的指標b_,但是並沒有對該指標分配記憶體空間,當然會有一些奇怪的值列印出來。可以修改為:

#include <iostream>

class B;

class A {
  public:
    A() {
      aa_ = 'A';
      b_ = new B();
    }
    char aa_;
    B *b_;
};

class B {
  public:
    B() {
      bb_ = 'B';
    }
    char bb_;
    A a_;
};

int main() {
  A tmp1;
  std::cout << tmp1.aa_ << " " << tmp1.b_->bb_ << std::endl;
  B tmp2;
  std::cout << tmp2.bb_ << " " << tmp2.a_.aa_ << std::endl;

  return 0;
}

編譯又出現問題了:

yngzmiao@yngzmiao-virtual-machine:~/test$ g++ main.cpp -o main --std=c++11
main.cpp: In constructor ‘A::A()’:
main.cpp:9:18: error: invalid use of incomplete type ‘class B’
       b_ = new B();
                  ^
main.cpp:3:7: error: forward declaration of ‘class B’
 class B;
       ^

編譯錯誤的原因還是incomplete type,即型別B的結構還不知道,怎麼能new出來呢?

最終程式碼

如果想要獲得正確的程式碼,不能將new的操作放在建構函式中,放在其他地方手動建立即可:

#include <iostream>

class B;

class A {
  public:
    A() {
      aa_ = 'A';
    }
    char aa_;
    B *b_;
};

class B {
  public:
    B() {
      bb_ = 'B';
    }
    char bb_;
    A a_;
};

int main() {
  A tmp1;
  tmp1.b_ = new B();
  std::cout << tmp1.aa_ << " " << tmp1.b_->bb_ << std::endl;
  B tmp2;
  std::cout << tmp2.bb_ << " " << tmp2.a_.aa_ << std::endl;

  return 0;
}

編譯並執行這段程式碼:

yngzmiao@yngzmiao-virtual-machine:~/test$ g++ main.cpp -o main --std=c++11
yngzmiao@yngzmiao-virtual-machine:~/test$ ./main 
A B
B A

原因分析

類A和類B相互引用比較麻煩的根本原因在於:定義A的時候,A的裡面有B,所以就需要去檢視B的佔空間大小,但是檢視的時候又發現需要知道A的佔空間大小,從而造成死迴圈


不同檔案

不同檔案指的是:型別A定義在A.h檔案,型別B定義在B.h檔案,同時在main.cpp中建立A、B型別的物件進行輸出。通過上文的一些經驗,可能還需要create()函式來對B指標進行new操作。

由於不同檔案的寫法,坑比較多,接下來就直接給出正確的程式碼內容。

在A.h檔案中,定義類A:

#ifndef A_H
#define A_H

class B;                // highlight 1

class A {
  public:
    A() {
      aa_ = 'A';
    }
    void create();
    char aa_;
    B *b_;
};

#endif

在A.cpp檔案中,定義類A方法的實現:

#include "B.h"        // highlight 2

void A::create() {
 b_ = new B();
}

在B.h檔案中,定義類B:

#ifndef B_H
#define B_H

#include "A.h"        // highlight 3

class B {
  public:
    B() {
      bb_ = 'B';
    }
    char bb_;
    A a_;
};

#endif

最終在main.cpp檔案中,使用類A和類B:

#include "B.h"
#include <iostream>

int main() {
  A tmp1;
  tmp1.create();
  std::cout << tmp1.aa_ << " " << tmp1.b_->bb_ << std::endl;
  B tmp2;
  std::cout << tmp2.bb_ << " " << tmp2.a_.aa_ << std::endl;

  return 0;
}

對這一段程式碼中的坑進行羅列:

  1. A.h中包含B.h且B.h中包含A.h,標頭檔案不能迴圈include。需要在定義非指標類的那個.h檔案include另一個;而定義指標類的那個.h檔案需要使用前置宣告
  2. create()函式不能在.h檔案中進行定義,因為在該函式中需要進行new操作,而該操作需要又另一個類的完整定義,即需要include。由於第一點原因,只好在.cpp檔案中進行方法的實現

總結

兩個類相互引用,一個用物件、include;另一個用指標、前置宣告、create手動new。手動new的過程不能在建構函式中進行,同時需要知道另一個類的完整定義(include)

注意:本文所舉例的部分都沒有對new出來的空間進行delete操作,會引發記憶體洩漏。這部分需要讀者自行補充。


相關閱讀

【C++】兩個類的相互引用

相關文章