章节目录

友元类和友元成员函数

本节阅读量:

友元类

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

下面是一个示例:

 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。

其次,可以将 Display::displayStorage() 的定义移出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
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 引用限定符

下一节