引言
OOP 的三大概念,这篇文章讲的是最基本的 「封装」。
理解封装
OOP 编程最基本的概念就是封装。我们通过把函数,变量的实现细节包装成一个单一整体,向外表现 一致 的的行为的技术。
为什么要封装
- 修改量减小,降低代码修改风险
- 修改弹性大,内部可调整程度高
- 提高复用性,好的封装会使类趋向于一个整体,减颗粒度
好的封装:
职能
类职能单一
一个类的功能越单一,它的职能就越纯粹,内部逻辑就越简单,可重用性就越好。语义一致性也就越强。而很多人写程序都喜欢一个 class 到底,这个 class 能处理输入,转换字符串,连接数据库 ……
函数职能单一
优点同上,而且还可以带来一个好处:异常安全
template <typename T>
class stack {
...
T pop_top();
...
};
template <typename T>
class stack {
...
T top();
void pop();
...
};
是不是看起来第一种比较好呢?我当时也是这么认为的,因为我们可以写出这样的语句 int x = s.pop_top();
而不是 int x = s.top(); s.pop();
但是你会发现,STL 库种大量使用了后一种,就是因为,在前一种情况下 不可能保证强异常安全。
一旦异常发生,前一种最多只能保证 弱异常安全 即:异常会正常抛出到调用者,可能会出现脏数据,容器可能会处于一个不一致的状态。
C++ 的容器都是异常安全的
想详细了解本内容可以参考 《Exceptional C++》条款 8 - 17
成员,函数
如何衡量封装性
- 外界看到的越多,封装性越差
- 当修改发生时,影响越大,封装性越差
成员
将成员变量声明为
private
——《Effective C++》条款22
private成员会带来哪些好处
- 外界看到的少了,访问成员必须通过接口
- 通过接口,访问过程变得受限,我们可以对数据进行一致性检查等
- 当修改发生时,我们可以通过修改接口来保证对外行为不变。
你需要做的,就是给每一个成员写上一对访问函数
而 public 与 protect 访问权限本质上是一样的, protect 并没有提供比 public 更好的封装程度。protect 其实就是 “面向子类的 private”。同样含有相同的问题。
如果你发现你的类里面充满了毫无意义的访问器,你应该看一看下一条
函数
宁以 非成员、非友元函数 替换 成员函数 ——《Effective C++》条款23
成员函数的封装性比非成员差
基于一下几个理由:
- 同样的功能如果既可以被成员函数,也可以被非成员函数实现,非成员函数一定会通过不少于成员函数调用完成
- 调用越多,中间层就越多,灵活性与安全性就越好
- 成员函数的访问能力比非成员函数高
因此在 实现同样功能下请选择非成员函数
同样的,友元函数破环了封装。
struct 仅提供 POD & 值语义
本条更倾向于语义,而且更多是是个人建议与偏好
POD
C++ 中有这样的一个概念 POD。
POD 这种类型十分简单,没有虚函数虚基类,最关键的是,它可以使用 memcpy() 来直接操作,这种类型没有复杂的内存结构,也没有复杂的继承链。
而 C 语言中的 struct 都是 POD,它们可以直接与系统底层进行交互,所以,我建议。所有的 POD 都应该是 struct。
值语义
struct 和 class 在 C++ 中一直是一个奇怪的存在,除了默认的访问权限不同之外,它们的表现是一样的。
对于我来说,struct 提供的是 值语义 ,而 class 是类语义。
struct 提供的是 数据的聚集。 打个比方说:
struct id_card_t {
std::string name;
int age;
std::string address;
};
struct Person {
public:
std::string getName();
int getAge();
std::string getAddress();
void walk();
void saySomething(const std::string&);
void eat();
Person();
private:
std::string name;
int age;
std::string address;
};
后面这个 Person 很显然是一个类,而不仅仅是数据的聚集。 Person 含有更多的行为,而且有可能参与继承。因此,Person 应该使用 class。
虽然以上例子中,Person 是一个 POD,但是依然不适合用 struct 来表达。
值语义比 POD 约束性更强,而且请组合 struct,不要继承。