class
friend
13.15 - 友元函数和友元类
本章中很大部分篇幅中我们都在强调封装(数据私有)的重要性。不过,有时候我们也会发现,有些类和函数会和其他其他外部类密切配合工作。例如,有一个类负责存放数据,而另外一个函数(或另外一个类)用于将数据打印到屏幕上。尽管存储类和显示代码出于便于维护的目的而被分割开来,但是显示代码实际上非常紧密地依靠着存储类。这样一来,将存储类的细节对显示代码隐藏起来并无太多帮助。
在这种情况下,我们有两种选择:
让显示代码使用存储类的公有函数。然而,这么做有几个潜在的缺点。首先,必须定义这些公共成员函数,这需要时间,而且可能会打乱存储类的接口。其次,存储类可能必须暴露一些接口给显示代码,但是这些代码并不应该暴露给其他模块,我们不希望其他任何人都能访问这些函数。没有办法指定这个函数只能被显示类使用。
或者,使用友元类和友元函数,你可以让显示代码访问存储类的私有细节。这允许显示代码直接访问存储类的所有私有成员和函数,同时将其他所有人排除在外!在这节课中,我们将仔细看看这是如何做到的。
友元函数
友元函数 可以像成员函数一样访问一个类的私有成员 。从各个方面来看,友元函数都和普通函数没什么区别。友元函数可以是普通函数,也可以是其他类的成员函数。声明一个友元函数,只需要在你希望成为友元的函数原型前添加friend
关键字即可。将友元函数定义在private还是public下没什么区别。
下面是一个使用友元函数的例子:
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 class Accumulator
{
private :
int m_value { 0 };
public :
void add ( int value ) { m_value += value ; }
// 使 reset() 函数成为该类的友元函数
friend void reset ( Accumulator & accumulator );
};
// reset() 现在是 Accumulator 类的友元
void reset ( Accumulator & accumulator )
{
// 可以访问 Accumulator 的私有成员
accumulator . m_value = 0 ;
}
int main ()
{
Accumulator acc ;
acc . add ( 5 ); // add 5 to the accumulator
reset ( acc ); // reset the accumulator to 0
return 0 ;
}
在这个例子中,我们声明了一个名为 reset()
的函数,它接受 Accumulator
类的对象,并将 m_value
的值设置为0。因为reset()
不是 Accumulator
类的成员,通常情况下,reset()
不能访问 Accumulator
的私有成员。但是,因为 Accumulator
已经特别声明了这个 reset()
函数是这个类的友元,所以让 reset()
函数访问 Accumulator
的私有成员。
注意,我们必须向 reset()
传递一个 Accumulator
对象。这是因为 reset()
不是成员函数。它没有*this
指针,也没有 Accumulator
对象可以用来操作,除非给定一个。
再看下面的例子:
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 #include <iostream>
class Value
{
private :
int m_value {};
public :
Value ( int value )
: m_value { value }
{
}
friend bool isEqual ( const Value & value1 , const Value & value2 );
};
bool isEqual ( const Value & value1 , const Value & value2 )
{
return ( value1 . m_value == value2 . m_value );
}
int main ()
{
Value v1 { 5 };
Value v2 { 6 };
std :: cout << std :: boolalpha << isEqual ( v1 , v2 );
return 0 ;
}
在这个例子中我们将函数 isEqual()
函数声明为 Value
类的友元。isEqual()
接受两个Value
对象作为形参 。因为 isEqual()
是Value
的友元函数,所以它可以访问 Value
对象的所有私有成员。在这个例子中,该函数可以用来比较两个对象,如果相等则返回 true。
虽然上面的两个例子都是刻意设计的,但第二个例子与我们稍后会讨论运算符重载时遇到的情况非常相似!
多个友元
一个函数可以同时成为多个类的友元。例如,考虑下面的例子:
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 Humidity ;
class Temperature
{
private :
int m_temp {};
public :
Temperature ( int temp = 0 )
: m_temp { temp }
{
}
friend void printWeather ( const Temperature & temperature , const Humidity & humidity );
};
class Humidity
{
private :
int m_humidity {};
public :
Humidity ( int humidity = 0 )
: m_humidity { humidity }
{
}
friend void printWeather ( const Temperature & temperature , const Humidity & humidity );
};
void printWeather ( const Temperature & temperature , const Humidity & humidity )
{
std :: cout << "The temperature is " << temperature . m_temp <<
" and the humidity is " << humidity . m_humidity << '\n' ;
}
int main ()
{
Humidity hum { 10 };
Temperature temp { 12 };
printWeather ( temp , hum );
return 0 ;
}
关于这个示例,有两点值得注意。首先,因为 printWeather
是这两个类的友元函数,所以它可以访问这两个类的对象的私有数据。其次,注意示例中顶部的这一行:
这是一个类原型,它告诉编译器我们将在未来定义一个名为Humidity
的类。如果没有这一行,编译器在解析 Temperature
类中 printWeather()
的原型时就不知道 Humidity
是什么。类原型的作用与函数原型相同——它们告诉编译器某个东西是什么样子的,以便现在可以使用它,以后可以定义它。然而,与函数不同的是,类没有返回类型或参数,因此类原型总是简单的class ClassName
,其中ClassName
是类的名称。
友元类
也可以让整个类成为另一个类的友元。这使友元类的所有成员都可以访问另一个类的私有成员。下面是一个例子:
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 #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 }
{
}
void displayItem ( 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 . displayItem ( storage );
return 0 ;
}
因为 Display
类是 Storage
的友元,所以 Display
的任何成员都可以 Storage
的私有成员。上述程序输出结果为:
有关友元类,还有一些事情需要注意。首先,即便 Display
是 Storage
的友元,但是 Display
并不能直接访问Storage
对象的 *this
指针。其次,因为Display
是 Storage
的友元类,但是这不代表 Storage
也是 Display
的友元。如果你希望两个类互为友元,阿么必须分别在类中将对方声明为友元。最后,如果A是B的友元,而B是C的友元,但不代表A是C的友元。
在使用友函数和类时要小心,因为它允许友函数或类打破封装。如果类改变了,友元的也需要被迫改变。因此,尽量减少对友函数和友元类的使用。
友元成员函数
可以将单个成员函数设为友元,而不是将整个类设为友元。这与将普通函数设为友元类似,只是在使用成员函数的名称时包含了className::
前缀(例如Display::displayItem
)。
不过,这么做比预期的要复杂一些。让我们改写一下前面的示例,使Display::displayItem
成为友元成员函数。我们可以这样做:
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 #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::displayItem 成员函数成为 Storage 的友元
friend void Display :: displayItem ( const Storage & storage ); // 错误: Storage 此时并不知道 Display 类的完整定义。
};
class Display
{
private :
bool m_displayIntFirst {};
public :
Display ( bool displayIntFirst )
: m_displayIntFirst { displayIntFirst }
{
}
void displayItem ( 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' ;
}
};
很遗憾,这么做并不能正确工作。为了让一个成员函数称为友元,编译器必须看到该成员函数的完整定义而不仅仅是前向声明 。因为 Storage
还没有看到Display
的完整定义,所以当我们在此处声明友元函数时会,编译器会报错。
幸运的是,我们只要将 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 #include <iostream>
class Display
{
private :
bool m_displayIntFirst {};
public :
Display ( bool displayIntFirst )
: m_displayIntFirst { displayIntFirst }
{
}
void displayItem ( const Storage & storage ) // 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::displayItem member function a friend of the Storage class
friend void Display :: displayItem ( const Storage & storage ); // okay now
};
不过, 这样一来会带来另外的问题,因为 Display::displayItem()
使用 Storage
作为引用参数。而当我们将Storage
的定义移动到Display
后面时,编译器就会抱怨它不知道Storage
是什么,我们不能再调整顺序了,不然又会出现之前的问题。
幸运地是,只需要几步就可以解决该问题。首先,我们可以添加对于Storage
的前向声明。第二,我们可以将 Display::displayItem()
移动到类外部,放在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 #include <iostream>
class Storage ; // Storage 的前向声明
class Display
{
private :
bool m_displayIntFirst {};
public :
Display ( bool displayIntFirst )
: m_displayIntFirst { displayIntFirst }
{
}
void displayItem ( const 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::displayItem 声明为 Storage 类的友元 (必须看到 Display 的完整定义)
friend void Display :: displayItem ( const Storage & storage );
};
// 现在,我们可以定义 Display::displayItem 了,它必须看到 Storage 的完整定义
void Display::displayItem ( 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 . displayItem ( storage );
return 0 ;
}
现在,代码可以正常编译了:Storage
的前向声明对于 Display::displayItem()
的声明来说足够了。Display
的完整定义可以确保Display::displayItem()
被定义为Storage
的友元函数,如果你还是感到困惑,请仔细看看上面代码中的注释。
如果这令你感到困难——的确是的。幸运的是,这么做的原因只是因为我们试图在一个文件中完成所有工作。更好的解决方案是将每个类定义放在单独的头文件中,成员函数定义放在相应的.cpp文件中。这样,所有的类定义都将立即在.cpp文件中可见,并且不需要重新排列类或函数!
小结
友元函数或友元类是可以访问另一个类的私有成员的函数或类,就好像它是该类的成员一样。这允许友元函数或友元类与另一个类紧密地一起工作,而不必暴露一个私有成员(例如通过访问函数)。
友元通常在定义重载操作符(我们将在下一章讨论)时使用,或者(少数时候)在两个或多个类需要以亲密的方式一起工作时。
注意,要使特定的成员函数成为友元,首先需要看到成员函数类的完整定义。