Lesson 12:类的基本

以类、对象、数据成员、成员函数和访问控制维护对象不变量。

学习目标

  • 能解释类是类型定义、对象是该类型实例,并区分数据成员与成员函数
  • 能实现 public/private 边界,让状态只能通过保持不变量的操作修改
  • 能分析对象创建、成员访问与方法调用路径,发现未初始化状态和泄漏表示的问题

来源、版次与标准边界

“Lesson 12:类的基本”以 SB Creative 出版社书页核定高桥麻奈著、第 5 版、2017 年 6 月 14 日发行、ISBN 978-4-7973-9259-3;出版社页面标 596 页。CiNii Books 书目与 16 课目录记录正文 xxiii+571 页,日本国会图书馆书目记录 571 页。页数口径存在前置页差异,因此本课程不拿页码充当目录证据。

“Lesson 12:类的基本”的中文说明、示例、交互、练习和答案均为独立教学重写。公开资料只确认 16 个 Lesson 标题;本页列出的细分概念是课程教学映射,不冒充原书逐级小节。技术规则参考 ISO C++ 标准入口复核,但 2017 年教材的 Visual Studio 2017 语境不会被静默升级为 C++23;现代写法只能明确标为迁移说明。

围绕“怎样让每个公开操作都保持对象有效,而不是把 private 仅当作语法限制?”,本页要求保留“类接口、构造初值、对象不变量、公开操作前后状态、非法输入和封装反例。”。若故障“暴露可写数据成员,使调用者绕过验证直接破坏状态”无法在同一输入下制造首个分岔,应拒绝当前解释,而不是追加随机样例。

为什么公开字段不足以维护规则

结构体可以把数据组合成记录,但若任何代码都能直接把余额写成非法值、把分数设为 500,规则就分散在所有调用点。类把数据和操作放进同一类型,并通过访问控制决定调用者能直接看到什么。

class Score {
public:
    void set(int value);
    int get() const;
 
private:
    int value_ = 0;
};

调用者看到 set/get 契约,看不到 value_ 的表示细节。未来即使把存储改成较小整数或增加有效标志,只要公开接口不变,调用代码无需理解内部变化。

数据成员保存每个对象的状态

数据成员属于对象。两个 Score 对象各自拥有一份 value_;修改一个不会自动修改另一个。成员声明顺序还会影响初始化顺序,后续学习构造函数时必须以声明顺序为准。

Score math;
Score english;
 
math.set(95);
english.set(88);

状态应使用能表达不变量的最小集合。若一个字段可以从其他字段计算出来,同时保存两份就需要永久同步;能在需要时计算的派生值通常不应成为独立可写成员。

成员函数操作当前对象

成员函数定义类提供的行为,普通非 static 成员调用时隐含当前对象。math.set(95) 选择 math 为当前对象,再把 95 传入参数;函数体访问的 value_ 因而属于 math。

void Score::set(int value) {
    if (value < 0) value = 0;
    if (value > 100) value = 100;
    value_ = value;
}
 
int Score::get() const {
    return value_;
}

类外定义使用 Score:: 指明成员所属作用域。声明末尾的 const 表示该调用不通过当前对象修改其可观察状态,因此 const Score 也能调用 get。

public 与 private 建立访问边界

public 成员是调用者可依赖的接口,private 成员只允许类自身和特定友元访问。class 默认 private,struct 默认 public;二者都可以拥有方法、访问标签和构造函数,核心差异不是“struct 只能放数据”。

Score score;
score.set(120);       // 公开操作,内部归一化为 100
std::cout << score.get();
// score.value_ = 120; // private,外部不可直接写

private 不等于安全加密,也不防对象自身实现写错;它缩小可直接破坏状态的代码范围,使不变量只需在少数入口维护。

类不变量定义所有公开操作后的合法状态

类不变量是在对象可被外部观察的每个稳定时刻都应成立的事实。Score 的不变量是 0 <= value_ <= 100。每个构造入口和可写 public 成员都必须保持它;只读成员可以依赖它。

只在调用点检查不够,因为可能有多个调用者;只在 get 时修正又让对象长期处于非法状态。最好在状态进入对象的边界验证,使内部方法在统一前提下工作。

对象创建必须建立首个合法状态

上例用 int value_ = 0 给成员默认初始化器,因此默认创建 Score 后不变量成立。若成员没有可靠初值,对象在第一次 set 前可能无效。下一课会系统学习构造函数;此处先建立最低标准:对象一旦构造完成,就应可安全调用所有公开方法。

class Counter {
public:
    int value() const { return value_; }
    void increment() { ++value_; }
 
private:
    int value_ = 0;
};

成员默认初始化器是建立初值的一种方式。涉及参数校验、资源获取或多个成员依赖时,需要构造函数集中完成。

点运算符与箭头运算符选择对象路径

