c++类与对象(中)

文章目录

  • 前言
  • 一、构造函数
    • 1、构造函数介绍
    • 2、构造函数特性
  • 二、析构函数
    • 1、析构函数介绍
    • 2、析构函数特性
  • 三、拷贝构造函数
    • 1、拷贝构造函数介绍
    • 2、拷贝构造函数特征
    • 3、拷贝构造函数的应用 -- 求n天后的日期
  • 四、赋值运算符重载
    • 1、运算符重载
    • 2、一些运算符重载的实现
    • 3、赋值运算符重载
    • 4、日期类的实现
    • 5、前置++和后置++ && 前置--和后置--
    • 6、求两个日期相减的天数(d1 - d2)
    • 7、总结
    • 8、<<流插入和>>流提取
      • 8.1 <<流插入符重载函数
      • 8.2 >>流提取符重载函数
  • 五、const成员
  • 六、取地址及const取地址操作符重载


前言

如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。


一、构造函数

1、构造函数介绍

我们回想在前面实现的队列,栈,单链表等,在使用这些数据结构时,都要先调用对应的Init()函数,即初始化函数,如果我们忘记调用,则这些数据结构里面存的就会是随机值。但是我们也不能保证每次使用这些数据结构时都能先调用初始化函数。所以在c++中就增加了构造函数这一个特殊的成员函数。
我们第一个要介绍的就是构造函数,构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。即构造函数在类实例化为对象时会自动调用。

2、构造函数特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下
1.函数名与类名相同。
2.无返回值。
3.对象实例化时编译器自动调用对应的构造函数。
4.构造函数可以重载。
在这里插入图片描述
在这里插入图片描述

class Date
{
public://无参构造函数Date(){_year = 1;_month = 1;_day = 1;cout << "无参构造函数;" << endl;}//有参构造函数Date(int year, int month, int day){_year = year;_month = month;_day = day;cout << "有参构造函数;" << endl;}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}private:int _year;int _month;int _day;
};int main()
{//此时创建对象d1时会自动调用无参构造函数Date d1;//此时创建对象d2时会自动调用有参构造函数Date d2(2023, 8, 3);//当创建对象d3时向自动调用无参构造函数时,不可以加括号//因为此时会报出未调用原型函数的警告。//Date d3();return 0;
}

5.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
用户显式定义编译器将不再生成。

例如下面当将写的有参构造函数和无参构造函数都注释时,此时创建一个对象d1,则编译器会自动生成一个无参的默认构造函数,然后创建对象d1时会自动调用这个自动生成的默认构造函数。
在这里插入图片描述
在这里插入图片描述
但是如果将有参的构造函数的注释去掉,即代表用户显示定义了构造函数,则编译器就不会再自动生成无参构造函数,此时再次创建d1对象并调用无参构造函数时,就会出现错误。这是因为用户只定义了有参构造函数,而Date d1;创建对象时自动调用的是无参构造函数,但是用户没有定义无参构造函数,编译器此时也不会自动生成无参构造函数,所以就会报出没有合适的默认构造函数可用的错误。
在这里插入图片描述

在这里插入图片描述
所以用户自己定义构造函数时,一定要考虑到使用的场景,并且为每个场景都提供合适的构造函数。其实如果我们学习了缺省参数的话,上面分开定义无参构造函数和有参构造函数的方式其实可以使用缺省参数,将这两个函数和为一个。使用缺省参数定义构造函数的话,使用的场景更多了,在创建对象时如果不传参数则缺省参数的值就会生效,如果传一个参数则该参数的缺省值就失效,此时创建对象时,可以只传入第一个参数,也可以只传入前两个参数,还可以传入三个参数。

class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;cout << "构造函数;" << endl;}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}private:int _year;int _month;int _day;
};int main()
{//此时创建对象d1时会自动调用无参构造函数Date d1;Date d2(2023, 3, 23);Date d3(2023);Date d4(2023, 4);return 0;
}

6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
所以如果使用缺省参数来实现默认构造函数的话,无参的默认构造函数就不可以再出现了,因为此时就相当于有了两个默认无参构造参数,编译器不明确使用哪一个,就会产生歧义。所以就会报出对重载函数的调用不明确的错误。

7. C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,编译器生成的默认构造函数对自定义类型成员调用它的默认构造函数,而对于内置类型成员是不做处理的。
我们知道构造函数的作用就类似于初始化函数,那么当我们使用编译器自动生成的默认无参构造函数,编译器将我们的成员变量都初始化为了什么值呢?是0还是其它的值呢?我们可以使用下面的代码来查看一下,但是看到结果时我们发现此时成员变量的值不是0,而是随机值,这是为什么呢?

:这是因为c++中将类型分为内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,比如:int、char…;而自定义类型就是我们使用class、struct、union等自己定义的类型。编译器生成的默认构造函数会对自定义类型成员调用它的默认构造函数,而编译器生成的默认构造函数对于内置类型成员是不做处理的,这就造成了下面的代码中查看_year、_month、_day时发现是随机值。而对于自定义类型成员_birthday,则会调用Birthday的默认构造函数。

class Birthday
{
public:Birthday(){cout << "Birthday构造函数" << endl;}};class Date
{
public:void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}private://内置类型int _year;int _month;int _day;//自定义类型Birthday _birthday;
};int main()
{//此时创建对象d1时会自动调用无参构造函数Date d1;d1.Print();return 0;
}

在这里插入图片描述
虽然这样的设计会让使用者在使用时容易出错,但是c++已经发布了很久了,不可能更改这一特性,但是C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。这样在类中声明时就可以先给内置类型成员设置一个默认值,这样内置类型成员就不会为一个随机值了。即可以在声明位置给缺省值,此处并不是初始化,因为还没有实例化对象,并没有开辟空间。此处的意思就是如果没有写构造函数,并且默认生成的构造函数对这些内置类型也不做处理,则内置类型的值就为给的缺省值;如果写了构造函数的话,就不会将这些内置类型的值置为缺省值。


class Birthday
{
public:Birthday(){cout << "Birthday构造函数" << endl;}};class Date
{
public:void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}private://内置类型int _year = 2000;int _month = 1;int _day = 1;//自定义类型Birthday _birthday;
};int main()
{//此时创建对象d1时会自动调用无参构造函数Date d1;d1.Print();return 0;
}

在这里插入图片描述

二、析构函数

1、析构函数介绍

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

2、析构函数特性

析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

class Birthday
{
public:Birthday(){cout << "Birthday构造函数" << endl;}//Birthday类的默认析构函数~Birthday(){cout << "Birthday析构函数" << endl;}};class Date
{
public:/*Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;cout << "构造函数;" << endl;}*/void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}//没有显式定义析构函数的话,编译器会自动生成默认的析构函数。private://内置类型int _year = 2000;int _month = 1;int _day = 1;//自定义类型Birthday _birthday;
};int main()
{//此时创建对象d1时会自动调用无参构造函数Date d1;d1.Print();return 0;//对象生命周期结束时,C++编译系统系统自动调用析构函数,即在return之后会调用d1对象的默认析构函数。
}

