15.9 — 友元类和友元成员函数

友元类

友元类是可以访问另一个类的私有和受保护成员的类。

这是一个示例

#include <iostream>

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 }
    {
    }

    // Because Display is a friend of Storage, Display members can access the private members of Storage
    void displayStorage(const 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';
    }

    void setDisplayIntFirst(bool b)
    {
         m_displayIntFirst = b;
    }
};

int main()
{
    Storage storage { 5, 6.7 };
    Display display { false };

    display.displayStorage(storage);

    display.setDisplayIntFirst(true);
    display.displayStorage(storage);

    return 0;
}

由于 `Display` 类是 `Storage` 的友元,`Display` 的成员可以访问它们有权访问的任何 `Storage` 对象的私有成员。

此程序产生以下结果:

6.7 5
5 6.7

关于友元类的一些额外说明。

首先,即使 `Display` 是 `Storage` 的友元,`Display` 也无法访问 `Storage` 对象的 `*this` 指针(因为 `*this` 实际上是一个函数参数)。

其次,友元关系不是相互的。仅仅因为 `Display` 是 `Storage` 的友元,并不意味着 `Storage` 也是 `Display` 的友元。如果你希望两个类互为友元,则两者都必须声明对方为友元。

作者注

如果这个有点触及痛处,抱歉!

类友元关系也不是传递的。如果类 A 是 B 的友元,B 是 C 的友元,这并不意味着 A 是 C 的友元。

致进阶读者

友元关系也不会被继承。如果类 A 将 B 设为友元,则从 B 派生的类不是 A 的友元。

友元类声明充当被友元类的前向声明。这意味着我们不需要在友元化之前前向声明被友元的类。在上面的示例中,`friend class Display` 既充当 `Display` 的前向声明,又充当友元声明。

友元成员函数

除了将整个类设为友元之外,你还可以将单个成员函数设为友元。这与将非成员函数设为友元类似,只是使用了成员函数的名称。

然而,实际上,这可能比预期的要棘手一些。让我们将前面的示例转换为将 `Display::displayStorage` 设为友元成员函数。你可能会尝试这样做

#include <iostream>

class Display; // forward declaration for class 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::displayStorage member function a friend of the Storage class
	friend void Display::displayStorage(const Storage& storage); // error: Storage hasn't seen the full definition of class Display
};

class Display
{
private:
	bool m_displayIntFirst {};

public:
	Display(bool displayIntFirst)
		: m_displayIntFirst { displayIntFirst }
	{
	}

	void displayStorage(const 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.displayStorage(storage);

    return 0;
}

然而,事实证明这行不通。为了将单个成员函数设为友元,编译器必须已经看到了友元成员函数所属类的完整定义(而不仅仅是前向声明)。由于类 `Storage` 尚未看到类 `Display` 的完整定义,因此在尝试将成员函数设为友元时,编译器会报错。

幸运的是,这很容易解决,只需将 `Display` 类的定义移动到 `Storage` 类的定义之前(可以在同一个文件中,也可以通过将 `Display` 的定义移动到头文件并在定义 `Storage` 之前 `#include` 它)。

#include <iostream>

class Display
{
private:
	bool m_displayIntFirst {};

public:
	Display(bool displayIntFirst)
		: m_displayIntFirst { displayIntFirst }
	{
	}

