目录
1.赋值运算符重载
1.1运算符重载
1.2赋值运算符重载
1.3其它特性
2.const成员函数
3.取地址及const取地址操作符重载
hello,欢迎大家来到小恶魔频道,今天讲解的是C++里面的赋值运算符重载以及const成员函数
1.赋值运算符重载
1.1运算符重载
运算符重载是一种编程语言特性,它允许开发者为已有的运算符提供自定义的实现。这意味着你可以改变某些运算符在你自定义的类或数据类型上的行为。比如,你可以定义加号运算符(+)如何在你自定义的数据结构上进行运算
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 4, 22);
Date d2(2024, 1, 1);
return 0;
}
像以上代码
在这个代码中,我们如何比较d1和d2的是否相同呢?
常规方法:写一个函数去比较
bool Compare(const Date& dt1,const Date& dt2)
{
return dt1._year == dt2._year
&& dt1._month == dt2._month
&& dt1._day == dt2._day;
}
运行完后发现为0,也就是不相等
那么如果接下来我们想要直接比较d1==d2
这时候就会运用到运算符重载
运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似,注意这里说的重载与我们的函数重载不是一个意思
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类 型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
所以这里我们可以改写为:
bool operator==(const Date& dt1,const Date& dt2)
{
return dt1._year == dt2._year
&& dt1._month == dt2._month
&& dt1._day == dt2._day;
}
int main()
{
Date d1(2024, 4, 22);
Date d2(2024, 1, 1);
cout << (d1 == d2) << endl;
return 0;
}
注意:这里的d1和d2必须用括号括起来,不然会无法运行
这样也是直接成功了,我们可以看一下反义编码去验证一下
这里发现调用了operator==,直接进行了函数比较
但是问题也就来了
这里我们的函数比较是建立在全局变量上的,也就是说我们的函数成员需要变成共有才能够使用函数,既然是共有的成员变量,怎么保证其分装性呢?
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& dt2)
{
return _year == dt2._year
&& _month == dt2._month
&& _day == dt2._day;
}
private:
int _year = -1;
int _month = -1;
int _day = -1;
};
int main()
{
Date d1(2024, 4, 22);
Date d2(2024, 1, 1);
cout << d1.operator==(d2) << endl;
cout << (d1 == d2) << endl;
return 0;
}
在这里我们将函数分装到类的里面,依旧是利用的运算符重载==
只不过这里我们值传递了一个参数,另一个参数我们利用这里隐藏的this指针去实现代码的构建,从而实现==
细说剖析:
- 首先呢,值传递一个参数也就是const Date& dt2,它是右边的比较数的参数,而我们的左边比较数利用的是这里面隐藏的this指针,也及时this指针指向的对象。
- 这里呢,我们加入了关键字:const,同时也加入了引用传参。加入这两个是为了避免传递值的修改以及避免空间的开辟浪费和传递的效率。
- 最后就是bool值,我们这里的函数返回类型利用的是bool值,如果他们的年月日相同就会返回真(true)也就是1,不同就会返回假(false)也就是0。
我们是这调用这个函数 看一看
我们再通过反义编码看一看
我们发现这里都调用了operator==函数
在上面的讲解之后,相信大家对运算符重载有了一定的了解,他就是允许自定义对象使用运算符,它的返回值是根据运算符来决定的,比如完成加减操作,我们就返回int类型,判断是否大于小于,就用bool类型
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数(自定义类型参数)
- 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的 操作符有一个默认的形参this,限定为第一个形参
- .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。
1.2赋值运算符重载
在这里我们知道拷贝赋值有两种,一种是 拷贝构造一种是拷贝赋值
Date d1(2024,4,22);
Date d2(d1);
这样直接进行拷贝构造
那如果运用运算符重载呢?
d2 = d1;
这两者有什么区别呢?
cout << "Date" << endl;
- 拷贝构造函数在对象创建时使用,用于初始化新对象。赋值运算符重载在对象已存在时使用,用于将一个对象的值赋给另一个对象
- 其目的是,拷贝构造函数的目的是创建一个新的、状态相同的对象副本。赋值运算符的目的是改变一个已存在对象的状态,使其与另一个对象的状态相同
- 拷贝构造函数通常接收一个对同类对象的常引用。赋值运算符重载通常返回对象的引用,并接收一个对同类对象的常引用作为参数
我们在初始化哪里加上一个
cout << "Date" << endl;
运行发现这里只打印了一次Date
也就变向说明了两者的区别
既然运算符重载是将对象的值赋值给另一个对象,我们想一下
可不可以进行连续赋值呢?
比如:a=b=10这种?
在C语言中我们通常是这样的
int a,b;
a = b = 10;
但是在赋值运算符重载中,我们则是需要更新一下自己的方式:返回*this
Date operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
在这里我们的返回类型是Date,没有使用引用,而是进行的传值返回
所以我们这里返回的不是*this,而是他的一个拷贝
可以看到返回this指针时调用了他的拷贝
所以为了加快效率我们这次加入引用
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
int main()
{
Date d1(2024, 4, 22);
Date d2(1,1,1);
Date d3 = d2 = d1;
return 0;
}
这次就没有拷贝
但是问题来了,如果这里我传参传的是自己呢?如果我给自己赋值会怎么样?
这里是不行的
为什么不行呢?
自赋值在大多数情况下是可以工作的,但是在特定的情况下,如果没有正确处理,它可能会引起错误或意外的行为。考虑自赋值的主要原因是为了确保当对象赋值给自身时,程序仍然能够正确、安全地运行。特别是在类中涉及到动态内存管理时,不正确处理自赋值可能会导致问题。例如,假设一个类内部分配了动态内存,如果在赋值操作中首先释放了这块内存(预备重新分配),而源对象和目标对象实际上是同一个对象,那么这个操作实际上会破坏源对象的状态,导致未定义行为
所以这里我们还需要改进以下代码
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
}
我们这里判断条件是地址的比较,如果地址不相同说明不是同一个对象,可以赋值
1.3其它特性
这里我们看到报错了
然后我们把成员类型设置为公有的,发现还是报错
这是因为:赋值运算符只能重载成类的成员函数不能重载成全局函数
赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了
故赋值运算符重载只能是类的成员函数
如果我们不写赋值运算符重载,编译器是否会默认生成呢?
答案是会的
这里我们把operator=给注释掉后 结果仍旧一样
所以这里与我们拷贝构造等函数性质一致:
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值
既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?
答案需要的
如果我们使用的是栈的话
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc fail");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
_array[_size++] = data;
}
~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;
}
- s1对象调用构造函数创建,在构造函数中,默认申请了10个元素的空间,然后存了4个元素1 2 3 4
- s2对象调用构造函数创建,在构造函数中,默认申请了10个元素的空间,没有存储元素
- 由于Stack没有显式实现赋值运算符重载,编译器会以浅拷贝的方式实现一份默认的赋值运算符重载即只要发现Stack的对象之间相互赋值,就会将一个对象中内容原封不动拷贝到另一个对象中
- s2 = s1;当s1给s2赋值时,编译器会将s1中内容原封不动拷贝到s2中,这样会导致两个问题:首先是:s2原来的空间丢失了,存在内存泄漏,
其次是:s1和s2共享同一份内存空间,最后销毁时会导致同一份内存空间释放两次而引起程序崩溃
这里有点类似于我们之前学习的拷贝构造,如果不分开,也是占用的同一块内存,最后会崩溃报错
所以如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
2.const成员函数
假如我们现在定义一个const对象,想访问它的Print函数,我们发现是调用不了的:
class Date
{
public:
Date(int year, int month, int day)
{
//加上这里的打印是为了调试更加清楚
cout << "Date" << endl;
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& dt2)
{
return _year == dt2._year
&& _month == dt2._month
&& _day == dt2._day;
}
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year = -1;
int _month = -1;
int _day = -1;
};
这里权限进行放大了,接着,我们来介绍const成员函数
原来是const Date*
,而我的this类型是Date*
,意味着需要将this指针也改为const Date*
,所以才有了下面的函数
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改,内容是只读的
class Date
{
public:
Date(int year, int month, int day)
{
//加上这里的打印是为了调试更加清楚
cout << "Date" << endl;
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& dt2)
{
return _year == dt2._year
&& _month == dt2._month
&& _day == dt2._day;
}
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print() const
{
cout << "Print()" << endl;
cout << "year:" << _year << endl;
cout << "month:" << _month << endl;
cout << "day:" << _day << endl << endl;
}
private:
int _year = -1;
int _month = -1;
int _day = -1;
};
我们在Print函数后加上const
这样就可以了
如果没有const修饰的函数呢,我Date类型的对象能否调用const成员函数呢?
可以的,这里是权限的缩小
请思考下面的几个问题:
- const对象可以调用非const成员函数吗? 不可以,权限放大
- 非const对象可以调用const成员函数吗? 可以,权限缩小
- const成员函数内可以调用其它的非const成员函数吗? 不可以,权限放大
- 非const成员函数内可以调用其它的const成员函数吗?可以,权限缩小
指针和引用才存在权限放大
3.取地址及const取地址操作符重载
这个我们主要是了解,不作为重点讲解,后边用到会更新讲解
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
这里是默认成员函数,我们删去这两个函数照样可以取地址
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//Date* operator&()
//{
// return this;
//}
//const Date* operator&()const
//{
// return this;
//}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
void main()
{
Date d1;
const Date d2;
cout << &d1 << endl;
cout << &d2 << endl;
}
这里,我们没有必要深究这个东西究竟有什么用,我们只进行简单的语法了解即可
看到这里,一键三连支持一下呗。