目录
编辑
一、运算符重载
1.1 运算符重载概念
1.2 全局运算符重载
1.3 运算符重载为成员函数
二、赋值运算符重载的特性
2.1 赋值运算符重载需要注意的点
2.2 赋值运算符重载格式
2.2.1 传值返回
2.2.2 传引用返回
2.2.3 检查自己给自己赋值
三、赋值运算符重载的应用
四、总结
一、运算符重载
1.1 运算符重载概念
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数。也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
这里虽然用了重载,但是运算符重载和函数重载不是一个东西:
函数重载:允许函数名相同参数不同的函数存在;
运算符重载:让自定义类型的对象可以用运算操作符(必须是C\C++语法存在的运算符)。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator 操作符 (参数列表)
1.2 全局运算符重载
使用全局的operator==,程序如下:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, 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;
}
int main()
{
Date d1(2025, 3, 8);
Date d2(2025, 3, 7);
cout << (d1 == d2) << endl;
//cout << (operator==(d1, d2)) << endl;//两种写法是一样的
return 0;
}
上述程序中,全局的运算符重载的形式为:bool operator==(const Date& d1, const Date& d2),这就需要把Date类的成员变量改为私有,即注释掉private。
注:赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数。
那么这样操作,就破坏了Date类的封装性。封装性如何保证?
- 使用友元函数;
- 重载为成员函数(常用)。
1.3 运算符重载为成员函数
将上述程序作进一步修改:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d)
{
return this->_year == d._year
&& this->_month == d._month
&& this->_day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 3, 8);
Date d2(2025, 3, 7);
cout << (d1 == d2) << endl;
cout << d1.operator==(d2) << endl;//两种写法是一样的
return 0;
}
运算符重载为成员函数的形式为:bool operator==(const Date& d),这里需要注意的是,左操作数是this,指向调用函数的对象。
1.4 运算符重载需要注意的点
运算符重载的使用需要注意一下5点:
1. 不能通过连接其他符号来创建新的操作符:比如operator@;
2. 重载操作符必须有一个类类型参数;
3. 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义;
4. 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this;
5. .* : : sizeof ?: . 注意以上5个运算符不能重载。
针对5中的.*,可以写如下程序:
class ob
{
public:
void func()
{
cout << "void func()" << endl;
}
};
typedef void(ob::*pobfunc)()
int main()
{
pobfunc p = &ob::func;//成员函数取地址,要用&操作符,不然取不到
//等同于void (ob:: *pi)() = &ob::func;
ob tmp;
(tmp.*p)();//通过对象去调用成员函数的指针,成员函数指针要传this
//*p();//普通的函数指针的调用
return 0;
}
函数指针和数组指针都是特殊的指针,普通变量的重命名为:typedef 类型 重命名
上述程序中,typedef void(ob::*pobfunc)()是成员函数指针类型的重定义。其中,pobfunc是指向ob类中成员函数的函数指针类型。
1. void (ob*::)()
函数指针类型,它指向一个返回值为void,且没有参数的成员函数。ob*::表示函数指针指向ob类的成员函数。
2. typedef void(ob*::pobfunc)()
使用typedef关键字重定义一个指向ob类中的成员函数的函数指针类型pobfunc。
二、赋值运算符重载
2.1 赋值运算符重载格式
- 参数类型:const Date&,传递引用可以提高传参效率;
- 返回值类型:Date&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值;
- 检测是否自己给自己赋值;
- 返回*this :要复合连续赋值的含义。
相较于传值传参和传引用传参。
2.1.1 传值返回
将1.3的程序在Date类中进行补充,补充的程序为:
Date operator=(const Date& d2)
{
this->_year = d2._year;
this->_month = d2._month;
this->_day = d2._day;
return *this;//*this就是d1,相当于拿到左操作数
}
因为,Date operator=(const Date& d)中的Date表明是传值返回,意味着return *this;不会返回*this,而是返回它的拷贝(拷贝以后或存放在寄存器中)。
所以,同类型的传值拷贝又会调用一个拷贝构造。
2.1.2 传引用返回
将1.3的程序在Date类中进行补充,补充的程序为:
Date& operator=(const Date& d2)
{
this->_year = d2._year;
this->_month = d2._month;
this->_day = d2._day;
return *this;//*this就是d1,相当于拿到左操作数
}
2.1.3 检查自己给自己赋值
这可能会造成性能的浪费;成员变量可能依赖于其他成员变量的值,如果这些成员变量的值被覆盖,可能会引发错误。
基于2.2.2,可通过判断地址来进一步改写:
Date& operator=(const Date& d2)
{
if(this != &d2)
{
this->_year = d2._year;
this->_month = d2._month;
this->_day = d2._day;
}
return *this;
}
2.2 赋值运算符只能重载成类的成员函数
C++规定,其他运算符可以重载成全局的,赋值重载不可以,只能重载为成员函数。
对于默认成员函数,如果不写,编译器会生成一份。如果放在全局,类中没有,编译器会生成一份,那调用的时候会产生冲突。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
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 =”必须是非静态成员
2.3 没有显式,编译器会生成一个默认赋值运算符重载
不写赋值运算符重载,编译器会不会生成默认的呢? - 会,因为是6个默认成员函数之一。
默认生成的对内置类型会完成值拷贝(浅拷贝),对自定义类型会去再调用它的赋值。
怎么知道赋值默认生成的赋值的行为是什么? - 同拷贝构造。
那是不是意味着自定义赋值操作符重载就可以不写了呢? - 不是。
注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必
须要实现。
三、赋值运算符重载的应用
如果是内置类型,编译器是可以调用相关指令的;如果是自定义类型,编译器首先会去看有没有重载运算符,如果没有就会报错。
#include<iostream>
using namespace std;
class Date
{
public:
Date(int _year = 1, int _month = 1, int _day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& d)
{
if(this != &d)
{
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
return *this;
}
Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 3, 9);
Date d2(2025, 3, 9);
d1 = d2;
Date d3(d1);
d1 = d2 = d3;//自定义类型,连续赋值是要有返回值的
d3.Print();
int i, j = 0;
cout << (i = j = 10) << endl;
return 0;
}
程序Date d3(d1);为拷贝构造,还是一个构造。构造是指对象创建实例化的时候自动调用的初始化。其他的构造可能是用一些普通的参数进行初始化,而拷贝构造是用同类型一个存在的对象进行初始化要创建的对象。
程序d1 = d2;,已经存在的两个对象,一个拷贝赋值给另一个,这里边用到了=运算符,所以就要重载这个运算符。
程序d1 = d2 = d3;为自定义类型,连续赋值是要有返回值的。
程序i = j = 10;,内置类型支持连续赋值。执行动作为:10赋值给j作为一个表达式,这个表达式有返回值,返回值就是左操作数j。同理,再向左,返回值为左操作数i。
四、总结
默认生成的函数行为总结:
- 构造和析构:内置类型不处理,自定义类型调用对应的构造和析构。
- 拷贝构造和赋值运算符重载:内置类型值拷贝,自定义类型调用对应的拷贝构造和赋值重载。