• Effective C++条款22:将成员变量声明为private(Declare data members private)



    《Effective C++》是一本轻薄短小的高密度的“专家经验积累”。本系列就是对Effective C++进行通读:


    条款22:将成员变量声明为private

      OK,首先看一下为什么数据成员不应该是public的,然后我们将会看到应用在public数据成员上的论证同样适用于protected成员。最后够得出结论:数据成员应该是private的。

    1、为什么不将成员变量声明为 public

    1.1 语法一致性

      让我们从句法的一致性开始(见条款18)。如果数据成员不是public的,那么客户访问对象的唯一方法就是通过成员函数。如果所有的公共接口都是函数,客户就不必记住访问一个类的成员时是否使用括号了。这方便了客户的使用。

    1.2 对数据成员访问的精确控制

      如果一致性没有让你信服,那么使用函数可以使你对数据成员的访问有更加精确的控制呢?如果你将数据成员声明成public的,每个人对其都有读写权限,但是如果你使用函数来对值进行获取(get)或者设置(set),你就可以实现不可访问(no access),只读访问(read only)和读写(read-write)访问。如果你需要,你甚至可以实现只写(write-only)访问:

    class AccessLevels
    {
    public:
    		...
        int getReadOnly()const { return readOnly; }
        void setReadWrite(int value) { readWrite = value; }
        int getReadWrite()const { return readWrite; }
        void setWriteOnly(int value) { writeOnly = value; }
    private:
        int noAccess;  //外部不能进行任何操作
        int readOnly;  //外部只读
        int readWrite; //外部可读可写
        int writeOnly; //外部只写
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

      这种细微的划分访问控制是很有必要的,因为许多数据成员应该被隐藏起来。很少情况下需要所有的数据成员都有一个getter和一个setter。

    1.3 封装

      如果这个仍然不能说服你,那该是使出杀手锏的时候了:封装。如果你通过一个函数来实现对一个数据成员的访问,日后你可能会用计算来替代数据成员,使用你的类的任何客户不会觉察出类的变化。

      举个例子,假设正在写一个自动测试程序。当每汽车通过的时候,速度被计算出来,然后将结果保存在一个数据集中,这个数据集记录了迄今为止收集的所有速度数据:

    class SpeedDataCollection {
    	...
    public:
    	void addValue(int speed); // 增加一笔新数据
    	double averageSoFar() const; // 返回平均速度
    	...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

      现在考虑成员函数averageSoFar的实现。一种实现的方法是在类中定义一个数据成员,用来表示迄今为止所有速度数据的平均值。当averageSoFar被调用的时候,它只是返回这个数据成员的值。另外一种方法是在每次调用averageSoFar的时候重新计算平均值,这可以通过检查数据集中的每个数据值来做到。

      上一种方法使得每个SpeedDataCollection对象变大,因为你必须为保存平均速度,累积总量以及数据点数量的数据成员分配空间。然而,averageSoFar可以被很高效的实现出来;它只是一个返回平均速度的内联函数(见条款30)。相反,在请求的时候才计算平均值会使得averageSoFar运行非常缓慢,但是每个SpeedDataCollection对象会比较小。

      谁能确定哪个才是更好的呢?在一台内存吃紧的机器上,并且应用中对平均值的需要不是很频繁,每次计算平均值可能会是一个更好的选择。在一个对平均值需求频繁的应用中,速度很重要,但内存充足,你可能更喜欢将平均速度保存为数据成员。这里的重要一点是通过一个成员函数来访问平均值(也就是将其封装起来),你可以在这些不同实现之间来回切换,客户端至多只需要重新编译就可以了。(通过条款31中描述的技术,你甚至可以不用重新编译)

      将成员变量隐藏在函数接口后边,可以为“所有的实现”提供弹性。例如,它可以使下面这些实现变得很简单:当数据成员被读或者写的时候通知其它对象;验证类的不变性和函数的先置和后置条件;在多线程环境中执行同步等等。从其它语言(像Delphi和C#)转到C++的程序员将会识别出来C++的这种功能同其它语言中的“属性”是等同的,但是需要额外加一对括号。

      封装比它起初看起来要重要。如果你对客户隐藏你的数据成员(也就是封装它们),你就能够确保类能一直维持不变性,因为只有成员函数能够影响它们。进一步来说,你保留了日后对实现决策进行变动的权利。如果你没有将这些决策隐藏起来,你将会很快发现即使你拥有一个类的源码,但是你修改public成员的能力是及其受限的,因为如果修改public成员,太多的客户代码会被破坏。Public意味这没有封装,更实际的讲,未封装意味这不能变化,特别对被广泛使用的类更是如此。因此对广泛使用的类最需要进行封装,因为它们最能受益于将一个实现替换为一个更好的实现。

    2、最好也不要把成员变量声明为protected

      protected的论点与上面的其实也十分类似,事实上,它们是完全相同的,虽然一开始看上去不是这样。在论证数据成员不能为public时,句法一致性和细粒度访问控制这两个原因同样适用于protected成员,但是封装呢?protected数据成员不是有比public数据成员更好的封装性么?令人感到吃惊的回答是,它们不是。

    • 条款23将会介绍到,成员变量的封装性与“成员变量的内容改变(改变就是指移除)时所破坏的代码数量成反比”。也就是说封装性越好,成员变量改变时破坏的代码就越少。

    • 如果我们把成员变量声明为public,那么当我们把成员变量移除的时候,程序中很多的代码就会被破坏(封装性差,破坏性强)。

    • 如果我们把成员变量声明为protected,那么当我们在基类中把这个成员变量移除的时候,那么很多使用到这个成员变量的派生类将会出错(封装性差,破坏性强)。

    3、牢记

    • 切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微性划分访问控制、允许约束条件获得保证,并提供class作者以充分的实现弹性

    • protected并不比public更具封装性

    总结

    期待大家和我交流,留言或者私信,一起学习,一起进步!

  • 相关阅读:
    使用Axure RP和内网穿透技术制作静态站点并实现公网访问
    记录极致CMS非富文本标签调用不改变格式
    python进阶系列 - 11 python随机数
    离线部署欧拉系统OpenEuler20.03 LSP3 所需要的依赖,思路通用于各个Linux系统,看这一篇就够了
    京东工业API接口解析,实现根据ID取商品详情
    阿里云易立:以增效促降本,容器服务全面进入智能化时代
    <C++>再识构造函数和static成员
    关于:未同意隐私政策,应用获取ANDROID ID问题2
    JavaScript数据类型转换
    生产者-消费者问题详细分析【操作系统原理】
  • 原文地址:https://blog.csdn.net/CltCj/article/details/128140240