C++基础教程面向对象(学习笔记(13))

友元函数和类

在本章的大部分内容中,我们一直在传播保护数据私密性的优点。但是,您可能偶尔会发现一些情况,您会发现在这些类之外需要紧密协作的类和函数。例如,您可能有一个存储数据的类,以及一个在屏幕上显示数据的函数(或另一个类)。虽然存储类和显示代码已经分开以便于维护,但显示代码与存储类的细节密切相关。因此,通过从显示代码隐藏存储类细节没有太多用处。

在这种情况下,有两种选择:
1)让显示代码使用存储类的公开功能。但是,这有几个潜在的缺点。首先,必须定义这些公共成员函数,这需要时间,并且可能使存储类的接口混乱。其次,存储类可能必须公开显示代码的功能,而这些功能并不是其他任何人都不想访问的。没有办法说“此功能仅供显示类使用”。

2)或者,使用友元类和友元函数,您可以让显示代码访问存储类的私有细节。这使得显示代码可以直接访问存储类的所有私有成员和功能,同时保持其他的独立性!在本课中,我们将详细介绍如何完成此操作。

友元函数

一个友元函数是可以访问类的私有成员,友元函数就像一个普通的函数。在所有其他方面,友元函数就像一个普通的函数。友元函数可以是普通函数,也可以是其他类的成员函数。要声明友元函数,只需在您希望成为类的友元函数原型前面使用friend关键字。是否在类的私有或公共部分声明了友元函数并不重要。

以下是使用友元函数的示例:

class Accumulator
{
private:
    int m_value;
public:
    Accumulator() { m_value = 0; } 
    void add(int value) { m_value += value; }
 
    // 使reset()函数成为此类的友元
    friend void reset(Accumulator &accumulator);
};
 
// reset()现在是Accumulator类的友元
void reset(Accumulator &accumulator)
{
    // 并且可以访问Accumulator对象的私有数据
    accumulator.m_value = 0;
}
 
int main()
{
    Accumulator acc;
    acc.add(5); // 将5添加到 accumulator
    reset(acc); // 复位accumulato到0
 
    return 0;
}

在这个例子中,我们声明了一个名为reset()的函数,它接受类Accumulator的对象,并将m_value的值设置为0.因为reset()不是Accumulator类的成员,所以通常reset()不会能够访问Accumulator的私人成员。但是,因为Accumulator已经专门声明这个reset()函数是该类的友元函数,所以reset()函数可以访问Accumulator的私有成员。

注意,我们必须将Accumulator对象传递给reset()。这是因为reset()不是成员函数。除非给定一个,否则它没有* this指针,也没有要使用的Accumulator对象。

这是另一个例子:

class Value
{
private:
    int m_value;
public:
    Value(int value) { m_value = value; }
    friend bool isEqual(const Value &value1, const Value &value2);
};
 
bool isEqual(const Value &value1, const Value &value2)
{
    return (value1.m_value == value2.m_value);
}

在这个例子中,我们声明isEqual()函数是Value类的友元函数。isEqual()将两个Value对象作为参数。因为isEqual()是Value类的朋友,所以它可以访问所有Value对象的私有成员。在这种情况下,它使用该访问权对两个对象进行比较,如果它们相等则返回true。

虽然上面的两个例子都是相当人为的,但后一个例子与我们在讨论运算符重载时在第9章中遇到的情况非常相似!

多个友元函数

函数可以同时是多个类的友元函数。例如,请考虑以下示例:

class Humidity;
 
class Temperature
{
private:
    int m_temp;
public:
    Temperature(int temp=0) { m_temp = temp; }
 
    friend void printWeather(const Temperature &temperature, const Humidity &humidity);
};
 
class Humidity
{
private:
    int m_humidity;
public:
    Humidity(int humidity=0) { m_humidity = humidity; }
 
    friend void printWeather(const Temperature &temperature, const Humidity &humidity);
};
 