在这里插入图片描述
我们经过分析知道了编译器自动生成的默认构造函数对内置类型成员和自定义类型成员所做的处理,那么编译器自动生成的析构函数对内置类型成员和自定义类型成员做什么样的处理呢?
:编译器自动生成的析构函数对内置类型成员和自定义类型成员所做的处理也是不同的,该默认析构函数不会对内置类型成员做资源清理,因为内置类型成员系统会直接将其所占用的内存回收,默认析构函数对调用自定义类型成员的默认析构函数,这样才能保证其内部每个自定义对象都可以销毁。例如上面的代码中对象d1的生命周期为main函数的生命周期,所以当return 0;之后系统就会自动调用Date类的默认析构函数,而Date类的默认析构函数是编译器自动生成的,在该默认析构函数中又会调用类Birthday的默认析构函数,这样就将_birthday对象所占用的内存清理了,这样处理就保证了对象d1内部每个自定义对象都可以正确销毁。

注意:创建哪个类的对象则调用该类的构造函数,销毁哪个类的对象则调用该类的析构函数。

三、拷贝构造函数

1、拷贝构造函数介绍

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。
那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存
在的类类型对象创建新对象时由编译器自动调用。

2、拷贝构造函数特征

拷贝构造函数也是特殊的成员函数,其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式。

class Birthday
{
public:Birthday(){cout << "Birthday构造函数" << endl;}//Birthday类的默认析构函数~Birthday(){cout << "Birthday析构函数" << endl;}};class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;cout << "构造函数;" << endl;}//拷贝构造函数//可以看到拷贝构造函数是构造函数的一个重载形式Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}//下面的函数不是拷贝构造函数,只是一个形参为指针类型的普通构造函数。//拷贝构造函数的参数只有单个形参,该形参是对本类类型对象的引用Date(Date* d){_year = d->_year;_month = d->_month;_day = d->_day;}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}//没有显式定义析构函数的话,编译器会自动生成默认的析构函数。private://内置类型int _year = 2000;int _month = 1;int _day = 1;//自定义类型Birthday _birthday;
};int main()
{Date d1(2023,2,3);//即将d1对象拷贝生成d2对象Date d2(d1);//拷贝构造还可以这样写Date d3 = d1;d1.Print();d2.Print();d3.Print();return 0;//对象生命周期结束时,C++编译系统系统自动调用析构函数,即在return之后会调用d1对象的默认析构函数。
}

在这里插入图片描述

2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
我们知道当函数使用传值传参时,本质是在栈区中将传入的实参拷贝一份临时变量来当作形参,然后在函数中改变的都是这个临时变量,而不会影响真正的实参。当我们对下面的代码进行调试时会发现当执行到Func1(d1);时会直接跳转到Date类中的拷贝构造函数内。这是因为对于内置类型编译器可以直接拷贝值过去;而对于自定义类型的话,编译器不知道怎么拷贝,所以编译器会调用该自定义类型中的拷贝构造函数。所以在调用Func1(d1)时会跳到Date类的拷贝构造函数中去,因为Func1()函数为传值传参,而传值传参会将实参拷贝一份临时变量,此时发生了Date类类型的拷贝,所以编译器会去Date类中调用该类的拷贝构造函数。

在这里插入图片描述
在这里插入图片描述

class Birthday
{
public:Birthday(){cout << "Birthday构造函数" << endl;}//Birthday类的默认析构函数~Birthday(){cout << "Birthday析构函数" << endl;}};class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;cout << "构造函数;" << endl;}//拷贝构造函数//可以看到拷贝构造函数是构造函数的一个重载形式Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}//没有显式定义析构函数的话,编译器会自动生成默认的析构函数。private://内置类型int _year = 2000;int _month = 1;int _day = 1;//自定义类型Birthday _birthday;
};void Func1(Date d)
{}void Func2(Date& d)
{}
int main()
{Date d1(2023,2,3);Func1(d1);Func2(d1);return 0;
}

当我们从上面的分析得出了传值传参传入的为自定义类型时,会先调用该自定义类型的拷贝构造函数的结论时。我们就可以分析出**为什么使用传值方式编译器直接报错,因为会引发无穷递归调用。**的原因了。因为传值传参传入的为自定义类型时,会先调用该自定义类型的拷贝构造函数,而该自定义类型的拷贝构造函数就是传值传参的函数,并且该函数需要传入该自定义类型,而传值传参函数传入的为自定义类型时,就会先调用该自定义类型的拷贝构造函数。这就引发了无穷递归调用,所以拷贝构造需要传引用调用,因为传引用调用不会发生自定义类型的拷贝。

在这里插入图片描述
在这里插入图片描述
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
即默认拷贝构造函数就是将该类类型的内置类型的值直接拷贝过去,然后调用自定义类型的拷贝构造函数完成拷贝。

class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;cout << "构造函数;" << endl;}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}//没有显式定义析构函数的话,编译器会自动生成默认的析构函数。private://内置类型int _year = 2000;int _month = 1;int _day = 1;
};int main()
{Date d1(2023, 2, 3);Date d2(d1);d1.Print();d2.Print();return 0;
}

在这里插入图片描述
在这里插入图片描述
4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然还需要,因为编译器生成的默认拷贝构造函数只能完成浅拷贝,而很多时候对象的拷贝并不像上面那么简单。
例如我们之前实现的Stack类,该类里面定义了一个int*data指针类型的成员变量,如果要对Stack类创建时使用编译器自动生成的拷贝构造函数,则会出现很大的问题。因为如果我们创建了一个s1对象,然后将s2对象拷贝s1对象时,此时调用编译器自动生成的拷贝构造函数,我们可以发现该拷贝构造函数直接将s1对象的data数组的地址拷贝给了s2,那么就造成了s1和s2两个栈共用一个数组来存储数据,这是肯定不行的,所以这种情况编译器自动生成的拷贝构造函数就不能用了,需要用户自己写一个深拷贝的拷贝构造函数。

注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。在这里插入图片描述
在这里插入图片描述
向上面的情况使用编译器自动生成的拷贝构造函数就会出现错误,因为上面的情况就需要深拷贝来完成拷贝。下面我们可以试着写一个简单的深拷贝。下面的代码中Stack类的默认拷贝构造函数就为一个深拷贝,因为该函数中又申请了一片新的数组空间来给s2对象的_data,并且还将s1对象的数据拷贝给了s2。此时可以看到s1的_data和s2的_data指向的不是同一片内存空间。

typedef int STDataType;class Stack
{
public://成员函数Stack(int n = 4){_data = (int*)malloc(sizeof(int) * n);if (nullptr == _data){perror("malloc fail");exit(-1);}_top = _capacity = 0;}Stack(const Stack& s){STDataType* tmp = (STDataType*)malloc(sizeof(STDataType) * s._capacity);if (nullptr == tmp){perror("malloc fail");exit(-1);}_data = tmp;memcpy(_data, s._data, sizeof(STDataType) * s._top);_capacity = s._capacity;_top = s._top;}void CheckCapacity(){int newCapacity = _capacity == 0 ? 4 : _capacity * 2;if (_top == _capacity){STDataType* tmp = (STDataType*)realloc(_data, newCapacity * sizeof(STDataType));if (tmp == NULL){perror("realloc fail");exit(-1);}_data = tmp;_capacity = newCapacity;}}void Push(STDataType x){CheckCapacity();_data[_top] = x;_top++;}bool IsEmpty(){if (_top == 0){return true;}return false;}void Pop(){assert(!IsEmpty());_top--;}STDataType Top(){assert(!IsEmpty());return _data[_top - 1];}private://成员变量/类属性STDataType* _data;int _top;int _capacity;
};int main()
{Stack s1(10);s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2(s1);cout << s1.Top() << endl;cout << s2.Top() << endl;s2.Push(5);cout << s1.Top() << endl;cout << s2.Top() << endl;return 0;
}

