详解C++类的成员函数做友元产生的循环依赖问题

目录
  • 类的声明
  • 类的成员函数做友元以及可能产生的循环依赖问题
    • 情况一:B类的成员函数func是A类的友元,且B类不依赖A类
    • 情况二:类B的成员函数func成员函数是类A的友元,且B类依赖于不完整的A类
    • 情况三:类B的成员函数func是类A的友元,且B类依赖于完整的A类
  • 补充
    • 1.内联函数与循环依赖问题
    • 2.什么情况会需要类的声明?什么情况又需要类的定义?
    • 3.《C++ Primer》一书 “友元再探” 小节的错误
    • 4.没列举出来的情况(可以忽略这断内容)
    • 5.分文件编写时,注意头文件声明的顺序
    • 6.更多细节,要自己敲下代码才能发觉
  • 总结

类的成员函数做友元时,极易产生循环依赖问题,导致程序无法编译通过。何谓循环依赖,简单举个例子,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.更多细节,要自己敲下代码才能发觉

写这篇文章的难度确实超过了我自己的预计,越发思考归纳,发现的细节问题越多,我也无法通过一文将细节问题一一说明。对这一块困惑的话就自己举几个例子简单练练吧,希望这篇文章对你有帮助。文章若有问题也请指正。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • C++中的友元函数与友元类详情

    目录 一.问题背景 二.友元函数 三.友元类 一.问题背景 对类的封装是C++三大特性中的一个重要特性,封装好的数据在类的外部是访问不到的但是一旦出了问题,想要操作被封装的数据怎么办呢?由此友元函数友元类诞生了.在类中用friend关键字声明的函数或类,可以对类体中的任何权限成员属性进行操作有好处就有坏处友元函数.友元类严重破坏类的封装性,不到迫不得已不要使用. 二.友元函数 声明部分: friend 返回类型 函数名 (参数列表); 定义部分:返回类型 函数名 (参数列表){        函

  • C++类中的特殊成员函数示例详解

    前言 C++类中有几个特殊的非静态成员函数,当用户未定义这些函数时,编译器将给出默认实现.C++11前有四个特殊函数,C++11引入移动语义特性,增加了两个参数为右值的特殊函数.这六个函数分别是: 1.默认构造函数 默认构造函数指不需要参数就能初始化的构造函数.包含无参和所有参数有默认值两种类型的构造函数. 2.复制构造函数 复制构造函数指使用该类的对象作为参数的构造函数.可以有其他参数,但必须提供默认值. 3.复制赋值运算符 重载等号=,将该类的对象赋值给已定义对象. 4.析构函数 没啥可说的

  • C++实现循环队列

    本文实例为大家分享了C++实现循环队列的具体代码,供大家参考,具体内容如下 circularQueue.h #pragma once #pragma once #ifndef CIRCULARQUEUE_H #define CIRCULARQUEUE_H #include<iostream> #include<ostream> using std::cout; using std::cin; using std::endl; using std::ostream; template

  • C++类的空指针调用成员函数的代码

    类的实例调用成员函数的原理 其实不管是通过对象实例或指针实例调用,其实底层调用的过程都是一样的,都是把当前对象的指针作为一个参数传递给被调用的成员函数.通过下面的相关实例代码进行检验: 实验的C++代码 class Student { private: int age; public: Student() {} Student(int age) : age(age) {} int getAge() { return this->age; } }; int main(int argc, char

  • C++ 双向循环链表类模版实例详解

    目录 1.插入某个节点流程 2.构造函数修改 3.重新实现append和prepend函数 4.修改迭代器类 5.LinkedList.h代码如下 6.测试运行 总结 在上章C++图解单向链表类模板和iterator迭代器类模版详解 我们学习了单链表,所以本章来学习双向循环链表 我们在上个文章代码上进行修改, 由于双向循环链表在我们之前学的单链表上相对于较为复杂,所以需要注意的细节如下所示. 1.插入某个节点流程 如下图所示: 对应代码如下所示: /*插入一个新的节点*/ bool insert

  • C++类的静态成员变量与静态成员函数详解

    目录 1.类的静态成员变量 2.静态成员函数 总结 1.类的静态成员变量 C++类的静态成员变量主要有以下特性: 1.静态成员变量需要类内定义,类外初始化 2.静态成员变量不依赖于类,静态成员变量属于全局区,不属于类的空间. 3.静态成员变量通过类名访问,也可以通过对象访问,同一类的不同对象,静态成员共享同一份数据 下面通过代码验证以上三种说法: #include <iostream> using namespace std; class Base{ public: static int va

  • 详解C++类的成员函数做友元产生的循环依赖问题

    目录 类的声明 类的成员函数做友元以及可能产生的循环依赖问题 情况一:B类的成员函数func是A类的友元,且B类不依赖A类 情况二:类B的成员函数func成员函数是类A的友元,且B类依赖于不完整的A类 情况三:类B的成员函数func是类A的友元,且B类依赖于完整的A类 补充 1.内联函数与循环依赖问题 2.什么情况会需要类的声明?什么情况又需要类的定义? 3.<C++ Primer>一书 “友元再探” 小节的错误 4.没列举出来的情况(可以忽略这断内容) 5.分文件编写时,注意头文件声明的顺序

  • C++获取类的成员函数的函数指针详解及实例代码

    C++获取类的成员函数的函数指针详解 用一个实际代码来说明. class A { public: staticvoid staticmember(){cout<<"static"<<endl;} //static member void nonstatic(){cout<<"nonstatic"<<endl;} //nonstatic member virtualvoid virtualmember(){cout<

  • C++中成员函数和友元函数的使用及区别详解

    为什么使用成员函数和友元函数 这个问题至关重要,直接影响着后面的理解: 程序数据: 数据是程序的信息,会受到程序函数的影响.封装是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受到外界的干扰和误用,从而确保了安全. 数据封装引申出了另一个重要的 OOP 概念,即 数据隐藏 .数据封装 是一种把数据和操作数据的函数捆绑在一起的机制, 数据抽象 是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制.C++ 通过创建类来支持封装和数据隐藏(public.protected.p

  • 详解Python类和对象内容

    目录 一.什么是Python类? 二.Python类中的方法和属性 2.1.Python类中的方法 2.2.Python类中的属性 三.面向对象的概念 3.1.Python类:继承 3.2.Python类:多态性 3.3.Python类:抽象 一.什么是Python类? python中的类是创建特定对象的蓝图.它使您可以以特定方式构建软件.问题来了,怎么办?类允许我们以一种易于重用的方式对我们的数据和函数进行逻辑分组,并在需要时进行构建.考虑下图. 在第一张图片(A)中,它代表了一个可以被视为C

  • 详解JavaScript中的箭头函数的使用

    目录 前言 箭头函数语法 无圆括号语法 隐式返回 注意隐式返回错误 无法命名箭头函数 如何处理this关键字 匿名箭头函数 不正常工作的情况 箭头函数作为对象方法 箭头函数与第三方库 箭头函数没有arguments对象 总结 前言 本文可以让你了解所有有关JavaScript箭头函数的信息.我们将告诉你如何使用ES6的箭头语法,以及在代码中使用箭头函数时需要注意的一些常见错误.你会看到很多例子来说明它们是如何工作的. JavaScript的箭头函数随着ECMAScript 2015的发布而到来,

  • jQuery position() 函数详解以及jQuery中position函数的应用

    position()函数用于返回当前匹配元素相对于其被定位的祖辈元素的偏移,也就是相对于被定位的祖辈元素的坐标.该函数只对可见元素有效. 所谓"被定位的元素",就是元素的CSS position属性值为absolute.relative或fixed(只要不是默认的static即可). 该函数返回一个坐标对象,该对象有一个left属性和top属性.属性值均为数字,它们都以像素(px)为单位. 与offset()不同的是:position()返回的是相对于被定位的祖辈元素的坐标,offse

  • 怎么实现类的成员函数作为回调函数

    如果试图直接使用C++的成员函数作为回调函数将发生错误,甚至编译就不能通过.其错误是普通的C++成员函数都隐含了一个传递函数作为参数,亦即"this"指针,C++通过传递this指针给其成员函数从而实现程序函数可以访问C++的数据成员.这也可以理解为什么C++类的多个实例可以共享成员函数却-有不同的数据成员.由于this指针的作用,使得将一个CALL-BACK型的成员函数作为回调函数安装时就会因为隐含的this指针使得函数参数个数不匹配,从而导致回调函数安装失败.要解决这一问题的关键就

  • 详解配置类为什么要添加@Configuration注解

    不加@Configuration导致的问题 我们先来看看如果不在配置类上添加@Configuration注解会有什么问题,代码示例如下: @ComponentScan("com.dmz.source.code") //@Configuration public class Config{ @Bean public A a(){ return new A(dmzService()); } @Bean public DmzService dmzService(){ return new D

  • 详解pandas.DataFrame.plot() 画图函数

    首先看官网的DataFrame.plot( )函数 DataFrame.plot(x=None, y=None, kind='line', ax=None, subplots=False, sharex=None, sharey=False, layout=None,figsize=None, use_index=True, title=None, grid=None, legend=True, style=None, logx=False, logy=False, loglog=False,

  • 详解Java 类的加载、连接和初始化

    系统可能在第一次使用某个类时加载该类,也可能采用预加载机制来加载某个类.本节将会详细介绍类加载.连接和初始化过程中的每个细节. JVM 和类 当调用 java 命令运行某个 Java 程序时,该命令将会启动一个 Java 虚拟机进程,不管该 Java 程序有多么复杂,该程序启动了多少个线程,它们都处于该 Java 虚拟机进程里.正如前面介绍的,同一个 JVM 的所有线程.所有变量都处于同一个进程里,它们都使用该 JVM 进程的内存区.当系统出现以下几种情况时,JVM 进程将被终止. 程序运行到最

随机推荐