C++之重载运算符
重载运算符概念
首先看下面的语句块:
int a=2, b=8, c;
c = a + b;
这两行代码非常直观易懂,这里我们用到了 +运算符,将 a和 b的和赋给了变量 c。运算符 +相当于执行了一个函数,函数返回的结果就是 a和 b的和。
如果是我们自定义的类,我们就无法使用 +运算符来计算,你可能想到了直接写一个和的函数即可,这是其中的一种解决方法。通过运算符重载,我们也能用 +来完成类似函数的效果。
重载运算符是具有特殊名字的函数,它们的名字由关键字 operator和其后要定义的运算符号共同组成。和函数一样,重载的运算符也包含返回类型、参数列表和函数体。
请注意,我们无法改变内置类型的运算对象运算符的含义。
我们可以重载大多数运算符,如下表:
- 算术运算符:+、-、*、/、% 等
- 关系运算符:==、!=、<、>、<=、>= 等
- 逻辑运算符:&&、||、! 等
- 位运算符:&、|、^、~、<<、>> 等
- 赋值运算符:=、+=、-=、*=、/= 等
- 下标运算符:[]
- 函数调用运算符:()
- 成员访问运算符:->
- 逗号运算符:,
以下运算符不能重载:
- 成员访问运算符
.和.*:这两个运算符用于访问类的成员和成员指针。 - 作用域解析运算符
:::这个运算符用于在类的作用域外部引用成员。 - 条件运算符
?::条件运算符用于在条件为真时返回一个值,在条件为假时返回另一个值。 - sizeof 运算符:
sizeof运算符用于获取类型或对象的大小。 - 成员选择运算符
.和->:这两个运算符用于访问类的成员和成员指针。 - 类型转换运算符
typeid和dynamic_cast:这些运算符用于类型识别和类型转换。 - 逗号运算符
,:逗号运算符用于分隔表达式。 - 三个点运算符
...:这个运算符用于可变参数列表。
运算符函数也可以被直接调用,如下:
data1 + data2;
operator+(data1,data2); // 两种操作方法等价
接下来我们将详细介绍运算符重载。
输入输出运算符
以下我们会使用自定义的复数类 Complex进行演示,类的基本结构如下:
class Complex
{
private:
double real;
double imag;
public:
Complex(double r, double i):real(r),imag(i){}
~Complex(){}
};
<<输出运算符
通常情况下,输出运算符的第一个形参是一个非常量ostream对象的引用,是引用的原因是因为我们无法直接复制一个ostream对象,第二个形参一般是一个常量的引用,该常量是我们想要打印的类的类型。引用的原因是因为我们希望尽力避免复制实参;常量的原因是因为打印对象不想改变对象的内容。
为了与其他输出运算符保持已知,operator<<一般要返回 ostream形参。输出运算符函数如下:
friend std::ostream& operator<<(std::ostream& os, const Complex& item)
{
if (item.imag > 0)
os << item.real << "+" << item.imag << "i";
else if (item.imag < 0)
os << item.real << "-" << -item.imag << "i";
else
os << item.real;
return os;
}
测试:
Complex sample(3,4);
std::cout << sample << std::endl; // 输出:3+4i
你可能发现,上述的函数是友元函数,意味着这个函数是定义在类外部的函数,也正是因此,我们能访问到类的私有成员。
输入输出运算符必须是非成员函数,该函数一般被声明成友元。
>>输入运算符
输入运算符的第一个形参是运算符将要读取的流的引用,第二个形参是将要读入到的(非常量)对象的引用,返回流的引用。第二个对象非常量的原因是我们的目的就是将数据读入到这个对象中。
friend std::istream& operator>>(std::istream& is, Complex& item)
{
is >> item.real >> item.imag;
return is;
}
测试:
Complex sample(3,4);
std::cout << sample << std::endl; // 输出:3+4i
std::cin >> sample; //输入5 7
std::cout << sample << std::endl; // 输出:5+7i:
注意输入错误,如果数据输入到一般,会导致对象的数据部分被改变。通常我们需要检查对象,保证正确性。
算术和关系运算符
==和 !=相等运算符
这两个运算符传入的参数均为常量引用,返回的类型是布尔类型。
friend bool operator==(const Complex& lhs, const Complex& rhs)
{
return lhs.real == rhs.real && lhs.imag == rhs.imag;
}
friend bool operator!=(const Complex& lhs, const Complex& rhs)
{
return !(lhs == rhs);
}
这个范例为我们提供了一种思想:如果这个类定义了 operator==,则这个类也应该定义 operator!=。两者是相对应的关系。
<和 >运算符
这个运算符引入的实参以及返回类型和相等运算符一样,这里就不再赘述。
friend bool operator<(const Complex& lhs, const Complex& rhs)
{
return (lhs.real * lhs.real + lhs.imag * lhs.imag) < (rhs.real * rhs.real + rhs.imag * rhs.imag);
}
friend bool operator>(const Complex& lhs, const Complex& rhs)
{
return !(lhs == rhs || lhs < rhs);
}
有一些类不适宜定义关系运算符。
+运算符
+,-,*,/运算符的基本用法类似,传入常量引用,传出该类型变量。
friend Complex operator+(const Complex& lhs, const Complex& rhs)
{
Complex res(0, 0);
res.real = lhs.real + rhs.real;
res.imag = lhs.imag + rhs.imag;
return res;
}
friend Complex operator-(const Complex& lhs, const Complex& rhs)
{
Complex res(0, 0);
res.real = lhs.real - rhs.real;
res.imag = lhs.imag - lhs.imag;
return res;
}
friend Complex operator*(const Complex& lhs, const Complex& rhs)
{
//(a+bi)(c+di)=(ac-bd)+(bc+ad)i
Complex res(0, 0);
res.real = lhs.real * rhs.real - lhs.imag * rhs.imag;
res.imag = lhs.imag * rhs.real + lhs.real * rhs.imag;
return res;
}
friend Complex operator/(const Complex& lhs, const Complex& rhs)
{
//(a+bi)/(c+di)=(ac+bd)/(c^2+d^2) +((bc-ad)/(c^2+d^2))i
Complex res(0, 0);
res.real = (lhs.real * rhs.real + lhs.imag * rhs.imag) /
(rhs.real * rhs.real + rhs.imag * rhs.imag);
res.real = (lhs.imag * rhs.real - lhs.real * rhs.imag) /
(rhs.real * rhs.real + rhs.imag * rhs.imag);
return res;
}
赋值运算符
为了之后的讲解方便,我们在类中加入默认构造函数和 print成员函数:
class Complex
{
private:
double real;
double imag;
public:
Complex() :real(0.0f), imag(0.0f) {}
Complex(double r, double i):real(r),imag(i){}
~Complex(){}
void print()
{
std::cout << *this << std::endl;
}
...
};
=运算符
注意该运算符只能被重载为类的成员函数,其传入的参数为某类型的引用,为常量,返回类型就是该类的类型的引用。
Complex& operator=(const Complex& other)
{
real = other.real;
imag = other.imag;
return *this;
}
这回我们可以用以下数据进行测验:
Complex n;
n = Complex(1, 1);
n.print();// 输出1+1i
+=复合赋值运算符
该运算符的重载和 +重载类似,只能作为类的成员函数:
Complex& operator+=(const Complex& other)
{
real += other.real;
imag += other.imag;
return *this;
}
Complex n(3,4);
n += Complex(1, 1);
n.print();// 输出4+5i
下标运算符
现在需要给复数类定义下标运算符,要求0对应的是实部,1对应的是虚部,其余情况则抛出错误。
需要注意,下标运算符必须是成员函数。
如果一个类包含下标运算符,则会通常定义两个版本,一个返回普通引用,一个返回类的常量成员并返回常量引用。
double& operator[](size_t index)
{
switch (index)
{
case 0: return real;
case 1: return imag;
default:
throw "Index Error!";
}
}
const double& operator[](size_t index) const
{
switch (index)
{
case 0: return real;
case 1: return imag;
default:
throw "Index Error!";
}
}
测试:
Complex n(3,4);
std::cout << n[0] << "," << n[1] << std::endl; // 3,4
// std::cout << n[2] << std::endl; // 抛出异常
const Complex m(1, 2);
n[0] = 5;
n[1] = 3;
std::cout << n[0] << "," << n[1] << std::endl; // 5,3
//m[0] = 5; // 错误,常量不可修改
递增递减运算符
这里只讲述递增运算符,递减运算符同理。
++var前向递增运算符
虽然 ++不适合复数类,但是为了演示,我们把 ++定义为实部和虚部都+1。
前向递增运算符返回类型为该类,作为类的成员函数时不需要传入任何参数:
Complex& operator++()
{
real++;
imag++;
return *this;
}
通过语句 ++n来进行调用。
var++后向递增运算符
后置版本接受一个额外的不被使用的 int类型的形参,如果我们想要实现后置递增的功能,我们需要在一开始记录对象的状态,然后进行处理,最后返回记录的状态,因此函数的返回类型不能是引用。
Complex operator++(int)
{
Complex temp = *this;
real++;
imag++;
return temp;
}
进行测试:
Complex n(2,3);
std::cout << ++n << std::endl; // 3+4i
std::cout << n++ << std::endl; // 3+4i
n.print(); // 4+5i
成员访问运算符、函数调用运算符和重载、类型转换与运算符将会未来的智能指针这一环节讲解。
完善复数类
这个复数类离完美不远了,我们为其添加求反运算符:
Complex operator-()
{
Complex res(-real, -imag);
return res;
}
再添加一个求共轭的成员函数:
Complex Conjugate() const
{
Complex res;
res.real = real;
res.imag = -imag;
return res;
}
为类添加一个友元函数 double abs(const Complex n),用于求复数的绝对值。函数内如下:
#include <math.h>
double abs(const Complex n)
{
return sqrt(n.real * n.real + n.imag * n.imag);
}
至此,这个
复数类就完美了。
源代码
你可以在这里获取本次编写的源代码。
评论区