在这里插入图片描述

5. 拷贝构造函数典型调用场景:
(1). 使用已存在对象创建新对象
(2). 函数参数类型为类类型对象
(3). 函数返回值类型为类类型对象

(1)和(2)我们在上面已经分析过,而函数返回值类型为类类型对象时也会调用类的拷贝构造函数,我们知道传值返回函数在返回值时,编译器都会生成一个函数返回对象的拷贝,用来作为函数调用返回值,这样调用函数的栈帧销毁了,返回值也还有拷贝的一份,不会让函数的返回值丢失。所以这就用到了一次自定义类型的拷贝,所以会调用拷贝构造函数。

class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;cout << "构造函数;" << endl;}Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;cout << "构造函数" << endl;}//函数参数类型为类类型对象//函数返回值类型为类类型对象Date& Test(Date d){//使用已存在对象创建新对象Date tmp(d);return tmp;}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}//没有显式定义析构函数的话,编译器会自动生成默认的析构函数。private://内置类型int _year = 2000;int _month = 1;int _day = 1;
};int main()
{Date d1(2023, 2, 3);//使用已存在对象创建新对象Date d2(d1);Date d3 = d1;d1.Test(d1);return 0;
}

3、拷贝构造函数的应用 – 求n天后的日期

计算n天以后的日期为多少。
第一种方法

//求n天后的日期
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}int GetMonthDay(int year, int month){assert(month > 0 && month < 13);int monthArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))){return 29;}else{return monthArray[month];}}Date GetAfterDay(int x){_day += x;//判断如果天数大于该月数的天数就进位while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);++_month;//如果月数为13就进位if (_month == 13){_year++;_month = 1;}}//返回该Date对象return *this;}void Print(){cout << _year << "年" << _month<< "月" << _day << "日" << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1(2023, 9, 11);Date d2 = d1.GetAfterDay(100);//d1对象也会被改变d1.Print();d2.Print();return 0;
}

第二种方法
第一种方法会将d1对象的值也改变,所以我们可以在GetAfterDay()方法中创建一个tmp临时变量,然后将该变量返回,这样d1对象的值就不会改变了。

//求n天后的日期
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}int GetMonthDay(int year, int month){assert(month > 0 && month < 13);int monthArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))){return 29;}else{return monthArray[month];}}Date GetAfterDay(int x){//会调用拷贝构造函数Date tmp = *this;tmp._day += x;//判断如果天数大于该月数的天数就进位while (tmp._day > GetMonthDay(tmp._year, tmp._month)){tmp._day -= GetMonthDay(tmp._year, tmp._month);++(tmp._month);//如果月数为13就进位if (tmp._month == 13){tmp._year++;tmp._month = 1;}}//返回该Date对象//会调用拷贝构造函数return tmp;}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1(2023, 9, 11);Date d2 = d1.GetAfterDay(100);//d1对象不会被改变d1.Print();d2.Print();return 0;
}

上面的两种方法可以总结为一个为改变对象,一个不改变对象。合在一起可以写两个函数,一个AddEqual()函数为改变对象,其中将该函数的返回值设置为了传引用返回,因为*this为main中的d1对象,不会随着AddEqual函数的销毁而销毁,所以可以使用传引用返回,这样就减少了一次拷贝构造。因为传值返回编译器都会生成一个函数返回对象的拷贝,这样就进行了一次拷贝构造函数的调用,而使用传引用返回就减少了一次拷贝。而另一个不改变对象的Add()函数需要有两次拷贝构造函数的调用。

class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}int GetMonthDay(int year, int month){assert(month > 0 && month < 13);int monthArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))){return 29;}else{return monthArray[month];}}//对象改变Date& AddEqual(int x){_day += x;//判断如果天数大于该月数的天数就进位while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);++_month;//如果月数为13就进位if (_month == 13){_year++;_month = 1;}}//返回该Date对象//会调用拷贝构造函数return *this;}//对象不改变Date Add(int x){//调用一次拷贝构造函数Date tmp = *this;tmp._day += x;//判断如果天数大于该月数的天数就进位while (tmp._day > GetMonthDay(tmp._year, tmp._month)){tmp._day -= GetMonthDay(tmp._year, tmp._month);++(tmp._month);//如果月数为13就进位if (tmp._month == 13){tmp._year++;tmp._month = 1;}}//返回该Date对象//再调用一次拷贝构造函数return tmp;}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1(2023, 9, 11);Date d2 = d1.Add(100);d1.Print();d2.Print();Date d3 = d1.AddEqual(200);d1.Print();d3.Print();return 0;
}

四、赋值运算符重载

1、运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表) 例如:bool operator>(const Date& d)。
注意:
(1). 不能通过连接其他符号来创建新的操作符:比如operator@
(2). 重载操作符必须有一个类类型参数
(3). 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
(4). 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
(5). .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//private:
public:int _year;int _month;int _day;
};//运算符重载
//运算符的操作数有几个,就有几个参数
//并且因为==只是判断d1对象和d2对象是否相等,所以我们使用传引用传参,可以减少调用拷贝构造函数,以便减少开销
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(2023, 9, 12);Date d2(2023, 3, 12);//上面的运算符重载就类似于定义了一个名字为operator==的函数,所以可以使用函数名加()函数调用符来进行调用cout << operator==(d1, d2) << endl;//但是c++中加入了运算符重载,编译器在判断d1 == d2时会检查有没有实现==的运算符重载//如果实现了==的运算符重载,则会直接call到operator==函数的地址cout << (d1 == d2) << endl;  //要注意运算符优先级,<<的优先级大于==//上面的d1 == d2也会转换成去调用operator==()这个函数return 0;
}

可以看到d1==d2在底层也是调用的operator == 这个函数,即会直接call到operator = =函数的地址去。
在这里插入图片描述
在这里插入图片描述
上面的写法中因为我们将运算符重载函数写到了Date类的外面,所以此时如果访问Date类的成员变量,就需要将Date类的成员变量变为public,即可以公共访问,这样上面的operator ==函数才不会出错。而当我们将该函数定义在Date类中,变为Date类的成员函数,此时就不需要将Date类的成员变量设置为public,而且在Date类中定义 operator = =时,就不需要再写两个参数了,因为类的成员函数有一个隐式的this指针。

class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//因为有一个隐式的this指针来表示d1,所以只需定义形参d2即可bool operator==(const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;}//此时成员变量为私有
private:int _year;int _month;int _day;
};int main()
{Date d1(2023, 9, 12);Date d2(2023, 3, 12);//此时就需要这样访问operator==函数了cout << d1.operator==(d2)<<endl;cout << (d1 == d2) << endl;  //要注意运算符优先级,<<的优先级大于==//上面的d1 == d2也会转换成去d1.operator==(d2)这个函数return 0;
}

