網站首頁 編程語言 正文
類的成員函數做友元時,極易產生循環依賴問題,導致程序無法編譯通過。何謂循環依賴,簡單舉個例子,A類的定義需要完整的B類,B類的定義又需要完整的A類,兩者相互依賴,都無法完成定義,這種現象便是循環依賴。在講解循環依賴問題之前,要先說一下類的聲明問題。
類的聲明
就像可以把函數的聲明和定義分離開一樣,我們也可以僅聲明類但暫時不定義它
class A; ????????//這是A類的聲明
這種聲明有時被稱為向前聲明,它向程序中引入了名字A并且指明了A是一種類類型。對于類型A來說,在它聲明之后定義之前,它是一個不完整類型,編譯器僅僅知道A是一個類類型,但是A類到底有哪些成員,到底占用了多少空間是無從得知的。不完整類型也是無法創建其對象的。
一個不完整類型能使用的情形的非常有限的,可以定義指向不完整類型的指針或引用,可以聲明(但不能定義)以不完整類型作為參數或者返回值類型的函數。
類的成員函數做友元以及可能產生的循環依賴問題
情況一:B類的成員函數func是A類的友元,且B類不依賴A類
首先說明,類A聲明類的B的某個成員函數為友元這一行為,已經讓類A依賴于完整的類B。因為,只有當類B定義完成,成為一個完整的類后,編譯器才能知道類B有哪些成員,才知道類B是否真的具有成員函數func。
這種情況并未形成循環依賴,但是但凡要將類的成員函數做友元,我們都必須組織規劃好程序的結構以滿足聲明和定義的彼此依賴關系。我們需按照如下方式設計程序:
1.完成B類的定義,且成員函數func只能聲明,不能在類內定義
2.完成A類的定義,包括成員函數func的友元聲明
3.在類外完成函數func的定義
實際上情況一較少出現,B類的成員函數func已經是A類的友元了,說明函數func有使用A類成員的意圖,但凡想使用A類的成員,就難免要依賴于不完整或是完整的A類。?
示例代碼和說明:
#include#include using namespace std; class manage//定義manage類,完成定義后manage將成為完整的類 { public: //printPerson函數的定義將使用person類對象的成員,其定義依賴于完整的person類,故此處不能定義,只能聲明,否則將產生循環依賴 ostream& printPerson(ostream&)const; }; class person//定義person類 { //聲明manage的成員函數printPerson為友元,需要完整的manage類,即manage類的定義 friend ostream& manage::printPerson(ostream&)const; public: person() = default; person(string name, unsigned int age) :m_name(name), m_age(age) {} private: string m_name; unsigned int m_age = 0; }; //成員函數printPerson的定義需要完整的person類 //實際上這是一個比較雞肋的函數,并沒有什么實際意義,這里更多的只是為了展示情況一下該如何組織程序結構 ostream& manage::printPerson(ostream& os)const { person p("zhenlllz", 21); os << p.m_name << '\t' << p.m_age; return os; } int main() { manage m; m.printPerson(cout) << endl;//結果為 “zhenlllz 21” system("pause"); return 0; }
情況二:類B的成員函數func成員函數是類A的友元,且B類依賴于不完整的A類
這種情況也并未形成循環依賴,同樣的,我們也需要組織規劃好程序的結構。我們需按照如下方式設計程序:
1.對A類進行聲明
2.完成B類的定義,且成員函數func只能聲明,不能在類內定義
3.完成A類的定義,包括成員函數func的友元聲明
4.在類外完成函數func的定義
其實情況一和情況二的總體思路就是優先完成依賴度低的類的定義,再依次完成依賴條件已達成的類或函數的定義。
示例代碼和說明:
#include#include using namespace std; class person;//向前聲明person類,person類現在為不完整的類 class manage//定義manage類 { public: //printPerson函數的聲明至少需要不完整的person類,即person類的聲明 //printPerson函數的定義將使用person類對象的成員,其定義依賴于完整的person類,故此處不能定義,只能聲明,否則將產生循環依賴 ostream& printPerson(ostream&, const person&)const; }; class person//定義person類 { //聲明manage的成員函數printPerson為友元需要完整的manage類,即manage類的定義 friend ostream& manage::printPerson(ostream&, const person&)const; public: person() = default; person(string name, unsigned int age) :m_name(name), m_age(age) {} private: string m_name; unsigned int m_age = 0; }; //成員函數printPerson的定義需要完整的person類 ostream& manage::printPerson(ostream& os, const person& p)const { os << p.m_name << '\t' << p.m_age; return os; } int main() { person p("zhenlllz", 21); manage m; m.printPerson(cout, p) << endl;//結果為 “zhenlllz 21” system("pause"); return 0; }
讓我們再把上面的程序豐富一下,內容更多,原理相同:
#include#include using namespace std; class person;//向前聲明person類,person類現在為不完整的類 class manage//定義manage類 { public: //printPerson函數的聲明至少需要不完整的person類,即person類的聲明 //printPerson函數的定義將使用person類對象的成員,其定義依賴于完整的person類,故此處不能定義,只能聲明,否則將產生循環依賴 ostream& printPerson(ostream&, const person&)const; }; class person//定義person類 { //聲明manage的成員函數printPerson為友元需要完整的manage類,即manage類的定義 friend ostream& manage::printPerson(ostream&, const person&)const; public: person() = default; person(string name, unsigned int age) :m_name(name), m_age(age) {} private: string m_name; unsigned int m_age = 0; }; //成員函數printPerson的定義需要完整的person類 ostream& manage::printPerson(ostream& os, const person& p)const { os << p.m_name << '\t' << p.m_age; return os; } int main() { person p("zhenlllz", 21); manage m; m.printPerson(cout, p) << endl;//結果為 “zhenlllz 21” system("pause"); return 0; }
情況三:類B的成員函數func是類A的友元,且B類依賴于完整的A類
這種情況便形成了循環依賴,只依靠組織規劃程序的結構已經無解,一種較為有效且通用的解決辦法便是添加一個銜接過度的類Help。Help類的引入使得程序結構可以相對自由,規劃程序結構的思路是:
類和非成員函數的聲明不是必須在它們的友元聲明之前。當一個名字第一次出現在一個友元聲明中時,我們隱式地假設該名字在當前作用域中是可見的,所以類做友元和非成員函數做友元沒有太多程序結構上的限制,我們利用這一點,加入一個過度的Help類有效幫助我們化解循環依賴問題。
在B類依賴于完整的A類的前提下,那么B類的定義只能在A類的后面,函數func不再可能聲明為A類的友元,函數func也就無法再使用A類的私有成員。讓Help類幫來搭建函數func和A類的橋梁,將Help類聲明為A類的友元,在Help類中添加函數func的實現手段即一個名為doFunc的靜態函數,再讓B類聲明為Help的友元,Help類可以訪問A類的私有成員,而B類又可以訪問Help類的私有成員,B類間接訪問A類的途徑就形成了。
doFunc定義為靜態函數的原因在于,我們不希望類的使用者知道Help類的存在,更不希望去創建Help類的對象,將doFunc聲明為靜態函數就可以讓我們不創建類的對象,直接通過類去調用靜態成員函數。函數doFunc負責功能的實現,而函數func則是接口,它負責傳遞參數調用doFunc。
推薦通過示例來了解進一步了解,該示例和上一個示例的區別在于,m_v容器給予了類內初始值,使得manage類必須依賴于完整的person類,形成了循環依賴。
#include#include #include using namespace std; class person//person類的定義 { friend class Help; public: person() = default; person(string name, unsigned int age) :m_name(name), m_age(age) {} private: string m_name; unsigned int m_age = 0; }; class Help { friend class manage; using index = vector ::size_type; //manage類的成員函數change的實現 static void doChange(person& p, string name, unsigned int age) { p.m_age = age; p.m_name = name; } //manage類的成員函數printPerson的實現 static ostream& doPrintPerson(const person& p, ostream& os = cout) { os << p.m_name << '\t' << p.m_age; return os; } }; class manage { public: using index = vector ::size_type; void add(const person& p) { m_v.push_back(p); } inline void change(index, string, unsigned int); inline void printPerson(index, ostream & = cout)const; inline void printPerson(ostream & = cout)const; private: vector m_v{ person("默認",0) }; }; void manage::change(index i, string name, unsigned int age) { if (i >= m_v.size()) return; person& p = m_v[i]; Help::doChange(p, name, age); } void manage::printPerson(index i, ostream& os)const { if (i >= m_v.size()) return; const person& p = m_v[i]; Help::doPrintPerson(p, os) << endl; } void manage::printPerson(ostream& os)const { for (auto p : m_v) Help::doPrintPerson(p, os) << endl; } int main() { person p1("一號", 20); person p2("二號", 30); person p3("三號", 40); manage m; m.add(p1); m.add(p2); m.add(p3); m.change(2, "zhenlllz", 21); m.printPerson(2, cout); m.printPerson(); system("pause"); return 0; }
補充
1.內聯函數與循環依賴問題
成員函數是否為內聯函數對定義和聲明的依賴性沒有影響,類內定義的成員函數是隱式內聯的,我們也可以在函數聲明的返回類型前面加上 inline 使得該函數顯示的內聯。將簡單函數聲明為內聯,可以提高程序的運行效率,故示例程序中大部分成員函數都顯示或隱式的定義為了內聯函數。
2.什么情況會需要類的聲明?什么情況又需要類的定義?
簡單來說,當我們只需要知道有這么一個類存在時,有類的聲明即可,比如定義該類的指針或引用,將該類作為函數聲明中的返回類型或者參數;但我們需要知道類的具體內容是什么,類的成員有哪些時,就需要類的定義,比如要定義一個該類的對象。
3.《C++ Primer》一書 “友元再探” 小節的錯誤
我正在學習該書,書本這里的錯誤確實讓我苦惱了蠻久,這也是我寫下篇文章的原因之一。書本案例中的Screen類和Window_mgr類已經形成了循環依賴,而書本卻指導用情況一的方案去解決該問題,顯然是行不通的。
4.沒列舉出來的情況(可以忽略這斷內容)
還有一種更加雞肋的情況我沒有列舉出來,B類的成員函數func是A類的友元,B類不依賴A類,且函數func的定義中也未使用任何A類的成員。這種情況只需滿足B類的定義在A類定義之前,函數func的定義在B類的定義之后或是在類內定義即可,程序的結構是比較自由的。但問題在于,我都把func聲明為A類的友元了,卻不使用A類的成員,缺乏實際意義。
5.分文件編寫時,注意頭文件聲明的順序
示例中并沒有進行分文件編寫,分文件編寫會相對的再麻煩一點,不過只要按方法規劃好程序的組織結構,合理安排頭文件順序,也并不困難。
6.更多細節,要自己敲下代碼才能發覺
寫這篇文章的難度確實超過了我自己的預計,越發思考歸納,發現的細節問題越多,我也無法通過一文將細節問題一一說明。對這一塊困惑的話就自己舉幾個例子簡單練練吧,希望這篇文章對你有幫助。文章若有問題也請指正。
總結
原文鏈接:https://blog.csdn.net/qq_56054422/article/details/121805548
相關推薦
- 2022-08-14 .Net使用日志框架NLog_實用技巧
- 2023-03-27 Android?Framework原理Binder驅動源碼解析_Android
- 2022-07-30 go?redis之redigo的使用_Golang
- 2023-03-23 Pandas時間數據處理詳細教程_python
- 2022-12-22 Python進階之import導入機制原理詳解_python
- 2022-08-21 如何使用C語言將數字、字符等數據寫入、輸出到文本文件中_C 語言
- 2022-09-13 Python使用os模塊實現更高效地讀寫文件_python
- 2022-05-22 C#開發Winform實現窗體間相互傳值_C#教程
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支