	void displayStorage(const Storage& storage) // compile error: compiler doesn't know what a Storage is
	{
		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::displayStorage member function a friend of the Storage class
	friend void Display::displayStorage(const Storage& storage); // okay now
};

int main()
{
    Storage storage { 5, 6.7 };
    Display display { false };
    display.displayStorage(storage);

    return 0;
}

然而,我们现在遇到了另一个问题。由于成员函数 `Display::displayStorage()` 使用 `Storage` 作为引用参数,并且我们刚刚将 `Storage` 的定义移动到 `Display` 的定义之下,编译器会抱怨它不知道 `Storage` 是什么。我们无法通过重新排列定义顺序来解决这个问题,因为那样会撤销我们之前的修复。

幸运的是,这也可以通过几个简单的步骤解决。首先,我们可以添加 `class Storage` 作为前向声明,这样编译器在看到类的完整定义之前,对 `Storage` 的引用就没问题了。

其次,我们可以将 `Display::displayStorage()` 的定义移出类,放在 `Storage` 类的完整定义之后。

这看起来像这样

#include <iostream>

class Storage; // forward declaration for class Storage

class Display
{
private:
	bool m_displayIntFirst {};

public:
	Display(bool displayIntFirst)
		: m_displayIntFirst { displayIntFirst }
	{
	}

	void displayStorage(const Storage& storage); // forward declaration for Storage needed for reference here
};

class Storage // full definition of Storage class
{
private:
	int m_nValue {};
	double m_dValue {};
public:
	Storage(int nValue, double dValue)
		: m_nValue { nValue }, m_dValue { dValue }
	{
	}

	// Make the Display::displayStorage member function a friend of the Storage class
	// Requires seeing the full definition of class Display (as displayStorage is a member)
	friend void Display::displayStorage(const Storage& storage);
};

// Now we can define Display::displayStorage
// Requires seeing the full definition of class Storage (as we access Storage members)
void Display::displayStorage(const 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.displayStorage(storage);

    return 0;
}

现在一切都将正确编译:`class Storage` 的前向声明足以满足 `Display` 类中 `Display::displayStorage()` 的声明。`Display` 的完整定义满足将 `Display::displayStorage()` 声明为 `Storage` 的友元。`Storage` 类的完整定义足以满足成员函数 `Display::displayStorage()` 的定义。

如果这有点令人困惑,请参见上面程序中的注释。关键点是类的前向声明满足对类的引用。但是,访问类的成员需要编译器已经看到了完整的类定义。

如果这看起来很麻烦——确实如此。幸运的是,这种“舞蹈”只在我们将所有内容都放在一个文件中时才需要。更好的解决方案是将每个类定义放在单独的头文件中,并将成员函数定义放在相应的 .cpp 文件中。这样,所有类定义都将在 .cpp 文件中可用,并且不需要重新排列类或函数!

小测验时间

问题 #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。

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

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

给定以下程序:

#include <iostream>

class Vector3d
{
private:
	double m_x{};
	double m_y{};
	double m_z{};

public:
	Vector3d(double x, double y, double z)
		: m_x{x}, m_y{y}, m_z{z}
	{
	}

	void print() const
	{
		std::cout << "Vector(" << m_x << ", " << m_y << ", " << m_z << ")\n";
	}
};

class Point3d
{
private:
	double m_x{};
	double m_y{};
	double m_z{};

public:
	Point3d(double x, double y, double z)
		: m_x{x}, m_y{y}, m_z{z}
	{ }

	void print() const
	{
		std::cout << "Point(" << m_x << ", " << m_y << ", " << m_z << ")\n";
	}

	void moveByVector(const Vector3d& v)
	{
		// implement this function as a friend of class 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;
}

> 步骤 #1

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

显示答案

> 步骤 #2

不是将类 `Point3d` 设为类 `Vector3d` 的友元,而是将成员函数 `Point3d::moveByVector` 设为类 `Vector3d` 的友元。

显示答案

> 步骤 #3

使用 5 个单独的文件重新实现前一步的解决方案:Point3d.h、Point3d.cpp、Vector3d.h、Vector3d.cpp 和 main.cpp。

感谢读者 Shiva 的建议和解决方案。

显示答案

guest
您的电子邮箱地址将不会被显示
发现错误?请在上方留言!
与勘误相关的评论在处理后将被删除,以帮助减少混乱。感谢您帮助使网站对每个人都更好!
来自 https://gravatar.com/ 的头像与您提供的电子邮箱地址相关联。
有回复时通知我:  
92 评论
最新
最早 最多投票
内联反馈
查看所有评论