2、一些运算符重载的实现

我们在上面实现了==运算符的重载后,还可以继续尝试实现其它的运算符重载。下面我们可以实现一下<号的运算符重载。

class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//因为有一个隐式的this指针来表示d1,所以只需定义形参d2即可bool operator==(const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;}// d1 < d2bool operator<(const Date& d){/*if (_year < d._year){return true;}else if ((_year == d._year) && (_month < d._month)){return true;}else if ((_year == d._year) && (_month == d._month) && (_day < d._day)){return true;}else{return false;}*///上面的代码可以简化为这样return (_year > d._year) || (_year == d._year && _month < d._month) || (_year == d._year && _month == d._month && _day < d._day);}//此时成员变量为私有
private:int _year;int _month;int _day;
};int main()
{Date d1(2023, 9, 12);Date d2(2023, 3, 12);Date d3(2022, 3, 1);cout << d1.operator<(d2) << endl;cout << (d3 < d2) << endl;return 0;
}

当我们实现了==和<的运算符重载函数后,其他的类似的运算符重载的函数我们就可以复用上面实现的函数来实现。这样我们就实现了这些运算符的重载函数。

class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//因为有一个隐式的this指针来表示d1,所以只需定义形参d2即可bool operator==(const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;}// d1 < d2bool operator<(const Date& d){/*if (_year < d._year){return true;}else if ((_year == d._year) && (_month < d._month)){return true;}else if ((_year == d._year) && (_month == d._month) && (_day < d._day)){return true;}else{return false;}*///上面的代码可以简化为这样return (_year > d._year) || (_year == d._year && _month < d._month) || (_year == d._year && _month == d._month && _day < d._day);}// d1 <= d2bool operator<=(const Date& d){//*this就表示d1对象,d表示d2对象//先调用<的运算符重载函数判断d1是否小于d2//如果d1<d2,再调用==的运算符重载函数判断d1是否等于d2return *this < d || *this == d;}// d1 > d2bool operator>(const Date& d){//如果d1不小于等于d2,那么就是d1大于d2,//所以直接对<=运算符重载函数逻辑取反即可得到>的运算符重载函数return !(*this <= d);}// d1 >= d2bool operator>=(const Date& d){//如果d1不小于d2,那么就是d1大于等于d2//所以可以直接对<的运算符重载函数逻辑取反得到>=的运算符重载函数return !(*this < d);}// d1 != d2bool operator!=(const Date& d){//如果d1==d2不成立,那么d1!=d2//所以直接对==的运算符重载函数逻辑取反得到!=的运算符重载函数return !(*this == d);}//此时成员变量为私有
private:int _year;int _month;int _day;
};int main()
{Date d1(2023, 9, 12);Date d2(2023, 3, 12);Date d3(2023, 3, 12);cout << d1.operator<(d2) << endl;cout << (d3 < d2) << endl;cout << (d1 > d2) << endl;cout << (d2 >= d3) << endl;cout << (d2 <= d3) << endl;cout << (d1 != d2) << endl;return 0;
}

3、赋值运算符重载

  1. 赋值运算符重载格式
    参数类型:const T&,传递引用可以提高传参效率
    返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
    检测是否自己给自己赋值
    返回*this :要复合连续赋值的含义
    当我们将=的运算符重载函数写成这样,那么在遇到像 d1=d2=d3 这样的连续赋值时,就会报出错误。
// d1 = d2void operator=(const Date& d){_year = d._year;_month = d._month;_day = d._day;}

所以我们需要给该函数返回一个Date对象,这样才能保持运算符的特性,而因为d1、d2、d3不会随着operator=函数的销毁而销毁,所以使用传引用返回比较好,这样就减少了拷贝构造函数的调用。

	// d1 = d2//返回值为了支持连续赋值,保持运算符的特性Date& operator=(const Date& d){_year = d._year;_month = d._month;_day = d._day;//将d1对象返回return *this;}

我们在使用=赋值运算符时还会遇到这样的情况,将自己赋值给自己,例如d1=d1,所以我们还需要进行判断,是不是进行了将自己赋值给自己,如果是的话就不需要做任何操作。

class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}// d1 = d2//返回值为了支持连续赋值,保持运算符的特性Date& operator=(const Date& d){//比较两个对象的地址,如果地址不一样则不为d1=d1这样的自己给自己赋值if (this != &d){_year = d._year;_month = d._month;_day = d._day;}//将d1对象返回return *this;}//此时成员变量为私有
private:int _year;int _month;int _day;
};int main()
{Date d1(2023, 9, 12);Date d2(2023, 3, 12);Date d3(2023, 3, 12);d1.Print();d2.Print();d1 = d2;d1.Print();d2.Print();d1 = d2 = d3;d1.Print();d2.Print();d3.Print();d1 = d1;d1.Print();return 0;
}

赋值运算符只能重载成类的成员函数不能重载成全局函数
我们在前面写了==的全局的运算符重载函数,但是为什么=的运算符重载函数就不能写为全局的了呢?
这是因为赋值运算符如果不在类中显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
在这里插入图片描述
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现赋值运算符重载函数吗?答案是当然需要了,这其实又和拷贝构造函数遇到同样的问题了,对于我们写的Date时间类,值拷贝就可以了,但是如果遇到像前面的Stack类时,该类里面的成员变量中有一个指针变量data指向一个数组,此时如果使用默认的赋值运算符重载函数就会在s1=s2后,将s1和s2指向同一片内存空间,即s1和s2使用一个数组存储数据,这显然是不可以的。所以需要 注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。

4、日期类的实现

我们已经初步学习了c++中的类与对象的一些知识,可以将前面的知识综合起来,实现一个日期类,并且为该日期类实现运算符的重载函数。而且可以计算该日期加上一个天数后日期为多少。

