跳到主要内容

C++ 二元运算符重载

什么是二元运算符重载

在C++中,二元运算符是需要两个操作数的运算符,例如加法(+)、减法(-)、乘法(*)等。运算符重载是C++中一项强大的特性,它允许我们为自定义数据类型定义这些运算符的行为。

通过二元运算符重载,我们可以使自定义类的对象像内置数据类型一样使用这些运算符,从而提高代码的可读性和表达能力。

备注

二元运算符: 需要两个操作数的运算符,如 +, -, *, /, %, ==, !=, >, <, >=, <= 等。

二元运算符重载的基本语法

二元运算符重载可以通过以下两种方式实现:

  1. 成员函数方式
cpp
class ClassName {
public:
ReturnType operator运算符(const ParameterType& param);
};
  1. 友元函数方式
cpp
class ClassName {
friend ReturnType operator运算符(const ClassName& obj1, const ParameterType& obj2);
};

ReturnType operator运算符(const ClassName& obj1, const ParameterType& obj2) {
// 实现代码
}

成员函数与友元函数的区别

当使用成员函数重载二元运算符时,左侧操作数必须是类的对象,右侧操作数作为参数传入。例如:

cpp
Complex c1, c2;
Complex c3 = c1 + c2; // 相当于调用 c1.operator+(c2)

当使用友元函数重载二元运算符时,两个操作数都作为参数传入。这种方式更灵活,尤其是在处理混合类型操作数的情况下:

cpp
Complex c1;
double d = 5.0;
Complex c2 = c1 + d; // 使用友元函数,可以是 operator+(c1, d)
Complex c3 = d + c1; // 使用友元函数,可以是 operator+(d, c1)
提示

一般来说,对于二元运算符,如果要支持混合类型运算(例如对象+基本类型),或需要交换操作数位置的情况,推荐使用友元函数方式实现重载。

常见二元运算符重载实例

1. 算术运算符重载

让我们实现一个简单的复数类,并重载 +* 运算符:

cpp
#include <iostream>

class Complex {
private:
double real;
double imag;

public:
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}

// 成员函数方式重载 + 运算符
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}

// 友元函数方式重载 * 运算符
friend Complex operator*(const Complex& c1, const Complex& c2);

void display() const {
std::cout << real << " + " << imag << "i";
}
};

// 实现友元函数的重载
Complex operator*(const Complex& c1, const Complex& c2) {
// (a+bi) * (c+di) = (ac-bd) + (ad+bc)i
double resultReal = c1.real * c2.real - c1.imag * c2.imag;
double resultImag = c1.real * c2.imag + c1.imag * c2.real;
return Complex(resultReal, resultImag);
}

int main() {
Complex c1(3.0, 4.0);
Complex c2(1.0, 2.0);

// 使用重载的 + 运算符
Complex c3 = c1 + c2;

std::cout << "c1 = ";
c1.display();
std::cout << std::endl;

std::cout << "c2 = ";
c2.display();
std::cout << std::endl;

std::cout << "c1 + c2 = ";
c3.display();
std::cout << std::endl;

// 使用重载的 * 运算符
Complex c4 = c1 * c2;
std::cout << "c1 * c2 = ";
c4.display();
std::cout << std::endl;

return 0;
}

输出结果:

c1 = 3 + 4i
c2 = 1 + 2i
c1 + c2 = 4 + 6i
c1 * c2 = -5 + 10i

2. 比较运算符重载

下面是一个自定义String类,重载了比较运算符:

cpp
#include <iostream>
#include <cstring>

class String {
private:
char* data;

public:
String(const char* str = nullptr) {
if (str == nullptr) {
data = new char[1];
data[0] = '\0';
} else {
data = new char[strlen(str) + 1];
strcpy(data, str);
}
}

~String() {
delete[] data;
}

// 拷贝构造函数
String(const String& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}

// 重载 == 运算符
bool operator==(const String& other) const {
return strcmp(data, other.data) == 0;
}

// 重载 != 运算符
bool operator!=(const String& other) const {
return !(*this == other);
}

// 重载 < 运算符
bool operator<(const String& other) const {
return strcmp(data, other.data) < 0;
}

// 输出函数
friend std::ostream& operator<<(std::ostream& os, const String& str);
};

