友元
1 友元的定义
在一个类中可以有公用的(public)成员和私有的(private)成员。在类外可以访问公用成员,只有本类中的函数可以方法本类的私有成员。例如:
class student{
private: //私有成员
char name[32]; //姓名
public: //公用成员
char addr[32]; //家庭地址
long long number; //电话号码
public: //公有成员
void print(){
cout << "name = " << name << endl;
cout << "addr = " << addr << endl;
cout << "number = " << number << endl;
}
private: //私有成员
long long get_number(){
return number;
}
};
此时,定义了一个student类。在类中定义name成员变量和get_number()成员函数是“private私有”属性。所以,只能够在类内访问。不可以在类外访问。对于addr, number成员变量和print()成员函数,是“public公有”属性,所以,可以在类外访问。
那么,有没有一些办法,可以让类的private成员可以被某些指定的对象访问?例如,可以授权给某些对象,让它可以访问类的private成员。
现在,我们来学习C++的一个新特性:友元(friend)。
friend 的意思是朋友,或者说是友好,与友好的关系显然要比一般人亲密一些。那么,一些比较隐私的事情,可以与朋友一起沟通,让朋友知道。那么,在C++中,这种“朋友”关系以关键字 friend 声明。中文多译为“友元”。友元可以访问与其有友好关系的类中的私有成员。友元包括友元函数和友元类。
例如有C++类TestA和TestB,其中,TestA是TestB的“友元”,那么,TestA是TestB的朋友,可以访问TestB类的私有成员。
2 友元函数
如果在本类以外的其他地方定义了一个函数(这个函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数),在对本类进行声明时,在类体中用friend对该函数进行声明,此函数就称为本类的“友元函数”。一个类的友元函数可以访问这个类中的私有成员。
例如,定义一个func()函数,在C++的TestA类中声明func()函数是当前类的“友元”函数,那么,这个func()函数就可以访问TestA类的私有成员。
2.1 普通函数声明为友元函数
我们可以定义一个普通的函数,希望这个函数能够访问一个类中的成员变量,那么,我们就在定义类的时候,声明这个函数是该类的友元函数,相当是说:在定义类的时候,把该普通函数声明为自己的一个朋友,允许这个朋友访问自己的成员变量。
这样,一个函数可以被多个类声明为“朋友”,它可以访问多个类中的成员变量。
通过下面的例子,可以理解友元函数的性质和作用。如下是一个友元函数的测试例子:
程序运行结果如下:
可以看到,在print()函数中可以引用stud对象的private私有成员。是因为,print()函数在student类中声明为友元。
请注意print()是一个在类外定义的函数,它不是属于student类的一个函数,所以,在定义的时候,并没有使用类前缀。它是非成员函数,不属于任何类。但是,我们把它声明为student类的一个“朋友”,让它可以访问student类的私有成员变量。
所以,使用 friend 关键字,在student类中声明定义在别处的print() 函数为student类的一个友元,这样,print() 是student类的朋友,就可以访问它的私有成员变量。
注意:在print() 中访问私有成员变量的时候,必须加上“对象名”,不能够如下:
void print(student& s)//定义友元函数
{
cout << "name = " << name << endl;
cout << "addr = " << addr << endl;
cout << "number = " << number << endl;
}
因为,print() 不是student类中的一个成员函数,它只是student类的一个朋友。不能够默认引用student类的成员变量,必须指定要访问的对象。
2.2 友元成员函数
在前面的例子中,我们定义了一个普通的函数,然后,在定义一个类的时候,把这个函数声明为该类的“朋友”,可以让该函数访问类中的成员变量。
同样,我们也在定义一个类的时候,把另一个类的成员函数声明为该类的朋友,让它可以访问自己的成员变量。
所以,firent 函数不仅可以定义一个普通函数(非成员函数),而且,可以是另一个类中的成员函数。下面举例介绍友元成员函数的使用。
在这个例子中,除了介绍有关友元成员函数的简单应用外,还将用到类的“提前引用声明”。就是说,一个类在使用之前,没有被定义,而是放在后面定义,那么,我们可以在使用之前先声明它。有如下的类定义:
class student;//提前声明student类;
class my_print//定义 my_print 类;
{
public:
void print(student& s); //声明函数;
};
class student{
private: //定义私有类型成员变量
char name[32]; //姓名
char addr[32]; //家庭地址
long long number; //电话号码
public: //以下部分是公有部分
student(char* pn, char* pa, long long n)
{
strcpy(name, pn);
strcpy(addr, pa);
number = n;
}
friend void my_print::print(student& s);//声明友元函数;
};
可以看到,定义了my_print类和student类。其中,在my_print类中引用student类。而且,student类在最后面定义。
那么,就在my_print类之前,编写如下的语句:
class student;//提前声明student类;
表示提前声明student类,那么,就可以在my_print类中引用student类。完整的测试代码如下:
程序编译结果如下:
wkf@ubuntu:~/c++$ g++ test.cpp -o exe
test.cpp: In member function ‘void my_print::print(student&)’:
test.cpp:14: error: invalid use of incomplete type ‘struct student’
test.cpp:6: error: forward declaration of ‘struct student’
test.cpp:15: error: invalid use of incomplete type ‘struct student’
test.cpp:6: error: forward declaration of ‘struct student’
test.cpp:16: error: invalid use of incomplete type ‘struct student’
test.cpp:6: error: forward declaration of ‘struct student’
可以看到,在my_print::print()函数中,引用student类型的数据异常。为什么会出现这个问题?
因为,在my_print::print()函数中,引用student类的时候,这个student类还没有定义。我们只是在my_print类之前提前声明了student类。并没有定义student类,所以,编译器在my_print::print()函数中,无法访问s.name, s.addr等成员变量。
所以,我们可以把my_print::print()函数放在student类定义的后面。表示定义了student类之后,再定义my_print::print()函数,此时,编译器才知道s.name, s.addr成员变量是合法的定义。完整的测试代码如下:
程序运行结果如下:
可以看到,my_print::print()函数能够正确引用了student类中定义的private私有成员。这是因为,在student类中,声明my_print类的print()函数是友元类型,如下:
class student{
...
friend void my_print::print(student& s);//声明友元函数;
};
所以,student类中的私有成员,就可以在my_print类的print()函数中被访问。
2.3 编译器对类提前声明的处理
在一般情况下,类必须先声明,然后,才能使用它。但是,在特殊的情况下(例如上面的例子),在正式声明定义类之前,需要使用该类名。但是,提前声明的使用范围是有限的,提前声明的时候,只能使用它去声明一个对象,例如,声明一个参数,但是,不能够调用它去定义一个对象。因为,这个类还没有编译,还不知道它有什么成员变量,所以,还不能够使用它去定义对象。如下是一个测试的例子:
程序编译运行结果如下:
g++ test.cpp -o exe
wkf@ubuntu:~/c++$ ./exe
调用print()函数
程序编译OK,可以正常运行。因为,在print()函数中,只是定义了student类的引用,并没有实际定义对象。
那么,我们修改print()函数如下:
//定义函数引用 student 类;
void print(student& s)
{
cout << "调用print()函数" << endl;
//定义student类对象
student stud("wkf", "www.mylinux.vip", 13926572996);
}
程序编译异常,异常信息如下:
wkf@ubuntu:~/c++$ g++ test.cpp -o exe
test.cpp: In function ‘void print(student&)’:
test.cpp:12: error: variable ‘student stud’ has initializer but incomplete type
异常信息描述在print()函数中,定义的stud对象并不是合法的student类。因为,此时,编译器还没有编译student类,并不知道student类的具体定义,所以,无法使用student类来定义对象。
同样的道理,编译器还没有执行student类的编译时,无法知道student类的具体定义,所以,就不可以使用student类来定义对象。同时,也不可以访问student类中的成员。所以,我们把print()函数放在student类定义之前。程序测试代码如下:
编译运行结果如下:
wkf@ubuntu:~/c++$ g++ test.cpp -o exe
test.cpp: In function ‘void print(student&)’:
test.cpp:11: error: invalid use of incomplete type ‘struct student’
test.cpp:6: error: forward declaration of ‘struct student’
test.cpp:12: error: invalid use of incomplete type ‘struct student’
test.cpp:6: error: forward declaration of ‘struct student’
test.cpp:13: error: invalid use of incomplete type ‘struct student’
test.cpp:6: error: forward declaration of ‘struct student’
可以看到,在print()函数中引用student对象的成员变量,是非法的操作。
那么,根据这个思路,我们可以把print()函数放在student类定义来后面,就可以正确引用student类的成员了。测试代码如下:
程序运行结果如下:
可以看到,在student类定义的后面,在定义print()函数。那么,在print()函数中就可以访问student类的成员变量。
所以,充分理解这个机制,理解编译系统的工作过程,在编写代码的时候,就可以合理地设计代码,当编译C++代码,有编译错误的时候,可以知道怎么样解决。
特别是在设计模式中,多个类对象之间相互引用,此时,需要充分理解C++语言的特性,才可以根据设计模式的思想,编写出稳定、高效的代码。
3 友元类
不仅可以将一个函数声明为一个类的“朋友”,还可以将一个类(例如 B类)声明为另一个类(例如 A类)的“朋友”。这时B类就是A类的友元类。友元类B中的所有函数都是A类的友元函数,可以访问A类中的所有成员。
声明友元类的一般形式为:
friend class 类名;
所以,可以在A类的定义体中,可以使用如下语句声明B类为其友元类:
friend class B;
那么,类B是类A的友元类。所以,类B的成员函数就可以访问类A的私有成员。
如下是一个测试例子:
程序运行结果如下:
可以看到,定义了my_print类和student类。其中,在student类定义的时候,有:
friend class my_print; //声明友元类
所以,声明my_print类是student类的友元类。那么,my_print类的成员函数,就可以访问student类的私有成员。
所以,my_print::print()函数中,可以引用输出student类的name, addr等私有成员。
注意:my_print::print()函数的定义,需要在student类的后面,具体原理可以参考“编译器对类提前声明的处理”章节。就是上一章节,已经说了编译对类提前声明的处理。
有关友元,有下面两点需要说明:
(1) 友元的关系是单向的,而不是双向的。如果声明类A是类B的友元类,不等于类B也是类A的友元类。
(2) 友元的关系不能够传递,如果类B是类A 的友元,类C是类B的友元,并不等于类C就是类A的友元。
在实际工作中,除非确实有必要,否则,一般并不把整个类声明为友元类。而只将确实有需要的成员函数声明为友元函数,这样更方便安全一些。
有关友元利弊的分析:面向对象程序设计的一个基本原则是封装性和信息隐蔽,而友元却可以访问其他类中的私有成员,不能不说这是封装原则的一个小的破坏。但是,它能有助于数据共享,能提高程序的效率,在使用友元的时候,要注意到它的副作用,不用过多地使用友元,只有在使用它能使程序精炼,并大大提高程序的效率时才用友元。也就是说,要在数据共享和信息隐蔽之间选择一个恰当的平衡点。
总结
本章节我们学习了C++的友元特性,可以定义友元函数和友元类。
在面向对象编程的设计思想中,有一个重要的特性就是:信息的封装和隐藏;例如,C++提供了private关键字,可以定义C++类对象的成员类型,是“私有”属性,不可以被外界访问,从而降低对象之间传递消息的效率。
那么,为了提高对象之间传递消息的效率,就需要打破这种信息“隐藏”的特性。
所以,C++提供了友元的特性,可以解决这个问题。例如,C++声明一个函数是自己的“朋友(友元)”,那么,该函数就可以访问C++隐藏的信息。从而提高获取对象信息的效率。
读者要仔细分析,掌握好每一个知识点,然后,自己总结出对该知识点的理解。自己总结出来的知识,理解才深刻,才是自己掌握的知识。