#include<iostream>
using namespace std;class Date
{
public:Date(int year = 1900, int month = 1, int day = 1);void Print();bool operator==(const Date& d);bool operator<(const Date& d);bool operator<=(const Date& d);bool operator>(const Date& d);bool operator>=(const Date& d);bool operator!=(const Date& d);Date& operator=(const Date& d);Date& operator+=(int day);int GetMonthDay(int year, int month);Date operator+(int day);//此时成员变量为私有
private:int _year;int _month;int _day;
};
#include"Date.h"Date::Date(int year, int month, int day)
{if (month > 0 && month < 13 && (day > 0 && day <= GetMonthDay(year, month))){_year = year;_month = month;_day = day;}else{cout << "日期非法" << endl;}
}void Date::Print()
{cout << _year << "年" << _month << "月" << _day << "日" << endl;
}//因为有一个隐式的this指针来表示d1,所以只需定义形参d2即可
bool Date::operator==(const Date& d)
{return _year == d._year&& _month == d._month&& _day == d._day;
}// d1 < d2
bool Date::operator<(const Date& d)
{/*if (_year < d._year){return true;}else if ((_year == d._year) && (_month < d._month)){return true;}else if ((_year == d._year) && (_month == d._month) && (_day < d._day)){return true;}else{return false;}*///上面的代码可以简化为这样return (_year > d._year)|| (_year == d._year && _month < d._month)|| (_year == d._year && _month == d._month && _day < d._day);
}// d1 <= d2
bool Date::operator<=(const Date& d)
{//*this就表示d1对象,d表示d2对象//先调用<的运算符重载函数判断d1是否小于d2//如果d1<d2,再调用==的运算符重载函数判断d1是否等于d2return *this < d || *this == d;
}// d1 > d2
bool Date::operator>(const Date& d)
{//如果d1不小于等于d2,那么就是d1大于d2,//所以直接对<=运算符重载函数逻辑取反即可得到>的运算符重载函数return !(*this <= d);
}// d1 >= d2
bool Date::operator>=(const Date& d)
{//如果d1不小于d2,那么就是d1大于等于d2//所以可以直接对<的运算符重载函数逻辑取反得到>=的运算符重载函数return !(*this < d);
}// d1 != d2
bool Date::operator!=(const Date& d)
{//如果d1==d2不成立,那么d1!=d2//所以直接对==的运算符重载函数逻辑取反得到!=的运算符重载函数return !(*this == d);
}// d1 = d2
//返回值为了支持连续赋值,保持运算符的特性
Date& Date::operator=(const Date& d)
{//比较两个对象的地址,如果地址不一样则不为d1=d1这样的自己给自己赋值if (this != &d){_year = d._year;_month = d._month;_day = d._day;}//将d1对象返回return *this;
}int Date::GetMonthDay(int year, int month)
{int arr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };if (month == 2 && (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))){return 29;}else{return arr[month];}
}//因为+=也可以连续赋值,所以需要返回值,而该赋值运算符为+=,所以将改变后的d1返回
//使用传引用返回可以减少一次拷贝构造函数的调用
Date& Date::operator+=(int day)
{_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);++_month;if (_month == 13){++_year;_month = 1;}}return *this;
}Date Date::operator+(int day)
{//可以复用前面写的+=来实现+的赋值运算符重载函数//先调用拷贝构造函数将d1拷贝给tmp对象Date tmp(*this);//然后改变tmp对象tmp += day;//将tmp对象返回,此时会再调用一次拷贝构造函数return tmp;
}
//日期类
#include"Date.h"
int main()
{Date d1(2023, 5, 6);Date d2 = d1+100;d1.Print();d2.Print();d1 += 100;d1.Print();return 0;
}

上面的代码就是Date类的常规实现,当我们在实现+的运算符重载函数时,我们复用了+=的运算符重载函数,而其实我们也可以先实现+的运算符重载函数,然后在实现+=的运算符函数时复用+的运算符重载函数。对比了两种实现方法后,我们可以发现第一种方法比较好一点,因为+的运算符重载函数需要两次拷贝构造函数的调用,第二种方法中复用+的运算符重载函数来实现+=时使+=运算符重载函数也调用了两次拷贝构造函数,而第一种方法中+=的运算符重载函数不需要调用拷贝构造函数,减少了程序开销。
先实现+=的运算符重载函数,然后复用+=的运算符重载函数实现+的运算符重载函数。

//因为+=也可以连续赋值,所以需要返回值,而该赋值运算符为+=,所以将改变后的d1返回
//使用传引用返回可以减少一次拷贝构造函数的调用
Date& Date::operator+=(int day)
{_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);++_month;if (_month == 13){++_year;_month = 1;}}return *this;
}Date Date::operator+(int day)
{//可以复用前面写的+=来实现+的赋值运算符重载函数//先调用拷贝构造函数将d1拷贝给tmp对象Date tmp(*this);//然后改变tmp对象tmp += day;//将tmp对象返回,此时会再调用一次拷贝构造函数return tmp;
}

先实现+的运算符重载函数,然后复用+的运算符重载函数实现+=的运算符重载函数。

Date Date::operator+(int day)
{Date tmp(*this);tmp._day += day;while (tmp._day > GetMonthDay(tmp._year, tmp._month)){tmp._day -= GetMonthDay(tmp._year, tmp._month);++tmp._month;if (tmp._month == 13){++tmp._year;tmp._month = 1;}}return tmp;
}Date& Date::operator+=(int day)
{*this = *this + day;return *this;
}

+=我们按照进位的逻辑来实现,那么-=我们就可以按照借位的逻辑来实现,我们可以按照这个结论来先实现 -= 。

Date& Date::operator-=(int day)
{//先让d1的天数为负数_day -= day;//如果天为0时也要转换,因为此时为上一个月的最后一天,例如2月0日为1月31日while (_day <= 0){//向月去借位--_month;//如果月为0就去向年借位if (_month == 0){_month = 12;--_year;}//将借的位加上_day += GetMonthDay(_year, _month);}return *this;
}

上面写的+=和-=只适合+和-正数,当这样使用时就会出现错误,d1 += -100 或 d1 -= -100;这时得到的日期就是错误的。此时就需要再对+=和-+运算符的重载函数进行修改。即判断如果天数为负数时就做另外的运算。

Date& Date::operator+=(int day)
{//当+=一个负数,就相当于-=这个负数的绝对值if (day < 0){//复用-=运算符重载函数*this -= -day;return *this;}_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);++_month;if (_month == 13){++_year;_month = 1;}}return *this;
}Date& Date::operator-=(int day)
{//如果-=一个负数,就相当于+=这个负数的绝对值if (day < 0){*this += -day;return *this;}//先让d1的天数为负数_day -= day;//如果天为0时也要转换,因为此时为上一个月的最后一天,例如2月0日为1月31日while (_day <= 0){//向月去借位--_month;//如果月为0就去向年借位if (_month == 0){_month = 12;--_year;}//将借的位加上_day += GetMonthDay(_year, _month);}return *this;
}

5、前置++和后置++ && 前置–和后置–

在实现了Date类的一些运算符重载函数后,我们还可以复用这些运算符重载函数来实现前置++和后置++,我们知道前置++是先将目标+1,然后返回+1的目标;后置++是先返回目标,然后再将目标+1。经过下面的代码我们可以看到自定义类型中前置++的效率高,因为不需要拷贝,而后置++需要创建临时变量tmp,需要拷贝。而且前置++和后置++形成了函数重载,那么在使用时编译器是怎样知道要调用前置++还是后置++呢?在这里其实编译器做了一个特殊处理,即调用后置++时向函数中传入了一个值,这样在链接时就可以区分去调用前置++还是后置++函数了。

Date& Date::operator++()
{*this += 1;return *this;
}//可以看到运算符重载函数也可以实现重载
//int参数的设计,仅仅是为了占位,这样才能和前置++的运算符重载函数形成重载
Date Date::operator++(int)
{//先将没有++的*this存起来Date tmp(*this);//然后将*this+1*this += 1;//返回没有+1的tmpreturn tmp;
}
//日期类
#include"Date.h"
int main()
{Date d1(2023, 5, 6);//编译器在调用后置++时做了一个特殊处理,即调用函数时传入了一个值。d1++;  //d1.operator++(0)++d1;  //d1.operator++()return 0;
}

