跳到主要内容

C++ 运算符重载基础

什么是运算符重载?

运算符重载是C++中一项强大的特性,它允许程序员重新定义运算符对自定义数据类型的行为。通过运算符重载,我们可以使自定义类的对象能够像内置数据类型一样使用各种运算符,从而提高代码的可读性和表达能力。

备注

运算符重载本质上是一种特殊的函数,它的名称为operator加上运算符符号。

为什么需要运算符重载?

假设我们有一个表示复数的类:

cpp
class Complex {
private:
double real;
double imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 其他成员函数...
};

如果没有运算符重载,我们需要这样进行复数加法:

cpp
Complex a(1, 2);
Complex b(3, 4);
Complex c = a.add(b); // 不直观

通过重载+运算符,我们可以像使用基本数据类型一样:

cpp
Complex a(1, 2);
Complex b(3, 4);
Complex c = a + b; // 更直观、更自然

运算符重载的基本语法

成员函数形式

cpp
返回类型 类名::operator运算符(参数列表) {
// 实现代码
}

全局函数形式

cpp
返回类型 operator运算符(参数列表) {
// 实现代码
}

常见运算符重载示例

算术运算符重载

让我们为Complex类重载+运算符:

cpp
class Complex {
private:
double real;
double imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}

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

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

使用示例:

cpp
int main() {
Complex c1(3.0, 4.0);
Complex c2(5.0, 6.0);

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

std::cout << "c1 = ";
c1.display(); // 输出:c1 = 3 + 4i

std::cout << "c2 = ";
c2.display(); // 输出:c2 = 5 + 6i

std::cout << "c1 + c2 = ";
c3.display(); // 输出:c1 + c2 = 8 + 10i

return 0;
}

比较运算符重载

cpp
class Complex {
// 前面的代码...

// 重载==运算符
bool operator==(const Complex &obj) const {
return (real == obj.real && imag == obj.imag);
}

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

使用示例:

cpp
int main() {
Complex c1(3.0, 4.0);
Complex c2(3.0, 4.0);
Complex c3(5.0, 6.0);

if (c1 == c2)
std::cout << "c1等于c2" << std::endl; // 会输出

if (c1 != c3)
std::cout << "c1不等于c3" << std::endl; // 会输出

return 0;
}

流运算符重载

要实现方便的输入输出,我们可以重载<<>>运算符:

cpp
// 必须作为全局函数
std::ostream& operator<<(std::ostream &out, const Complex &c) {
out << c.getReal() << " + " << c.getImag() << "i";
return out;
}

std::istream& operator>>(std::istream &in, Complex &c) {
double r, i;
std::cout << "输入实部: ";
in >> r;
std::cout << "输入虚部: ";
in >> i;
c = Complex(r, i);
return in;
}

这需要在Complex类中添加访问器:

cpp
class Complex {
// 前面的代码...

double getReal() const { return real; }
double getImag() const { return imag; }

// 声明友元函数
friend std::ostream& operator<<(std::ostream &out, const Complex &c);
friend std::istream& operator>>(std::istream &in, Complex &c);
};

使用示例:

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

std::cout << "c1 = " << c1 << std::endl; // 输出:c1 = 3 + 4i

std::cin >> c2;
std::cout << "您输入的复数是: " << c2 << std::endl;

return 0;
}

运算符重载注意事项

可以重载的运算符

C++允许重载大多数内置运算符,包括:

  • 算术运算符: +, -, *, /, %, ++, --
  • 关系运算符: ==, !=, <, >, <=, >=
  • 逻辑运算符: !, &&, ||
  • 位运算符: &, |, ^, ~, <<, >>
  • 赋值运算符: =, +=, -=, *=, /=, %=
  • 下标运算符: []
  • 函数调用运算符: ()
  • 成员访问运算符: ->
  • 内存管理运算符: new, delete

不能重载的运算符

以下运算符不能重载:

  • 成员访问运算符: .
  • 作用域解析运算符: ::
  • 条件运算符: ?:
  • 预处理符号: #

