詳解C++類的成員函數(shù)做友元產(chǎn)生的循環(huán)依賴問題
類的成員函數(shù)做友元時(shí),極易產(chǎn)生循環(huán)依賴問題,導(dǎo)致程序無法編譯通過。何謂循環(huán)依賴,簡單舉個(gè)例子,A類的定義需要完整的B類,B類的定義又需要完整的A類,兩者相互依賴,都無法完成定義,這種現(xiàn)象便是循環(huán)依賴。在講解循環(huán)依賴問題之前,要先說一下類的聲明問題。
類的聲明
就像可以把函數(shù)的聲明和定義分離開一樣,我們也可以僅聲明類但暫時(shí)不定義它
class A; //這是A類的聲明
這種聲明有時(shí)被稱為向前聲明,它向程序中引入了名字A并且指明了A是一種類類型。對(duì)于類型A來說,在它聲明之后定義之前,它是一個(gè)不完整類型,編譯器僅僅知道A是一個(gè)類類型,但是A類到底有哪些成員,到底占用了多少空間是無從得知的。不完整類型也是無法創(chuàng)建其對(duì)象的。
一個(gè)不完整類型能使用的情形的非常有限的,可以定義指向不完整類型的指針或引用,可以聲明(但不能定義)以不完整類型作為參數(shù)或者返回值類型的函數(shù)。
類的成員函數(shù)做友元以及可能產(chǎn)生的循環(huán)依賴問題
情況一:B類的成員函數(shù)func是A類的友元,且B類不依賴A類
首先說明,類A聲明類的B的某個(gè)成員函數(shù)為友元這一行為,已經(jīng)讓類A依賴于完整的類B。因?yàn)?,只有?dāng)類B定義完成,成為一個(gè)完整的類后,編譯器才能知道類B有哪些成員,才知道類B是否真的具有成員函數(shù)func。
這種情況并未形成循環(huán)依賴,但是但凡要將類的成員函數(shù)做友元,我們都必須組織規(guī)劃好程序的結(jié)構(gòu)以滿足聲明和定義的彼此依賴關(guān)系。我們需按照如下方式設(shè)計(jì)程序:
1.完成B類的定義,且成員函數(shù)func只能聲明,不能在類內(nèi)定義
2.完成A類的定義,包括成員函數(shù)func的友元聲明
3.在類外完成函數(shù)func的定義
實(shí)際上情況一較少出現(xiàn),B類的成員函數(shù)func已經(jīng)是A類的友元了,說明函數(shù)func有使用A類成員的意圖,但凡想使用A類的成員,就難免要依賴于不完整或是完整的A類。
示例代碼和說明:
#include<iostream> #include<string> using namespace std; class manage//定義manage類,完成定義后manage將成為完整的類 { public: //printPerson函數(shù)的定義將使用person類對(duì)象的成員,其定義依賴于完整的person類,故此處不能定義,只能聲明,否則將產(chǎn)生循環(huán)依賴 ostream& printPerson(ostream&)const; }; class person//定義person類 { //聲明manage的成員函數(shù)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; }; //成員函數(shù)printPerson的定義需要完整的person類 //實(shí)際上這是一個(gè)比較雞肋的函數(shù),并沒有什么實(shí)際意義,這里更多的只是為了展示情況一下該如何組織程序結(jié)構(gòu) 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;//結(jié)果為 “zhenlllz 21” system("pause"); return 0; }
情況二:類B的成員函數(shù)func成員函數(shù)是類A的友元,且B類依賴于不完整的A類
這種情況也并未形成循環(huán)依賴,同樣的,我們也需要組織規(guī)劃好程序的結(jié)構(gòu)。我們需按照如下方式設(shè)計(jì)程序:
1.對(duì)A類進(jìn)行聲明
2.完成B類的定義,且成員函數(shù)func只能聲明,不能在類內(nèi)定義
3.完成A類的定義,包括成員函數(shù)func的友元聲明
4.在類外完成函數(shù)func的定義
其實(shí)情況一和情況二的總體思路就是優(yōu)先完成依賴度低的類的定義,再依次完成依賴條件已達(dá)成的類或函數(shù)的定義。
示例代碼和說明:
#include<iostream> #include<string> using namespace std; class person;//向前聲明person類,person類現(xiàn)在為不完整的類 class manage//定義manage類 { public: //printPerson函數(shù)的聲明至少需要不完整的person類,即person類的聲明 //printPerson函數(shù)的定義將使用person類對(duì)象的成員,其定義依賴于完整的person類,故此處不能定義,只能聲明,否則將產(chǎn)生循環(huán)依賴 ostream& printPerson(ostream&, const person&)const; }; class person//定義person類 { //聲明manage的成員函數(shù)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; }; //成員函數(shù)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;//結(jié)果為 “zhenlllz 21” system("pause"); return 0; }
讓我們?cè)侔焉厦娴某绦蜇S富一下,內(nèi)容更多,原理相同:
#include<iostream> #include<string> using namespace std; class person;//向前聲明person類,person類現(xiàn)在為不完整的類 class manage//定義manage類 { public: //printPerson函數(shù)的聲明至少需要不完整的person類,即person類的聲明 //printPerson函數(shù)的定義將使用person類對(duì)象的成員,其定義依賴于完整的person類,故此處不能定義,只能聲明,否則將產(chǎn)生循環(huán)依賴 ostream& printPerson(ostream&, const person&)const; }; class person//定義person類 { //聲明manage的成員函數(shù)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; }; //成員函數(shù)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;//結(jié)果為 “zhenlllz 21” system("pause"); return 0; }
情況三:類B的成員函數(shù)func是類A的友元,且B類依賴于完整的A類
這種情況便形成了循環(huán)依賴,只依靠組織規(guī)劃程序的結(jié)構(gòu)已經(jīng)無解,一種較為有效且通用的解決辦法便是添加一個(gè)銜接過度的類Help。Help類的引入使得程序結(jié)構(gòu)可以相對(duì)自由,規(guī)劃程序結(jié)構(gòu)的思路是:
類和非成員函數(shù)的聲明不是必須在它們的友元聲明之前。當(dāng)一個(gè)名字第一次出現(xiàn)在一個(gè)友元聲明中時(shí),我們隱式地假設(shè)該名字在當(dāng)前作用域中是可見的,所以類做友元和非成員函數(shù)做友元沒有太多程序結(jié)構(gòu)上的限制,我們利用這一點(diǎn),加入一個(gè)過度的Help類有效幫助我們化解循環(huán)依賴問題。
在B類依賴于完整的A類的前提下,那么B類的定義只能在A類的后面,函數(shù)func不再可能聲明為A類的友元,函數(shù)func也就無法再使用A類的私有成員。讓Help類幫來搭建函數(shù)func和A類的橋梁,將Help類聲明為A類的友元,在Help類中添加函數(shù)func的實(shí)現(xiàn)手段即一個(gè)名為doFunc的靜態(tài)函數(shù),再讓B類聲明為Help的友元,Help類可以訪問A類的私有成員,而B類又可以訪問Help類的私有成員,B類間接訪問A類的途徑就形成了。
doFunc定義為靜態(tài)函數(shù)的原因在于,我們不希望類的使用者知道Help類的存在,更不希望去創(chuàng)建Help類的對(duì)象,將doFunc聲明為靜態(tài)函數(shù)就可以讓我們不創(chuàng)建類的對(duì)象,直接通過類去調(diào)用靜態(tài)成員函數(shù)。函數(shù)doFunc負(fù)責(zé)功能的實(shí)現(xiàn),而函數(shù)func則是接口,它負(fù)責(zé)傳遞參數(shù)調(diào)用doFunc。
推薦通過示例來了解進(jìn)一步了解,該示例和上一個(gè)示例的區(qū)別在于,m_v容器給予了類內(nèi)初始值,使得manage類必須依賴于完整的person類,形成了循環(huán)依賴。
#include<iostream> #include<vector> #include<string> 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<person>::size_type; //manage類的成員函數(shù)change的實(shí)現(xiàn) static void doChange(person& p, string name, unsigned int age) { p.m_age = age; p.m_name = name; } //manage類的成員函數(shù)printPerson的實(shí)現(xiàn) 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<person>::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<person> m_v{ person("默認(rèn)",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("一號(hào)", 20); person p2("二號(hào)", 30); person p3("三號(hào)", 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; }
補(bǔ)充
1.內(nèi)聯(lián)函數(shù)與循環(huán)依賴問題
成員函數(shù)是否為內(nèi)聯(lián)函數(shù)對(duì)定義和聲明的依賴性沒有影響,類內(nèi)定義的成員函數(shù)是隱式內(nèi)聯(lián)的,我們也可以在函數(shù)聲明的返回類型前面加上 inline 使得該函數(shù)顯示的內(nèi)聯(lián)。將簡單函數(shù)聲明為內(nèi)聯(lián),可以提高程序的運(yùn)行效率,故示例程序中大部分成員函數(shù)都顯示或隱式的定義為了內(nèi)聯(lián)函數(shù)。
2.什么情況會(huì)需要類的聲明?什么情況又需要類的定義?
簡單來說,當(dāng)我們只需要知道有這么一個(gè)類存在時(shí),有類的聲明即可,比如定義該類的指針或引用,將該類作為函數(shù)聲明中的返回類型或者參數(shù);但我們需要知道類的具體內(nèi)容是什么,類的成員有哪些時(shí),就需要類的定義,比如要定義一個(gè)該類的對(duì)象。
3.《C++ Primer》一書 “友元再探” 小節(jié)的錯(cuò)誤
我正在學(xué)習(xí)該書,書本這里的錯(cuò)誤確實(shí)讓我苦惱了蠻久,這也是我寫下篇文章的原因之一。書本案例中的Screen類和Window_mgr類已經(jīng)形成了循環(huán)依賴,而書本卻指導(dǎo)用情況一的方案去解決該問題,顯然是行不通的。
4.沒列舉出來的情況(可以忽略這斷內(nèi)容)
還有一種更加雞肋的情況我沒有列舉出來,B類的成員函數(shù)func是A類的友元,B類不依賴A類,且函數(shù)func的定義中也未使用任何A類的成員。這種情況只需滿足B類的定義在A類定義之前,函數(shù)func的定義在B類的定義之后或是在類內(nèi)定義即可,程序的結(jié)構(gòu)是比較自由的。但問題在于,我都把func聲明為A類的友元了,卻不使用A類的成員,缺乏實(shí)際意義。
5.分文件編寫時(shí),注意頭文件聲明的順序
示例中并沒有進(jìn)行分文件編寫,分文件編寫會(huì)相對(duì)的再麻煩一點(diǎn),不過只要按方法規(guī)劃好程序的組織結(jié)構(gòu),合理安排頭文件順序,也并不困難。
6.更多細(xì)節(jié),要自己敲下代碼才能發(fā)覺
寫這篇文章的難度確實(shí)超過了我自己的預(yù)計(jì),越發(fā)思考?xì)w納,發(fā)現(xiàn)的細(xì)節(jié)問題越多,我也無法通過一文將細(xì)節(jié)問題一一說明。對(duì)這一塊困惑的話就自己舉幾個(gè)例子簡單練練吧,希望這篇文章對(duì)你有幫助。文章若有問題也請(qǐng)指正。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
基于Qt制作一個(gè)定時(shí)關(guān)機(jī)的小程序
這篇文章主要為大家詳細(xì)介紹了如何基于Qt制作一個(gè)有趣的定時(shí)關(guān)機(jī)的小程序,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-12-12QT應(yīng)用啟動(dòng)失敗排查方法小結(jié)
啟動(dòng)QT應(yīng)用經(jīng)常會(huì)碰到應(yīng)用啟動(dòng)失敗,qt platform plugin無法啟動(dòng),本文就來介紹一下QT應(yīng)用啟動(dòng)失敗排查方法小結(jié),具有一定的參考價(jià)值,感興趣的可以了解以下2023-09-09總結(jié)UNIX/LINUX下C++程序計(jì)時(shí)的方法
本文總結(jié)了下UNIX/LINUX下C++程序計(jì)時(shí)的一些函數(shù)和方法,對(duì)日常使用C++程序的朋友很有幫助,有需要的小伙伴們可以參考學(xué)習(xí),下面一起來看看吧。2016-08-08C++文件關(guān)鍵詞快速定位出現(xiàn)的行號(hào)實(shí)現(xiàn)高效搜索
這篇文章主要為大家介紹了C++文件關(guān)鍵詞快速定位出現(xiàn)的行號(hào)實(shí)現(xiàn)高效搜索,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10C++中靜態(tài)存儲(chǔ)區(qū)與棧以及堆的區(qū)別詳解
本篇文章是對(duì)C++中靜態(tài)存儲(chǔ)區(qū)與棧以及堆的區(qū)別進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05