当知道了前置++和后置++的写法后,前置–和后置–也是一样的处理。

Date& Date::operator--()
{*this -= 1;return *this;
}Date& Date::operator--(int)
{Date tmp(*this);*this -= 1;return tmp;
}
#include"Date.h"
int main()
{Date d1(2023, 5, 6);//编译器在调用后置--时做了一个特殊处理,即调用函数时传入了一个值//这样才能区分去调用前置--还是后置--的运算符重载函数d1--;  //d1.operator--(0)--d1;  //d1.operator--()return 0;
}

6、求两个日期相减的天数(d1 - d2)

因为一个日期减去一个天数没有多大意义,所以我们将-运算符的重载函数写为求两个日期相减的天数。

int Date::operator-(const Date& d)
{Date max = *this;Date min = d;int flag = 1;//如果发现d1<d2,就将d2设置为max,将d1设置为min,此时d1-d2就为一个负数,所以将flag设为-1if (*this < d){max = d;min = *this;flag = -1;}int n = 0;while (min != max){//自定义类型中前置++效率高一点++min;++n;}return n;
}

7、总结

上面就是日期类的全部实现,将完整的代码放入到下面。

#pragma once
#include<iostream>
using namespace std;class Date
{
public:Date(int year = 1900, int month = 1, int day = 1);void Print();bool operator==(const Date& d);bool operator<(const Date& d);bool operator<=(const Date& d);bool operator>(const Date& d);bool operator>=(const Date& d);bool operator!=(const Date& d);Date& operator=(const Date& d);Date& operator+=(int day);int GetMonthDay(int year, int month);Date operator+(int day);Date& operator-=(int day);int operator-(const Date& d);Date& operator++();//int参数的设计,仅仅是为了占位,这样才能和前置++的运算符重载函数形成重载Date operator++(int);Date& operator--();//int参数的设计,仅仅是为了占位,这样才能和前置--的运算符重载函数形成重载Date& operator--(int);
//此时成员变量为私有
private:int _year;int _month;int _day;
};
#define _CRT_SECURE_NO_WARNINGS
#include"Date.h"Date::Date(int year, int month, int day)
{if (month > 0 && month < 13 && (day > 0 && day <= GetMonthDay(year, month))){_year = year;_month = month;_day = day;}else{cout << "日期非法" << endl;}
}void Date::Print()
{cout << _year << "年" << _month << "月" << _day << "日" << endl;
}//因为有一个隐式的this指针来表示d1,所以只需定义形参d2即可
bool Date::operator==(const Date& d)
{return _year == d._year&& _month == d._month&& _day == d._day;
}// d1 < d2
bool Date::operator<(const Date& d)
{/*if (_year < d._year){return true;}else if ((_year == d._year) && (_month < d._month)){return true;}else if ((_year == d._year) && (_month == d._month) && (_day < d._day)){return true;}else{return false;}*///上面的代码可以简化为这样return (_year < d._year)|| (_year == d._year && _month < d._month)|| (_year == d._year && _month == d._month && _day < d._day);
}// d1 <= d2
bool Date::operator<=(const Date& d)
{//*this就表示d1对象,d表示d2对象//先调用<的运算符重载函数判断d1是否小于d2//如果d1<d2,再调用==的运算符重载函数判断d1是否等于d2return *this < d || *this == d;
}// d1 > d2
bool Date::operator>(const Date& d)
{//如果d1不小于等于d2,那么就是d1大于d2,//所以直接对<=运算符重载函数逻辑取反即可得到>的运算符重载函数return !(*this <= d);
}// d1 >= d2
bool Date::operator>=(const Date& d)
{//如果d1不小于d2,那么就是d1大于等于d2//所以可以直接对<的运算符重载函数逻辑取反得到>=的运算符重载函数return !(*this < d);
}// d1 != d2
bool Date::operator!=(const Date& d)
{//如果d1==d2不成立,那么d1!=d2//所以直接对==的运算符重载函数逻辑取反得到!=的运算符重载函数return !(*this == d);
}// d1 = d2
//返回值为了支持连续赋值,保持运算符的特性
Date& Date::operator=(const Date& d)
{//比较两个对象的地址,如果地址不一样则不为d1=d1这样的自己给自己赋值if (this != &d){_year = d._year;_month = d._month;_day = d._day;}//将d1对象返回return *this;
}int Date::GetMonthDay(int year, int month)
{int arr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };if (month == 2 && (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))){return 29;}else{return arr[month];}
}//因为+=也可以连续赋值,所以需要返回值,而该赋值运算符为+=,所以将改变后的d1返回
//使用传引用返回可以减少一次拷贝构造函数的调用
Date& Date::operator+=(int day)
{//当+=一个负数,就相当于-=这个负数的绝对值if (day < 0){//复用-=运算符重载函数*this -= -day;return *this;}_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);++_month;if (_month == 13){++_year;_month = 1;}}return *this;
}Date Date::operator+(int day)
{//可以复用前面写的+=来实现+的赋值运算符重载函数//先调用拷贝构造函数将d1拷贝给tmp对象Date tmp(*this);//然后改变tmp对象tmp += day;//将tmp对象返回,此时会再调用一次拷贝构造函数return tmp;
}//Date Date::operator+(int day)
//{
//	Date tmp(*this);
//	tmp._day += day;
//	while (tmp._day > GetMonthDay(tmp._year, tmp._month))
//	{
//		tmp._day -= GetMonthDay(tmp._year, tmp._month);
//		++tmp._month;
//		if (tmp._month == 13)
//		{
//			++tmp._year;
//			tmp._month = 1;
//		}
//	}
//
//	return tmp;
//}
//
//
//Date& Date::operator+=(int day)
//{
//	*this = *this + day;
//	return *this;
//}Date& Date::operator-=(int day)
{//如果-=一个负数,就相当于+=这个负数的绝对值if (day < 0){*this += -day;return *this;}//先让d1的天数为负数_day -= day;//如果天为0时也要转换,因为此时为上一个月的最后一天,例如2月0日为1月31日while (_day <= 0){//向月去借位--_month;//如果月为0就去向年借位if (_month == 0){_month = 12;--_year;}//将借的位加上_day += GetMonthDay(_year, _month);}return *this;
}int Date::operator-(const Date& d)
{Date max = *this;Date min = d;int flag = 1;//如果发现d1<d2,就将d2设置为max,将d1设置为min,此时d1-d2就为一个负数,所以将flag设为-1if (*this < d){max = d;min = *this;flag = -1;}int n = 0;while (min != max){//自定义类型中前置++效率高一点++min;++n;}return n;
}Date& Date::operator++()
{*this += 1;return *this;
}//可以看到运算符重载函数也可以实现重载
//int参数的设计,仅仅是为了占位,这样才能和前置++的运算符重载函数形成重载
Date Date::operator++(int)
{//先将没有++的*this存起来Date tmp(*this);//然后将*this+1*this += 1;//返回没有+1的tmpreturn tmp;
}Date& Date::operator--()
{*this -= 1;return *this;
}Date& Date::operator--(int)
{Date tmp(*this);*this -= 1;return tmp;
}
//日期类
#include"Date.h"
void Test1()
{Date d1(2023, 9, 13);Date d2(2000, 5, 22);cout << d1 - d2 << endl;
}
void Test2()
{Date d1(2023, 9, 13);Date d2 = d1 + 1000;d2.Print();}
void Test3()
{Date d1(2023, 9, 13);d1 -= 8514;d1.Print();
}
int main()
{Test1();Test2();Test3();return 0;
}