void printWeather(const Temperature &temperature, const Humidity &humidity)
{
    std::cout << "The temperature is " << temperature.m_temp <<
       " and the humidity is " << humidity.m_humidity << '\n';
}
 
int main()
{
    Humidity hum(10);
    Temperature temp(12);
 
    printWeather(temp, hum);
 
    return 0;
}

关于这个例子,有两点值得注意。首先,因为PrintWeather是两个类的朋友,所以它可以从两个类的对象访问私有数据。其次,请注意示例顶部的以下行:

class Humidity;

这是一个类原型,它告诉编译器我们将来要定义一个名为Humidity的类。如果没有这一行,编译器会告诉我们在解析Temperature类中的PrintWeather()原型时它不知道Humidity是什么。类原型与函数原型具有相同的作用 - 它们告诉编译器什么样的东西,所以它现在可以使用并在以后定义。但是,与函数不同,类没有返回类型或参数,因此类原型总是简单的class ClassName,其中ClassName是类的名称。

友元类

也可以使整个类成为另一个类的友元。这使得友元类的所有成员都可以访问另一个类的私有成员。这是一个例子:

class Storage
{
private:
    int m_nValue;
    double m_dValue;
public:
    Storage(int nValue, double dValue)
    {
        m_nValue = nValue;
        m_dValue = dValue;
    }
 
    // Make the Display class a friend of Storage
    friend class Display;
};
 
class Display
{
private:
    bool m_displayIntFirst;
 
public:
    Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; }
 
    void displayItem(Storage &storage)
    {
        if (m_displayIntFirst)
            std::cout << storage.m_nValue << " " << storage.m_dValue << '\n';
        else // display double first
            std::cout << storage.m_dValue << " " << storage.m_nValue << '\n';
    }
};
 
int main()
{
    Storage storage(5, 6.7);
    Display display(false);
 
    display.displayItem(storage);
 
    return 0;
}

由于Display类是Storage的友元,因此任何使用Storage类对象的Display成员都可以直接访问Storage的私有成员。该程序产生以下结果:
6.7 5
关于友元类的一些补充说明。首先,即使Display是Storage的朋友,Display也无法直接访问存储对象的* this指针。其次,仅仅因为Display是Storage的友元,这并不意味着Storage也是Display的朋友。如果你想让两个班级成为彼此的友元,那么他们都必须将另一个类称为友元。最后,如果A类是B的朋友,而B是C的友元,那并不意味着A是C的友元。

注意:使用友元函数和类时要小心,因为它允许友元函数或类,违反封装。如果类的细节发生变化,友元的详细信息也将被迫改变。因此,将您对友元函数和类的使用限制在最低限度。

友元成员函数

您可以将单个成员函数设为友元,而不是将整个类视为友元。这与将普通函数作为友元类似地完成,除了使用包含className ::前缀的成员函数的名称(例如Display :: displayItem)。

然而,实际上,这可能比预期的要复杂一些。让我们转换前面的例子,使Display :: displayItem成为朋友成员函数。您可以尝试这样的事情:

class Display; //类Display的前向声明
 
class Storage
{
private:
	int m_nValue;
	double m_dValue;
public:
	Storage(int nValue, double dValue)
	{
		m_nValue = nValue;
		m_dValue = dValue;
	}
 
	// Make the Display::displayItem member function a friend of the Storage class
	friend void Display::displayItem(Storage& storage); // 错误:存储没有看到类Display的完整定义
};
 
class Display
{
private:
	bool m_displayIntFirst;
 
public:
	Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; }
 
	void displayItem(Storage &storage)
	{
		if (m_displayIntFirst)
			std::cout << storage.m_nValue << " " << storage.m_dValue << '\n';
		else // display double first
			std::cout << storage.m_dValue << " " << storage.m_nValue << '\n';
	}
};

