不变式: 类和结构体的区别 (class invariant)#
前置内容
类 (class
) 和结构体 (struct
) 到底有什么区别?
从语法上, 它们没有区别, 写 struct Widget
或者 class Widget
都是自定义了类型 Widget
.
那么为什么要专门分成两个关键词呢? 这就要引入 C++ 的一个核心概念, 类的不变式 (class invariant).
示例: 最简分式#
C++ Core Guidelines (C++ 核心准则) 将不变式定义为 "程序某一时刻或某一段时间里必须得到满足的条件", 对于类而言, 就是 类的数据成员自己和之间的逻辑关系.
例如我们打算实现最简分式类, 则根据最简分式的数学定义, 可以将分子、分母作为数据成员, 并且分子分母应该满足下列关系:
分子和分母之间不应该具有除 1 以外的公因数.
分母不为 0.
最简分式结构体#
假如我们将该类型作为一个结构体, 按照结构体的习惯, 将分子分母作为公用 (public
) 数据成员, 可以简单地实现为以下代码:
1struct Irreducible_fraction {
2 public:
3 int numerator; // 分子
4 int denominator; // 分母
5};
完成了!
可是, 如何保证使用者不破坏 "最简分式" 的定义? 他可以对数据成员做任何事!
1int main() {
2 Irreducible_fraction fraction1 = {1, 3}; // 1/3, 是最简分式
3 fraction1.numerator = 3; // 3/3, 分子和分母之间有公因数 3, 这不是最简分式!
4 fraction1.denominator = 0; // 3/0, 分母为 0, 这不是最简分式!
5
6 Irreducible_fraction fraction2 = {3, 3}; // 3/3, 从构造起就不是最简分式了!
7}
使用者为什么要破坏我们的最简分式? 因为我们让分子分母是公用数据成员, 而公用就是开放给他们任意使用的.
阻止使用者破坏最简分式#
上面的例子中, 使用者有两处地方可以破坏我们的最简分式:
对象构造时:
Irreducible_fraction fraction2 = {3, 3};
. 这意味着我们应该规定如何构造对象——我们应该定义构造函数.对象使用时:
fraction1.numerator = 3;
. 这意味着我们应该规定如何使用对象——我们应该将数据设为私用成员 (private
), 用公用成员函数告知使用者应该怎么访问.
即,
1#include <exception> // std::terminate()
2#include <numeric> // C++17 提供了求最大公因数函数 gcd
3
4struct Irreducible_fraction {
5 public:
6 Irreducible_fraction(int numerator, int denominator) {
7 if (denominator == 0) { // 分母为 0
8 std::terminate(); // 进行错误处理, 此处选择直接终止程序
9 }
10
11 numerator_ = numerator;
12 denominator_ = denominator;
13 revert_to_irreducible();
14 }
15
16 void add_by(Irreducible_fraction const& other) {
17 /* 进入成员函数时, 不变式成立 */
18
19 /* 经过函数内的运算, 不变式可能被打破了 */
20 numerator_
21 = numerator_ * other.denominator_ + other.numerator_ * denominator_;
22 denominator_ *= other.denominator_;
23
24 /* 退出成员函数前, 恢复不变式 */
25 revert_to_irreducible();
26 }
27
28 private:
29 void revert_to_irreducible() { // 化简分式从而恢复不变式
30 int const gcd = std::gcd(numerator_, denominator_);
31 numerator_ /= gcd;
32 denominator_ /= gcd;
33 }
34
35 int numerator_; // 分子
36 int denominator_; // 分母
37};
38
39int main() {
40 Irreducible_fraction lhs(2, 6); // 1/3, 是最简分式!
41 Irreducible_fraction rhs(3, 6); // 1/2, 是最简分式!
42 lhs.add_by(rhs); // 5/6, 是最简分式!
43}
最简分式类#
C++ 因此特意引入了新的关键字 class
, 当使用关键字 class
, 我们就是在告诉未来的代码读者 (可能是别人, 也可能是你自己) 这个类具有不变式.
1// ↓使用 class 而非 struct, 语法上没有任何区别, 但告诉读者这个类具有不变式
2class Irreducible_fraction {
3 /* 与上面的代码完全相同 */
4};
综上所述, class
和 struct
在语法上没有区别, 我们基于语义上有无不变式来进行选择:
如果有不变式, 使用
class
而非struct
.如果有不变式, 则需要使用构造函数来建立不变式.
如果有不变式, 则默认的拷贝行为可能无法维护该不变式, 我们也许需要自定义拷贝构造/赋值函数和析构函数.
如果有不变式, 则需要将数据设为私用并定义公用成员函数, 从而限制使用者如何使用.
因为类存在不变式, 才有了构造函数、拷贝构造/赋值函数、析构函数, 才有了公用、私用等访问说明, 才有了成员函数…… 理解了类的不变式, 就能据此推导理解类的其他特性.
语法上的区别#
虽然前面一直说 class
和 struct
在语法上没有区别, 但还有细微的差异:
class
默认访问说明符为private
, 这其实隐含了它应该将数据设为私用, 即拥有不变式.struct
默认访问说明符为public
, 这其实隐含了它应该将数据设为公用, 即没有不变式.
但干嘛要记忆这种默认如何如何呢? 不直接写明访问说明符是啥就好了!
1struct Widget {
2 public: // 管它默认是什么, 我直接写明不就好了
3};
所以这里说 class
和 struct
在语法上没有区别, 唯一的区分在于是否有不变式.
最佳实践#
如果类具有不变式, 则使用 class
; 如果数据成员相互独立, 则使用 struct
. 即,
要么
class
+ 构造函数 (+ 私用数据成员).要么
struct
+ 无构造函数 (+ 公用数据成员).
一个教学的黑点#
教学里完全没有不变式这个知识点: "这是类的语法, 这是我们怎么定义构造函数, 这是我们怎么定义成员函数, 这是公用私用的区别……", 但从来不会解释为什么要有这些.