8、<<流插入和>>流提取

8.1 <<流插入符重载函数

我们可以看到c++库中对于<<符号就使用到了运算符重载函数,即c++中对<<运算符进行了运算符重载,并且<<运算符的重载函数也进行了重载,即<<符号可以接收不同的参数,这就是为什么c++中不需要进行类型判断,因为<<对每个类型都进行了重载,即参数为任何内置类型的<<函数都有。而且cout就是一个ostream对象。
在这里插入图片描述
我们写出<<运算符的重载函数如下。

//因为cout为ostream对象,所以我们将形参设为cout对象的引用,即out就表示cout
void Date::operator<<(ostream& out)
{out << _year << "年" << _month << "月" << _day << "日" << endl;
}

但是当我们使用时会发现cout<<d1;会报错,而d1<<cout;和d1.operator(cout)不会报错,这是因为我们写的<<运算符重载函数中的this为一个Date类类型的对象,而cout<<d1这样的形式调用的运算符重载函数中this为一个ostream类类型的对象,所以如果想要cout<<d1这样调用<<运算符的重载函数,我们就需要将第一个参数置为ostream,但是类成员函数默认的将隐式参数this作为第一个参数。

在这里插入图片描述
在这里插入图片描述
所以我们就可以将<<运算符的重载函数变为全局的函数,然后设置两个形参,将cout对象设置为第一个形参。但是此时函数内就不能访问Date类的私有成员变量了,我们可以将该函数设置为Date类的友元函数,这样该函数就可以访问Date类中的私有成员变量了。 此时将第一个函数设置为ostream类类型的对象,这样就可以使用cout<<d1来调用该函数了。
在这里插入图片描述

//将第一个参数设置为ostream对象,第二个参数为Date对象,这样就可以cout<<d1这样调用函数了。
//并且将该函数设置为Date类的友元函数,这样该函数就可以访问Date类中的私有成员变量了。
void operator<<(ostream& out, const Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}

在这里插入图片描述
但是此时又出现了一个问题,当我们连续插入时又会出现错误。这时因为cout<<d1执行完后,<<d2的右边没有ostream对象了,相当于operator<<(operator<<(cout,d1),d2);而在执行完operator<<(cout,d1)后执行operato<<r( ,d2)时发现少了第一个参数,所以该函数还需要一个返回值,返回cout这个ostream类类型的对象。然后再连续插入时就不会出错了。
在这里插入图片描述

ostream& operator<<(ostream& out, const Date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日" << endl;return out;
}

在这里插入图片描述
此时我们再对比c++库中对<<的运算符重载函数,可以发现库中的写法也是这样写的。
在这里插入图片描述
但是为什么c++中需要将<<运算符进行运算符重载呢?
这是因为c语言中的printf无法打印自定义类型的成员变量,因为成员变量为私有,受访问限制。所以c++写了流插入,用户也可以进行<<的运算符重载函数,这样就可以进行自定义类型的成员变量的打印。但是cout的效率比printf低,因为printf打印不同类型的值只需要一次调用,而cout打印不同的值需要多次调用函数,因为在<<运算符重载函数中每使用一次<<都会调用一次函数。
在这里插入图片描述

8.2 >>流提取符重载函数

上面我们实现了<<流插入符重载函数,下面我们就可以实现>>流提取符重载函数了。>>的运算符重载函数中因为需要改变d1对象中成员变量的值,所以不能将该函数的第二个参数使用const修饰。并且我们写了这两个运算符重载函数后发现它们的实际代码量很少,而且还需要经常调用,所以我们其实可以将这两个函数写为内联函数。

//该函数就不能将Date& d用const修饰,因为我们需要修改d1对象中成员变量的值
istream& operator>>(istream& in, Date& d)
{in >> d._year >> d._month >> d._day;return in;
}

在这里插入图片描述

五、const成员

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
当我们用const修饰我们创建的对象时,该对象调用它的成员方法就会报错。
在这里插入图片描述
上面出错的原因是因为发生了权限的放大。const A aa实例化了一个aa对象,当aa调用Print函数时,因为c++会Print()函数设置一个隐式形参,即void Print(A* this),该隐式形参为一个A* 类型的指针。而aa.Print调用函数时,因为aa被const修饰,所以aa.Print()调用函数时传入的是一个cosnt A* 类型的指针,即将一个权限为只读的指针变量传参给一个权限为读写的指针变量,所以发生了权限放大,才会报错。此时需要将该隐式形参也使用const修饰才不会报错,但是c++中不可以显示定义和修改this。
在这里插入图片描述
但是c++也为我们提供了上面这种情况的解决办法,即将函数写为void Print() const这个形式,这个const就代表修饰的为隐式形参*this。此时我们就可以发现错误没有了。
在这里插入图片描述
那么我们又会有疑问了,上面将类实例化的对象使用const修饰我们几乎都不会使用到,还废那么大劲解决这个问题干啥。虽然上面的情况我们很少会遇到,但是下面的情况我们却会经常遇到。即我们在Func函数中将形参定义的引用使用const修饰,此时在该函数中调用这个对象的方法就会出现错误。而如果在函数Print()后面加上const就不会出错了。
在这里插入图片描述
在这里插入图片描述

所以当类的成员函数里面改变*this对象的值时,该函数不能用const修饰。
因为* this已经被const修饰了,则就不能通过解引用this指针来修改this指针指向的对象。
在这里插入图片描述
当类的成员函数内部不改变*this对象的值时,该函数最好加上const修饰
所以前面我们实现Date类时写的很多函数其实都需要加上const修饰,并且声明和定义分离的函数,声明和定义都需要加上const。
在这里插入图片描述

六、取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
如果自己实现的话也很简单,只需将this指针的值返回即可。

class A
{
public:void Print() const  //等价于void Print(const A* this){cout << _a << endl;}//取地址符&的操作符重载函数A* operator&(){return this;}//const取地址操作符重载函数const A* operator&() const{return this;}
private:int _a = 20;
};void Func(const A& x)
{cout << &x << endl;
}int main()
{A aa;const A bb;cout << &aa << endl;cout << &bb << endl;Func(aa);return 0;
}

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!

//取地址符&的操作符重载函数A* operator&(){//此时返回nullptr即为000000000地址return nullptr;}//const取地址操作符重载函数const A* operator&() const{//此时返回nullptr即为000000000地址return nullptr;}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/131767.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++数据结构X篇_12_树的基本概念和存储

学习二叉树之前先学习树的概念。 文章目录 1. 树的基本概念1.1 树的定义1.2 树的特点1.3 若干术语 2. 树的表示法2.1 图形表示法2.2 广义表表示法 3. 树的存储3.1 双亲表示法&#xff1a;保存父节点关系3.2 孩子表示法3.3 左孩子右兄弟表示法 1. 树的基本概念 之前所学均为线性…

