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 保证了这一点。
  • 返回新对象:加法产生的是一个新的复数,不是修改原来的。
小技巧:我习惯把 operator+ 实现为成员函数。但如果你想让左操作数也能是普通 double 类型(比如 3.0 + c1),那就得用非成员函数了。这个我们后面会讲到。

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
    );
}
注意:浮点数判等用 == 0.0 其实不太严谨。严格来说应该判断绝对值是否小于某个极小值,比如 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 知识体系总览

下面这张图把复数类算术运算符重载的整个知识结构梳理了一下,方便你对照着看:

复数类算术运算符重载 二元运算符 operator+ operator- operator* operator/ 复合赋值运算符 operator+= operator-= operator*= operator/= 混合类型运算 double + Complex double - Complex double * Complex double / Complex 关键设计要点 const 成员函数 const 引用参数 除零检查 friend 友元

2.9 避坑指南

最后,分享几个我踩过的坑:

  • 忘记处理除零:这个前面说过了,浮点数除法一定要检查分母。
  • operator+ 返回了引用:有人会写成 Complex& operator+(...),然后返回局部对象的引用。这是未定义行为,程序随时可能崩溃。
  • 混合类型只做了一半:只写了 Complex + double,忘了 double + Complex。两个方向都要实现。
  • friend 声明写错位置:friend 声明要写在类体内部,而且不受 public/private 影响。我见过有人写在类外面,编译直接报错。

好了,复数类的算术运算符重载就讲到这里。代码虽然不长,但里面的设计思路和注意事项,放到其他自定义类型上也是通用的。你写自己的类时,照着这个模板改就行。


公众号:蓝海资料掘金营,微信deep3321