前言复数计算器是练习运算符重载的经典题目它需要值语义、需要 - * /四则运算、需要输入输出还要处理除以零浮点相等这些边界。写一个能跑的版本不难写一个语义正确的版本要注意不少细节。先说明一点C 标准库自带complex提供std::complexdouble四则运算、共轭、模长、超越函数都有生产代码应当直接用它。本文手写一个Complex类目的是把运算符重载、值语义和浮点比较讲清楚如果只是要在项目里做复数运算用标准库即可。常见的误解有三个把除法公式的符号写反用直接比较两个浮点结果把operator写成修改左操作数。下面从表示开始一步步实现并在最后串成一个小型计算器。一、表示与值语义复数写成a bi其中a是实部、b是虚部都用double存。类应当满足值语义拷贝之后两个对象互不影响运算返回新对象而不是改原来的对象。// 适用于 C17#include cmath#include stdexceptclass 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;};乘法公式是(abi)(cdi) (ac-bd) (adbc)i。注意operator*里先把a、b、c、d存进局部变量如果直接写re_ a*c - b*d;然后在下一条语句里用re_计算虚部用到的就是已经被覆盖的新值。这类原地更新时读到旧值的错误在复合赋值里非常常见最稳的做法就是先用局部变量算出全部结果再统一写回。二、除法与共轭除法要把分母实数化分子分母同乘分母的共轭。(abi)/(cdi) [(abi)(c-di)] / (c²d²) [(acbd) (bc-ad)i] / (c²d²)注意虚部的符号是bc - ad不是ad - bc。写成后者会得到符号相反的虚部而且在小数据上不容易发现。// 适用于 C17Complex 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或无穷。有了复合赋值二元运算符可以写成按值接收左操作数 复用复合赋值// 适用于 C17friend 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这样的链式写法。一元负号单独实现接收一个参数与二元减号靠参数个数区分。三、解析与格式化输入格式定为abi、a-bi、bi、a四种允许中间有空格不支持指数记法。解析失败返回std::optional把输入错误和计算错误分开前者是预期内的不需要异常。// 适用于 C17#include algorithm#include cctype#include optional#include ostream#include string// 类的静态成员函数声明为 static std::optionalComplex parse(std::string);static std::optionalComplex 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、3idouble 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正常解析成带符号的数所以34i切出3与4后直接调用即可。std::stod()会抛异常被外层catch接住并转成std::nullopt。输出时要注意负虚部的符号// 适用于 C17friend 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)做容差比较适合判断计算结果是否接近预期。// 适用于 C17bool 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量级的数可能过严此时应改用相对误差。五、完整的小型计算器// 适用于 C17#include iostreamint 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(); // 5std::cout sum diff prod quot conj mod \n;if (auto parsed Complex::parse(34i)) {std::cout *parsed \n;}return 0;}手工核对几个结果a * b (3·1 - 4·(-2)) (3·(-2) 4·1)i 11 - 2ia / b的分母是145实部(3·1 4·(-2))/5 -1虚部(4·1 - 3·(-2))/5 2模长是sqrt(916)5。这些都可以逐项验算不依赖运行。常见坑点1. 乘法原地更新时覆盖旧值// ❌ 第二行用到了已被覆盖的 rere_ 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 而不是 2im_ (a * d - b * c) / denom;// ✅ 共轭相乘的虚部是 bc - adim_ (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) { /* ... */ }// ✅ 容差比较且不要塞进 operatorif (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-2ios c.re_ c.im_ i;// ✅ 按符号分支if (c.im_ 0.0) os c.re_ c.im_ i;else os c.re_ c.im_ i;7. 解析不校验尾部多余字符// ❌ 34iabc 也能解析成功double v std::stod(s);// ✅ 用 pos 确认整个字符串都被消费if (pos ! s.size()) return std::nullopt;8. 重复实现而不使用标准库// ❌ 自己写的复数缺少边界处理且没有经过充分测试// ✅ 生产代码优先 std::complexdouble#include complex总结运算公式注意点加法(ac) (bd)i逐分量相加乘法(ac-bd) (adbc)i先存旧值再写回除法[(acbd) (bc-ad)i] / (c²d²)虚部符号、除零共轭a - bi只改虚部符号模长sqrt(a²b²)可能溢出必要时用hypot相等精确与容差比较分开容差不能进operator复数类的骨架就是两个double 四个复合赋值 四个二元运算符。真正需要用心的是三处原地更新时不要覆盖旧值、除法公式的符号、以及浮点比较不能想当然。把这三处写对它就是一个合格的值类型再往上加解析与格式化才是一个能用的计算器。
阅读完成 · 觉得有帮助?