前言:运算符重载是面向对象的一个重要的知识点。我们都知道内置类型可以进行一般的运算符的运算。但是如果是一个自定义类型, 这些运算符就无法使用了。那么为了解决这个问题, 我们的祖师爷就在c++中添加了运算符重载的概念。 本篇主要通过实例的实现来讲述了运算符重载的知识点。
运算符重载的应用
首先, 先了解以下运算符重载能做些什么,先看下面这张图
在这张图中, 红框框是我定义的一个自定义类型。 这个自定义类型的成员包括了string类型的_name。 int类型的_age.它代表的就是一个人的类型。 每一个类的实例化对象都有他们的姓名, 也有他们的年龄。 那么很显然, 它的实例化对象也可以进行年龄的加法,也就是增长年龄。
所以, 绿色箭头指向的位置, 如果我的本意是想要让实例化对象p1的_age加2, 也就是p1这个实例化对象的年龄增加了2。但是很显然, 这个运算对于+这个运算符来说, 是做不到的, 因为对于运算符来说, 他们默认只能处理内置类型, 而不能处理自定义类型。
这里如果想要处理自定义类型, 就需要使用我们的运算符重载, 运算符重载的目的就是为了让一个运算符可以处理自定义类型的运算。
如下是我重载的peo类的一个加法运算符。
//这里我将整个类搬过来方便观察。
struct peo
{
string _name;
int _age;
//默认构造, 这里是默认构造, 因为每一个参数都有缺省, 那么这个就是一个默认构造函数
peo(string str = string(), int age = 0)
:_age(age)
{
_name = str;
}
//定义的peo类的加法运算
peo operator+(const int x)
{
peo tmp = *this;
tmp._age += x;
return tmp;
}
};
拥有了这个运算之后我们原先的代码就可以跑了:
现在是还没有执行加法运算的时候:
下图是运算之后的p1:
可以看到, p1的年龄从原本的18变成了20.
运算符重载的定义
知道了运算符重载的作用,那么我们如何来重载一个运算符呢。 首先, 重载运算符的函数名统一都要使用"operator", 然后在operator后面加要重载的运算符。
比如, 我如果想要重载一个加法运算符, 那么函数名就是这样: "operator+"。
然后就是参数的问题, c++规定, 被重载的运算符是按照操作数的前后进行传参的。 也就是说, 如果我重载了一个运算符+, 那么当我使用这个操作符的时候, “+”前面的操作符就是要传的第一个形参。 “+"后面就是要传的第二个形参。 我们利用上面的代码进行举例:
这里需要着重注意的是第二个参数, 也就是那个const int x。 如果这里的参数, 我不使用传值接收, 而是使用传引用接收,也就是int& x, 那么就是不可以的。这里的const不能丢。 因为这里我们通常传送整形传送的都是常量,也就是右值,不能取地址的值。而如果形参引用想要接收右值有两个方法:一个是使用const 将左值引用变为常性, 那么就可以接收右值了。 一个是使用右值引用,右值引用时c++11的语法, 这个内容现在先知道有就行, 后续文章会学习到。
所以, 如果使用左值引用接收, 也就是我们平常使用的引用&来接收, 就需要使用const, 其实这里也时我们对于const 理解的进一步加深。 就是我们平时在使用左值引用参数接收左值时, 一定要分析好, 这个左值引用是否需要接收右值, 如果不接受右值, 那么可以不加const, 但是如果接收右值的话, 一定要加const。 而其中比较烦人的就是函数的返回值, 要知道, 函数的返回值如果是一个传值返回, 那么它在返回的时候会创建一个将亡值。 如果我们要对这个函数的返回值进行运算,或者其他需要调用函数的操作。 一定要注意,调用的这个函数是一个左值引用接收的话, 这个左值引用参数一定要加const。
好, 这里这个板块的内容大致就这些, 我们再重载几个运算符,这个板块的内容就这么过了。
struct peo
{
string _name;
int _age;
peo(string str = string(), int age = 0)
:_age(age)
{
_name = str;
}
//加法运算符重载
peo operator+(const int x)
{
peo tmp = *this;
tmp._age += x;
return tmp;
}
//比较运算符重载
bool operator>(const peo& d)
{
return _age > d._age;
}
//比较运算符重载
bool operator==(const peo& d)
{
return _age == d._age;
}
//赋值运算符重载
peo& operator=(const peo& d)
{
_name = d._name;
_age = d._age;
}
};
赋值运算符重载
其他的运算符都很好说, 本篇真正要着重讲解的运算符其实只有三个, 也可以说是两个, 因为有两个是差不多的。 这三个运算符就是:赋值运算符, 流提取运算符, 流插入运算符。这里流提取和流插入的性质是类似的。
这个板块先来讲述赋值运算符。
我的前面的文章——解析默认构造函数的那篇文章中主要分析了三个默认函数——默认构造函数,默认拷贝构造,默认析构函数。
这三个默认函数的性质我们再来复习以下:默认构造函数是对内置类型不做处理, 对于自定义类型会去调用它的默认构造函数。
默认拷贝构造是对内置类型就是进行浅拷贝, 对于自定义类型会去调用它的拷贝构造。
默认析构函数就是对内置类型不做处理, 对于自定义类型会去调用它的析构函数。
但是, 其实除了上面的三个 ”我们不写,系统自动生成“ 的函数, 还有一个函数如果我们不写,系统也会自动生成。 它就是——默认赋值运算符重载。现在, 我们不写, 让编译器自己重载一个赋值运算符帮我们计算一下我们定义的peo类。
注意, 我们没有进行重载赋值符号, 我们现在通过调试观察编译器是如何帮我们进行处理的:
现在还没有调用赋值符号, 此时p1的_age成员的值是18。
f10下一步, 执行赋值符号, 这里的p1的_age成员的值已经变了, 说明我们虽然没有自己实现赋值运算符的重载, 但是编译器自己帮我们生成了。
编译器默认生成的赋值运算符重载的性质:编译器默认生成的赋值重载对于内置类型进行浅拷贝, 对于自定义类型会去调用它的赋值重载。
其实, 对于默认赋值运算符重载我们可以这么理解:
class A
{
public:
int a;
int b;
//......其他成员变量
A(const A& ka)
{
//a = ka.a;
//b = ka.b;
//....其他成员变量使用赋值运算符进行赋值
}
};
就是类似于上图, 其实编译器默认生成的赋值运算符重载我们可以理解为:对象的每一个成员变量都是用赋值符号进行直接赋值操作。 那么对于内置类型就是普通的浅拷贝。 对于自定义类型就会去调用这个自定义类型的赋值运算符重载。
对于赋值重载来说, 其实最重要的就是默认赋值重载。而赋值重载的重载方式与其他的重载符号没有什么不同。
流提取和流插入
const问题
我们平时使用的>>就是库里面重载的一个流提取运算符。对于istream,由于知识储备不足, 博主不能给出详细的解释。 我们在这个板块只讨论很浅的知识点。我们看下面的一段代码:
#include<iostream>
using namespace std;
#include<assert.h>
struct student
{
char _name[20];
int _age;
char _gender[10];
int _num;
};
const istream& operator>>(const istream& in, student& stu)
{
cout << "请输入姓名, 年龄, 性别, 和学号:>";
in >> stu._name >> stu._age >> stu._gender >> stu._num;
return in;
}
这一串代码中的流提取是有问题的。 为什么?
这里我们可以这么理解: const 修饰istream类型的对象 in,对象in具有常性。然后在istream类里面的流指针成员变量(这里可以想象成有一个流指针成员变量)也具有了常性。 那么, 如果类被赋予了常性, 成员指针变成什么?其实是变成指针常量, 也就是不能修改指向。 所以, 如果istream类型的in具有常性, 那么它里面的指针成员就会都变成指针常量。 流指针无法在终端一个字符一个字符的改变读取数据。所以图中的流提取是不对的。
流插入也是同样的道理, 如果给ostream的实例化对象赋予常性, 那么里面的流指针就无法改变指向, 那么就无法依次在终端输入数据。 所以, 对于流插入和流提取的运算符, 最重要的一点就是不能给他们加上const。 否则这里就无法正常编译。 正确的重载如下:
#include<iostream>
using namespace std;
#include<assert.h>
struct student
{
char _name[20];
int _age;
char _gender[10];
int _num;
};
istream& operator>>(istream& in, student& stu)
{
cout << "请输入姓名, 年龄, 性别, 和学号:>";
in >> stu._name >> stu._age >> stu._gender >> stu._num;
return in;
}
ostream& operator<<(ostream& out, const student& stu)
{
out << stu._name << " " << stu._age << " " << stu._gender << " " << stu._num << endl;;
return out;
}
定义位置的问题
然后, 对于流插入和流提取还有一个比较重要的问题就是它的定义的位置。 我们知道, 对于一个类来说, 它里面的不管是非静态成员函数还是运算符重载的第一个参数都默认是this指针。但是, 要知道, 我们在进行流插入或者流提取操作时, 谁在前, 谁在后?(就像 cin >> a)
很明显, 流插入或者流提取符号在前, 然后标识符在后。 所以, 我们在重载流插入和流提取操作符的时候就要让第一个参数是istream或者ostream。 但是, 如果我们将这两种重载运算符放在类域中。那么第一个参数就会变成this指针, 这个就无法满足流插入流提取运算符重载的规则了。 就不对了。 所以, 我们应该将流插入运算符或者流提取运算符的定义放在类域外。
就像上面的一串代码, 这里搬一下放在下面:
就像如图, 这里我定义的流提取和流插入就是在类域外。