类成员包括类的成员变量和成员函数,它们分别用来描述类的属性和行为。而类成员的访问控制决定了哪些成员是公开的,能够被外界访问,也能够被自身访问;哪些成员是私有的,只能在类的内部访问,外界没法访问。就像一我的的钱包,只有他本身能动,别人是不能动的。又如同本身藏的私房钱也只有本身知道,对其余人而言,私房钱是彻底隐藏的。安全
你们可能会问,为何要对类成员的访问加以控制,大公无私地谁均可以访问不是挺好的吗?这是由于在现实世界中,人们对事物的访问是受到控制的,咱们能够访问到某些事物,但不可能访问到任何事物。就如同咱们只能知道本身钱包里有多少钱,而没法知作别人钱包里有多少钱同样。这一点反映到C++中,就成了类成员的访问控制。能够设想这样的情形:钱包里的钱只能由本身访问,别人是无权访问的。当它成为“人”这个类的一个属性,用某个成员变量来表示后,其访问天然也应该受到控制,只能由“人”这个类自身的行为(成员函数)来访问,对于外界其余函数而言,这个成员变量就被隐藏起来是不可见的,天然也就没法访问。若是不对访问加以控制,谁均可以访问,那就颇有可能其余函数(小偷)会错误地修改这个本不应它修改的数据,数据安全没法获得保证。换句话说,要想让咱们钱包里的钱安安稳稳的,要想让类的成员避免不安全的访问,咱们必须对类成员的访问加以控制。函数
在C++中,对类成员的访问控制是经过设置成员的访问级别来实现的。按照访问范围的大小,访问级别被分为公有类型(public)、保护类型(protected)和私有类型(private) 三种,如图6-9所示。spa
公有类型的成员用关键字public修饰,在成员变量或者成员函数前加上public关键字就表示这是一个公有类型的成员。公有类型的成员的访问不受限制,在类的内外均可以访问,但它更多的仍是做为类提供给外界访问的接口,是类跟外界交流的通道,外界经过访问公有类型的成员完成跟类的交互。例如Teacher类的表示上课行为的GiveLesson()成员函数,这是它向外界提供的服务,应该被外界访问,因此这个成员函数应该设置为公有类型。设计
图6-9 访问级别code
保护类型的成员用关键字protected修饰,其声明格式跟public类型相同。保护类型的成员在类的外部没法访问,可是能够在类的内部及继承这个类而得的派生类中访问,它主要用于将属性或者方法遗传给它的下一代子类。例如,对于Teacher类的表示姓名属性的m_strName成员变量,谁也不肯意本身的名字被别人修改,因此应该限制外界对它的访问;而显然本身能够修改本身的名字,因此Teacher类内部能够访问这个成员变量;同时,若是Teacher类有派生类,好比表示大学老师的Lecturer,咱们也但愿这个成员变量能够遗传给派生类,使其派生类也拥有这个成员变量,能够对其进行访问。通过这样的分析,把m_strName成员变量设置为保护类型最为合适。对象
私有类型的成员用关键字private修饰,其声明格式跟public类型相同。私有类型的成员只能在类的内部被访问,全部来自外部的访问都是非法的,这样就把类的成员彻底隐藏在类当中,很好地保护了类中数据和行为的安全。因此,赶快把咱们的钱包声明为私有成员吧,这样小偷就不会访问到它了。blog
这里须要说明的是,若是class关键字定义的类当中类成员没有显式地说明它的访问级别,那么默认状况下它就是私有类型,外界是没法访问的,因而可知类实际上是很是“自私”的(相反地,由struct定义的类中,默认的访问级别为公有类型)。继承
咱们把前面例子中的Teacher类根据访问控制加以改写,以更加真实地反映现实的状况:接口
// 进行访问控制后的Teacher类 class Teacher { // 公有类型成员 // 外界经过访问这些成员跟该类进行交互,得到类提供的服务 public: // 冒号后的变量或者函数都受到它的修饰 // 构造函数应该是公有的,这样外界才能够利用构造函数建立该类的对象 Teacher(string strName) : m_strName(strName) { // … } // 老师要为学生们上课,它应该被外界调用,因此这个成员函数是公有类型的 void GiveeLesson() { // 在类的内部,能够访问自身的保护类型和私有类型成员 PrepareLesson(); // 先备课,访问保护类型成员 cout<<"老师上课。"<<endl; m_nWallet += 100; // 一节课100块钱,访问私有类型成员 } // 咱们不让别人修更名字,可总得让别人知道咱们的名字吧, // 对于只可供外界只读访问的成员变量, // 能够提供一个公有的成员函数供外界对其进行读取访问 string GetName() { return m_strName; } // 保护类型成员 // 不能被外界访问,可是能够被自身访问,也能够遗传给下一级子类, // 供下一级子类访问 protected: // 只有本身备课,子类也须要备课,因此设置为保护类型 void PrepareLesson() { cout<<"老师备课。"<<endl; } // 只有本身能够修改本身的名字,子类也须要这样的属性 string m_strName; private: // 私有类型 int m_nWallet; // 钱包只有本身能够访问,因此设置为私有类型 };
有了访问控制以后,如今再对Teacher类对象的成员进行访问时就须要注意了,咱们只能在外界访问其公有成员,若是试图访问其保护类型或者私有类型的成员,就会被绝不留情地拒之门外,吃人家的闭门羹:生命周期
int main() { // 建立对象时会调用类的构造函数 // 在Teacher类中,构造函数是公有类型,因此能够直接调用 Teacher MrChen("ChenLiangqiao"); // 外部变量,用于保存从对象得到的数据 string strName; // 经过类的公有类型成员函数,读取得到类中的保护类型的成员变量 strName = MrChen.GetName(); // 错误:没法直接访问类的保护类型和私有类型成员 // 想改个人名字?先要问我答应不答应 MrChen.m_strName = "WangGang"; // 想从我钱包中拿走200块,那更不行了 MrChen.m_nWallet -= 200; return 0; }
在主函数中,咱们首先建立一个Teacher类对象,这个建立过程会调用它的构造函数,这就要求构造函数是公有类型。在构造函数中,咱们会访问成员变量m_strName,虽然它是保护类型的,可是能够在类自身的构造函数中对其进行修改。另外一方面,为了让外界也可以安全地读取该成员变量的值,咱们为Teacher类添加了一个公有类型的成员函数GetName(),这样就能够经过它让外界访问类中保护类型的成员得到必要的数据。除此以外,Teacher类还提供了一个公有类型的GiveLesson()函数,外界能够直接调用这个函数得到Teacher类提供的上课服务。而在这个公有类型的成员函数内部,咱们还访问了类中的保护类型和私有类型成员。
以上的访问都是合理合法的,可是,若是想在类的外部直接访问保护类型或私有类型的成员,编译器会帮咱们检测出这个非法访问,产生一个编译错误提示没法访问保护或私有类型的成员。这样,有编译器帮咱们看着,小偷就再也别想动咱们的钱包了。
经过对类成员的访问进行控制,起到了很好地保护数据和行为的做用,防止了数据被外界随意修改,同时也限制了外界使用不合理的行为。若是对象的某些成员变量由于业务逻辑的须要容许外界访问(好比这里的m_strName),也建议采用提供公有接口的方法,让外界经过公有接口来访问这些成员变量,而不是直接把这些成员变量设置为公有类型。通常状况下,类的成员变量都应该设置为保护或私有类型。
采用类成员的访问控制机制以后,很好地实现了数据和行为的隐藏,这种隐藏有效地避免了来自外界的非法访问,保护了数据和行为的安全。可是,这种严格的成员访问控制是不问原因的,任何来自外界的对类中隐藏信息(保护或私有类型成员)的访问都会被拒绝,这样天然也会将一些合理的访问挡在门外,给类成员的访问带来一些麻烦。例如,有时须要定义某个函数,这个函数不是类的一部分,但又须要频繁地访问类的隐藏信息;又或者须要定义某个新的类,由于某种缘由,这个类须要访问另一个类的隐藏信息,就像现实世界中老婆须要访问老公的钱包同样。在这些状况下,咱们须要从外界直接访问类的保护或私有类型成员,但却被严格的成员访问控制机制挡在了门外。
凡事都有例外。为了给访问控制机制开个后门,让外界值得信任的函数或者类可以访问某个类的隐藏信息,C++提供了友元机制。它利用“friend”关键字,能够将外界的某个函数或者类声明为类的友元函数或者友元类,二者统称为友元。当成为类的友元后,就能够对类的隐藏信息进行访问了。
友元函数其实是一个定义在类外部的普通函数,它不属于任何类。当使用“friend”关键字在类的定义中加以声明后,这个函数就成为类的友元函数,以后它就能够不受类成员访问控制的限制,直接访问类的隐藏信息。在类中声明友元函数的语法格式以下:
class 类名 { friend 返回值类型 函数名(形式参数列表); // 类的其余声明和定义… };
友元函数的声明跟类的普通成员函数的声明是相同的,只不过在函数声明前加上了friend关键字修饰而且定义在类的外部,并不属于这个类。友元函数的声明不受访问控制的影响,既能够放在类的私有部分,也能够放在类的公有部分,它们是没有区别的。另外,一个函数能够同时是多个类的友元函数,只是须要在各个类中分别声明。
跟友元函数类似,友元类是定义在某个类以外的另一个独立的普通类。由于须要访问这个类的隐藏信息,因此利用“friend”关键字将其声明为这个类的友元类,赋予它访问这个类的隐藏信息的能力。成为这个类的友元类以后,友元类的全部成员函数也就至关于成为了这个类的友元函数,天然也就能够访问这个类中的隐藏信息。
在C++中,声明友元类的语法格式以下:
class 类名 { friend class 友元类名; // 类的其余声明和定义 };
友元类的声明跟友元函数的声明相似,这里就再也不赘述。惟一须要注意的是这里两个类之间的相互关系,若是咱们但愿A类可以访问B类的隐藏信息,就在B类中将A类声明为它的友元类。这就表示A类是B类通过认证后的值得信赖的“朋友”,这样A类才能够访问B类的隐藏信息。为了更好地理解友元的做用,仍是来看一个实际的例子。假设在以前定义的Teacher类中有一个成员变量m_nSalary记录了老师的工资信息。工资信息固然是我的隐私须要保护了,因此将其访问控制级别设置为保护类型,只有本身和派生的子类能够访问:
class Teacher { // … // 保护类型的工资信息 protected: int m_nSalary; };
将m_nSalary 设置为保护类型,能够很好地保护数据安全。可是,在某些特殊状况下,咱们又不得不在外界对其进行访问。好比,税务局(用TaxationDep类表示)要来查老师的工资收入,他固然应该有权力也有必要访问Teacher类中m_nSalary这个保护类型的成员;又或者学校想用AdjustSalary()函数给老师调整工资,老师天然乐意它来访问m_nSalary这个保护类型的成员。在这种状况下,咱们就须要把TaxationDep类和AdjustSalary()函数声明为Teacher类的友元,让它们能够访问Teacher类的隐藏信息:
// 拥有友元的Teacher类 class Teacher { // 声明TaxationDep类为友元类 friend class TaxationDep; // 声明AdjustSalary()函数为友元函数 friend int AdjustSalary(Teacher* teacher); // 其余类的定义… protected: int m_nSalary; // 保护类型的成员 }; // 在类的外部定义的友元函数 int AdjustSalary(Teacher* teacher) { // 在Teacher类的友元函数中访问它的保护类型的成员m_nSalary if( teacher != nullptr && teacher->m_nSalary < 1000) { teacher->m_nSalary += 500; // 涨工资 return teacher->m_nSalary; } return 0; } // 友元类 class TaxationDep { // 类的其余定义… public: void CheckSalary( Teacher* teacher ) { // 在Teacher类的友元类中访问它的保护类型成员m_nSalary if(teacher != nullptr && teacher->m_nSalary > 1000) { cout<<"这位老师应该交税"<<endl; } } };
能够看到,当Teacher类利用“friend”关键字将AdjustSalary()函数和TaxationDep类声明为它的友元以后,在友元中就能够直接访问它的保护类型的成员m_nSalary,这就至关于为友元打开了一个后门,使其能够翻过访问控制这道保护墙而直接访问到类的隐藏信息。
友元虽然能给咱们带来必定的便利,可是“开后门”毕竟不是一件多么正大光明的事情,在使用友元的时候,还应该注意如下几点:
l 友元关系不能被继承。这一点很好理解,咱们跟某个类是朋友(便是某个类的友元类),并不表示咱们跟这个类的儿子(派生类)一样是朋友。
l 友元关系是单向的,不具备交换性。好比,TaxationDep类是Teacher类的友元类,税务官员能够检查老师的工资,但这并不表示Teacher类也是TaxationDep类的友元类,老师也能够检查税务官员的工资。
l 将某个函数或者类声明为友元,即意味着对方是通过审核认证的,是值得信赖的,因此才受权给它访问本身的隐藏信息。这也提示咱们在将函数或类声明为友元以前,有必要对其进行必定的审核。只有值得信赖的函数和类才能将其声明为友元。
友元的使用并无破坏封装
在友元函数或者友元类中,咱们能够直接访问类的保护或私有类型的成员,这个“后门”打开后,不少人担忧这样会让类的隐藏信息暴露出来,破坏类的封装。但事实是,合理地使用友元,不只不会破坏封装,反而会加强封装。
在面向对象的设计中,咱们强调的是“高内聚、低耦合”的设计原则。当一个类的不一样成员变量拥有两个不一样的生命周期时,为了保持类的“高内聚”,咱们常常须要将这些不一样生命周期的成员变量分割成两部分,也就是将一个类分割成两个类。在这种状况下,被分割的两部分一般须要直接存取彼此的数据。实现这种状况的最安全途径就是使这两个类成为彼此的友元。可是,一些“高手”想固然地认为友元破坏了类的封装,转而经过提供公有的 get()和set()成员函数使两部分能够互相访问数据。实际上,他们不知道这样的作法正是破坏了封装。在大多数状况下,这些 get()和set()成员函数和公有数据同样差劲:它们仅仅隐藏了私有数据的名称,而没有隐藏对私有数据的访问。
友元的使用,只是向必要的客户公开类的隐藏数据,好比Teacher类只是向TaxationDep类公开它的隐藏数据,只是让税务官能够知道它的工资是多少。这要远比使用公有的get()/set()成员函数,而让全世界均可以知道它的工资要安全隐蔽的多。