5.3 友元

如果程序员想允许显式地不属于当前结构的一个成员函数访问当前结构中的数据,那该怎么办呢?他可以在该结构内部声明这个函数为friend(友元)。注意,一个friend必须在一个结构内声明,这一点很重要,因为程序员(和编译器)必须能读取这个结构的声明以理解这个数据类型的大小、行为等方面的所有规则。有一条规则在任何关系中都很重要,那就是“谁可以访问我的私有实现部分”。

类控制着哪些代码可以访问它的成员。如果不是一个friend的话,程序员没有办法从类外“破门而入”,他不能声明一个新类,然后说“嘿,我是类Bob的朋友(友元)”,不能指望这样就可以访问类Bob的private成员和protected成员。

程序员可以把一个全局函数声明为friend,也可以把另一个结构中的成员函数甚至整个结构都声明为friend,请看下面的例子:

5.3 友元 - 图1

5.3 友元 - 图2

struct Y有一个成员函数f(),它将修改X类型的对象。这里有一个难题,因为C++的编译器要求在引用任一变量之前必须先声明,所以struct Y必须在它的成员Y:f(X)被声明为struct X的一个友元之前声明,但要声明Y:f(X),又必须先声明struct X。

解决的办法:注意到Y:f(X*)引用了一个X对象的地址(address)。这一点很关键,因为编译器知道如何传递一个地址,这一地址具有固定的大小,而不管被传递的是什么对象,即使它还没有完全知道这种对象类型大小。然而,如果试图传递整个对象,编译器就必须知道X的全部定义以确定它的大小以及如何传递,这就使得程序员无法去声明一个类似于Y:g(X)的函数。

通过传递X的地址,编译器允许程序员在声明Y:f(X*)之前做一个X的不完全的类型说明(incomplete type specification)。这一点是用如下的声明时完成的:

5.3 友元 - 图3

该声明仅仅是告诉编译器,有一个叫X的struct,所以当它被引用时,只要不涉及名字以外的其他信息,就不会产生错误。

这样,在struct X中,就可以成功地声明Y:f(X*)为一个friend函数,如果程序员在编译器获得Y的全部说明信息之前声明它,就会产生一条错误,这种安全措施保证了数据的一致性,同时减少了错误的出现。

再来看看其他两个friend函数,第一个声明将一个全局函数g()作为一个friend,但g()在这之前并没有在全局范围内作过声明,这表明friend可以在声明函数的同时又将它作为struct的友元。这种扩展声明对整个结构同样有效:

5.3 友元 - 图4

是Z的一个不完全的类型说明,并把整个结构都当做一个friend。

5.3.1 嵌套友元

嵌套的结构并不能自动获得访问private成员的权限。要获得访问私有成员的权限,必须遵守特定的规则:首先声明(而不定义)一个嵌套的结构,然后声明它是全局范围使用的一个friend,最后定义这个结构。结构的定义必须与friend声明分开,否则编译器将不把它看做成员。请看下面的例子:

5.3 友元 - 图5

5.3 友元 - 图6

一旦Pointer被声明,它就可以通过下面语句来获得访问Holder的私有成员的权限:

5.3 友元 - 图7

struct Holder包含一个int数组和一个Pointer,可以通过Pointer来访问这些整数。因为Pointer与Holder紧密联系,所以有必要将它作为结构Holder中的一个成员。但是,又因为Pointer是同Holder分开的,所以程序员可以在函数main()中定义它们的多个实例,然后用它们来选择数组的不同部分。由于Pointer是一个结构而不是C语言中原始意义上的指针,因此程序员可以保证它总是安全地指向Holder的内部。

使用标准C语言库函数memset()(在<cstring>中)可以使上面的程序变得容易。它把起始于某一特定地址的内存(该内存作为第一个参数)从起始地址直至其后的n(n作为第三个参数)个字节的所有内存都设置成同一个特定的值(该值作为第二个参数)。当然,程序员可以使用一个简单的循环来反复设置需要使用的所有内存,而且,memset()是可用的,经过了很好的测试不太可能引入错误,而且比起手工编码来更有效。