然而,事实证明这不起作用。为了使成员函数成为朋友,编译器必须已经看到了friend成员函数的类的完整定义(而不仅仅是前向声明)。由于类Storage尚未看到类Display的完整定义,因此编译器在我们尝试使成员函数成为朋友时会出错。

幸运的是,只需在类Storage的定义之前移动类Display的定义,就可以轻松解决这个问题。

class Display
{
private:
	bool m_displayIntFirst;
 
public:
	Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; }
 
	void displayItem(Storage &storage) // 错误:编译器不知道Storage是什么
	{
		if (m_displayIntFirst)
			std::cout << storage.m_nValue << " " << storage.m_dValue << '\n';
		else // display double first
			std::cout << storage.m_dValue << " " << storage.m_nValue << '\n';
	}
};
 
class Storage
{
private:
	int m_nValue;
	double m_dValue;
public:
	Storage(int nValue, double dValue)
	{
		m_nValue = nValue;
		m_dValue = dValue;
	}
 
	// Make the Display::displayItem member function a friend of the Storage class
	friend void Display::displayItem(Storage& storage); // ok.
};

但是,我们现在有另一个问题。因为成员函数Display :: displayItem()使用Storage作为引用参数,我们只是将存储定义移动到Display的定义之下,编译器会抱怨它不知道存储是什么。我们无法通过重新排列定义顺序来修复此问题,因为我们将撤消之前的修复。

幸运的是,这也可以通过几个简单的步骤来解决。首先,我们可以添加类存储作为前向声明。其次,在完整定义Storage类之后,我们可以将Display :: displayItem()的定义移出类。

这是这样的:

class Storage; // 类存储的前向声明
 
class Display
{
private:
	bool m_displayIntFirst;
 
public:
	Display(bool displayIntFirst) { m_displayIntFirst = displayIntFirst; }
	
	void displayItem(Storage &storage); // 此声明行所需的上述声明
};
 
class Storage // Storage完全定义
{
private:
	int m_nValue;
	double m_dValue;
public:
	Storage(int nValue, double dValue)
	{
		m_nValue = nValue;
		m_dValue = dValue;
	}
 
	//使Display :: displayItem成员函数成为Storage类的友元(需要查看类Display的完整声明,如上所述)
	friend void Display::displayItem(Storage& storage);
};
 
//现在我们可以定义Display :: displayItem,它需要看到类Storage的完整声明
void Display::displayItem(Storage &storage)
{
	if (m_displayIntFirst)
		std::cout << storage.m_nValue << " " << storage.m_dValue << '\n';
	else // display double first
		std::cout << storage.m_dValue << " " << storage.m_nValue << '\n';
}
 
int main()
{
    Storage storage(5, 6.7);
    Display display(false);
 
    display.displayItem(storage);
 
    return 0;
}

现在一切都会正确编译:类Storage的前向声明足以满足Display类中Display :: displayItem()的声明,Display的完整定义满足声明Display :: displayItem()作为Storage的朋友,并且类Storage的完整定义足以满足成员函数Display :: displayItem()的定义。如果这有点令人困惑,请参阅上面程序中的注释。

这看似乎起来很痛苦。幸运的是,这种功能只是必要的,因为我们试图在一个文件中做所有事情。更好的解决方案是将每个类定义放在单独的头文件中,并在相应的.cpp文件中使用成员函数定义。这样,所有的类定义都会立即在.cpp文件中可见,并且不需要重新安排类或函数!

Summary

友元函数或类是可以访问另一个类的私有成员的函数或类,就好像它是该类的成员一样。这允许友元函数或类与其他类密切合作,而不会让其他类公开其私有成员(例如通过访问功能)。

当两个或多个类需要以一种亲密的方式一起工作时,或者更常见的是,在定义重载运算符时(我们将在第9章中介绍),很少使用Friending。

注意,使特定成员函数成为友元需要首先看到成员函数的类的完整定义。

Quiz time

