详解C++类的成员函数做友元产生的循环依赖问题
作者:榛栗栗栗子
类的成员函数做友元时,极易产生循环依赖问题,导致程序无法编译通过。何谓循环依赖,简单举个例子,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<iostream> #include<string> 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<iostream> #include<string> 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<iostream> #include<string> 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<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类的成员函数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<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("默认",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.更多细节,要自己敲下代码才能发觉
写这篇文章的难度确实超过了我自己的预计,越发思考归纳,发现的细节问题越多,我也无法通过一文将细节问题一一说明。对这一块困惑的话就自己举几个例子简单练练吧,希望这篇文章对你有帮助。文章若有问题也请指正。
总结
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注脚本之家的更多内容!