C++实现小型复数计算器
前言
复数计算器是练习运算符重载的经典题目:它需要值语义、需要+ - * /四则运算、需要输入输出,还要处理"除以零""浮点相等"这些边界。写一个能跑的版本不难,写一个语义正确的版本要注意不少细节。
先说明一点:C++ 标准库自带<complex>,提供std::complex<double>,四则运算、共轭、模长、超越函数都有,生产代码应当直接用它。本文手写一个Complex类,目的是把运算符重载、值语义和浮点比较讲清楚;如果只是要在项目里做复数运算,用标准库即可。
常见的误解有三个:把除法公式的符号写反;用==直接比较两个浮点结果;把operator+写成修改左操作数。下面从表示开始,一步步实现并在最后串成一个小型计算器。
一、表示与值语义
复数写成a + bi,其中a是实部、b是虚部,都用double存。类应当满足值语义:拷贝之后两个对象互不影响,运算返回新对象而不是改原来的对象。
// 适用于 C++17
#include <cmath>
#include <stdexcept>
class Complex {
public:
constexpr Complex(double re = 0.0, double im = 0.0) : re_(re), im_(im) {}
double real() const { return re_; }
double imag() const { return im_; }
Complex conjugate() const { return Complex(re_, -im_); }
double magnitude() const { return std::sqrt(re_ * re_ + im_ * im_); }
Complex& operator+=(const Complex& o) {
re_ += o.re_;
im_ += o.im_;
return *this;
}
Complex& operator-=(const Complex& o) {
re_ -= o.re_;
im_ -= o.im_;
return *this;
}
Complex& operator*=(const Complex& o) {
const double a = re_, b = im_;
const double c = o.re_, d = o.im_;
re_ = a * c - b * d;
im_ = a * d + b * c;
return *this;
}
private:
double re_ = 0.0;
double im_ = 0.0;
};乘法公式是(a+bi)(c+di) = (ac-bd) + (ad+bc)i。注意operator*=里先把a、b、c、d存进局部变量:如果直接写re_ = a*c - b*d;然后在下一条语句里用re_计算虚部,用到的就是已经被覆盖的新值。这类"原地更新时读到旧值"的错误在复合赋值里非常常见,最稳的做法就是先用局部变量算出全部结果,再统一写回。
二、除法与共轭
除法要把分母实数化:分子分母同乘分母的共轭。
(a+bi)/(c+di) = [(a+bi)(c-di)] / (c²+d²)
= [(ac+bd) + (bc-ad)i] / (c²+d²)注意虚部的符号是bc - ad,不是ad - bc。写成后者会得到符号相反的虚部,而且在小数据上不容易发现。
// 适用于 C++17
Complex& operator/=(const Complex& o) {
const double denom = o.re_ * o.re_ + o.im_ * o.im_;
if (denom == 0.0) throw std::domain_error("division by zero");
const double a = re_, b = im_;
const double c = o.re_, d = o.im_;
re_ = (a * c + b * d) / denom;
im_ = (b * c - a * d) / denom;
return *this;
}denom == 0.0只能挡住精确的零。如果分母非常接近零,结果会溢出成无穷大或NaN,这属于浮点语义而不是异常;是否要把"接近零"也判为错误,取决于业务对精度的要求。至少应当拒绝精确的零,否则结果一定是NaN或无穷。
有了复合赋值,二元运算符可以写成"按值接收左操作数 + 复用复合赋值":
// 适用于 C++17
friend Complex operator+(Complex a, const Complex& b) { return a += b; }
friend Complex operator-(Complex a, const Complex& b) { return a -= b; }
friend Complex operator*(Complex a, const Complex& b) { return a *= b; }
friend Complex operator/(Complex a, const Complex& b) { return a /= b; }
friend Complex operator-(Complex a) { return Complex(-a.re_, -a.im_); }第一个参数按值传递,函数内部直接修改这份副本,既能复用+=,又不会影响调用方。返回类型是值,支持a + b + c这样的链式写法。一元负号单独实现,接收一个参数,与二元减号靠参数个数区分。
三、解析与格式化
输入格式定为a+bi、a-bi、bi、a四种,允许中间有空格,不支持指数记法。解析失败返回std::optional,把"输入错误"和"计算错误"分开:前者是预期内的,不需要异常。
// 适用于 C++17
#include <algorithm>
#include <cctype>
#include <optional>
#include <ostream>
#include <string>
// 类的静态成员函数,声明为 static std::optional<Complex> parse(std::string);
static std::optional<Complex> parse(std::string s) {
s.erase(std::remove_if(s.begin(), s.end(),
[](unsigned char c) { return std::isspace(c) != 0; }),
s.end());
if (s.empty()) return std::nullopt;
bool has_i = false;
if (s.back() == 'i' || s.back() == 'I') {
has_i = true;
s.pop_back();
}
try {
if (!has_i) { // 纯实数
std::size_t pos = 0;
double v = std::stod(s, &pos);
if (pos != s.size()) return std::nullopt;
return Complex(v, 0.0);
}
std::size_t split = std::string::npos;
for (std::size_t i = 1; i < s.size(); ++i) {
if (s[i] == '+' || s[i] == '-') split = i; // 最后一个符号
}
if (split == std::string::npos) { // 纯虚数:i、+i、-i、3i
double im = 1.0;
if (!s.empty()) {
std::size_t pos = 0;
im = std::stod(s, &pos);
if (pos != s.size()) return std::nullopt;
}
return Complex(0.0, im);
}
const std::string re_s = s.substr(0, split);
const std::string im_s = s.substr(split);
std::size_t p1 = 0, p2 = 0;
const double re = std::stod(re_s, &p1);
if (p1 != re_s.size()) return std::nullopt;
const double im = std::stod(im_s, &p2);
if (p2 != im_s.size()) return std::nullopt;
return Complex(re, im);
} catch (const std::exception&) {
return std::nullopt;
}
}std::stod会把"+4"、"-3"正常解析成带符号的数,所以3+4i切出3与+4后直接调用即可。std::stod("+")会抛异常,被外层catch接住并转成std::nullopt。
输出时要注意负虚部的符号:
// 适用于 C++17
friend std::ostream& operator<<(std::ostream& os, const Complex& c) {
if (c.im_ >= 0.0) return os << c.re_ << '+' << c.im_ << 'i';
return os << c.re_ << c.im_ << 'i'; // im_ 自带负号
}不要写成c.re_ << "+-" << ...,也不要在虚部为负时再补一个+。
四、浮点比较
复数的相等不能直接用==比较两个double。0.1 + 0.2与0.3在二进制浮点下并不相等,经过几次乘除后误差会累积。工程上提供两种接口:
operator==保持精确比较,语义明确,适合作为容器键或与0.0精确比较;nearly_equal(a, b, eps)做容差比较,适合判断"计算结果是否接近预期"。
// 适用于 C++17
bool nearly_equal(const Complex& a, const Complex& b, double eps = 1e-9) {
return std::abs(a.real() - b.real()) <= eps &&
std::abs(a.imag() - b.imag()) <= eps;
}不要把容差比较偷偷塞进operator==:那会让"相等"不再具有传递性(a 与 b 接近、b 与 c 接近,a 与 c 可能超出容差),破坏std::map、std::find等依赖等价关系的算法。容差参数怎么取,取决于数值量级——绝对误差1e-9对1e6量级的数可能过严,此时应改用相对误差。
五、完整的小型计算器
// 适用于 C++17
#include <iostream>
int main() {
Complex a(3.0, 4.0);
Complex b(1.0, -2.0);
Complex sum = a + b; // (4, 2)
Complex diff = a - b; // (2, 6)
Complex prod = a * b; // (11, -2)
Complex quot = a / b; // (-1, 2)
Complex conj = a.conjugate(); // (3, -4)
double mod = a.magnitude(); // 5
std::cout << sum << ' ' << diff << ' ' << prod << ' '
<< quot << ' ' << conj << ' ' << mod << '\n';
if (auto parsed = Complex::parse("3+4i")) {
std::cout << *parsed << '\n';
}
return 0;
}手工核对几个结果:a * b = (3·1 - 4·(-2)) + (3·(-2) + 4·1)i = 11 - 2i;a / b的分母是1+4=5,实部(3·1 + 4·(-2))/5 = -1,虚部(4·1 - 3·(-2))/5 = 2;模长是sqrt(9+16)=5。这些都可以逐项验算,不依赖运行。
常见坑点
1. 乘法原地更新时覆盖旧值
// ❌ 第二行用到了已被覆盖的 re
re_ = a * c - b * d;
im_ = re_ * d + b * c;
// ✅ 先存旧值,或用局部变量算完再写回
const double a = re_, b = im_;
re_ = a * c - b * d;
im_ = a * d + b * c;2. 除法虚部符号写反
// ❌ 得到 -2 而不是 2
im_ = (a * d - b * c) / denom;
// ✅ 共轭相乘的虚部是 bc - ad
im_ = (b * c - a * d) / denom;3. 不检查除零
// ❌ 分母为零时得到 NaN 或无穷
re_ = ... / denom;
// ✅ 显式拒绝
if (denom == 0.0) throw std::domain_error("division by zero");4. 用==判断浮点结果相等
// ❌ 0.1 + 0.2 与 0.3 不相等
if (a == b) { /* ... */ }
// ✅ 容差比较,且不要塞进 operator==
if (nearly_equal(a, b)) { /* ... */ }5.operator+修改了左操作数
// ❌ a + b 之后 a 变了,违反值语义
Complex& operator+(const Complex& o) { re_ += o.re_; return *this; }
// ✅ 按值接收副本,返回新对象
friend Complex operator+(Complex a, const Complex& b) { return a += b; }6. 输出负虚部时多写一个加号
// ❌ 得到 "3+-2i"
os << c.re_ << '+' << c.im_ << 'i';
// ✅ 按符号分支
if (c.im_ >= 0.0) os << c.re_ << '+' << c.im_ << 'i';
else os << c.re_ << c.im_ << 'i';7. 解析不校验尾部多余字符
// ❌ "3+4iabc" 也能解析成功
double v = std::stod(s);
// ✅ 用 pos 确认整个字符串都被消费
if (pos != s.size()) return std::nullopt;8. 重复实现而不使用标准库
// ❌ 自己写的复数缺少边界处理,且没有经过充分测试
// ✅ 生产代码优先 std::complex<double>
#include <complex>总结
| 运算 | 公式 | 注意点 |
|---|
| 加法 | (a+c) + (b+d)i | 逐分量相加 |
| 乘法 | (ac-bd) + (ad+bc)i | 先存旧值再写回 |
| 除法 | [(ac+bd) + (bc-ad)i] / (c²+d²) | 虚部符号、除零 |
| 共轭 | a - bi | 只改虚部符号 |
| 模长 | sqrt(a²+b²) | 可能溢出,必要时用hypot |
| 相等 | 精确==与容差比较分开 | 容差不能进operator== |
复数类的骨架就是"两个double+ 四个复合赋值 + 四个二元运算符"。真正需要用心的是三处:原地更新时不要覆盖旧值、除法公式的符号、以及浮点比较不能想当然。把这三处写对,它就是一个合格的值类型;再往上加解析与格式化,才是一个能用的计算器。