对象使用 . 调用成员,指针使用 ->,后者等价于先解引用再用点:pointer->get() 对应 (*pointer).get()。无论哪条路径,成员访问仍受 public/private 约束。

Score current;
Score* pointer = &current;
 
current.set(70);
pointer->set(80);

通过指针调用前还要证明指针有效;类封装保护状态规则,不会替调用者修复空指针或悬空生命周期。

this 表示当前对象

成员函数内部可以用 this 指针访问当前对象,this->value_ 与无歧义时的 value_ 等价。参数与成员同名时常见 this->value = value,但使用尾部下划线等命名规则可以减少遮蔽。

this 不应泄漏成长期悬空地址。若成员函数返回 this 或把它注册到外部回调,必须确保外部使用期不超过对象生命周期。

从接口测试而不是窥探 private

测试 Score 应通过公开接口输入 -1、0、100、101,再观察 get;无需直接访问 value_。这种测试对内部表示重构更稳定,也验证了真实调用者能依赖的契约。

先预测每个输入后的稳定状态,并检查多个对象互不影响。若测试必须破解 private 才能建立状态,通常说明公开接口缺少必要操作,或测试过度绑定实现。

三步设计最小类

分步1 / 3

第一步:写状态与不变量

列出对象必须保存的最小状态,删除可计算重复字段,并写出构造完成后始终成立的范围与关联规则。

正式节点与章专属证据

  • :在“Lesson 12:类的基本”中核对输入、状态变化、失败模式和可复现证据;第 1 个节点必须能回到“对象从构造完成到析构开始始终满足公开声明的不变量。”。
  • 对象:在“Lesson 12:类的基本”中核对输入、状态变化、失败模式和可复现证据;第 2 个节点必须能回到“对象从构造完成到析构开始始终满足公开声明的不变量。”。
  • 数据成员:在“Lesson 12:类的基本”中核对输入、状态变化、失败模式和可复现证据;第 3 个节点必须能回到“对象从构造完成到析构开始始终满足公开声明的不变量。”。
  • 成员函数:在“Lesson 12:类的基本”中核对输入、状态变化、失败模式和可复现证据;第 4 个节点必须能回到“对象从构造完成到析构开始始终满足公开声明的不变量。”。
  • 访问控制:在“Lesson 12:类的基本”中核对输入、状态变化、失败模式和可复现证据;第 5 个节点必须能回到“对象从构造完成到析构开始始终满足公开声明的不变量。”。

先用输入合同检查本页正式节点,再在相同初值下逐步比较正常和失败轨迹,最后只启用“暴露可写数据成员,使调用者绕过验证直接破坏状态”完成反例与复位。三个交互都必须能独立重置,且重置后再次满足“对象从构造完成到析构开始始终满足公开声明的不变量。”。

输入与状态合同

Lesson 12:类的基本

怎样让每个公开操作都保持对象有效,而不是把 private 仅当作语法限制?

必须先声明

为类声明输入类型、有效范围、对象生命周期和失败策略。

可复核证据

保存Lesson 12:类的基本的原始输入、初值与第一条可检查诊断。

正式节点:类、对象、数据成员、成员函数、访问控制

编译与运行轨迹

同一输入下比较正常与失败路径

  1. 01构造有效对象
  2. 02通过公开成员验证输入
  3. 03更新私有状态
  4. 04从公开观察验证不变量

不变量:对象从构造完成到析构开始始终满足公开声明的不变量。

故障定位与复位

一次只破坏一个前提

基线可继续:对象从构造完成到析构开始始终满足公开声明的不变量。

小结

  • 类定义状态、操作与访问边界,对象是拥有独立状态和生命周期的具体实例
  • 数据成员保存每个对象状态,成员函数通过当前对象操作这些成员
  • public 是调用者可依赖的接口,private 缩小直接破坏表示的范围但不替代正确实现
  • 类不变量必须在构造完成和每个公开操作结束后成立,接口应表达领域行为而非机械字段映射
  • 点访问对象、箭头访问指针目标;封装不会自动解决调用路径的空指针和生命周期问题

练习

  1. 问题 1:区分类与对象。 class ScoreScore math; Score english; 分别是什么?
  1. 问题 2:检查封装。 为什么公开 setValue(any int) 不一定比 public 字段更好?
  1. 问题 3:验证不变量。 Score 对 -1、0、100、101 应如何测试,为什么不用直接读 private?

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

定义用户类型状态、操作和访问边界的结构。
对象
按类类型创建并拥有独立状态与生命周期的实例。
数据成员
为每个对象保存状态的成员变量。
成员函数
可访问当前对象状态的类内函数。
访问控制
用 public/private 等标签限制成员可见性的机制。
类不变量
构造与每次公开操作后都应成立的状态约束。

资料与写作方式声明

本章以高桥麻奈 2017 年 C++ 入门教材第 5 版权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…