// 重载输出运算符
std::ostream& operator<<(std::ostream& os, const String& str) {
os << str.data;
return os;
}

int main() {
String s1("hello");
String s2("hello");
String s3("world");

std::cout << "s1 = " << s1 << std::endl;
std::cout << "s2 = " << s2 << std::endl;
std::cout << "s3 = " << s3 << std::endl;

std::cout << "s1 == s2: " << (s1 == s2 ? "true" : "false") << std::endl;
std::cout << "s1 != s3: " << (s1 != s3 ? "true" : "false") << std::endl;
std::cout << "s1 < s3: " << (s1 < s3 ? "true" : "false") << std::endl;

return 0;
}

输出结果:

s1 = hello
s2 = hello
s3 = world
s1 == s2: true
s1 != s3: true
s1 < s3: true

3. 重载输入输出运算符

输入输出运算符 <<>> 重载是二元运算符重载的常见应用:

cpp
#include <iostream>

class Point {
private:
int x, y;

public:
Point(int x = 0, int y = 0) : x(x), y(y) {}

// 重载输出运算符 <<
friend std::ostream& operator<<(std::ostream& os, const Point& p);

// 重载输入运算符 >>
friend std::istream& operator>>(std::istream& is, Point& p);
};

// 重载输出运算符
std::ostream& operator<<(std::ostream& os, const Point& p) {
os << "(" << p.x << ", " << p.y << ")";
return os;
}

// 重载输入运算符
std::istream& operator>>(std::istream& is, Point& p) {
std::cout << "请输入点的x坐标: ";
is >> p.x;
std::cout << "请输入点的y坐标: ";
is >> p.y;
return is;
}

int main() {
Point p1(10, 20);
std::cout << "初始点p1: " << p1 << std::endl;

Point p2;
std::cout << "请输入一个新点p2的坐标:" << std::endl;
std::cin >> p2;

std::cout << "你输入的点p2是: " << p2 << std::endl;

return 0;
}

输出结果(假设用户输入x=5, y=15):

初始点p1: (10, 20)
请输入一个新点p2的坐标:
请输入点的x坐标: 5
请输入点的y坐标: 15
你输入的点p2是: (5, 15)

二元运算符重载的最佳实践

在实现二元运算符重载时,应该遵循以下几点最佳实践:

  1. 保持语义一致性 - 重载后的运算符应该保持与内置类型相似的语义。例如,+ 应该执行某种"加法"操作,而不是做完全不相关的事情。

  2. 返回类型选择 - 尽可能返回合适的类型。例如,算术运算符通常返回相同类型的新对象,比较运算符返回布尔值。

  3. 参数传递方式 - 通常应该通过常量引用传递对象参数,以避免不必要的拷贝。

  4. 实现互补操作 - 如果重载了 ==,通常也应该重载 !=;如果重载了 <,考虑也重载 >, <=>=

  5. 成员函数 vs 友元函数 - 如果操作强烈地绑定到类型语义,使用成员函数;如果需要支持混合类型操作数或左边操作数不是当前类,使用友元函数。

警告

重载运算符时应小心谨慎,避免定义直观上令人困惑的行为。不要过度使用运算符重载,仅在能真正提高代码清晰度和可读性时使用它。

实际案例:向量类的实现

让我们通过实现一个简单的向量类,来展示二元运算符重载的实际应用:

cpp
#include <iostream>
#include <vector>
#include <cmath>

class Vector {
private:
std::vector<double> elements;

public:
// 构造函数
Vector() {}

Vector(const std::vector<double>& elems) : elements(elems) {}

// 获取向量维度
size_t size() const {
return elements.size();
}

// 访问元素
double& operator[](size_t index) {
return elements[index];
}

const double& operator[](size_t index) const {
return elements[index];
}

// 重载 + 运算符(向量加法)
Vector operator+(const Vector& other) const {
if (size() != other.size()) {
throw std::invalid_argument("向量维度不匹配");
}

std::vector<double> result(size());
for (size_t i = 0; i < size(); ++i) {
result[i] = elements[i] + other[i];
}

return Vector(result);
}

// 重载 - 运算符(向量减法)
Vector operator-(const Vector& other) const {
if (size() != other.size()) {
throw std::invalid_argument("向量维度不匹配");
}

std::vector<double> result(size());
for (size_t i = 0; i < size(); ++i) {
result[i] = elements[i] - other[i];
}

return Vector(result);
}

// 重载 * 运算符(向量点积)
friend double operator*(const Vector& v1, const Vector& v2);

// 重载 * 运算符(标量乘法)
friend Vector operator*(const Vector& v, double scalar);
friend Vector operator*(double scalar, const Vector& v);

// 向量长度(模)
double magnitude() const {
double sum = 0.0;
for (const auto& elem : elements) {
sum += elem * elem;
}
return std::sqrt(sum);
}

// 重载输出运算符
friend std::ostream& operator<<(std::ostream& os, const Vector& v);
};