1)在几何中,点是空间中的位置。我们可以将3d空间中的点定义为坐标x,y和z的集合。例如,Point(2.0,1.0,0.0)将是坐标空间x = 2.0,y = 1.0和z = 0.0的点。

在物理学中,向量是具有幅度(长度)和方向(但没有位置)的量。我们可以将3d空间中的向量定义为x,y和z值,表示向量沿x,y和z轴的方向(长度可以从这些中导出)。例如,Vector(2.0,0.0,0.0)将是表示沿着正x轴(仅)的方向的向量,长度为2.0。

可以将矢量应用于点以将点移动到新位置。这是通过将向量的方向添加到点的位置以产生新位置来完成的。例如,Point(2.0,1.0,0.0)+ Vector(2.0,0.0,0.0)将产生点(4.0,1.0,0.0)。

点和矢量通常用在计算机图形中(表示形状顶点的点,矢量表示形状的移动)。

给出这个程序:

#include <iostream>
 
class Vector3d
{
private:
	double m_x, m_y, m_z;
 
public:
	Vector3d(double x = 0.0, double y = 0.0, double z = 0.0)
		: m_x(x), m_y(y), m_z(z)
	{
 
	}
 
	void print()
	{
		std::cout << "Vector(" << m_x << " , " << m_y << " , " << m_z << ")\n";
	}
};
 
class Point3d
{
private:
	double m_x, m_y, m_z;
 
public:
	Point3d(double x = 0.0, double y = 0.0, double z = 0.0)
		: m_x(x), m_y(y), m_z(z)
	{
 
	}
 
	void print()
	{
		std::cout << "Point(" << m_x << " , " << m_y << " , " << m_z << ")\n";
	}
 
	void moveByVector(const Vector3d &v)
	{
		// 将此函数实现为Vector3d类的友元
	}
};
 
int main()
{
	Point3d p(1.0, 2.0, 3.0);
	Vector3d v(2.0, 2.0, -3.0);
 
	p.print();
	p.moveByVector(v);
	p.print();
 
	return 0;
}

1a)使Point3d成为Vector3d的友元类,并实现函数Point3d :: moveByVector()

解决方案:

#include <iostream>
 
class Vector3d
{
private:
	double m_x, m_y, m_z;
 
public:
	Vector3d(double x = 0.0, double y = 0.0, double z = 0.0)
		: m_x(x), m_y(y), m_z(z)
	{
 
	}
 
	void print()
	{
		std::cout << "Vector(" << m_x << " , " << m_y << " , " << m_z << ")\n";
	}
 
	friend class Point3d; // Point3d现在是Vector3d类的友元
};
 
 
class Point3d
{
private:
	double m_x, m_y, m_z;
 
public:
	Point3d(double x = 0.0, double y = 0.0, double z = 0.0)
		: m_x(x), m_y(y), m_z(z)
	{
 
	}
 
	void print()
	{
		std::cout << "Point(" << m_x << " , " << m_y << " , " << m_z << ")\n";
	}
 
 
	void moveByVector(const Vector3d &v)
	{
		m_x += v.m_x;
		m_y += v.m_y;
		m_z += v.m_z;
	}
};
 
 
int main()
{
	Point3d p(1.0, 2.0, 3.0);
	Vector3d v(2.0, 2.0, -3.0);
 
	p.print();
	p.moveByVector(v);
	p.print();
 
	return 0;
}

2b)不要让类Point3d成为类Vector3d的友元,而是使成员函数Point3d :: moveByVector成为Vector3d类的友元。

解决方案:

class Vector3d; //首先,我们需要告诉编译器存在一个名为Vector3d的类
 
class Point3d
{
private:
	double m_x, m_y, m_z;
 
public:
	Point3d(double x = 0.0, double y = 0.0, double z = 0.0)
		: m_x(x), m_y(y), m_z(z)
	{
 
	}
 
	void print()
	{
		std::cout << "Point(" << m_x << " , " << m_y << " , " << m_z << ")\n";
	}
 
