2. 重载算术运算符:实现复数类的加减乘除
算术运算符重载,可以说是C++里最直观、最常用的运算符重载场景了。你想想看,我们写数学公式的时候,a + b 多自然,要是每次都得写 a.add(b),那代码读起来得多别扭。
今天我们就拿复数类来练手。复数这东西,数学上就是 a + bi 的形式,有实部和虚部。加减乘除的规则大家中学都学过,我就不啰嗦了。关键是怎么用C++的运算符重载,让我们的复数类用起来跟内置类型一样顺手。
2.1 复数类的设计思路
先搭个架子。我个人习惯是把复数类设计成值类型,而不是用指针或引用去包装。为什么?因为复数本身就是一个简单的数值,拷贝成本很低,没必要搞复杂。
class Complex {
private:
double real_; // 实部
double imag_; // 虚部
public:
Complex(double real = 0.0, double imag = 0.0)
: real_(real), imag_(imag) {}
// 后面我们会添加运算符重载
};
嗯,这里要注意:构造函数用了默认参数,这样 Complex c1; 会得到 0+0i,Complex c2(3.0); 会得到 3+0i,Complex c3(1.0, 2.0); 才是 1+2i。这个设计很实用,我在项目中经常这么用。
2.2 加法与减法:最基础的二元运算符
加法和减法是最简单的。规则就是实部加减实部,虚部加减虚部。我们来看看怎么写:
class Complex {
// ... 前面的成员变量和构造函数 ...
public:
Complex operator+(const Complex& other) const {
return Complex(real_ + other.real_,
imag_ + other.imag_);
}
Complex operator-(const Complex& other) const {
return Complex(real_ - other.real_,
imag_ - other.imag_);
}
};
这里有几个细节我想强调一下:
- 参数用 const 引用:避免拷贝,提高效率。复数虽然小,但养成好习惯很重要。
- 函数标记为 const:因为加法操作不会修改当前对象,const 保证了这一点。
- 返回新对象:加法产生的是一个新的复数,不是修改原来的。
2.3 乘法:稍微复杂一点
复数乘法规则是 (a+bi)(c+di) = (ac-bd) + (ad+bc)i。代码实现其实也不难:
Complex operator*(const Complex& other) const {
return Complex(
real_ * other.real_ - imag_ * other.imag_,
real_ * other.imag_ + imag_ * other.real_
);
}
我曾经在写一个信号处理库的时候,复数乘法被频繁调用。当时我犯了一个错误——把乘法的中间结果用临时变量存了,结果编译器优化得不够好,性能差了一截。后来改成直接返回表达式,反而更快。所以,对于这种简单的运算,直接写表达式就好,别画蛇添足。
2.4 除法:最需要小心的地方
复数除法公式是 (a+bi)/(c+di) = (ac+bd)/(c²+d²) + (bc-ad)/(c²+d²)i。这里有个坑:分母不能为零。
Complex operator/(const Complex& other) const {
double denom = other.real_ * other.real_ +
other.imag_ * other.imag_;
if (denom == 0.0) {
throw std::runtime_error("Division by zero in Complex");
}
return Complex(
(real_ * other.real_ + imag_ * other.imag_) / denom,
(imag_ * other.real_ - real_ * other.imag_) / denom
);
}
std::abs(denom) < 1e-12。不过这里为了演示清晰,我就简化了。
我曾经在一个金融计算项目里,因为没处理除零异常,导致程序在特定输入下崩溃。那次教训让我养成了习惯:凡是除法,必检查分母。
2.5 混合类型运算:让 double 也能参与
现在我们的复数类可以跟复数做运算了。但如果想写 c1 + 3.0 或者 3.0 + c1 呢?
对于 c1 + 3.0,编译器会尝试把 double 隐式转换成 Complex(因为我们的构造函数允许单参数隐式转换),所以这个能工作。但 3.0 + c1 就不行了,因为 double 类型里没有 operator+ 能接受 Complex。
解决办法是用非成员函数:
Complex operator+(double lhs, const Complex& rhs) {
return Complex(lhs + rhs.real_, rhs.imag_);
}
Complex operator-(double lhs, const Complex& rhs) {
return Complex(lhs - rhs.real_, -rhs.imag_);
}
Complex operator*(double lhs, const Complex& rhs) {
return Complex(lhs * rhs.real_, lhs * rhs.imag_);
}
Complex operator/(double lhs, const Complex& rhs) {
double denom = rhs.real_ * rhs.real_ + rhs.imag_ * rhs.imag_;
if (denom == 0.0) {
throw std::runtime_error("Division by zero in Complex");
}
return Complex(lhs * rhs.real_ / denom,
-lhs * rhs.imag_ / denom);
}
这些非成员函数需要访问 Complex 的私有成员,所以要在类里加上 friend 声明:
class Complex {
// ...
friend Complex operator+(double, const Complex&);
friend Complex operator-(double, const Complex&);
friend Complex operator*(double, const Complex&);
friend Complex operator/(double, const Complex&);
};
2.6 复合赋值运算符:+=、-= 等
除了二元运算符,我们通常还会提供复合赋值版本。它们跟二元运算符的区别是:修改当前对象,返回引用。
Complex& operator+=(const Complex& other) {
real_ += other.real_;
imag_ += other.imag_;
return *this;
}
Complex& operator-=(const Complex& other) {
real_ -= other.real_;
imag_ -= other.imag_;
return *this;
}
Complex& operator*=(const Complex& other) {
double r = real_ * other.real_ - imag_ * other.imag_;
double i = real_ * other.imag_ + imag_ * other.real_;
real_ = r;
imag_ = i;
return *this;
}
Complex& operator/=(const Complex& other) {
double denom = other.real_ * other.real_ +
other.imag_ * other.imag_;
if (denom == 0.0) {
throw std::runtime_error("Division by zero in Complex");
}
double r = (real_ * other.real_ + imag_ * other.imag_) / denom;
double i = (imag_ * other.real_ - real_ * other.imag_) / denom;
real_ = r;
imag_ = i;
return *this;
}
Complex operator+(const Complex& a, const Complex& b) {
Complex result = a;
result += b;
return result;
}
这样写的好处是,维护加法逻辑时只需要改 operator+= 一处。
2.7 完整代码示例
把上面所有内容整合起来,就是一个完整的复数类了。这里我给出一个精简版本:
#include <stdexcept>
class Complex {
private:
double real_;
double imag_;
public:
Complex(double real = 0.0, double imag = 0.0)
: real_(real), imag_(imag) {}
// 二元运算符
Complex operator+(const Complex& other) const {
return Complex(real_ + other.real_, imag_ + other.imag_);
}
Complex operator-(const Complex& other) const {
return Complex(real_ - other.real_, imag_ - other.imag_);
}
Complex operator*(const Complex& other) const {
return Complex(
real_ * other.real_ - imag_ * other.imag_,
real_ * other.imag_ + imag_ * other.real_
);
}
Complex operator/(const Complex& other) const {
double denom = other.real_ * other.real_ +
other.imag_ * other.imag_;
if (denom == 0.0) {
throw std::runtime_error("Division by zero");
}
return Complex(
(real_ * other.real_ + imag_ * other.imag_) / denom,
(imag_ * other.real_ - real_ * other.imag_) / denom
);
}
// 复合赋值运算符
Complex& operator+=(const Complex& other) {
real_ += other.real_;
imag_ += other.imag_;
return *this;
}
Complex& operator-=(const Complex& other) {
real_ -= other.real_;
imag_ -= other.imag_;
return *this;
}
Complex& operator*=(const Complex& other) {
double r = real_ * other.real_ - imag_ * other.imag_;
double i = real_ * other.imag_ + imag_ * other.real_;
real_ = r;
imag_ = i;
return *this;
}
Complex& operator/=(const Complex& other) {
double denom = other.real_ * other.real_ +
other.imag_ * other.imag_;
if (denom == 0.0) {
throw std::runtime_error("Division by zero");
}
double r = (real_ * other.real_ + imag_ * other.imag_) / denom;
double i = (imag_ * other.real_ - real_ * other.imag_) / denom;
real_ = r;
imag_ = i;
return *this;
}
// 友元:混合类型运算
friend Complex operator+(double lhs, const Complex& rhs) {
return Complex(lhs + rhs.real_, rhs.imag_);
}
friend Complex operator-(double lhs, const Complex& rhs) {
return Complex(lhs - rhs.real_, -rhs.imag_);
}
friend Complex operator*(double lhs, const Complex& rhs) {
return Complex(lhs * rhs.real_, lhs * rhs.imag_);
}
friend Complex operator/(double lhs, const Complex& rhs) {
double denom = rhs.real_ * rhs.real_ + rhs.imag_ * rhs.imag_;
if (denom == 0.0) {
throw std::runtime_error("Division by zero");
}
return Complex(lhs * rhs.real_ / denom,
-lhs * rhs.imag_ / denom);
}
};
2.8 知识体系总览
下面这张图把复数类算术运算符重载的整个知识结构梳理了一下,方便你对照着看:
2.9 避坑指南
最后,分享几个我踩过的坑:
- 忘记处理除零:这个前面说过了,浮点数除法一定要检查分母。
- operator+ 返回了引用:有人会写成
Complex& operator+(...),然后返回局部对象的引用。这是未定义行为,程序随时可能崩溃。 - 混合类型只做了一半:只写了
Complex + double,忘了double + Complex。两个方向都要实现。 - friend 声明写错位置:friend 声明要写在类体内部,而且不受 public/private 影响。我见过有人写在类外面,编译直接报错。
好了,复数类的算术运算符重载就讲到这里。代码虽然不长,但里面的设计思路和注意事项,放到其他自定义类型上也是通用的。你写自己的类时,照着这个模板改就行。
公众号:蓝海资料掘金营,微信deep3321