C++学习:六个月从基础到就业——面向对象编程:重载运算符(上)
本文是我C++学习之旅系列的第十二篇技术文章,主要探讨C++中的运算符重载机制,这是面向对象编程中一个强大而独特的特性。由于内容较多,本主题将分为上下两篇文章。查看完整系列目录了解更多内容。
引言
C++的运算符重载是一种强大的特性,它允许我们为自定义类型定义标准运算符的行为。通过运算符重载,我们可以创建行为类似于内置类型的用户定义类型,从而编写更加直观、可读性更强的代码。例如,我们可以创建一个复数类,并为它定义加法运算符,使两个复数对象可以像简单的内置类型一样相加:Complex a, b, c; c = a + b;
。
运算符重载是C++相比其他许多编程语言的独特优势之一,它为面向对象编程提供了更为优雅和自然的语法,但也需要谨慎使用,以避免导致混淆和误解。在本文(上篇)中,我们将探讨运算符重载的基础知识,包括语法、使用场景和最常用的运算符重载实现,如算术运算符、赋值运算符和流运算符等。
运算符重载基础
什么是运算符重载?
运算符重载允许程序员为自定义类型重新定义C++内置运算符的行为。通过这种方式,我们可以使自定义类型表现得像内置类型一样,支持各种常见的运算符操作。
运算符重载本质上是一种特殊形式的函数调用,当运算符应用于类类型的对象时,它会被翻译为对相应函数的调用。例如,表达式 a + b
可能被解释为函数调用 operator+(a, b)
或 a.operator+(b)
,具体取决于如何定义。
运算符重载的语法
在C++中,运算符重载可以通过两种方式实现:
- 作为类的成员函数
- 作为全局函数(非成员函数)
成员函数形式
class MyClass {
public:// 成员函数形式的运算符重载ReturnType operator+(const MyClass& rhs) const;
};
全局函数形式
// 全局函数形式的运算符重载
ReturnType operator+(const MyClass& lhs, const MyClass& rhs);
可重载与不可重载的运算符
C++允许重载大多数内置运算符,但也有一些例外:
可重载的运算符:
+ - * / % ^ & | ~
! = < > += -= *= /= %=
^= &= |= << >> >>= <<= == !=
<= >= && || ++ -- , ->* ->
[] () new delete new[] delete[]
不可重载的运算符:
. .* :: ?: sizeof typeid alignof nullptr
成员函数与非成员函数的选择
选择将运算符重载为成员函数还是非成员函数,取决于几个因素:
- 成员函数: 左操作数必须是该类的对象
- 非成员函数: 可以处理转换左操作数的情况
一般来说:
- 赋值类运算符(
=
,+=
,-=
等)最好是成员函数 - 需要改变对象状态的运算符(如
++
,--
)通常是成员函数 - 二元运算符(如
+
,-
,*
,/
)通常是非成员函数(可能是友元函数) - I/O运算符(
<<
,>>
)必须是非成员函数 - 下标运算符(
[]
)、函数调用运算符(()
)、成员访问运算符(->
)必须是成员函数
以下是一个简单的例子,展示了两种形式:
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& rhs) const {return Complex(real + rhs.real, imag + rhs.imag);}// 获取方法(用于非成员函数形式)double getReal() const { return real; }double getImag() const { return imag; }// 非成员函数形式的重载的友元声明friend Complex operator-(const Complex& lhs, const Complex& rhs);
};// 非成员函数形式的重载实现
Complex operator-(const Complex& lhs, const Complex& rhs) {return Complex(lhs.real - rhs.real, lhs.imag - rhs.imag);
}
常见运算符重载
算术运算符(+、-、*、/)
算术运算符通常应该定义为不修改其操作数,并且返回一个新对象。它们可以定义为成员函数或非成员函数,但非成员函数通常更为灵活,因为它们允许左操作数进行隐式类型转换。
示例:Vector2D类的算术运算符重载
#include <iostream>class Vector2D {
private:double x;double y;public:Vector2D(double x = 0.0, double y = 0.0) : x(x), y(y) {}// 获取x和y坐标double getX() const { return x; }double getY() const { return y; }// 显示向量void display() const {std::cout << "(" << x << ", " << y << ")" << std::endl;}// 成员函数形式的加法运算符Vector2D operator+(const Vector2D& rhs) const {return Vector2D(x + rhs.x, y + rhs.y);}// 友元函数形式的减法运算符friend Vector2D operator-(const Vector2D& lhs, const Vector2D& rhs);
};// 减法运算符实现
Vector2D operator-(const Vector2D& lhs, const Vector2D& rhs) {return Vector2D(lhs.getX() - rhs.getX(), lhs.getY() - rhs.getY());
}int main() {Vector2D v1(3.0, 4.0);Vector2D v2(1.0, 2.0);std::cout << "v1: ";v1.display();std::cout << "v2: ";v2.display();Vector2D v3 = v1 + v2; // 使用重载的加法运算符std::cout << "v1 + v2: ";v3.display();Vector2D v4 = v1 - v2; // 使用重载的减法运算符std::cout << "v1 - v2: ";v4.display();return 0;
}
加入其他算术运算符:
class Vector2D {// ...前面的代码...// 标量乘法(向量 * 标量)Vector2D operator*(double scalar) const {return Vector2D(x * scalar, y * scalar);}// 标量除法(向量 / 标量)Vector2D operator/(double scalar) const {if (scalar == 0) {throw std::invalid_argument("Division by zero");}return Vector2D(x / scalar, y / scalar);}// 点积(向量 * 向量)double dot(const Vector2D& rhs) const {return x * rhs.x + y * rhs.y;}
};// 标量乘法(标量 * 向量)
Vector2D operator*(double scalar, const Vector2D& vec) {return vec * scalar; // 重用成员运算符
}
复合赋值运算符(+=、-=、*=、/=)
复合赋值运算符应该定义为成员函数,因为它们修改了左操作数。它们通常返回*this
的引用,以支持链式操作。
class Vector2D {// ...前面的代码...// 复合赋值运算符Vector2D& operator+=(const Vector2D& rhs) {x += rhs.x;y += rhs.y;return *this;}Vector2D& operator-=(const Vector2D& rhs) {x -= rhs.x;y -= rhs.y;return *this;}Vector2D& operator*=(double scalar) {x *= scalar;y *= scalar;return *this;}Vector2D& operator/=(double scalar) {if (scalar == 0) {throw std::invalid_argument("Division by zero");}x /= scalar;y /= scalar;return *this;}
};
使用复合赋值运算符实现算术运算符是一种常见的做法,这样可以减少代码重复:
// 基于复合赋值运算符的加法
Vector2D operator+(Vector2D lhs, const Vector2D& rhs) {lhs += rhs; // 使用复合赋值运算符return lhs;
}// 基于复合赋值运算符的减法
Vector2D operator-(Vector2D lhs, const Vector2D& rhs) {lhs -= rhs; // 使用复合赋值运算符return lhs;
}// 基于复合赋值运算符的乘法
Vector2D operator*(Vector2D vec, double scalar) {vec *= scalar; // 使用复合赋值运算符return vec;
}// 基于复合赋值运算符的除法
Vector2D operator/(Vector2D vec, double scalar) {vec /= scalar; // 使用复合赋值运算符return vec;
}
一元运算符(+、-)
一元运算符也可以重载,通常定义为成员函数:
class Vector2D {// ...前面的代码...// 一元加号运算符(+v)Vector2D operator+() const {return *this; // 返回当前对象的副本}// 一元减号运算符(-v)Vector2D operator-() const {return Vector2D(-x, -y); // 返回所有分量取反的向量}
};
相等和关系运算符(==、!=、<、>、<=、>=)
相等和关系运算符通常定义为非成员函数,以保证对称性,特别是当涉及类型转换时。这些运算符应该返回布尔值。
示例:Complex类的相等运算符
class Complex {
private:double real;double imag;public:Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}double getReal() const { return real; }double getImag() const { return imag; }// 相等运算符作为友元friend bool operator==(const Complex& lhs, const Complex& rhs);friend bool operator!=(const Complex& lhs, const Complex& rhs);
};// 相等运算符实现
bool operator==(const Complex& lhs, const Complex& rhs) {return lhs.real == rhs.real && lhs.imag == rhs.imag;
}// 不等运算符实现
bool operator!=(const Complex& lhs, const Complex& rhs) {return !(lhs == rhs); // 重用相等运算符
}
对于需要排序功能的类,也应该定义关系运算符:
class Person {
private:std::string name;int age;public:Person(const std::string& n, int a) : name(n), age(a) {}const std::string& getName() const { return name; }int getAge() const { return age; }// 关系运算符作为友元friend bool operator<(const Person& lhs, const Person& rhs);friend bool operator>(const Person& lhs, const Person& rhs);friend bool operator<=(const Person& lhs, const Person& rhs);friend bool operator>=(const Person& lhs, const Person& rhs);
};// 小于运算符实现
bool operator<(const Person& lhs, const Person& rhs) {if (lhs.name != rhs.name) {return lhs.name < rhs.name; // 首先按名字排序}return lhs.age < rhs.age; // 名字相同时按年龄排序
}// 大于运算符实现
bool operator>(const Person& lhs, const Person& rhs) {return rhs < lhs; // 重用小于运算符
}// 小于等于运算符实现
bool operator<=(const Person& lhs, const Person& rhs) {return !(rhs < lhs); // 重用小于运算符
}// 大于等于运算符实现
bool operator>=(const Person& lhs, const Person& rhs) {return !(lhs < rhs); // 重用小于运算符
}
输入输出运算符(<<、>>)
输入输出运算符必须定义为非成员函数,通常是友元函数:
class Complex {
private:double real;double imag;public:Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}// 输出运算符作为友元friend std::ostream& operator<<(std::ostream& os, const Complex& c);// 输入运算符作为友元friend std::istream& operator>>(std::istream& is, Complex& c);
};// 输出运算符实现
std::ostream& operator<<(std::ostream& os, const Complex& c) {if (c.imag >= 0) {os << c.real << " + " << c.imag << "i";} else {os << c.real << " - " << -c.imag << "i";}return os;
}// 输入运算符实现
std::istream& operator>>(std::istream& is, Complex& c) {char ch1, ch2, ch3;double real, imag;// 期望输入格式如 "(1.2+3.4i)"is >> ch1 >> real >> ch2 >> imag >> ch3;if (is && ch1 == '(' && (ch2 == '+' || ch2 == '-') && ch3 == ')') {c.real = real;c.imag = (ch2 == '+') ? imag : -imag;} else {// 设置失败状态is.setstate(std::ios_base::failbit);}return is;
}
递增和递减运算符(++、–)
递增和递减运算符可以是前缀(++i)或后缀(i++)形式。它们通常定义为成员函数。
- 前缀形式返回对象的引用
- 后缀形式返回对象的旧值(副本)
class Counter {
private:int count;public:Counter(int c = 0) : count(c) {}int getCount() const { return count; }// 前缀递增 (++c)Counter& operator++() {++count;return *this;}// 后缀递增 (c++)// 注意:int参数是一个占位符,用于区分前缀和后缀形式Counter operator++(int) {Counter temp = *this;++count;return temp; // 返回递增前的副本}// 前缀递减 (--c)Counter& operator--() {--count;return *this;}// 后缀递减 (c--)Counter operator--(int) {Counter temp = *this;--count;return temp; // 返回递减前的副本}
};int main() {Counter c(5);std::cout << "初始值: " << c.getCount() << std::endl;++c; // 前缀递增std::cout << "++c后: " << c.getCount() << std::endl;c++; // 后缀递增std::cout << "c++后: " << c.getCount() << std::endl;--c; // 前缀递减std::cout << "--c后: " << c.getCount() << std::endl;c--; // 后缀递减std::cout << "c--后: " << c.getCount() << std::endl;// 区分前缀和后缀形式Counter c1(1), c2(1), c3, c4;c3 = ++c1; // 前缀递增:先递增c1,再将新值赋给c3c4 = c2++; // 后缀递增:先将c2的旧值赋给c4,再递增c2std::cout << "c1: " << c1.getCount() << std::endl; // 输出2std::cout << "c2: " << c2.getCount() << std::endl; // 输出2std::cout << "c3: " << c3.getCount() << std::endl; // 输出2std::cout << "c4: " << c4.getCount() << std::endl; // 输出1return 0;
}
下标运算符([])
下标运算符必须是成员函数,通常提供const和非const版本:
class Array {
private:int* data;size_t size;public:Array(size_t s) : size(s) {data = new int[size](); // 初始化为0}~Array() {delete[] data;}// 禁止复制Array(const Array&) = delete;Array& operator=(const Array&) = delete;// 非const版本的下标运算符(可修改元素)int& operator[](size_t index) {if (index >= size) {throw std::out_of_range("Index out of bounds");}return data[index];}// const版本的下标运算符(不可修改元素)const int& operator[](size_t index) const {if (index >= size) {throw std::out_of_range("Index out of bounds");}return data[index];}size_t getSize() const { return size; }
};int main() {Array arr(5);// 使用非const版本的下标运算符修改元素for (size_t i = 0; i < arr.getSize(); ++i) {arr[i] = i * 10;}// 使用const版本的下标运算符访问元素const Array& carr = arr;for (size_t i = 0; i < carr.getSize(); ++i) {std::cout << "arr[" << i << "] = " << carr[i] << std::endl;}return 0;
}
赋值运算符(=)
基本赋值运算符
赋值运算符是唯一一个由编译器自动生成的运算符(如果类没有显式定义),但自动生成的版本只执行成员的逐个复制(浅拷贝),这对于包含指针成员的类来说通常是不够的。
因此,对于管理资源的类,通常需要自定义赋值运算符:
class MyString {
private:char* data;size_t length;public:// 构造函数MyString(const char* str = nullptr) {if (str) {length = strlen(str);data = new char[length + 1];strcpy(data, str);} else {length = 0;data = new char[1];data[0] = '\0';}}// 析构函数~MyString() {delete[] data;}// 复制构造函数MyString(const MyString& other) {length = other.length;data = new char[length + 1];strcpy(data, other.data);}// 赋值运算符MyString& operator=(const MyString& other) {// 自赋值检查if (this == &other) {return *this;}// 释放现有资源delete[] data;// 复制新资源length = other.length;data = new char[length + 1];strcpy(data, other.data);// 返回*thisreturn *this;}// 获取字符串const char* c_str() const { return data; }// 获取长度size_t size() const { return length; }
};
copy-and-swap习惯用法
一种更安全的实现赋值运算符的方法是使用copy-and-swap习惯用法,它利用了异常安全的资源管理:
class MyString {// ...前面的代码...// 交换函数void swap(MyString& other) noexcept {std::swap(data, other.data);std::swap(length, other.length);}// 使用copy-and-swap习惯用法的赋值运算符MyString& operator=(MyString other) { // 注意:这里是传值,而不是引用swap(other); // 交换*this和other的内容return *this; // other销毁时会释放原来的资源}
};// 全局交换函数
void swap(MyString& lhs, MyString& rhs) {lhs.swap(rhs);
}
移动语义与移动运算符(C++11)
C++11引入了移动语义,允许"窃取"即将被销毁的对象的资源,而不是复制它们。这对于包含动态分配资源的类非常有用。
移动构造函数和移动赋值运算符
class MyString {// ...前面的代码...// 移动构造函数MyString(MyString&& other) noexcept {// 窃取资源data = other.data;length = other.length;// 将other置于有效但可销毁的状态other.data = nullptr;other.length = 0;}// 移动赋值运算符MyString& operator=(MyString&& other) noexcept {if (this != &other) { // 虽然不太可能,但还是检查自赋值// 释放现有资源delete[] data;// 窃取资源data = other.data;length = other.length;// 将other置于有效但可销毁的状态other.data = nullptr;other.length = 0;}return *this;}
};
使用移动语义的示例:
MyString createString(const char* str) {return MyString(str); // 返回的是临时对象,可以移动而不是复制
}int main() {MyString s1("Hello");std::cout << "s1: " << s1.c_str() << std::endl;// 使用移动语义MyString s2 = std::move(s1); // 显式移动std::cout << "s2 after move: " << s2.c_str() << std::endl;std::cout << "s1 after move: " << (s1.c_str() ? s1.c_str() : "null") << std::endl;// 从函数返回值移动MyString s3 = createString("World"); // 编译器可能应用移动优化std::cout << "s3: " << s3.c_str() << std::endl;return 0;
}
函数调用运算符(())
函数调用运算符使对象可以像函数一样被调用,这也被称为函数对象或仿函数:
class Adder {
private:int offset;public:Adder(int off) : offset(off) {}// 函数调用运算符int operator()(int x) const {return x + offset;}
};int main() {Adder add5(5);std::cout << add5(10) << std::endl; // 输出15std::cout << add5(20) << std::endl; // 输出25// 可用于STL算法std::vector<int> nums = {1, 2, 3, 4, 5};std::vector<int> results(nums.size());std::transform(nums.begin(), nums.end(), results.begin(), add5);for (int n : results) {std::cout << n << " "; // 输出6 7 8 9 10}std::cout << std::endl;return 0;
}
实际应用:Complex类
下面是一个复数类的完整实现,展示了多种运算符重载:
#include <iostream>
#include <cmath>class Complex {
private:double real; // 实部double imag; // 虚部public:// 构造函数Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}// 获取实部和虚部double getReal() const { return real; }double getImag() const { return imag; }// 复数的模double magnitude() const {return std::sqrt(real * real + imag * imag);}// 一元加法运算符Complex operator+() const {return *this; // 返回对象的副本}// 一元减法运算符Complex operator-() const {return Complex(-real, -imag);}// 复合赋值运算符Complex& operator+=(const Complex& rhs) {real += rhs.real;imag += rhs.imag;return *this;}Complex& operator-=(const Complex& rhs) {real -= rhs.real;imag -= rhs.imag;return *this;}Complex& operator*=(const Complex& rhs) {double temp_real = real * rhs.real - imag * rhs.imag;double temp_imag = real * rhs.imag + imag * rhs.real;real = temp_real;imag = temp_imag;return *this;}Complex& operator/=(const Complex& rhs) {double denominator = rhs.real * rhs.real + rhs.imag * rhs.imag;if (denominator == 0.0) {throw std::invalid_argument("Division by zero");}double temp_real = (real * rhs.real + imag * rhs.imag) / denominator;double temp_imag = (imag * rhs.real - real * rhs.imag) / denominator;real = temp_real;imag = temp_imag;return *this;}// 友元二元运算符friend Complex operator+(Complex lhs, const Complex& rhs);friend Complex operator-(Complex lhs, const Complex& rhs);friend Complex operator*(Complex lhs, const Complex& rhs);friend Complex operator/(Complex lhs, const Complex& rhs);// 友元相等和不等运算符friend bool operator==(const Complex& lhs, const Complex& rhs);friend bool operator!=(const Complex& lhs, const Complex& rhs);// 友元输入输出运算符friend std::ostream& operator<<(std::ostream& os, const Complex& c);friend std::istream& operator>>(std::istream& is, Complex& c);
};// 实现二元算术运算符
Complex operator+(Complex lhs, const Complex& rhs) {lhs += rhs; // 使用复合赋值运算符return lhs;
}Complex operator-(Complex lhs, const Complex& rhs) {lhs -= rhs; // 使用复合赋值运算符return lhs;
}Complex operator*(Complex lhs, const Complex& rhs) {lhs *= rhs; // 使用复合赋值运算符return lhs;
}Complex operator/(Complex lhs, const Complex& rhs) {lhs /= rhs; // 使用复合赋值运算符return lhs;
}// 实现相等和不等运算符
bool operator==(const Complex& lhs, const Complex& rhs) {return lhs.real == rhs.real && lhs.imag == rhs.imag;
}bool operator!=(const Complex& lhs, const Complex& rhs) {return !(lhs == rhs);
}// 实现输出运算符
std::ostream& operator<<(std::ostream& os, const Complex& c) {if (c.imag >= 0.0) {os << c.real << " + " << c.imag << "i";} else {os << c.real << " - " << -c.imag << "i";}return os;
}// 实现输入运算符
std::istream& operator>>(std::istream& is, Complex& c) {double r, i;char ch;is >> r; // 读取实部// 检查有无虚部if (is.peek() == '+' || is.peek() == '-') {is >> ch >> i; // 读取符号和虚部数值// 检查'i'if (is.peek() == 'i') {is.get(); // 提取'i'c.real = r;c.imag = (ch == '+') ? i : -i;} else {is.setstate(std::ios_base::failbit); // 格式错误}} else {c.real = r;c.imag = 0.0; // 如果未提供虚部,设为0}return is;
}// 用户自定义字面量(C++11)
Complex operator"" _i(long double val) {return Complex(0.0, static_cast<double>(val));
}int main() {Complex a(3.0, 4.0);Complex b(1.5, -2.5);std::cout << "a = " << a << std::endl;std::cout << "b = " << b << std::endl;// 使用各种运算符std::cout << "a + b = " << (a + b) << std::endl;std::cout << "a - b = " << (a - b) << std::endl;std::cout << "a * b = " << (a * b) << std::endl;std::cout << "a / b = " << (a / b) << std::endl;// 一元运算符std::cout << "+a = " << +a << std::endl;std::cout << "-a = " << -a << std::endl;// 复数的模std::cout << "|a| = " << a.magnitude() << std::endl;std::cout << "|b| = " << b.magnitude() << std::endl;// 相等比较Complex c(3.0, 4.0);std::cout << "a == c? " << std::boolalpha << (a == c) << std::endl;std::cout << "a != b? " << (a != b) << std::endl;// 使用用户自定义字面量Complex d = 2.0 + 3.0_i;std::cout << "d = " << d << std::endl;// 测试输入运算符std::cout << "请输入一个复数(格式:a+bi 或 a-bi 或 a):";Complex input;if (std::cin >> input) {std::cout << "你输入的复数是:" << input << std::endl;} else {std::cout << "输入格式错误!" << std::endl;}return 0;
}
小结与注意事项
在这篇文章中,我们探讨了C++运算符重载的基础知识和常见用例。通过合理地重载运算符,我们可以使自定义类型表现得更像内置类型,提高代码的可读性和表达力。然而,运算符重载也需要谨慎使用,遵循一些重要的原则:
- 保持语义一致性:重载的运算符应该与其原始语义保持一致,避免违反用户的直观预期。
- 维护关系运算符之间的一致性:如
a < b
和b > a
应该表达相同的含义。 - 通常为复合赋值运算符返回
*this
的引用:这使得它们可以进行链式操作。 - 避免意外的隐式转换:适当使用
explicit
关键字防止不希望的隐式类型转换。 - 考虑自赋值和异常安全:特别是在赋值运算符中,确保自赋值情况下的安全操作。
- 成对实现相关的运算符:例如,实现了
==
就应该实现!=
。
在下一篇文章中,我们将继续探讨更高级的运算符重载技术,包括自定义类型转换、内存管理运算符(new
和delete
)的重载,以及更多的实际应用案例。
这是我C++学习之旅系列的第十二篇技术文章。查看完整系列目录了解更多内容。