詳解C++類的成員函數(shù)做友元產(chǎn)生的循環(huán)依賴問(wèn)題
類的成員函數(shù)做友元時(shí),極易產(chǎn)生循環(huán)依賴問(wèn)題,導(dǎo)致程序無(wú)法編譯通過(guò)。何謂循環(huán)依賴,簡(jiǎn)單舉個(gè)例子,A類的定義需要完整的B類,B類的定義又需要完整的A類,兩者相互依賴,都無(wú)法完成定義,這種現(xiàn)象便是循環(huán)依賴。在講解循環(huán)依賴問(wèn)題之前,要先說(shuō)一下類的聲明問(wèn)題。
類的聲明
就像可以把函數(shù)的聲明和定義分離開(kāi)一樣,我們也可以僅聲明類但暫時(shí)不定義它
class A; //這是A類的聲明
這種聲明有時(shí)被稱為向前聲明,它向程序中引入了名字A并且指明了A是一種類類型。對(duì)于類型A來(lái)說(shuō),在它聲明之后定義之前,它是一個(gè)不完整類型,編譯器僅僅知道A是一個(gè)類類型,但是A類到底有哪些成員,到底占用了多少空間是無(wú)從得知的。不完整類型也是無(wú)法創(chuàng)建其對(duì)象的。
一個(gè)不完整類型能使用的情形的非常有限的,可以定義指向不完整類型的指針或引用,可以聲明(但不能定義)以不完整類型作為參數(shù)或者返回值類型的函數(shù)。
類的成員函數(shù)做友元以及可能產(chǎn)生的循環(huán)依賴問(wèn)題
情況一:B類的成員函數(shù)func是A類的友元,且B類不依賴A類
首先說(shuō)明,類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類的友元了,說(shuō)明函數(shù)func有使用A類成員的意圖,但凡想使用A類的成員,就難免要依賴于不完整或是完整的A類。
示例代碼和說(shuō)明:
#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ù),并沒(méi)有什么實(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ù)的定義。
示例代碼和說(shuō)明:
#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)無(wú)解,一種較為有效且通用的解決辦法便是添加一個(gè)銜接過(guò)度的類Help。Help類的引入使得程序結(jié)構(gòu)可以相對(duì)自由,規(guī)劃程序結(jié)構(gòu)的思路是:
類和非成員函數(shù)的聲明不是必須在它們的友元聲明之前。當(dāng)一個(gè)名字第一次出現(xiàn)在一個(gè)友元聲明中時(shí),我們隱式地假設(shè)該名字在當(dāng)前作用域中是可見(jiàn)的,所以類做友元和非成員函數(shù)做友元沒(méi)有太多程序結(jié)構(gòu)上的限制,我們利用這一點(diǎn),加入一個(gè)過(guò)度的Help類有效幫助我們化解循環(huán)依賴問(wèn)題。
在B類依賴于完整的A類的前提下,那么B類的定義只能在A類的后面,函數(shù)func不再可能聲明為A類的友元,函數(shù)func也就無(wú)法再使用A類的私有成員。讓Help類幫來(lái)搭建函數(shù)func和A類的橋梁,將Help類聲明為A類的友元,在Help類中添加函數(shù)func的實(shí)現(xiàn)手段即一個(gè)名為doFunc的靜態(tài)函數(shù),再讓B類聲明為Help的友元,Help類可以訪問(wèn)A類的私有成員,而B(niǎo)類又可以訪問(wèn)Help類的私有成員,B類間接訪問(wèn)A類的途徑就形成了。
doFunc定義為靜態(tài)函數(shù)的原因在于,我們不希望類的使用者知道Help類的存在,更不希望去創(chuàng)建Help類的對(duì)象,將doFunc聲明為靜態(tài)函數(shù)就可以讓我們不創(chuàng)建類的對(duì)象,直接通過(guò)類去調(diào)用靜態(tài)成員函數(shù)。函數(shù)doFunc負(fù)責(zé)功能的實(shí)現(xiàn),而函數(shù)func則是接口,它負(fù)責(zé)傳遞參數(shù)調(diào)用doFunc。
推薦通過(guò)示例來(lái)了解進(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)依賴問(wèn)題
成員函數(shù)是否為內(nèi)聯(lián)函數(shù)對(duì)定義和聲明的依賴性沒(méi)有影響,類內(nèi)定義的成員函數(shù)是隱式內(nèi)聯(lián)的,我們也可以在函數(shù)聲明的返回類型前面加上 inline 使得該函數(shù)顯示的內(nèi)聯(lián)。將簡(jiǎn)單函數(shù)聲明為內(nèi)聯(lián),可以提高程序的運(yùn)行效率,故示例程序中大部分成員函數(shù)都顯示或隱式的定義為了內(nèi)聯(lián)函數(shù)。
2.什么情況會(huì)需要類的聲明?什么情況又需要類的定義?
簡(jiǎn)單來(lái)說(shuō),當(dāng)我們只需要知道有這么一個(gè)類存在時(shí),有類的聲明即可,比如定義該類的指針或引用,將該類作為函數(shù)聲明中的返回類型或者參數(shù);但我們需要知道類的具體內(nèi)容是什么,類的成員有哪些時(shí),就需要類的定義,比如要定義一個(gè)該類的對(duì)象。
3.《C++ Primer》一書(shū) “友元再探” 小節(jié)的錯(cuò)誤
我正在學(xué)習(xí)該書(shū),書(shū)本這里的錯(cuò)誤確實(shí)讓我苦惱了蠻久,這也是我寫(xiě)下篇文章的原因之一。書(shū)本案例中的Screen類和Window_mgr類已經(jīng)形成了循環(huán)依賴,而書(shū)本卻指導(dǎo)用情況一的方案去解決該問(wèn)題,顯然是行不通的。
4.沒(méi)列舉出來(lái)的情況(可以忽略這斷內(nèi)容)
還有一種更加雞肋的情況我沒(méi)有列舉出來(lái),B類的成員函數(shù)func是A類的友元,B類不依賴A類,且函數(shù)func的定義中也未使用任何A類的成員。這種情況只需滿足B類的定義在A類定義之前,函數(shù)func的定義在B類的定義之后或是在類內(nèi)定義即可,程序的結(jié)構(gòu)是比較自由的。但問(wèn)題在于,我都把func聲明為A類的友元了,卻不使用A類的成員,缺乏實(shí)際意義。
5.分文件編寫(xiě)時(shí),注意頭文件聲明的順序
示例中并沒(méi)有進(jìn)行分文件編寫(xiě),分文件編寫(xiě)會(huì)相對(duì)的再麻煩一點(diǎn),不過(guò)只要按方法規(guī)劃好程序的組織結(jié)構(gòu),合理安排頭文件順序,也并不困難。
6.更多細(xì)節(jié),要自己敲下代碼才能發(fā)覺(jué)
寫(xiě)這篇文章的難度確實(shí)超過(guò)了我自己的預(yù)計(jì),越發(fā)思考?xì)w納,發(fā)現(xiàn)的細(xì)節(jié)問(wèn)題越多,我也無(wú)法通過(guò)一文將細(xì)節(jié)問(wèn)題一一說(shuō)明。對(duì)這一塊困惑的話就自己舉幾個(gè)例子簡(jiǎn)單練練吧,希望這篇文章對(duì)你有幫助。文章若有問(wèn)題也請(qǐng)指正。
總結(jié)
本篇文章就到這里了,希望能夠給你帶來(lái)幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
Qt物聯(lián)網(wǎng)管理平臺(tái)之實(shí)現(xiàn)告警短信轉(zhuǎn)發(fā)
系統(tǒng)在運(yùn)行過(guò)程中,會(huì)實(shí)時(shí)采集設(shè)備的數(shù)據(jù),當(dāng)采集到的數(shù)據(jù)發(fā)生報(bào)警后,可以將報(bào)警信息以短信的形式發(fā)送給指定的管理員。本文將利用Qt實(shí)現(xiàn)告警短信轉(zhuǎn)發(fā),感興趣的可以嘗試一下2022-07-07
C語(yǔ)言示例講解while循環(huán)語(yǔ)句的用法
在不少實(shí)際問(wèn)題中有許多具有規(guī)律性的重復(fù)操作,因此在程序中就需要重復(fù)執(zhí)行某些語(yǔ)句。一組被重復(fù)執(zhí)行的語(yǔ)句稱之為循環(huán)體,C語(yǔ)言while語(yǔ)句可以是單個(gè)語(yǔ)句,也可以是一個(gè)語(yǔ)句塊,其條件可以是任意表達(dá)式,true是任意非零值,當(dāng)條件為真時(shí),循環(huán)進(jìn)行迭代2022-06-06
C語(yǔ)言實(shí)現(xiàn)貪吃蛇游戲設(shè)計(jì)
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)貪吃蛇游戲設(shè)計(jì),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-07-07
Visual Studio 2019 如何新建 Win32項(xiàng)目的方法步驟
這篇文章主要介紹了Visual Studio 2019 如何新建 Win32項(xiàng)目的方法步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03
C語(yǔ)言中設(shè)置用戶識(shí)別碼的相關(guān)函數(shù)的簡(jiǎn)單講解
這篇文章主要介紹了C語(yǔ)言中設(shè)置用戶識(shí)別碼的相關(guān)函數(shù)的簡(jiǎn)單講解,包括setuid()函數(shù)和setreuid()函數(shù)以及setfsuid()函數(shù),需要的朋友可以參考下2015-08-08
詳解C++虛函數(shù)中多態(tài)性的實(shí)現(xiàn)原理
C++是一種面向?qū)ο蟮木幊陶Z(yǔ)言,在C++中,虛函數(shù)是實(shí)現(xiàn)多態(tài)性的關(guān)鍵。本文就來(lái)探討一下C++虛函數(shù)中多態(tài)性的實(shí)現(xiàn)原理及其在面向?qū)ο缶幊讨械膽?yīng)用吧2023-05-05