// 向量点积
double operator*(const Vector& v1, const Vector& v2) {
if (v1.size() != v2.size()) {
throw std::invalid_argument("向量维度不匹配");
}

double result = 0.0;
for (size_t i = 0; i < v1.size(); ++i) {
result += v1[i] * v2[i];
}

return result;
}

// 标量左乘
Vector operator*(double scalar, const Vector& v) {
std::vector<double> result(v.size());
for (size_t i = 0; i < v.size(); ++i) {
result[i] = scalar * v[i];
}
return Vector(result);
}

// 标量右乘
Vector operator*(const Vector& v, double scalar) {
return scalar * v; // 复用标量左乘
}

// 输出向量
std::ostream& operator<<(std::ostream& os, const Vector& v) {
os << "[";
for (size_t i = 0; i < v.size(); ++i) {
os << v[i];
if (i < v.size() - 1) {
os << ", ";
}
}
os << "]";
return os;
}

int main() {
// 创建两个向量
Vector v1({1.0, 2.0, 3.0});
Vector v2({4.0, 5.0, 6.0});

std::cout << "v1 = " << v1 << std::endl;
std::cout << "v2 = " << v2 << std::endl;

// 向量加法
Vector v3 = v1 + v2;
std::cout << "v1 + v2 = " << v3 << std::endl;

// 向量减法
Vector v4 = v1 - v2;
std::cout << "v1 - v2 = " << v4 << std::endl;

// 向量点积
double dot = v1 * v2;
std::cout << "v1 · v2 = " << dot << std::endl;

// 标量乘法
Vector v5 = 2.0 * v1;
std::cout << "2.0 * v1 = " << v5 << std::endl;

Vector v6 = v2 * 3.0;
std::cout << "v2 * 3.0 = " << v6 << std::endl;

// 向量长度
std::cout << "|v1| = " << v1.magnitude() << std::endl;

return 0;
}

输出结果:

v1 = [1, 2, 3]
v2 = [4, 5, 6]
v1 + v2 = [5, 7, 9]
v1 - v2 = [-3, -3, -3]
v1 · v2 = 32
2.0 * v1 = [2, 4, 6]
v2 * 3.0 = [12, 15, 18]
|v1| = 3.74166

这个案例展示了在数学库中常见的向量操作的实现,通过运算符重载使得向量操作直观且接近数学表达。

总结

二元运算符重载是C++中强大而灵活的特性,它可以:

  1. 使自定义类型的对象使用自然的语法进行操作
  2. 提高代码的可读性和直观性
  3. 使代码更接近于问题域的表达方式

在实现二元运算符重载时,需要考虑成员函数和友元函数两种方式的区别,并根据需要选择合适的实现方式。友元函数尤其适合处理混合类型运算和需要交换操作数顺序的情况。

重要的是,运算符重载应该符合直觉,保持与内置类型一致的语义,不要滥用这一特性,否则会导致代码难以理解和维护。

练习

  1. 为一个分数类(Fraction)重载 +, -, */ 运算符,使其支持基本的分数运算。

  2. 重载一个自定义字符串类的 + 运算符,使其支持字符串的连接操作,并且能处理字符串与C风格字符数组的混合操作。

  3. 为一个二维点类(Point2D)重载比较运算符 ==, !=, 使其可以比较两个点是否相等。

  4. 为Matrix(矩阵)类实现基本矩阵运算,包括加法、减法和乘法的运算符重载。

进一步学习资源

通过这些练习和资源,你可以更深入地理解和掌握C++中的二元运算符重载,为编写更优雅、直观的代码打下基础。