算术与关系运算符重载
定义与作用
算术运算符(+ - * / %)和关系运算符(== != < > <= >=)是自定义类型最常重载的运算符。通过合理重载,自定类型能像内置类型一样参与数学计算和条件判断。
核心原理
基于复合赋值的算术运算符实现模式
这个模式的优点:只需在一个地方(+=)维护核心逻辑,+ 自动跟随。
关系运算符的互推关系
只需实现 == 和 <,其余四个关系运算符都可以基于它们推演。
完整示例
示例一:三维向量的完整运算
场景说明:游戏开发或图形学中的 Vector3 类,支持完整的算术和比较运算。
#include <iostream>
#include <cmath>
class Vector3 {
public:
Vector3(double x = 0, double y = 0, double z = 0)
: x_(x), y_(y), z_(z) {}
// ====== 复合赋值(成员)======
Vector3& operator+=(const Vector3& rhs) {
x_ += rhs.x_; y_ += rhs.y_; z_ += rhs.z_;
return *this;
}
Vector3& operator-=(const Vector3& rhs) {
x_ -= rhs.x_; y_ -= rhs.y_; z_ -= rhs.z_;
return *this;
}
Vector3& operator*=(double scalar) {
x_ *= scalar; y_ *= scalar; z_ *= scalar;
return *this;
}
Vector3& operator/=(double scalar) {
x_ /= scalar; y_ /= scalar; z_ /= scalar;
return *this;
}
// ====== 一元运算符(成员)======
Vector3 operator-() const {
return Vector3(-x_, -y_, -z_);
}
Vector3 operator+() const {
return *this;
}
// ====== 访问器 ======
double x() const { return x_; }
double y() const { return y_; }
double z() const { return z_; }
double length() const {
return std::sqrt(x_ * x_ + y_ * y_ + z_ * z_);
}
double dot(const Vector3& v) const {
return x_ * v.x_ + y_ * v.y_ + z_ * v.z_;
}
Vector3 cross(const Vector3& v) const {
return Vector3(y_ * v.z_ - z_ * v.y_,
z_ * v.x_ - x_ * v.z_,
x_ * v.y_ - y_ * v.x_);
}
private:
double x_, y_, z_;
};
// ====== 二元算术(非成员 + 友元)======
inline Vector3 operator+(const Vector3& a, const Vector3& b) {
return Vector3(a.x() + b.x(), a.y() + b.y(), a.z() + b.z());
}
inline Vector3 operator-(const Vector3& a, const Vector3& b) {
return Vector3(a.x() - b.x(), a.y() - b.y(), a.z() - b.z());
}
inline Vector3 operator*(const Vector3& v, double scalar) {
return Vector3(v.x() * scalar, v.y() * scalar, v.z() * scalar);
}
inline Vector3 operator*(double scalar, const Vector3& v) {
return v * scalar; // 利用对称性
}
inline Vector3 operator/(const Vector3& v, double scalar) {
return Vector3(v.x() / scalar, v.y() / scalar, v.z() / scalar);
}
// ====== 关系运算符 ======
inline bool operator==(const Vector3& a, const Vector3& b) {
return a.x() == b.x() && a.y() == b.y() && a.z() == b.z();
}
inline bool operator!=(const Vector3& a, const Vector3& b) {
return !(a == b);
}
inline bool operator<(const Vector3& a, const Vector3& b) {
// 按长度比较
return a.length() < b.length();
}
inline bool operator>(const Vector3& a, const Vector3& b) {
return b < a;
}
inline bool operator<=(const Vector3& a, const Vector3& b) {
return !(b < a);
}
inline bool operator>=(const Vector3& a, const Vector3& b) {
return !(a < b);
}
// ====== 流输出 ======
inline std::ostream& operator<<(std::ostream& os, const Vector3& v) {
return os << "(" << v.x() << ", " << v.y() << ", " << v.z() << ")";
}
int main() {
std::cout << "===== 三维向量运算 =====" << std::endl;
Vector3 position(1.0, 2.0, 3.0);
Vector3 velocity(0.5, 0.5, 0.0);
Vector3 gravity(0.0, -9.8, 0.0);
std::cout << "初始位置: " << position << std::endl;
std::cout << "速度: " << velocity << std::endl;
std::cout << "重力: " << gravity << std::endl;
// 运动更新
Vector3 new_pos = position + velocity + gravity * 0.016; // dt=16ms
std::cout << "\n新位置(1 帧后): " << new_pos << std::endl;
// 缩放
Vector3 normalized = velocity * (1.0 / velocity.length());
std::cout << "归一化速度: " << normalized << std::endl;
std::cout << "归一化速度长度: " << normalized.length() << std::endl;
// 点积和叉积
Vector3 a(1, 0, 0);
Vector3 b(0, 1, 0);
std::cout << "\na · b = " << a.dot(b) << std::endl;
std::cout << "a × b = " << a.cross(b) << std::endl;
// 比较
std::cout << std::boolalpha;
std::cout << "\nposition == new_pos? " << (position == new_pos) << std::endl;
std::cout << "velocity < gravity? " << (velocity < gravity) << std::endl;
return 0;
}
预期输出:
===== 三维向量运算 =====
初始位置: (1, 2, 3)
速度: (0.5, 0.5, 0)
重力: (0, -9.8, 0)
新位置(1 帧后): (1.5, 2.3432, 3)
归一化速度: (0.707107, 0.707107, 0)
归一化速度长度: 1
a · b = 0
a × b = (0, 0, 1)
position == new_pos? false
velocity < gravity? true
逐段分析:
- 复合赋值(
+=-=*=/=)是成员函数——修改自身 - 二元运算(
+-*/)是非成员——支持隐式转换,且保持对称性 operator*(double, Vector3)调用operator*(Vector3, double)——只维护一份实现- 关系运算符推导链:只实现
==和<,其余四个全基于这两个推演——极简且一致 - 所有二元运算通过 public getter 访问私有成员,不需要友元——更好的封装
示例二:分数的算术与比较
#include <iostream>
#include <numeric> // std::gcd
#include <stdexcept>
class Fraction {
public:
Fraction(int num = 0, int den = 1) : num_(num), den_(den) {
if (den_ == 0) throw std::invalid_argument("分母不能为 0");
if (den_ < 0) { num_ = -num_; den_ = -den_; } // 负数分母标准化
reduce();
}
// 复合赋值
Fraction& operator+=(const Fraction& rhs) {
num_ = num_ * rhs.den_ + rhs.num_ * den_;
den_ = den_ * rhs.den_;
reduce();
return *this;
}
Fraction& operator*=(const Fraction& rhs) {
num_ *= rhs.num_;
den_ *= rhs.den_;
reduce();
return *this;
}
// 取倒数
Fraction reciprocal() const {
if (num_ == 0) throw std::domain_error("不能取 0 的倒数");
return Fraction(den_, num_);
}
double to_double() const {
return static_cast<double>(num_) / den_;
}
private:
void reduce() {
int g = std::gcd(num_, den_);
num_ /= g;
den_ /= g;
}
int num_, den_;
friend Fraction operator+(const Fraction& a, const Fraction& b);
friend Fraction operator*(const Fraction& a, const Fraction& b);
friend bool operator==(const Fraction& a, const Fraction& b);
friend bool operator<(const Fraction& a, const Fraction& b);
friend std::ostream& operator<<(std::ostream& os, const Fraction& f);
};
Fraction operator+(const Fraction& a, const Fraction& b) {
Fraction tmp = a;
tmp += b;
return tmp;
}
Fraction operator*(const Fraction& a, const Fraction& b) {
Fraction tmp = a;
tmp *= b;
return tmp;
}
Fraction operator-(const Fraction& a, const Fraction& b) {
return a + Fraction(-b.num_, b.den_); // a - b = a + (-b)
}
Fraction operator/(const Fraction& a, const Fraction& b) {
return a * b.reciprocal();
}
bool operator==(const Fraction& a, const Fraction& b) {
return a.num_ == b.num_ && a.den_ == b.den_;
}
bool operator!=(const Fraction& a, const Fraction& b) {
return !(a == b);
}
bool operator<(const Fraction& a, const Fraction& b) {
return a.num_ * b.den_ < b.num_ * a.den_;
}
bool operator>(const Fraction& a, const Fraction& b) { return b < a; }
bool operator<=(const Fraction& a, const Fraction& b) { return !(b < a); }
bool operator>=(const Fraction& a, const Fraction& b) { return !(a < b); }
std::ostream& operator<<(std::ostream& os, const Fraction& f) {
if (f.den_ == 1)
return os << f.num_;
return os << f.num_ << "/" << f.den_;
}
int main() {
std::cout << "===== 分数运算 =====" << std::endl;
Fraction f1(1, 2); // 1/2
Fraction f2(1, 3); // 1/3
Fraction f3(2, 4); // 自动约分为 1/2
std::cout << "f1 = " << f1 << std::endl;
std::cout << "f2 = " << f2 << std::endl;
std::cout << "f3 = " << f3 << " (2/4 自动约分)" << std::endl;
std::cout << "f1 + f2 = " << (f1 + f2) << std::endl;
std::cout << "f1 - f2 = " << (f1 - f2) << std::endl;
std::cout << "f1 * f2 = " << (f1 * f2) << std::endl;
std::cout << "f1 / f2 = " << (f1 / f2) << std::endl;
std::cout << "\n小数表示:" << std::endl;
std::cout << "f1 = " << f1.to_double() << std::endl;
std::cout << "f2 = " << f2.to_double() << std::endl;
std::cout << "\n比较:" << std::endl;
std::cout << std::boolalpha;
std::cout << "f1 == f3? " << (f1 == f3) << std::endl;
std::cout << "f1 > f2? " << (f1 > f2) << std::endl;
return 0;
}
预期输出:
===== 分数运算 =====
f1 = 1/2
f2 = 1/3
f3 = 1/2 (2/4 自动约分)
f1 + f2 = 5/6
f1 - f2 = 1/6
f1 * f2 = 1/6
f1 / f2 = 3/2
小数表示:
f1 = 0.5
f2 = 0.333333
比较:
f1 == f3? true
f1 > f2? true
逐段分析:
- 构造函数自动调用
reduce()做约分——任何方式创建分数都是最简形式 +基于+=实现(拷贝后复合赋值),-基于+和取负号实现,/基于*和取倒数——层层复用,极简维护- 只有
==和<是手写的,!=><=>=都是基于这两个推导 - 通分比较
a.num * b.den < b.num * a.den避免了浮点精度问题
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 算术运算符作为成员 | 不支持左操作数隐式转换 | 用非成员函数实现 |
| 关系运算符各自独立实现 | 不一致(如 > 和 <= 不互补) | 基于 == 和 < 推导其余 |
| 忘记约分 / 标准化 | 同一值有多种表示 | 构造和运算后统一标准化 |
| 除法未检查除零 | 运行时崩溃 | 抛异常或返回特殊值 |
operator== 使用浮点直接比较 | 精度问题 | 用 epsilon 容差 |
常见面试问题
为什么
+通常用非成员实现?——支持左操作数的隐式类型转换。成员函数形式a.operator+(b)要求a必须是该类对象。关系运算符的推导式实现是如何减少代码的?——六个关系运算符(
==!=<><=>=)只需自行实现==和<,其余四个都可通过逻辑推导实现,减少重复代码和维护成本。如何重载
operator*以同时支持Vector * scalar和scalar * Vector?——只实现Vector operator*(Vector, double),再写Vector operator*(double, Vector)调用前者即可。重载算术运算符时的返回值类型选择?——
+-*/返回新对象(按值),+=-=*=/=返回*this(按引用)。不返回临时对象的引用。
小结
- 算术运算符用复合赋值驱动:先实现
+=,+基于+=实现 - 关系运算符用
==和<驱动:其余四个基于这两个推导 - 二元算术是非成员函数——支持对称隐式转换
- 所有重载运算符保持自然语义——不创造"惊喜"