五、运算符重载
5.1 运算符重载
内置类型可以直接使用运算符运算,编译器知道要如何运算。
但自定义类型无法直接使用运算符,编译器也不知道要如何运算。需要自己实现运算符重载函数。
-
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名以及参数列表。其返回值类型与参数列表与普通的函数类似。
-
函数名为:关键字operator后面接需要重载的运算符符号。
-
函数原型:返回值类型 operator操作符(参数列表)
编译器在碰到类对象使用运算符运算时,会去类中查找对应的运算符有没有被重载,如果被重载就会将其替换为对应的重载函数。运行时直接调用函数即可。由此可以看出,运算符重载的意义就在于增强代码的可读性。(当然也可以显示调用重载函数,但毫无意义)
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@或#或`或?
.* :: sizeof ?: .
注意以上5个运算符也不能重载。这个经常在笔试选择题中出现。- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 运算符重载函数的函数名是固定的,返回值类型和参数列表根据运算符的特点定义。
- 当运算符有两个操作数时,函数的第一个参数是左操作数,第二个参数是右操作数。
- 运算符重载函数的参数是类类型对象的引用(传值要调用拷贝构造消耗很大,并且不会影响调用对象)。如果在函数内不改变对象,还应使用const进行修饰,防止误操作。
定义在全局的operator==
// 定义在全局的operator==
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year;
int _month;
int _day;
};
// 这里会发现运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证?
// 这里其实可以用我们后面学习的友元解决,或者干脆重载成成员函数。
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
void Test ()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
cout<<(d1 == d2)<<endl;
//相当于:cout<< operatro(d1,d2) <<endl;
}
5.2 运算符重载访问私有类成员
方法一:利用成员函数间接的获取或修改私有成员的值。
像上面的栗子,我们可以定义类成员函数GetYear,GetMonth,GetDay来获取d1,d2的年月日。
这种利用成员函数间接的获取或修改私有成员的方法可行但太过麻烦。
方法二:将运算符重载函数作为类成员函数
-
干脆我们就将运算符重载函数作为类成员函数,使得函数可以对私有类成员进行访问。
-
注意:作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this(左操作数)。
定义在类中的operator==
// 定义在类中的operator==
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(Date* const this, const Date& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator==(const Date& d2)
{
return _year == d2._year;
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
void Test ()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
cout<<(d1 == d2)<<endl;
//相当于:cout << d1.operator==(d2) << endl; // --> d1.operator==(&d1, d2); 隐式的传d1的地址(this指针)
}
方法三:设置友元函数
方法二虽然简单了不少,但他仍不能处理所有情况:
- 由于类成员函数的第一个参数是隐藏的this指针,这就使得函数的调用对象必须是左操作数
将流插入重载定义为成员函数:
class Date
{
public:
// 3.全缺省构造函数(与无参构造函数不能同时存在)
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
ostream& operator<<(ostream& out){
out<<_year<<"/"<<_month<<"/"<<_day;
return out;
}
private:
int _year;
int _month;
int _day;
};
int main(){
Date d1;
//cout << d1; //编译报错
d1 << cout << endl; //编译通过,且能正常运行
return 0;
}
运行结果:
虽然能正常显示日期但这不符合我们的使用习惯,感觉很不爽。那有没有解决的办法呢?
- 有,我们还是要把运算符重载定义成全局函数,再通过设置友元函数使得该函数可以对私有类成员进行访问。
将流插入重载定义为全局函数,并在类中设置友元:
class Date
{
//通过设置友元函数使得该函数可以对私有类成员进行访问
friend ostream& operator<<(ostream& out, const Date &d);
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
//还是要把运算符重载定义成全局函数
ostream& operator<<(ostream& out, const Date &d){
out<<d._year<<"/"<<d._month<<"/"<<d._day;
return out;
}
int main(){
Date d1;
cout << d1 << endl; //编译通过,且能正常运行
return 0;
}
运行结果:
5.3 赋值运算符的重载
赋值运算符重载格式
-
参数类型:const T&,传递引用可以提高传参效率
-
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
-
要检测是否是自己给自己赋值
-
返回*this :匹配返回类型
class Date
{
public :
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date (const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//赋值运算符的重载
Date& operator=(const Date& d)
{
//判断两指针是否指向同一个对象
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year ;
int _month ;
int _day ;
};
//赋值重载只能定义成类的成员函数不能定义成全局函数
//Date& operator=(Date& left, const Date& right)
//{
// if (&left != &right)
// {
// left._year = right._year;
// left._month = right._month;
// left._day = right._day;
// }
// return left;
//}
// 编译失败:
// error C2801: “operator =”必须是非静态成员
赋值重载只能定义成类的成员函数不能定义成全局函数
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
5.4 编译器自动生成的赋值重载
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
class Time
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
//Time类的赋值运算符重载
Time& operator=(const Time& t)
{
if (this != &t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
return *this;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 内置类型实现值拷贝
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型调用对应的赋值运算符重载
Time _t;
};
int main()
{
Date d1;
Date d2;
d1 = d2;
return 0;
}
和拷贝构造类似,默认生成的赋值重载只能实现简单的值拷贝,如果类中有申请资源的情况就会出错:
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType *_array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2;
s2 = s1;
return 0;
}
注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现(深拷贝)。
总结:(同拷贝构造函数)
- 涉及资源申请的类需要显示的写赋值运算符重载,以实现类的深拷贝。比如:Stack,Queue
- 未涉及资源申请的类不需要写赋值运算符重载,默认生成的就会完成类的值拷贝/浅拷贝。比如:Date
- 未直接涉及资源申请的类也不需要写赋值运算符重载,默认生成的就会调用其自定义类型成员的赋值运算符重载。比如:Myqueue
5.3 前置++和后置++重载
直接按前置和后置的特性重载,编译器是无法区分的;
因此需要特殊处理:使用标记参数进行区分,后置++重载增加一个int型参数跟前置++构成重载函数进行区分;
-
该标记参数不用手动传参,编译器会自动进行传参;
-
该标记参数不用在函数内接收(不需参数名),因为他不起传参的作用,仅仅用作标记区分。
函数声明(以日期类为例):
Date& operator++(); //前置++
Date operator++(int); //后置++