深入篇【C++】类与对象:运算符重载详解 -上
- ⏰.运算符重载
- 🕓Ⅰ.<运算符重载
- 🕐Ⅱ.>运算符重载
- 🕒Ⅲ.==运算符重载
- 🕑Ⅳ.=运算符重载
- ①.格式
- 1.改进1
- 2.改进2
- ②.默认成员函数
- 1.功能
- 2.不足
⏰.运算符重载
内置类型(int /double…… )是可以之间进行运算符之间的比较的,因为编译器知道它们之间的比较规则,可以之间转化为指令。
那如果自定义类型能否之间进行运算符之间的比较呢?当然不能了,因为编译器是不知道这个自定义类型的规则是什么,不知道如何进行比较。
1.内置类型是可以之间比较的。
2.自定义类型是无法之间进行比较的。
那如何使自定义类型也能进行比较呢?这时C++给出了办法,让这个运算符重载成一个函数,当自定义类型进行比较时,其本质就是在调用重载函数。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有器返回值类型,函数名字以及参数列表,其返回值类型与参数列表和普通函数类似。
函数的名字:关键字operator后面加需要重载的运算符符号。
函数的原型为:
返回值类型 operator运算符符号(参数列表)
【注意】
- 不能通过连接其他符号来创建一个新的操作符。比如:operator#
- 重载函数参数必须至少有一个为自定义类型参数,不能全是内置类型参数。
- 用于内置类型的运算符,其含义是不能改变的。原本的运算符对内置类型的含义是不变的。
- 重载函数作为成员函数时,其形参看起来要比操作数目少一个,那是因为成员函数的第一个参数默 认为this指针。
- *. / sizeof / :: / ? : / .`这五个运算符是不能被重载的。
那所有的运算符都可以重载吗?
当然不是,哪些运算符能够重载呢?–>哪些对类有用的运算符就可以实现重载。
我们先写一个日期类的对象:
class Data
{
public:
Data(int year = 2023, int month = 5, int day = 3)
{
_year= year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2023,2,1);
Data d2(2023,5,6);
}
下面我们将对这个日期对象进行运算符之间的比较,比较两个日期的是否大于或者小于。
🕓Ⅰ.<运算符重载
class Data
{
public:
Data(int year = 2023, int month = 5, int day = 3)
{
_year= year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
bool Less(const Data& x1, const Data& x2)
{
if (x1._year < x2._year)
{
return true;
}
else if (x1._year == x2._year && x1._month < x2._month)
{
return true;
}
else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
{
return true;
}
return false;
}
int main()
{
Data d1(2023,2,1);
Data d2(2023,5,6);
Less(d1,d2);
}
```c
我们写这个Less函数是用来比较第一个对象是否小于第二个对象的,这种写法跟我们以前写的函数是差不多的,不过有时候这种写法会出现问题,那就是比较专业的人会按照功能给函数命名,而不是很专业的人呢,就会乱起名字,导致我们可能不知道这个函数是干什么用,这里函数Less一看就知道是看是否小于了,如果写成别的可能就认不出来了,所以C++就给出了运算符重载形式,统一运算符,让运算符既可以对内置类型起作用,又可以对自定义类型起作用,这样又方便,又好识别。
C++规定运算符重载的写法如下:
返回值 operator>(参数)
所以上面的Less函数就可以写成这样的运算符重载函数:
bool operator<(const Data& x1,const Data& x2)
{
if (x1._year < x2._year)
{
return true;
}
else if (x1._year == x2._year && x1._month < x2._month)
{
return true;
}
else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
{
return true;
}
return false;
}
这个函数就是对<运算符进行重载,使这个运算符<可以被自定义类型使用,但注意的是,<运算符对内置类型的运算规则没有改变,仍然遵守原先的。
也就是当自定义类型使用这个<运算符时,编译器会自动跳到这个运算符重载函数里。
自定义类型使用运算符比较本质上就是在调用函数。
int main()
{
Data d1(2023,2,1);
Data d2(2023,5,6);
//< 运算符重载之后,自定义类型就可以使用这个运算符了。
//两个自定义类型之间的比较就可以直接这样写:
d1<d2;
//而自定义类型使用运算符本质上就是去调用函数:本质上就是这样:
operator(d1,d2);
//这两种写法都是一样的。
}
只不过这里有个问题,那就是该重载函数写在类外,无法访问类里面的私有成员,得先将类里面的成员变成公有的才可以访问。那么问题来咯,封装性如何保证?
【解决方法】
将该运算符重载函数直接写到类里面,变成成员函数。那样就可以直接访问成员变量了。
class Data
{
public:
Data(int year = 2023, int month = 5, int day = 3)
{
_year = year;
_month = month;
_day = day;
}
bool operator<(const Data& x)//参数只有一个,还有一个传给隐藏的this了
{
if (_year < x._year)
{
return true;
}
else if (_year == x._year && _month < x._month)
{
return true;
}
else if (_year == x._year && _month == x._month && _day < x._day)
{
return true;
}
return false;
}
private:
int _year;
int _month;
int _day;
};
不过写成成员函数要注意一点就是,成员函数会默认将将调用的对象传给隐藏的this指针,而参数看起来少了一个,其实不是,只是将调用的对象传给隐藏的this了。所以在写成成员函数时,当操作数有两个时,那么参数就只有一个。
int main()
{
Data d1(2023,2,1);
Data d2(2023,5,6);
d1<d2;
//运算符重载函数写成成员函数后,那么对应的函数调用也就不一样了
//原来 d1<d2 === operator<(d1,d2);
//现在 d1<d2 === d1.operator(d2);
}
🕐Ⅱ.>运算符重载
有了上面的<运算符重载,这个>运算符重载和它是一样的。
class Data
{
public:
Data(int year = 2023, int month = 5, int day = 3)
{
_year= year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
bool operator>(const Data& x1, const Data& x2)//运算符重载
{
if (x1._year > x2._year)
{
return true;
}
else if (x1._year == x2._year && x1._month > x2._month)
{
return true;
}
else if (x1._year == x2._year && x1._month == x2._month && x1._day > x2._day)
{
return true;
}
return false;
}
int main()
{
Data d1(2023,2,1);
Data d2(2023,5,6);
d1>d2;
//等同于
operator>(d1,d2);
}
写到类外时就无法访问类里的私有成员,这是这里的问题,所以还是写到类里面来,弄成成员函数。
当写成成员函数时,就要注意参数只有一个。
class Data
{
public:
Data(int year = 2023, int month = 5, int day = 3)
{
_year = year;
_month = month;
_day = day;
}
bool operator>( const Data& x)//>运算符重载
{
if (_year > x._year)
{
return true;
}
else if (_year == x._year && _month > x._month)
{
return true;
}
else if (_year == x._year && _month == x._month &&_day > x._day)
{
return true;
}
return false;
}
private:
int _year;
int _month;
int _day;
};
并且要理解自定义类型使用>运算符时是在调用成员函数operator>(d2);
int main()
{
Data d1(2023,2,1);
Data d2(2023,5,6);
d1>d2;
//等同于
d1.operator>(d2);
}
🕒Ⅲ.==运算符重载
//全局的operator==重载函数
class Data
{
public:
Data(int year = 2023, int month = 5, int day = 3)
{
_year= year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
bool operator==(const Data& d1, const Data& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Data d1(2023,2,1);
Data d2(2023,5,6);
d1==d2;
//本质和下面一样
operator==(d1,d2);
}
当写成成员函数时:
class Data
{
public:
Data(int year = 2023, int month = 5, int day = 3)
{
_year= year;
_month = month;
_day = day;
}
// bool operator==(Date* 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;
};
要理解自定义类型使用>运算符时是在调用成员函数 operator= =(d2);
int main()
{
Data d1(2023,2,1);
Data d2(2023,5,6);
d1==d2;
//等同于
d1.operator==(d2);
}
🕑Ⅳ.=运算符重载
赋值运算符重载就是对=运算符进行重载,使自定义类型也可以使用=运算符。
赋值就相当于将自己拷贝给对方,而它与拷贝构造函数的区别是什么呢?
1.运算符重载:是已存在的两个对象之间复制拷贝
2.拷贝构造函数:用一个已经存在的对象去初始化一个新对象
注意:下面的是拷贝构造而不是赋值运算符重载。
Data d1(2023,5,1);
Data d2=d1;
虽然用了=运算符,但是记住赋值运算符重载的条件是两个已经存在的对象之间进行赋值拷贝,而d2是新创建的对象。
①.格式
- 参数类型:引用传参,用const修饰,即const 类&
引用传参可以提高传参效率。 - 返回值类型:引用返回,即 类&
引用返回可以提高返回的效率,有返回值目的是为了支持连续赋值功能。 - 要检查是否给自己赋值
- 返回*this:要符合连续赋值的含义。
重载函数形式:返回值 operator=(参数)
class Data
{
public:
Data(int year = 2023, int month = 5, int day = 3)
{
_year = year;
_month = month;
_day = day;
}
void operator=(const Data& d)//赋值运算符重载
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2023, 5, 1);
Data d2(2023, 5, 6);
d1 = d2;
}
我们可以发现将赋值运算符重载后,自定义类型也可以使用赋值运算符了,而且使用的很成功,将d2的值赋给了d1。
并且我们要理解自定义类型使用运算符本质是在调用函数,也就是可以写成这样:
int main()
{
Data d1(2023, 5, 1);
Data d2(2023, 5, 6);
d1 = d2;
//本质是调用函数,这两者是一样的
d1.operator=(d2);
}
那这样就没有问题了吗?
我们知道在对内置类型进行赋值时我们可以连续赋值比如这样:
int a,b,c;
a=b=c=0;
赋值的顺序从右到左,0先赋值给c,然后得到一个结果,再赋值给b,得到一个结果,再赋值给a。
那自定义类型能否完成这样的连续赋值操作呢?
即这样:
Data d1(2023, 5, 1);
Data d2(2023, 5, 6);
Data d3(2023, 9, 9);
d1=d2=d3;
我们发现这样的操作编译器是无法编译通过的,为什么呢?
这是因为当调用运算符重载函数时,返回值是void,而调用完的结果要再赋值给d1,而void和d1的类型无法匹配。
而要支持上面的连续赋值,需要将赋值重载函数的返回值写成调用该函数的类型,这样当对象调用完后,返回的仍然是同类型,这样就可以支持连续赋值了。
class Data
{
public:
Data(int year = 2023, int month = 5, int day = 3)
{
_year = year;
_month = month;
_day = day;
}
Data operator=(const Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2023, 5, 1);
Data d2(2023, 5, 6);
Data d3(2023, 9, 9);
d1 = d2 = d3;
1.改进1
这样我们的自定义类型就可以很好的使用赋值运算符啦,接下来我们来对这个赋值运算符重载函数进行一些改进,哪些地方可以改进呢?
我们的参数是引用传参,这样可以减少拷贝,提高效率,那该函数的返回值是否可以用引用返回呢?
思考一下,我们返回的是什么?我们返回值的是调用对象,即this指针指向的对象。
而当该函数结束时,this指针会销毁,但this指针指向的对象还在呀,该对象并不在运算符重载函数里面
(*this是在该函数里面,作为形参压入函数栈帧)。
所以我们可以用引用返回,这样就可以减少拷贝,提高效率啦。
Data& operator=(const Data& d)//用引用返回,可以提高效率
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
2.改进2
我们要求不要对自己赋值,所以在赋值之前我们可以进行检查,看是否对自己赋值,只要检查两个对象的地址是否相同即可:
Data operator=(const Data& d)
{
if (this != &d)//如果两个对象的地址不相同那么就可以进行赋值
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
}
②.默认成员函数
赋值运算符也是作为默认成员函数的。
而默认成员函数有一个规定:只能写在类里面,不能写在类外面
(但是可以在类里面声明,在类外定义,声明和定义分开)
为什么呢?
因为默认成员在用户不显示写时,它会自动生成一个默认函数,帮你完成赋值工作。
而当你在类外写赋值重载函数时,编译器会认定你没有写,它会自动生成一个默认函数,那这样就和编译器生成的运算符重载冲突了,所以赋值运算符只能写在类里面。
总结:
赋值运算符不显示写,编译器会生成一个默认的,如果用户再在类外自己实现一个全局的赋值运算符重载,就和编译器生成的默认函数冲突了,故赋值运算符重载只能是类的成员函数。
1.功能
默认成员函数的意义就是,用户不显示实现,编译器会自动生成一个默认的函数,来帮你实现。
那编译器生成的默认函数的具体是如何实现的呢?它的具体功能是什么呢?
当用户不显示实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝,即浅拷贝。
即默认生成赋值运算符重载与默认生成的拷贝构造函数行为一致:
1.内置类型成员是之间赋值–值拷贝/浅拷贝。
2.自定义类型成员会去调用对应的它的赋值运算符重载,如果没有那也是浅拷贝。
class Time//时间类
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
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 = 2023;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
Date d2;
d1 = d2;
return 0;
}
我们先来分析一下该代码,该代码有两个类,一个是时间类Time,一个是日期类Data。
日期类里面的成员变量有内置类型又有自定义类型,并且没有写任何函数。
但我们知道编译器会给这个类自动生成一些默认函数,其中就有赋值运算符重载,接下来我们就看看,编译器生成的赋值运算符重载怎么样。
所以显而易见,对于这样日期类,时间类的,编译器自动生成的赋值运算符是可以满足使用的。
即编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝,这种已经符合部分类的需求了,那还要我们自己实现吗?
2.不足
当类成员涉及有关资源的开辟时,编译器生成的默认成员函数就无法使用。
typedef int DataType;
struct stack//class可以定义一个类
{
public://访问限定符
stack(int capacity = 4)//缺省值
{
cout << "stack(int capacipty=4)" << endl;
_array = (DataType*)malloc(sizeof(DataType) * capacity);
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
_array[_size] = data;
_size++;
}
void Pop()
{
if (Empty())
{
return;
}
--_size;
}
int Empty()
{
return _size == 0;
}
~stack()
{
cout << "~stack()" << endl;
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private://访问限定符
DataType* _array;
int _capacity;
int _size;
};
int main()
{
stack s1;
stack s2;
s2=s1;//使用编译器自动生成的赋值运算符重载函数来赋值
}
这样会出现什么问题呢?上一篇拷贝构造函数其实就讲了,因为它和拷贝构造函数的功能类似,所以遇到的问题也是类似。
因为编译器生成的默认函数都是浅拷贝,对于一些开辟了空间资源的变量,浅拷贝是完完全全的拷贝,没有动过任何处理,所以这就涉及赋值后,两个对象里面的变量都指向同一块空间,都属于同一块区域。那么这问题就大了去了。因为会出现析构两次,和我改变你也改变,你改变我也改变的状态。
1.同一块空间会析构两次,会报错。
当s2对象生命周期结束时,系统自动调用析构函数来清理数据,那么*a指向的空间就被销毁了。
而当s1对象生命周期结束时,又析构一次相同的空间,这样同一块空间就析构两次了。
2.一个变量修改会影响另一个变量,因为两个变量都存在同一块空间里。
所以编译器生成的赋值重载函数是有不足地方的,不能全部都依赖编译器生成的函数。不过对于日期类的,MyQueue类的编译器都可以直接实现功能,对于Stack类的就不可以了。
1.所以如果类中没有涉及资源管理,赋值运算符是否实现都是可以的;
2.但是一旦涉及资源管理就必须自己实现。