验证环境 vs2022 x86
1. 运算符的重载
1.1 引入
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 2022, int month = 5, int day = 25)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void PrintDate()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1(2022, 5, 26);
Date d2;
Date d3 = d1; // 等价于:Date d3(d1) 调用拷贝构造函数
d2 = d1; // 赋值
}
int main()
{
TestDate();
return 0;
}
还是之前的日期类,里面有一个构造函数,拷贝构造函数 其中 Date d3 = d1;这句就是调用拷贝构造函数来进行实现的 d2 = d1; 这句就是简简单单的赋值,很像浅拷贝,没有涉及到内存管理,要是涉及到内存管理则会发生什么事情呢?
再看之前写过的 stack 再来体会一下重载运算符的必要性
#include<iostream>
#include <malloc.h>
#include <assert.h>
using namespace std;
typedef int DataType;
class Stack
{
public:
Stack()
{
_array = (DataType*)malloc(sizeof(DataType) * 10);
if (NULL == _array)
{
assert(0);
return;
}
_capacity = 3;
_size = 0;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
void Push(const DataType& data)
{
_array[_size] = data;
_size++;
}
void Pop()
{
if (Empty())
return;
--_size;
}
DataType Top()
{
return _array[_size - 1];
}
size_t Size()
{
return _size;
}
bool Empty()
{
return 0 == _size;
}
private:
DataType* _array;
size_t _capacity;
size_t _size;
};
void TestStack()
{
Stack s1, s2;
s1.Push(1);
s1.Push(2);
s2 = s1;
}
int main()
{
TestStack();
return 0;
}
仔细想一想这个 s2 能有 s1的全部值吗?
直接触发了一个断点让我们来看看到底是发生什么事情了?
直接下上一个断点分析他的对象模型:
s2 = s1 之后:
为什么我敢断言他这个 = 就简简单单把值给过去了:
1. 我调用监视窗口实实在在看到了。
2. 我看底下的汇编代码了全是用mov操作的!
对于我在构造函数中给予了资源管理,我在析构函数中要给出资源管理否则就会造成内存泄漏,这是其一,其二是ok你要析构函数管理资源是吧我给了,针对这个例子 s2 第一次开辟的空间直接就给内存泄露了,因为在 = 的时候原来开辟的空间并没有有对应的资源管理直接造成内存泄漏,其三在最后析构函数的时候一块相同的空间被释放了两次, = 的副作用。所以我们得出以下结论:
如果类中并没有涉及到资源管理,赋值操作符是否显示提供都不重要,需要就提供,如果不需要可以不写,直接利用编译器完成赋值工作就可以了(mov)
如果涉及到资源管理,用户就要显示实现,否则版一起默认生成的赋值操作符是浅拷贝,则就会存在两个问题:
1. 资源内存的泄漏
2. 多个对象共享一份资源的时候,将来这些对象在销毁的时候,会将一份资源重复释放,从而引起代码崩溃。
那么要显示提供应该怎么给出呢? 那就是重载运算符了!
1.2 赋值运算符的重载
在来到之前的日期类对象我们有两个日期类对象是可以比较大小的把?
#include<iostream>
using namespace std;
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;
}
void PrintDate()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1(2022, 6, 7);
Date d2(2022, 6, 8);
if (d1 == d2)
{
}
}
int main()
{
}
一编译就开始报错
自定义的类型就不可以执行相关运算符的操作,内置类型就可以,换一个角度你自定义类型有那么多成员变量,编译器也不知道是只比较年月日,还是比较年月,所以规则得你自己给出来。
那既然要自己给规则我自己写个函数不就完事了?
#include<iostream>
using namespace std;
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;
}
void PrintDate()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
bool IsEqual(const Date& d)
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1(2022, 6, 7);
Date d2(2022, 6, 8);
if (d1.IsEqual(d2))
{
cout << "d1 == d2" << endl;
}
else {
cout << "d1 != d2" << endl;
}
}
int main()
{
TestDate();
}
这样是可以的但是这样好吗?要是成员变量很多怎么办?这样的代码可读性高吗?
运算符重载的目的:告诉编译器,自定义的对象遇到该运算符时应该如何操作
写法:返回值类型 operator 操作符 (参数列表)
#include<iostream>
using namespace std;
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;
}
void PrintDate()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
/*bool IsEqual(const Date& d)
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}*/
//private:
int _year;
int _month;
int _day;
};
bool operator == (const Date& left, const Date& right)
{
return left._year == right._year &&
left._month == right._month &&
left._day == right._day;
}
void TestDate()
{
Date d1(2022, 6, 7);
Date d2(2022, 6, 8);
/*if (d1.IsEqual(d2))
{
cout << "d1 == d2" << endl;
}
else {
cout << "d1 != d2" << endl;
}*/
if (d1 == d2)
{
cout << "d1 == d2" << endl;
}
else {
cout << "d1 != d2" << endl;
}
}
int main()
{
TestDate();
}
这样就可以成功了
但是重载也不是随便用的还有很多的注意事项。
1.3运算符重载注意事项
注意:
1. 不能通过连接其他符号来创建新的操作符,要重载的运算符一定是C++支持的运算符
2.重载的时候这个参数至少有一个是类类型的参数
+号这个原本就存在 你不重载他也有,你也不能重载出啥新玩意所以参数要求至少有一个类类型的参数在其中。
仔细看一下,为啥不支持呢?你这个其实是一个没有出口无限递归 a + b 调用的最后还是operator + (a , b)一直递归下去没有出口
3. 每个运算符要符合其含义
来看这段代码我实现了一个复数类重载了减号但是里面做的是加号的事情结果会是怎样呢?
class Complex {
public:
Complex(int real = 0.0, int image = 0.0)
{
_real = real;
_image = image;
}
double _real;
double _image;
};
Complex operator - (const Complex& left, const Complex& right)
{
return Complex(left._real + right._real, left._image + right._image);
}
int main()
{
Complex c1(1.0, 2.0);
Complex c2(3.0, 4.0);
Complex c;
c = c2 - c1;
}
那肯定是加因为你自己实现的是加法!
4.作为类成员的重载函数的时候,他的形参看起来比操作数的数目少一个,因为有this指针传进去了
这样编译器是报错的
我们来实际看一下到底传this指针了吗,我们大家应该都知道this指针其实是成员函数的一个形参
是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针,而且我们知道一般是用ecx这个寄存器来传的
class Complex {
public:
Complex(int real = 0.0, int image = 0.0)
{
_real = real;
_image = image;
}
Complex operator * (const Complex& c2)
{
return ((_real * c2._image) - (_image * c2._image), (_image * c2._real) + (_real * c2._image));
}
double _real;
double _image;
};
Complex operator + (const Complex& left, const Complex& right)
{
return Complex(left._real + right._real, left._image + right._image);
}
int main()
{
Complex c1(1.0, 2.0);
Complex c2(3.0, 4.0);
Complex c;
//c = c2 + c1;
//这样写理解更好
c = c2 .operator *(c1);
}
但不是所有都是通过ecx传递的
5. .* :: sizeof ? . # 这个6个运算符不能重载
6.赋值运算符必须要重载成类的成员函数,否则编译失败,因为赋值运算符重载是默认的成员函数,如果用户没有提供,编译器会自己生成一份如果自己在类外实现了一份那就是重定义了
而其你在类内写的时候参数少一个为啥呢?因为有一个隐藏的this指针!
class Complex {
public:
Complex(double real = 0.0, double image = 0.0)
{
_real = real;
_image = image;
}
void operator = (const Complex& c2)
{
_real = c2._real;
_image = c2._image;
}
Complex operator * (const Complex& c2)
{
return ((_real * c2._image) - (_image * c2._image), (_image * c2._real) + (_real * c2._image));
}
double _real;
double _image;
};
int main()
{
Complex c1(1.0, 2.0);
Complex c2(3.0, 4.0);
c1 = c2;
}
但是这样写却并不好因为连续赋值的话就不行了 ,其实=是调用了成员函数而这个成员函数没有返回值,连续的话就是要用一个对象来接受这个返回值,也就是说这个成员函数的返回值应该是类类型的引用
赋值运算符加返回值的目的:为了连续赋值
返回引用目的:为了提高效率,如果是用值返回你返回this会调用拷贝构造函数把这个生成的临时对象返回,带引用在返回时就不会调用拷贝构造函数了!
Complex& operator = (const Complex& c2)
{
_real = c2._real;
_image = c2._image;
return *this;
}
这个还不是最完美的要是自己给自己赋值就没必要做这操作加个判断就好了
Complex& operator = (const Complex& c2)
{
if (this != &c2)
{
_real = c2._real;
_image = c2._image;
}
return *this;
}
1.4 前置++与后置++
后置++ 为了让前置++和后置++存在语法规定在后置++形参列表加一个int 跟后置++形成重载
//前置++
Complex& operator ++()
{
_real += 1;
_image += 1;
return *this;
}
//后置++ 为了让前置++和后置++存在
//语法规定在后置++形参列表加一个int 跟后置++形成重载
Complex& operator++(int)
{
Complex temp(*this);
_real += 1;
_image += 1;
return temp;
}
前置--跟后置--与这个是异曲同工
1.5 &的重载
假如我目前有个要求在取对象地址的时候把地址打印出来
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date* operator &()
{
cout << this << endl;
return this;
}
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 8, 27);
Date* pt = &d1;
}
这里我们把Date对象变成const修饰的不知道还可以吗?
哎这是为啥呢,为啥const修饰的对象就没有调用重载函数,其实他是调用的别的函数就算是真的调用这个函数,const 修饰的对象本意就是不能修改,你调用了之后那我就可以用指针修改对象内容了!
const修饰的对象调用const 修饰的重载函数 ,普通的调用普通的。
这俩const出现的位置不一样,那么肯定意义是不一样的。
前面的const是修饰函数返回值的,你要是不修饰返回值外面通过一个普通指针一接收这不就可以那他来修改了。
后面的const是修饰成员函数的将const修饰的成员函数称为const成员函数,const成员函数的特性在函数内不能修改任何成员变量
2. const 成员
还是之前那个Date类里面有个print函数万一我们在成员函数里面把成员变量修改了,那我们看到的跟实际的对象就不一样了
为了杜绝这种现象const成员函数就是防止运行的整个阶段都不需要修改的成员变量用const函数修饰就好了
const修饰类成员函数其实是修饰成员函数隐含的this指针,表明在成员函数中不能对类的任何成员进行修改,我们可以通过两个函数验证一下是否修饰的是this指针。
void Setyear(int year)
{
cout << typeid(this).name() << endl;
_year = year;
}
int Getyear() const
{
cout << typeid(this).name() << endl;
return _year;
}
很明显的看出来两个this的类型不一样,但是其实类型不是非常
上面的函数就是可读可写:既可以读取对象的内容,也可以对对象的内容进行写入。
下面的函数是只读函数:该成员函数只能读取对象中的内容不能修改对象中的内容。
注意:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//普通成员函数
void Setyear(int year)
{
_year = year;
}
//const成员函数
void PrintDate()const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
int _year;
int _month;
int _day;
};
int main()
{
//普通对象:普通成员函数 和 const成员函数都可以调用
Date d1(2022, 8, 27);
d1.Setyear(2020);
d1.PrintDate();
//const类型的对象:只能调用const成员函数,不能调用普通成员函数
const Date d2(d1);
//d2只是一个只读的对象,及不运行修改,如果编译器允许d2调用成员函数成员函数运行
//结束的时候很有可能将d2对象中的成员修改掉,代码不安全
d2.PrintDate();
}
列表中压根就没有
普通对象:普通成员函数 和 const成员函数都可以调用
const类型的对象:只能调用const成员函数,不能调用普通成员函数。
d2只是一个只读的对象,及不运行修改,如果编译器允许d2调用成员函数成员函数运行结束的时候很有可能将d2对象中的成员修改掉,代码不安全!
const修饰成员函数本质就是在修饰this表命this指向对象中成员是不能修改的如果编译器允许咋const成员函数中调用Setyear的普通成员函数,这个函数执行完成之后很有可能会将this指向对象中成员修改了
const成员函数不能调用普通成员函数,可以调用const成员变量
1.构造函数,拷贝构造函数,赋值运算符的重载,析构函数都不可以用const修饰
2.关键字mutable修饰的成员变量,该成员变量可以在const中被修改