15-9 友元类和友元成员函数

友元类

友元类friend class是指能够访问另一个类的私有和受保护成员的类。

以下是一个示例:

#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 对象的私有成员。

该程序产生以下结果:

image

关于友元类的几点补充说明。

首先,尽管Display是Storage的友元类,但Display无法访问Storage对象的this指针(因为this实际上是函数参数)。

其次,友元关系并非互惠。Display是Storage的友元类,并不意味着Storage也是Display的友元类。若要使两个类成为彼此的友元类,双方都必须声明对方为友元类。

作者注:
若此文触及敏感话题,敬请谅解!

类友关系同样不具传递性。若类A是B的友类,而B又是C的友类,则不能推断A是C的友类。

进阶说明
友关系亦不可继承。当类A将B设为友类时,B的派生类不会自动成为A的友类。

声明友元类时,该声明同时充当被声明类的向前声明。这意味着我们无需在声明友元关系前预先声明被声明类。在上例中,友元类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;
}

image

image

然而事实证明这行不通。要使单个成员函数成为友元函数,编译器必须看到该友元成员函数所属类的完整定义(而不仅仅是前向声明)。由于类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;
}

image

但我的此处还有个错误, 并未解决, 使用了前向声明class Stronge;后才消除。
image

然而,我们现在又遇到另一个问题。由于成员函数 Display::displayStorage() 使用 Storage 作为引用参数,而我们刚刚将 Storage 的定义移到了 Display 定义之后,编译器会报错说它不知道 Storage 是指什么。我们不能通过重新排列定义顺序来修复这个问题,因为那样会使我们之前的修复失效。

所幸这个问题同样能通过几个简单步骤解决。首先,我们可以为类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;
}

image

现在一切都能正确编译:类Storage的前向声明足以满足Display类内部对Display::displayStorage()的声明。Display类的完整定义满足将Display::displayStorage()声明为Storage的友函数的要求。而类Storage的完整定义则足以满足成员函数Display::displayStorage()的定义。

若仍感困惑,请参阅上方程序中的注释。关键点在于:类的前向声明可满足对该类的引用,但访问类成员时,编译器必须已见过完整的类定义。

若觉得这很麻烦——确实如此。所幸这种操作仅因我们试图将所有内容置于单个文件中才必要。更优解是将每个类定义放在独立的头文件中,成员函数定义则放在对应的.cpp文件里。如此一来,所有类定义都可在.cpp文件中使用,无需重新排列类或函数!


测验时间

问题 #1

在几何学中,点是空间中的一个位置。我们可以将三维空间中的点定义为坐标 x、y 和 z 的集合。例如,点 { 2.0, 1.0, 0.0 } 就是坐标空间中 x=2.0、y=1.0、z=0.0 的点。

在物理学中,向量是具有大小(长度)和方向(但无位置)的量。我们可将三维空间中的向量定义为x、y、z三个值,分别表示向量沿x、y、z轴的方向(长度可由此推导)。例如,向量 { 2.0, 0.0, 0.0 } 表示沿正x轴方向(仅限该轴)且长度为2.0的向量。

向量可作用于点对象使其移动至新位置。具体实现方式是将向量方向与点坐标相加,从而得到新坐标。例如,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;
}

image


步骤 #1

将 Point3d 设为 Vector3d 的友元类,并实现函数 Point3d::moveByVector()。

image

显示解决方案

#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";
	}

	friend class Point3d; // Point3d is now a friend of class Vector3d
};


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

步骤 #2

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

image

显示解决方案

#include <iostream>

class Vector3d; // first, we need to tell the compiler that a class named Vector3d exists

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); // so we can use Vector3d here
       // note: we can't define this function here, because Vector3d hasn't been defined yet (just forward declared)
};

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";
	}

	friend void Point3d::moveByVector(const Vector3d& v); // Point3d::moveByVector() is now a friend of class Vector3d
};

// Now that Vector3d has been defined, we can define the function 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;
}

步骤 #3

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

image

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

显示解决方案
Point3d.h

// Header file that defines the Point3d class

#ifndef POINT3D_H
#define POINT3D_H

class Vector3d; // forward declaration for class Vector3d for function moveByVector()

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

    public:
        Point3d(double x, double y, double z);

        void print() const;
        void moveByVector(const Vector3d& v); // forward declaration above needed for this line
};

#endif

Point3d.cpp:

// Member functions of the Point3d class defined here

#include "Point3d.h" // Point3d class defined here
#include "Vector3d.h" // for the parameter of the function moveByVector()

#include <iostream>

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

void Point3d::moveByVector(const Vector3d& v)
{
    // Add the vector components to the corresponding point coordinates
    m_x += v.m_x;
    m_y += v.m_y;
    m_z += v.m_z;
}

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

Vector3d.h

// Header file that defines the Vector3d class

#ifndef VECTOR3D_H
#define VECTOR3D_H

#include "Point3d.h" // for declaring Point3d::moveByVector() as a friend

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

    public:
        Vector3d(double x, double y, double z);

        void print() const;
        friend void Point3d::moveByVector(const Vector3d& v);
};

#endif

Vector.cpp

// Member functions of the Vector3d class defined here

#include "Vector3d.h" // Vector3d class defined in this file

#include <iostream>

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

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

main.cpp

#include "Vector3d.h"
#include "Point3d.h"

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

这个自己没独立搞出来,呜呜呜, 参考了答案之后运行如下:
image
image

posted @ 2026-01-03 12:30  游翔  阅读(11)  评论(0)    收藏  举报