02 August 2022

C++ 核心指南目录

C.9: Minimize exposure of members

理由

数据封装。信息隐藏。避免意外访问。简化代码维护。

例子

template<typename T, typename U>
struct pair {
    T a;
    U b;
    // ...
};

因为 ab 是公开的,不管注释部分提供了什么接口, pair 的用户都可以随意且独立地修改 ab 的数据。在庞大的代码库中,我们难以轻松找到哪些代码修改了 pair 的哪些成员数据。如果我们想强化成员变量之间的关系,最好是把它们设置成私有数据,然后通过构造函数和成员函数来强化他们之间的约束关系(不变体, invariant )。

在以下例子中, meters 函数根据成员变量计算出以米为单位的距离数据。通过 set_unit 设置距离单位信息。这样,就不用直接访问成员数据进行计算,确保了 meters 返回的是正确的数值。想象一下,如果用户不知道 magnitudeunit 之间的关系,直接访问 magnitude 就可能用错数据值了。

class Distance {
public:
    // ...
    double meters() const { return magnitude*unit; }
    void set_unit(double u)
    {
            // ... check that u is a factor of 10 ...
            // ... change magnitude appropriately ...
            unit = u;
    }
    // ...
private:
    double magnitude;
    double unit;
    // 1 is meters, 1000 is kilometers,
    // 0.001 is millimeters, etc.
};

注意

如果哪些用户能直接访问哪些数据还不确定,那么就很难改进这个数据类型。 publicprotected 成员数据就是这种情况。

例子

类通常会提供两类接口给用户。一类接口是提供给继承此类的子类( protected ),另一类提供给普通用户( public )。比如,继承的类可以直接跳过运行时检查,因为继承关系已经确保运行时的数据是正确了。

class Foo {
public:
    int bar(int x) { check(x); return do_bar(x); }
    // ...
protected:
    int do_bar(int x); // do some operation on the data
    // ...
private:
    // ... data ...
};

class Dir : public Foo {
    //...
    int mem(int x, int y)
    {
        /* ... do something ... */
        return do_bar(x + y);
        // OK: derived class can bypass check
    }
};

void user(Foo& x)
{
    int r1 = x.bar(1);      // OK, will check
    int r2 = x.do_bar(2);   // error: would bypass check
    // ...
}

注意

尽量不要用 protected 级别的数据

注意

按照以下顺序排列成员变量:

  • public,
  • protected,
  • private

强化

  • 标记 protected 数据
  • 标记 publicprivate 数据混合在一起的地方