章节目录

友元类和友元成员函数

本节阅读量:

友元类

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

下面是一个示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>

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

    // 设置 Display 是 Storage 的友元类
    friend class Display;
};

class Display
{
private:
    bool m_displayIntFirst {};

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

    // 因为 Display 是 Storage 的友元, Display 中可以访问 Storage 的所有成员
    void displayStorage(const Storage& storage)
    {
        if (m_displayIntFirst)
            std::cout << storage.m_nValue << ' ' << storage.m_dValue << '\n';
        else // 首先显示 double 值
            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对象的私有成员。

该程序产生以下结果:

1
2
6.7 5
5 6.7

关于友元类的一些附加说明。

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

第二,友元不是对等的。仅仅因为Display是Storage的友元,并不意味着Storage也是Display的朋友。如果希望两个类成为彼此的友元,则两者都必须将对方声明为友元。

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

友元类可以充当前向声明。这意味着不需要在将类绑定到友元之前去向前声明它。在上面的示例中,「friend class Display;」同时充当Display的向前声明和友元声明。


友元成员函数

可以将单个成员函数设置为友元,而不是将整个类设置为友元。这类似于使非成员函数成为友元,只是改为使用成员函数。

然而,在现实中,这可能比预期的要复杂一些。让我们转换前面的示例,使Display::displayStorage成为友元成员函数。您可以尝试这样的操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>

class Display; // 前向声明 Display

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

	// 让 Display::displayStorage 成员函数成为 Storage 的友元函数
	friend void Display::displayStorage(const Storage& storage); // 编译失败: Storage 这里看不到 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
			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之前引用它),可以很容易地解决这个问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>

class Display
{
private:
	bool m_displayIntFirst {};

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

	void displayStorage(const 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 }
	{
	}

	// 让 Display::displayStorage 成员函数成为 Storage 的友元函数
	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是什么。不能通过重新排列定义顺序来修复此问题,因为这样将撤消以前的修复。

幸运的是,通过几个简单的步骤,这也是可以修复的。首先,可以添加类Storage作为前向声明,以便编译器在看到类的完整定义之前可以引用Storage。

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

下面是它的样子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <iostream>

class Storage; // 前向声明 Storage

class Display
{
private:
	bool m_displayIntFirst {};

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

	void displayStorage(const Storage& 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::displayStorage 成员函数成为 Storage 的友元函数
	// 需要看到 Display 类的完整定义
	friend void Display::displayStorage(const Storage& storage);
};

// 现在来定义 Display::displayStorage
// 需要看到 Storage 的完整定义 (因为要访问 Storage 的成员)
void Display::displayStorage(const Storage& storage)
{
	if (m_displayIntFirst)
		std::cout << storage.m_nValue << ' ' << storage.m_dValue << '\n';
	else
		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::displayStorage()的声明。Display的完整定义满足了将Display::displayStorage()声明为Storage的友元。类Storage的完整定义足以满足成员函数Display::displayStorage() 的定义。

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

这看起来像是一种痛苦。幸运的是,因为这是我们试图在单个文件中完成所有事情,才不得不这样做。更好的解决方案是将每个类定义放在单独的头文件中,成员函数定义放在相应的.cpp文件中。这样,所有的类定义都将在.cpp文件中可用,并且不需要重新排列类或函数!


15.7 友元非成员函数

上一节

15.9 引用限定符

下一节