重载运算符的规则

  1. 至少有一个操作数是自定义类型
  2. 不能改变运算符的优先级
  3. 不能改变运算符的结合性
  4. 不能改变运算符的操作数个数
  5. 不能创建新的运算符

成员函数与全局函数的选择

运算符重载可以通过成员函数或全局函数实现,选择何种方式主要考虑:

  1. 成员函数:

    • 左操作数必须是该类的对象
    • 可以访问类的私有成员
  2. 全局函数:

    • 需要访问私有成员时,可声明为友元
    • 当左操作数不是类对象时必须使用全局函数
提示

赋值运算符(=)、下标运算符([])、函数调用运算符(())和成员访问运算符(->)必须定义为成员函数。

实际应用案例:字符串类

下面是一个简单的MyString类,展示了运算符重载的实际应用:

cpp
class MyString {
private:
char* str;
int length;

public:
// 构造函数
MyString(const char* s = nullptr) {
if (s) {
length = strlen(s);
str = new char[length + 1];
strcpy(str, s);
} else {
length = 0;
str = new char[1];
str[0] = '\0';
}
}

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

// 析构函数
~MyString() {
delete[] str;
}

// 重载=运算符
MyString& operator=(const MyString& other) {
if (this == &other)
return *this;

delete[] str;
length = other.length;
str = new char[length + 1];
strcpy(str, other.str);
return *this;
}

// 重载+运算符(字符串连接)
MyString operator+(const MyString& other) const {
MyString result;
result.length = length + other.length;
result.str = new char[result.length + 1];
strcpy(result.str, str);
strcat(result.str, other.str);
return result;
}

// 重载[]运算符(访问单个字符)
char& operator[](int index) {
if (index >= 0 && index < length)
return str[index];
throw std::out_of_range("Index out of range");
}

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

// 获取字符串长度
int getLength() const {
return length;
}

// 获取C风格字符串
const char* c_str() const {
return str;
}

// 重载<<运算符的友元声明
friend std::ostream& operator<<(std::ostream& os, const MyString& s);
};

// 重载<<运算符的实现
std::ostream& operator<<(std::ostream& os, const MyString& s) {
os << s.str;
return os;
}

使用示例:

cpp
int main() {
MyString s1("Hello");
MyString s2(" World!");

// 使用+运算符连接字符串
MyString s3 = s1 + s2;
std::cout << "s1 + s2 = " << s3 << std::endl; // 输出:s1 + s2 = Hello World!

// 使用[]访问字符
std::cout << "s3[6] = " << s3[6] << std::endl; // 输出:s3[6] = W

// 使用==比较字符串
if (s1 == MyString("Hello"))
std::cout << "s1 equals \"Hello\"" << std::endl; // 会输出

// 赋值运算符
MyString s4;
s4 = s1;
std::cout << "s4 = " << s4 << std::endl; // 输出:s4 = Hello

return 0;
}

总结

运算符重载是C++的一个强大特性,使我们能够为自定义类型定义运算符行为,从而提高代码的可读性和易用性。本文介绍了:

  1. 运算符重载的基本概念与语法
  2. 常见运算符重载的实现方式
  3. 运算符重载的注意事项与限制
  4. 选择成员函数或全局函数的考量
  5. 实际应用案例

通过合理地使用运算符重载,我们可以创建出更加直观、更易于理解的代码。但也应当谨慎使用,避免滥用导致代码难以理解。

练习

  1. 为一个表示分数(Fraction)的类实现基本算术运算符(+、-、*、/)重载。
  2. 实现一个向量(Vector)类,并重载+、-、==、!=、[]等运算符。
  3. 为复数类额外实现-、、/、+=、-=、=、/=运算符。
  4. 实现一个矩阵(Matrix)类,并重载矩阵加法、乘法等运算符。

延伸阅读

  • C++ 运算符优先级表
  • 探索智能指针如何通过重载运算符模拟指针行为
  • 重载new和delete运算符进行内存管理的高级技巧

通过深入理解运算符重载,你将能够创建出更具表达力和可读性的C++代码。