你好,C++(35)类是如何藏私房钱的?6.2.4 拷贝构造函数

6.2.6  类成员的访问控制

类成员包括类的成员变量和成员函数,它们分别用来描述类的属性和行为。而类成员的访问控制决定了哪些成员是公开的,可以被外界访问,也可以被自身访问;哪些成员是私有的,只能在类的内部访问,外界无法访问。就像一个人的钱包,只有他自己能动,别人是不能动的。又如同自己藏的私房钱也只有自己知道,对其他人而言,私房钱是完全隐藏的。

大家可能会问,为什么要对类成员的访问加以控制,大公无私地谁都可以访问不是挺好的吗?这是因为在现实世界中,人们对事物的访问是受到控制的,我们可以访问到某些事物,但不可能访问到任何事物。就如同我们只能知道自己钱包里有多少钱,而无法知道别人钱包里有多少钱一样。这一点反映到C++中,就成了类成员的访问控制。可以设想这样的情形:钱包里的钱只能由自己访问,别人是无权访问的。当它成为“人”这个类的一个属性,用某个成员变量来表示后,其访问自然也应该受到控制,只能由“人”这个类自身的行为(成员函数)来访问,对于外界其他函数而言,这个成员变量就被隐藏起来是不可见的,自然也就无法访问。如果不对访问加以控制,谁都可以访问,那就很有可能其他函数(小偷)会错误地修改这个本不该它修改的数据,数据安全无法得到保证。换句话说,要想让我们钱包里的钱安安稳稳的,要想让类的成员避免不安全的访问,我们必须对类成员的访问加以控制。

在C++中,对类成员的访问控制是通过设置成员的访问级别来实现的。按照访问范围的大小,访问级别被分为公有类型(public)、保护类型(protected)和私有类型(private) 三种,如图6-9所示。

1. 公有类型

公有类型的成员用关键字public修饰,在成员变量或者成员函数前加上public关键字就表示这是一个公有类型的成员。公有类型的成员的访问不受限制,在类的内外都可以访问,但它更多的还是作为类提供给外界访问的接口,是类跟外界交流的通道,外界通过访问公有类型的成员完成跟类的交互。例如Teacher类的表示上课行为的GiveLesson()成员函数,这是它向外界提供的服务,应该被外界访问,所以这个成员函数应该设置为公有类型。

 

图6-9  访问级别

2. 保护类型

保护类型的成员用关键字protected修饰,其声明格式跟public类型相同。保护类型的成员在类的外部无法访问,但是可以在类的内部及继承这个类而得的派生类中访问,它主要用于将属性或者方法遗传给它的下一代子类。例如,对于Teacher类的表示姓名属性的m_strName成员变量,谁也不愿意自己的名字被别人修改,所以应该限制外界对它的访问;而显然自己可以修改自己的名字,所以Teacher类内部可以访问这个成员变量;同时,如果Teacher类有派生类,比如表示大学老师的Lecturer,我们也希望这个成员变量可以遗传给派生类,使其派生类也拥有这个成员变量,可以对其进行访问。经过这样的分析,把m_strName成员变量设置为保护类型最为合适。

3. 私有类型

私有类型的成员用关键字private修饰,其声明格式跟public类型相同。私有类型的成员只能在类的内部被访问,所有来自外部的访问都是非法的,这样就把类的成员完全隐藏在类当中,很好地保护了类中数据和行为的安全。所以,赶快把我们的钱包声明为私有成员吧,这样小偷就不会访问到它了。

这里需要说明的是,如果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),也建议采用提供公有接口的方法,让外界通过公有接口来访问这些成员变量,而不是直接把这些成员变量设置为公有类型。一般情况下,类的成员变量都应该设置为保护或私有类型。

6.2.7  在友元中访问类的隐藏信息

采用类成员的访问控制机制之后,很好地实现了数据和行为的隐藏,这种隐藏有效地避免了来自外界的非法访问,保护了数据和行为的安全。但是,这种严格的成员访问控制是不问缘由的,任何来自外界的对类中隐藏信息(保护或私有类型成员)的访问都会被拒绝,这样自然也会将一些合理的访问挡在门外,给类成员的访问带来一些麻烦。例如,有时需要定义某个函数,这个函数不是类的一部分,但又需要频繁地访问类的隐藏信息;又或者需要定义某个新的类,因为某种原因,这个类需要访问另外一个类的隐藏信息,就像现实世界中老婆需要访问老公的钱包一样。在这些情况下,我们需要从外界直接访问类的保护或私有类型成员,但却被严格的成员访问控制机制挡在了门外。

凡事都有例外。为了给访问控制机制开个后门,让外界值得信任的函数或者类能够访问某个类的隐藏信息,C++提供了友元机制。它利用“friend”关键字,可以将外界的某个函数或者类声明为类的友元函数或者友元类,两者统称为友元。当成为类的友元后,就可以对类的隐藏信息进行访问了。

1. 友元函数

友元函数实际上是一个定义在类外部的普通函数,它不属于任何类。当使用“friend”关键字在类的定义中加以声明后,这个函数就成为类的友元函数,之后它就可以不受类成员访问控制的限制,直接访问类的隐藏信息。在类中声明友元函数的语法格式如下:

class 类名
{
    friend 返回值类型 函数名(形式参数列表);
// 类的其他声明和定义…
};

友元函数的声明跟类的普通成员函数的声明是相同的,只不过在函数声明前加上了friend关键字修饰并且定义在类的外部,并不属于这个类。友元函数的声明不受访问控制的影响,既可以放在类的私有部分,也可以放在类的公有部分,它们是没有区别的。另外,一个函数可以同时是多个类的友元函数,只是需要在各个类中分别声明。

2. 友元类

跟友元函数相似,友元类是定义在某个类之外的另外一个独立的普通类。因为需要访问这个类的隐藏信息,所以利用“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()成员函数,而让全世界都可以知道它的工资要安全隐蔽的多。

posted @ 2015-04-01 14:45  你好,C++  阅读(1366)  评论(4编辑  收藏  举报