配套视频课程已更新完毕,大家可通过以下两种方式观看视频讲解:

关注公众号:爱编程的大丙,或者进入大丙课堂学习。


friend关键字在C++中是一个比较特别的存在。因为在大多数编程语言中是没有提供friend关键字的,比如Java。friend关键字用于声明类的友元,友元可以无视类中成员的属性( public、protected 或是 private ),友元类或友元函数都可以访问,这就完全破坏了面向对象编程中封装性的概念。但有的时候,friend关键字确实会让程序猿少写很多代码,因此 friend 还是在很多程序中被使用到。

1. 语法改进

在 C++11 标准中对 friend关键字进行了一些改进,以保证其更加好用:

声明一个类为另外一个类的友元时,不再需要使用class关键字,并且还可以使用类的别名(使用 typedef 或者 using 定义)。

我们可以看看下面的例子:

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
55
56
57
58
59
60
#include <iostream>
using namespace std;

// 类声明
class Tom;
// 定义别名
using Honey = Tom;

// 定义两个测试类
class Jack
{
// 声明友元
// friend class Tom; // C++98 标准语法
friend Tom; // C++11 标准语法
string name = "jack"; // 默认私有
void print() // 默认私有
{
cout << "my name is " << name << endl;
}
};

class Lucy
{
protected:
// 声明友元
// friend class Tom; // C++98 标准语法
friend Honey; // C++11 标准语法
string name = "lucy";
void print()
{
cout << "my name is " << name << endl;
}
};

class Tom
{
public:
void print()
{
// 通过类成员对象访问其私有成员
cout << "invoke Jack private member: " << jObj.name << endl;
cout << "invoke Jack private function: " << endl;
jObj.print();

cout << "invoke Lucy private member: " << lObj.name << endl;
cout << "invoke Lucy private function: " << endl;
lObj.print();
}
private:
string name = "tom";
Jack jObj;
Lucy lObj;
};

int main()
{
Tom t;
t.print();
return 0;
}

在上面的例子中 Tom 类分别作为了Jack类Lucy类的友元类,然后在Tom类中定义了Jack类Lucy类的对象jObjlObj,这样我们就可以在Tom类中通过这两个类对象直接访问它们各自的私有或者受保护的成员变量或者成员函数了。

2. 为类模板声明友元

虽然在C++11标准中对友元的改进不大,却会带来应用的变化——程序员可以为类模板声明友元了,这在C++98中是无法做到的。使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Tom;

template<typename T>
class Person
{
friend T;
};

int main()
{
Person<Tom> p;
Person<int> pp;
return 0;
}
  • 第11行:Tom类Person类的友元
  • 第12行:对于int类型的模板参数,友元声明被忽略(第6行)

这样一来,我们就可以在模板实例化时才确定一个模板类是否有友元,以及谁是这个模板类的友元。

下面基于一个实际场景来讲解一下如何给模板类指定友元:

假设有一个矩形类,一个圆形类,我们在对其进行了一系列的操作之后,需要验证一下矩形的宽度和高度、圆形的半径是否满足要求,并且要求这个校验操作要在另一个类中完成。

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
55
56
57
58
59
template<typename T>  
class Rectangle
{
public:
friend T;
Rectangle(int w, int h) : width(w), height(h) {}
private:
int width;
int height;
};

template<typename T>
class Circle
{
public:
friend T;
Circle(int r) : radius(r) {}
private:
int radius;
};

// 校验类
class Verify
{
public:
void verifyRectangle(int w, int h, Rectangle<Verify> &r)
{
if (r.width >= w && r.height >= h)
{
cout << "矩形的宽度和高度满足条件!" << endl;
}
else
{
cout << "矩形的宽度和高度不满足条件!" << endl;
}
}

void verifyCircle(int r, Circle<Verify> &c)
{
if (r >= c.radius)
{
cout << "圆形的半径满足条件!" << endl;
}
else
{
cout << "圆形的半径不满足条件!" << endl;
}
}
};

int main()
{
Verify v;
Circle<Verify> circle(30);
Rectangle<Verify> rect(90, 100);
v.verifyCircle(60, circle);
v.verifyRectangle(100, 100, rect);
return 0;
}
  • 第28行:在Verify类中 访问了 Rectangle类 的私有成员变量
  • 第40行:在Verify类中 访问了 Circle类 的私有成员变量

程序输出的结果:

1
2
圆形的半径满足条件!
矩形的宽度和高度不满足条件!

在上面的例子中我们定义了两个类模板RectangleCircle并且将其模板类型定义为了它们的友元(如果是模板类型是基础类型友元的定义就被忽略了)。在main()函数中测试的时候将Verify类指定为了两个模板类的实际友元类型。这样我们在Verify类中就可以通过Rectangle类Circle类的实例对象访问它们内部的私有成员变量了。

补充说明:

  1. 在上面的测试程序中Rectangle类Circle类我们没有提供对应的set方法来设置私有成员的值,为了简化程序直接通过构造函数的初始化列表完成了它们的初始化。
  2. 在上面的程序中也没有给Rectangle类Circle类提供get方法,这样如果想要在类外部访问私有(或受保护)成员就只能使用友元了(此处这样处理完全了为了测试的需要)。