	void moveByVector(const Vector3d &v); // 所以我们可以在这里使用Vector3d
       // 注意:我们不能在这里定义这个函数,因为还没有声明Vector3d(只是向前声明)
};
 
class Vector3d
{
private:
	double m_x, m_y, m_z;
 
public:
	Vector3d(double x = 0.0, double y = 0.0, double z = 0.0)
		: m_x(x), m_y(y), m_z(z)
	{
 
	}
 
	void print()
	{
		std::cout << "Vector(" << m_x << " , " << m_y << " , " << m_z << ")\n";
	}
 
	friend void Point3d::moveByVector(const Vector3d &v); // Point3d :: moveByVector()现在是Vector3d类的友元
};
 
// 现在已经声明了Vector3d,我们可以定义函数Point3d :: moveByVector()
void Point3d::moveByVector(const Vector3d &v)
{
	m_x += v.m_x;
	m_y += v.m_y;
	m_z += v.m_z;
}
 
int main()
{
	Point3d p(1.0, 2.0, 3.0);
	Vector3d v(2.0, 2.0, -3.0);
 
	p.print();
	p.moveByVector(v);
	p.print();
 
	return 0;
}

3b)使用5个单独的文件重新实现测验问题1b的解决方案:Point3d.h,Point3d.cpp,Vector3d.h,Vector3d.cpp和main.cpp。
解决方案:
Point3d.h:

// 定义Point3d类的头文件
 
#ifndef POINT3D_H
#define POINT3D_H
 
class Vector3d; //函数moveByVector()的类Vector3d的前向声明
 
class Point3d
{
    private:
        double m_x;
        double m_y;
        double m_z;
        
    public:
        Point3d(double x = 0.0, double y = 0.0, double z = 0.0) : m_x(x), m_y(y), m_z(z) {}
 
        void print();
        void moveByVector(const Vector3d &v); // 此行所需的前述声明
};
 
#endif

Point3d.cpp:

// 这里定义的Point3d类的成员函数
 
#include <iostream> // 引入std::cout
#include "Point3d.h" // Point3d类在此声明
#include "Vector3d.h" // 用于函数moveByVector()的参数
 
void Point3d::moveByVector(const Vector3d &v)
{
    // 将矢量分量添加到相应的点坐标
    m_x += v.m_x;
    m_y += v.m_y;
    m_z += v.m_z;
}
 
void Point3d::print()
{
    std::cout << "Point(" << m_x << " , " << m_y << " , " << m_z << ")\n";
}

Vector3d.h

// 定义Vector3d类的头文件
 
#ifndef VECTOR3D_H
#define VECTOR3D_H
 
#include "Point3d.h" // 用于将Point3d :: moveByVector()声明为友元
 
class Vector3d
{
    private:
        double m_x;
        double m_y;
        double m_z;
 
    public:
        Vector3d(double x = 0.0, double y = 0.0, double z = 0.0) : m_x(x), m_y(y), m_z(z) {}
 
        void print();
        friend void Point3d::moveByVector(const Vector3d &v);
};
 
#endif

Vector3d.cpp:

// 此处定义的Vector3d类的成员函数
 
#include <iostream>
#include "Vector3d.h" // 在此文件中声明的Vector3d类
 
void Vector3d::print()
{
    std::cout << "Vector(" << m_x << " , " << m_y << " , " << m_z << ")\n";
}

main.cpp中:

#include "Vector3d.h" // 用于创建Vector3d对象
#include "Point3d.h" // 用于创建Point3d对象
 
int main()
{
    Point3d p(1.0, 2.0, 3.0);
    Vector3d v(2.0, 2.0, -3.0);
 
    p.print();
    p.moveByVector(v);
    p.print();
 
    return 0;
}

我希望最后这个例子大家可以好好做一下,挺不错的!!!

猜你喜欢

转载自blog.csdn.net/qq_41879485/article/details/82961404