Chapter 11:Working with Classes
对齐第6版 Chapter 11:以向量类掌握运算符重载、成员与非成员选择、friend 对称操作、类类型转换和 explicit 边界。
学习目标
- 能解释重载表达式到成员/非成员函数调用的映射,并判断不可改变的优先级、结合性和元数规则
- 能设计向量类的
+、-、标量乘与输出运算,比较 member versus nonmember 并限制 friend 访问 - 能复现转换构造函数、
explicit和转换运算符参与重载解析的差异,选择可审计的类转换策略
机制总览
Chapter 11:Working with Classes:机制路径
- 1
为什么熟悉的符号也必须服从类契约
运算符重载(operator overloading)让用户定义类型沿用 a + b 、 out << value 等表达式形式,编译器再把它们映射到函数调用。它改善的是领域表达,不会自动赋予合理语义。若 Vector + Vector 突然修改左值,或比较运算不满足一致关系,熟悉语法反而掩盖缺陷。
- 2
重载只能选择函数,不能改写语法规则
重载不能创造新符号,不能改变优先级、结合性和操作数数量,也不能让纯内置类型表达式改走自定义实现。
- 3
成员与非成员的选择取决于左操作数
会修改当前对象的 += 、 [] 、赋值和调用运算通常自然地成为成员;需要两个操作数同等转换机会的二元算术和比较通常适合非成员。某些运算符受语言约束必须是成员,例如赋值、下标、函数调用和成员访问箭头。
章级决策实验
Chapter 11:Working with Classes:机制与证据
切换《Chapter 11:Working with Classes》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么熟悉的符号也必须服从类契约
运算符重载(operator overloading)让用户定义类型沿用 a + b 、 out << value 等表达式形式,编译器再把它们映射到函数调用。它改善的是领域表达,不会自动赋予合理语义。若 Vector + Vector 突然修改左值,或比较运算不满足一致关系,熟悉语法反而掩盖缺陷。
可核验证据
从干净构建开始,以固定输入运行本节示例,再加入一个边界或故障场景验证「为什么熟悉的符号也必须服从类契约」的状态变化。
学完《Chapter 11:Working with Classes》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
Chapter 11:Working with Classes:失效与核验
为什么熟悉的符号也必须服从类契约
典型失效
若只复述「为什么熟悉的符号也必须服从类契约」结论而不追踪状态、所有权和失败路径,示例扩展成多文件或多对象程序后就容易偏离预期。
核验证据
从干净构建开始,以固定输入运行本节示例,再加入一个边界或故障场景验证「为什么熟悉的符号也必须服从类契约」的状态变化。
重载只能选择函数,不能改写语法规则
典型失效
若只复述「重载只能选择函数,不能改写语法规则」结论而不追踪状态、所有权和失败路径,示例扩展成多文件或多对象程序后就容易偏离预期。
核验证据
从干净构建开始,以固定输入运行本节示例,再加入一个边界或故障场景验证「重载只能选择函数,不能改写语法规则」的状态变化。
成员与非成员的选择取决于左操作数
典型失效
若只复述「成员与非成员的选择取决于左操作数」结论而不追踪状态、所有权和失败路径,示例扩展成多文件或多对象程序后就容易偏离预期。
核验证据
从干净构建开始,以固定输入运行本节示例,再加入一个边界或故障场景验证「成员与非成员的选择取决于左操作数」的状态变化。
为什么熟悉的符号也必须服从类契约
运算符重载(operator overloading)让用户定义类型沿用 a + b、out << value 等表达式形式,编译器再把它们映射到函数调用。它改善的是领域表达,不会自动赋予合理语义。若 Vector + Vector 突然修改左值,或比较运算不满足一致关系,熟悉语法反而掩盖缺陷。
先写代数或状态契约,再决定是否值得提供符号:+ 通常返回新值而不改操作数,+= 修改左操作数并返回其引用,== 应与对象可观察语义一致。语法短不等于前置条件可以省略。
重载只能选择函数,不能改写语法规则
重载不能创造新符号,不能改变优先级、结合性和操作数数量,也不能让纯内置类型表达式改走自定义实现。.、.*、::、?:、sizeof 等运算符不能由普通用户代码重载。重载函数可作为成员或非成员,但两种形式的显式参数数量不同。
class Vector {
public:
Vector(double x, double y) : x_{x}, y_{y} {}
Vector operator+(const Vector& rhs) const {
return Vector{x_ + rhs.x_, y_ + rhs.y_};
}
Vector& operator+=(const Vector& rhs) {
x_ += rhs.x_;
y_ += rhs.y_;
return *this;
}
private:
double x_;
double y_;
};成员 a.operator+(b) 中左操作数由 this 隐式提供,函数只有一个显式参数;非成员 operator+(a,b) 有两个显式参数。const 使运算可用于 const 对象并承诺不修改左值。
成员与非成员的选择取决于左操作数
会修改当前对象的 +=、[]、赋值和调用运算通常自然地成为成员;需要两个操作数同等转换机会的二元算术和比较通常适合非成员。某些运算符受语言约束必须是成员,例如赋值、下标、函数调用和成员访问箭头。
若 Vector 只有成员 Vector::operator*(double),表达式 v * 2.0 可用,但 2.0 * v 的左操作数是 double,无法查找 Vector 成员。增加非成员重载能保持自然操作数顺序,并可委托给已有实现。
class Vector {
public:
Vector operator*(double scale) const;
friend Vector operator*(double scale, const Vector& value);
};
Vector operator*(double scale, const Vector& value) {
return value * scale;
}这里的 friend function 仍是非成员:没有 this,调用也不写 value.operator*。它只是被类声明授予访问 private 成员的权限。本例实际通过 public member 委托,甚至可以不需要 friend;只有确有最窄私有访问需求时才授予。
friend 解决访问,不负责建立语义
friend functions 常用于输出运算,因为 std::ostream 必须位于左侧,不能给标准库类型补一个 Vector 成员。重载应返回流引用以支持链式输出,并只读取稳定的可观察状态。
#include <ostream>
class Vector {
public:
friend std::ostream& operator<<(std::ostream& out, const Vector& value);
private:
double x_;
double y_;
};
std::ostream& operator<<(std::ostream& out, const Vector& value) {
return out << '(' << value.x_ << ", " << value.y_ << ')';
}friend 不是“关闭封装”。授权面越大,private representation 的变化就越难;因此先尝试 public observations,必要时只授予具体函数,不把整个外部模块设为 friend。
向量类要区分表示与语义
二维向量可用直角坐标 (x,y) 存储,也可接受极坐标 (magnitude, angle) 输入。若同时缓存两套表示,每次修改都要维护一致性;更稳妥的设计是选一套规范表示,其他形式在边界转换。
#include <cmath>
class Vector {
public:
static Vector fromPolar(double magnitude, double radians) {
return Vector{magnitude * std::cos(radians),
magnitude * std::sin(radians)};
}
double magnitude() const {
return std::hypot(x_, y_);
}
private:
Vector(double x, double y) : x_{x}, y_{y} {}
double x_;
double y_;
};命名 factory 把单位和方向写在调用点,避免两个 double 构造参数无法区分含义。运算符重载应基于规范表示实现,再通过测试验证零向量、负分量、大值和浮点误差策略。
单参数构造函数是一条输入转换边
可由一个实参调用且未标记 explicit 的构造函数可充当 converting constructor。编译器可能在赋值、参数传递或运算符候选匹配时创建临时对象。便利的代价是调用点看不见类型和单位变化。
class Distance {
public:
explicit Distance(double meters) : meters_{meters} {}
double meters() const { return meters_; }
private:
double meters_;
};
void travel(Distance distance);
travel(Distance{3.5}); // unit and conversion are visibleexplicit 不禁止直接初始化,只禁止 travel(3.5) 这类隐式路径。对货币、单位、资源句柄等语义强类型,默认显式通常更可审计。
转换运算符决定对象如何流向其他类型
operator double() const 可以把对象转为 double,但也会把内部含义扩散到算术和重载解析。转换函数没有显式返回类型;目标类型写在 operator 名称中。若信息可能丢失、单位可能混淆或候选会歧义,优先命名 observation,如 meters()。
class Distance {
public:
explicit operator double() const { return meters_; }
double meters() const { return meters_; }
private:
double meters_;
};转换构造函数和转换运算符同时存在时,两个方向都可能扩大候选集。一次隐式转换序列不会无限串联多个用户定义转换,但歧义仍可能发生;应从 API 所需的少数合法边中选择,而非追求“任何类型都能自动互转”。
先预测四种策略下 travel(3.5) 或提取 double 是否通过,再切换实验面板核对。记录调用点可见性、单位证据与重载候选数量,而不只记录“编译成功”。
三步审计类的运算与转换表面
第一步:把表达式还原成候选函数
为每个运算符写出成员和非成员等价调用,核对左操作数、const、元数和不可改变的语法规则。
小结
- operator overloading 把既有表达式映射到函数,不能改变优先级、结合性、元数或内置短路语义
- member versus nonmember 的核心差异是左操作数是否成为隐式 this,以及两侧获得的转换机会
- friend functions 是非成员,只获得明确授予的访问权,不自动拥有合理语义
- 向量类应选择规范表示,让算术重载保持值语义与对称性
- class conversions 是 API 图上的边;
explicit和命名 observation 让单位、损失和意图在调用点可见
练习
- 问题 1:设计有理数运算。
Rational + Rational、Rational += Rational和int + Rational分别选成员还是非成员?
- 问题 2:审计流输出。
operator<<为什么通常是非成员,它一定要 friend 吗?
- 问题 3:修复单位转换。
void setTimeout(Duration)被setTimeout(5)意外调用,怎样让单位和意图可见?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 运算符重载
- 为含用户定义类型的既有运算符提供函数实现。
- 成员运算符
- 以左操作数为隐式 this 的运算符成员函数。
- 非成员运算符
- 把所有操作数写为显式参数的命名空间级重载。
- 友元函数
- 获类授权访问 private、但仍不属于类成员的函数。
- 转换构造函数
- 可从源类型建立类对象并可能参与隐式转换的构造函数。
- explicit
- 要求多数转换在调用点显式写出目标类型的说明符。
- 转换运算符
- 从类对象生成目标类型值的特殊成员函数。