C++ 二元运算符重载
什么是二元运算符重载
在C++中,二元运算符是需要两个操作数的运算符,例如加法(+
)、减法(-
)、乘法(*
)等。运算符重载是C++中一项强大的特性,它允许我们为自定义数据类型定义这些运算符的行为。
通过二元运算符重载,我们可以使自定义类的对象像内置数据类型一样使用这些运算符,从而提高代码的可读性和表达能力。
二元运算符: 需要两个操作数的运算符,如 +
, -
, *
, /
, %
, ==
, !=
, >
, <
, >=
, <=
等。
二元运算符重载的基本语法
二元运算符重载可以通过以下两种方式实现:
- 成员函数方式
class ClassName {
public:
ReturnType operator运算符(const ParameterType& param);
};
- 友元函数方式
class ClassName {
friend ReturnType operator运算符(const ClassName& obj1, const ParameterType& obj2);
};
ReturnType operator运算符(const ClassName& obj1, const ParameterType& obj2) {
// 实现代码
}
成员函数与友元函数的区别
当使用成员函数重载二元运算符时,左侧操作数必须是类的对象,右侧操作数作为参数传入。例如:
Complex c1, c2;
Complex c3 = c1 + c2; // 相当于调用 c1.operator+(c2)
当使用友元函数重载二元运算符时,两个操作数都作为参数传入。这种方式更灵活,尤其是在处理混合类型操作数的情况下:
Complex c1;
double d = 5.0;
Complex c2 = c1 + d; // 使用友元函数,可以是 operator+(c1, d)
Complex c3 = d + c1; // 使用友元函数,可以是 operator+(d, c1)
一般来说,对于二元运算符,如果要支持混合类型运算(例如对象+基本类型),或需要交换操作数位置的情况,推荐使用友元函数方式实现重载。
常见二元运算符重载实例
1. 算术运算符重载
让我们实现一个简单的复数类,并重载 +
和 *
运算符:
#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类,重载了比较运算符:
#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. 重载输入输出运算符
输入输出运算符 <<
和 >>
重载是二元运算符重载的常见应用:
#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)
二元运算符重载的最佳实践
在实现二元运算符重载时,应该遵循以下几点最佳实践:
-
保持语义一致性 - 重载后的运算符应该保持与内置类型相似的语义。例如,
+
应该执行某种"加法"操作,而不是做完全不相关的事情。 -
返回类型选择 - 尽可能返回合适的类型。例如,算术运算符通常返回相同类型的新对象,比较运算符返回布尔值。
-
参数传递方式 - 通常应该通过常量引用传递对象参数,以避免不必要的拷贝。
-
实现互补操作 - 如果重载了
==
,通常也应该重载!=
;如果重载了<
,考虑也重载>
,<=
和>=
。 -
成员函数 vs 友元函数 - 如果操作强烈地绑定到类型语义,使用成员函数;如果需要支持混合类型操作数或左边操作数不是当前类,使用友元函数。
重载运算符时应小心谨慎,避免定义直观上令人困惑的行为。不要过度使用运算符重载,仅在能真正提高代码清晰度和可读性时使用它。
实际案例:向量类的实现
让我们通过实现一个简单的向量类,来展示二元运算符重载的实际应用:
#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++中强大而灵活的特性,它可以:
- 使自定义类型的对象使用自然的语法进行操作
- 提高代码的可读性和直观性
- 使代码更接近于问题域的表达方式
在实现二元运算符重载时,需要考虑成员函数和友元函数两种方式的区别,并根据需要选择合适的实现方式。友元函数尤其适合处理混合类型运算和需要交换操作数顺序的情况。
重要的是,运算符重载应该符合直觉,保持与内置类型一致的语义,不要滥用这一特性,否则会导致代码难以理解和维护。
练习
-
为一个分数类(Fraction)重载
+
,-
,*
和/
运算符,使其支持基本的分数运算。 -
重载一个自定义字符串类的
+
运算符,使其支持字符串的连接操作,并且能处理字符串与C风格字符数组的混合操作。 -
为一个二维点类(Point2D)重载比较运算符
==
,!=
, 使其可以比较两个点是否相等。 -
为Matrix(矩阵)类实现基本矩阵运算,包括加法、减法和乘法的运算符重载。
进一步学习资源
- 《C++ Primer》第14章:重载运算与类型转换
- 《Effective C++》第5条:了解C++默默编写并调用哪些函数
- C++ 参考手册 - 运算符重载
- C++ 标准库中的运算符重载示例
通过这些练习和资源,你可以更深入地理解和掌握C++中的二元运算符重载,为编写更优雅、直观的代码打下基础。