前端构建工具 webpack 笔记

1、了解 webpack 1、定义&#xff1a;本质上&#xff0c;webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具&#xff0c;当 webpack 处理应用它会在内部从一个或多个入口点构建一个依赖图(dependency graph)&#xff0c;然后将你项目中所程序时&#xff0c;需的…

【javaSE】 反射与反射的使用

文章目录 &#x1f332;反射的定义&#x1f38d;反射的用途&#x1f334;反射基本信息&#x1f340;反射相关的类&#x1f6a9;Class类(反射机制的起源 )&#x1f388;Class类中的相关方法 &#x1f6a9;反射示例&#x1f388;获得Class对象的三种方式&#x1f388;反射的使用 …

Activating More Pixels in Image Super-Resolution Transformer(HAT)超分

摘要 基于Transformer的方法在低级视觉任务&#xff08;如图像超分辨率&#xff09;上表现出令人印象深刻的性能。然而&#xff0c;我们发现这些网络只能通过归因分析利用有限的输入信息空间范围。这意味着Transformer的潜力在现有网络中仍未得到充分利用。为了激活更多输入像…

Spring 的创建和日志框架的整合

目录 一、第一个 Spring 项目 1、配置环境 2、Spring 的 jar 包 Maven 项目导入 jar 包和设置国内源的方法&#xff1a; 3、Spring 的配置文件 4、Spring 的核心 API ApplicationContext 4、程序开发 5、细节分析 &#xff08;1&#xff09;名词解释 &#xff08;2&…

uboot 顶层Makefile-make xxx_deconfig过程说明三

一. uboot 的 make xxx_deconfig配置 本文接上一篇文章的内容。地址如下&#xff1a;uboot 顶层Makefile-make xxx_deconfig过程说明二_凌肖战的博客-CSDN博客 本文继续来学习 uboot 源码在执行 make xxx_deconfig 这个配置过程中&#xff0c;顶层 Makefile有关的执行思路。 …

STM32纯中断方式发送接收数据(串行通信;keil arm5;)

除了main文件其他文件均无修改&#xff0c;正常运行--在keil arm5内

AJAX学习笔记9 搜索联想自动补全

AJAX学习笔记8 跨域问题及解决方案_biubiubiu0706的博客-CSDN博客 其实就一个功能 搜索联想 自动补全 键盘按下事件keydown 键盘弹起事件keyup 做模糊查询 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><t…

第9节-PhotoShop基础课程-移动抓手缩放工具

文章目录 前言1. 移动工具1.移动工具1.自动选择&#xff08;图层和组&#xff09;2.显示变换控件 &#xff08;Shift 变换/ Ctrl 变换&#xff09;3.自由变换 Ctrl T &#xff08;Shift 变换/ Ctrl 变换&#xff09;4.对齐功能 2.画板工具 V1. 创建画板并作图2.导出画板 2.路…

59、SpringBoot 自定义JSON的序列化器和反序列化器

Serialization(序列化)&#xff1a; 将java对象以一连串的字节码保存在磁盘文件中的过程&#xff0c;也可以说是保存java对象状态的过程。序列化可以将数据永久保存在磁盘上(通常保存在文件中)。 deserialization(反序列化)&#xff1a; 将保存在磁盘文件中的java字节码重新转…

Jmx协议远程连接java服务器

注意&#xff1a;本例里&#xff0c;我用的是jdk17 通常用jdk自带的jconsole&#xff0c;或者想要功能强大点的使用visualVM 需要java服务器在启动的时候加上以下参数 -Dcom.sun.management.jmxremote 启用jxm远程连接-Djava.rmi.server.hostname10.1.3.99 指定jxm监听地址&…

python 自(1)定义变量 数据类型 判断数据类型 转换数据类型 算数运算符 复合运算符 比较运算符 逻辑运算符 赋值运算符

注释 # 注释 就是一个 # 也可以 ctrl / 也可以出来注释 命名规范 # 标识符 由字母 下划线 数字 组成 数字不能开头 # 不可以使用 关键字 # 严格区分大小写 定义变量 # 变量定义 重复利用 # 例如 cons 你好小周 print(cons) print(cons)print("你好小周") #这…

【经典小练习】JavaSE—拷贝文件夹

&#x1f38a;专栏【Java小练习】 &#x1f354;喜欢的诗句&#xff1a;天行健&#xff0c;君子以自强不息。 &#x1f386;音乐分享【如愿】 &#x1f384;欢迎并且感谢大家指出小吉的问题&#x1f970; 文章目录 &#x1f384;效果&#x1f33a;代码&#x1f6f8;讲解&#x…

每日一题 2596. 检查骑士巡视方案

难度&#xff1a;中等 很简单&#xff0c;从第 0 步开始模拟即可&#xff0c;唯一sb的就是测试用例中如果&#xff08;0&#xff0c;0&#xff09;处不为0的话就直接false&#xff0c;而不是去找0在哪 我的代码&#xff1a; class Solution:def checkValidGrid(self, grid: L…

【PowerQuery】Excel的PowerQuery的复制

在Excel中构建符合要求的PowerQuery连接之后&#xff0c;所有的PowerQuery 连接已经顺利的保存在Excel 工作簿当中&#xff0c;但是如何去查看已经保存的PowerQuery连接呢&#xff1f;图6.3 显示了查看PowerQuery连接。 Excel界面->数据页签->查询与连接 如果你的Power…

python求解一维弦振动方程

1、一维弦振动方程 2、差分公式及求解 import numpy as np import matplotlib.pyplot as plt l1 #长度 nx 101 #x方向网格节点数量 dx l/(nx-1) #空间网格步长 t6 #总时间 dt 0.01 # 时间步长 t_setnp.array([0.00,0.10,0.20,0.30,0.40,0.50,0.60]) #需要观察的时…

GO语言篇之发布开源软件包

GO语言篇之发布开源软件包 文章目录 GO语言篇之发布开源软件包新建仓库拉取到本地初始化项目编写代码提交代码发布引用软件包 我们写GO语言程序的时候难免会引用第三方的软件包&#xff0c;那么你知道别人是怎么发布自己的软件包吗&#xff0c;别急&#xff0c;这篇博客教你怎么…

如何使用 Node.js和Express搭建服务器?

如何使用NodeJs搭建服务器 1. 准备工作1.1 安装Node.js 2. 安装express2.1 初始化package.json2.2 安装express2.3 Express 应用程序生成器 欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段…

geohash学习

geohash编解码 import ("github.com/mmcloughlin/geohash" )func Test_Demo(t *testing.T) {// 编码经纬度到 geohash 字符串lat, lon : 40.7128, -74.0060 // 纽约市的位置geoHash : geohash.Encode(lat, lon) // geohash 字符串fmt.Println(geoHash) …

Spring WebFlux详解

Spring 框架中包含的原始 Web 框架 Spring Web MVC 是专门为 Servlet API 和 Servlet 容器而设计的。后来在 5.0 版本中加入了 reactive 栈的 Web 框架 Spring WebFlux。它是完全非阻塞的&#xff0c;支持 Reactive Streams 背压&#xff0c;并在 Netty、Undertow 和 Servlet 容…