32 类和对象 · 中

目录

一、类的默认成员函数

二、构造函数

(一)构造函数的特点

(二)使用例

1、Date类

2、Stack类

(三)总结

三、析构函数

(一)析构函数的特点

(二)使用例

1、Stack类

(三)总结

四、拷贝构造函数

(一)拷贝构造的特点

(二)使用例

1、Date类

2、Stack类

(三)总结

五、赋值运算符重载函数

(一)运算符重载函数

1、运算符重载的特点

2、使用例

(1)Date类运算符重载在全局

(2)Date类运算符重载在类中

(二)赋值运算符重载函数

1、赋值运算符的特点

2、使用例

(1)Date类实现赋值重载

(三)总结

六、日期类的实现

(一)代码实现

(二)总结

七、取地址运算符重载

(一)const成员函数

1、const成员函数特点

2、使用例

(1)Date类

3、总结

(二)取地址运算符重载

1、取地址运算符重载的特点

2、使用例

(1)Date类

3、总结


一、类的默认成员函数

         

        默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。一个类,不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个。

        

        默认成员函数很重要,也比较复杂,学习时要牢记下面两个出发点:

        • 第一:我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。

        • 第二:编译器默认生成的函数不满足我们的需求时,需要自己实现,那么如何实现?

二、构造函数

        构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的 Init 函数 的功能,构造函数自动调用的特点就完美的替代了Init函数。

(一)构造函数的特点

        1. 函数名与类名相同。

        2. 返回值。 (返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)

        3. 对象实例化时系统会自动调用对应的构造函数。

        4. 构造函数可以重载。

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

        6. 无参构造函数、全缺省构造函数、不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且只有一个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。要注意默认构造函数不是编译器默认生成的那个默认构造,实际上无参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调用的构造就叫默认构造

        7. 我们不写,编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化,如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个自定义成员变量,需要用初始化列表才能解决,初始化列表,详见【类与对象 · 下】。

        说明:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的原生数据类型, 如:int/char/double/指针等,自定义类型就是我们使用class/struct等关键字自己定义的类型。

        构造函数的两个学习出发点思维导图如下:

(二)使用例

1、Date类

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;class Date
{
public:// 1.无参构造函数Date(){_year = 1;_month = 1;_day = 1;}// 2.带参构造函数Date(int year, int month, int day){_year = year;_month = month;_day = day;}// 3.全缺省构造函数Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << "/" << _month << "/" << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{// 如果留下其中一个默认构造函数,其他两个都要注释掉,否则会报错:Date d1; // 调用无参的构造函数// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则编译器无法区分这里是函数声明还是实例化对象:// Date d1(); —— warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)Date d2(2025, 1, 1); // 调用带参的构造函数Date d3();// 调用全缺省构造函数d1.Print();d2.Print();return 0;
}

2、Stack类

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}// ...
private:STDataType* _a;size_t _capacity;size_t _top;
};// 两个Stack实现队列
class MyQueue
{
public://编译器默认生成MyQueue的构造函数调用了Stack的构造,完成了两个成员的初始化
private:Stack pushst;Stack popst;
};int main()
{MyQueue mq;return 0;
}

(三)总结

        ① 默认构造函数没有参数时不能加括号,因为没有参数时加括号就会与参函数的声明混淆:Date d1(),函数名d1,无参,返回值为Date;有参数时可以直接在对象名后加参数;全缺省时可以在后面加括号。

        ② 类比整形的初始化:【int a = 10】; 【Date d1(2024, 10, 9)】。

        ③ 因为实例化后会自动调用构造函数,所以如果写了构造函数,在实例化时就一定会对数据进行初始化。

        ④ 使用带有缺省参数的构造函数是最方便的。

        ⑤ 如果我们没有显示地写构造函数,编译器就会自动生成,我们写了就不会生成。

        ⑥ 不传实参就可以调用的构造函数就叫默认构造,但只要写了有参构造函数,并且没有写其他构造函数,就不存在默认构造。

        ⑦ 一般情况下构造函数都需要自己写,只有少数情况,默认生成就可以用:两个栈模拟一个队列,因为实现该功能的还是栈,在构造时会调用栈的构造函数(归根到底还是自己写的),不用写队列的构造函数。

三、析构函数

        析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类比我们之前Stack实现的Destroy功能,而像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。

(一)析构函数的特点

        1. 析构函数名是在类名前加上字符 ~

        2. 参数无返回值。 (这里跟构造类似,也不需要加void)

        3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。

        4. 对象生命周期结束时,系统会自动调用析构函数。

        5. 跟构造函数类似,若我们不写,编译器自动生成的析构函数对内置类型成员不做处理,自定义类型成员会调用他的析构函数。

        6. 还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。

        7. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如Date;如果默认生成的析构就可以用,也就不需要显示写析构,如MyQueue;但是有资源申请时,一定要自己写析构,否则会造成资源泄漏,如Stack。

        8. 一个局部域的多个对象,C++规定后定义的先析构(从后向前)

        构造函数的两个学习出发点思维导图如下:

(二)使用例

1、Stack类

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>using namespace std;
typedef int STDataType;class Stack
{
public:Stack(int n = 4)//Stack的构造函数{_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}~Stack()//Stack的析构函数{cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}private:STDataType* _a;size_t _capacity;size_t _top;
};// 两个Stack实现队列
class MyQueue
{
public://编译器默认生成MyQueue的析构函数调用了Stack的析构,释放的Stack内部的资源//显示写析构,也会自动调用Stack的析构
/*~MyQueue()
{}*/
private:Stack pushst;Stack popst;
};int main()
{Stack st;MyQueue mq;return 0;
}

(三)总结

        ① 析构函数不能重载,只能有一个。

        ② 对象的生命周期结束,就会自动调用析构函数,注意是对象的生命周期而不是类的生命周期

        ③ 一般情况下显示申请了资源(栈),才需要自己实现析构,其他情况基本不需要显示写(栈要写,日期类不用,队列自动使用栈的析构)

四、拷贝构造函数

        如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数

(一)拷贝构造的特点

        1. 拷贝构造函数是构造函数的一个重载。

        2. 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。 拷贝构造函数也可以多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值。

        C++规定:自定义类型(类类型)必须调用拷贝构造函数完成传值传参的拷贝。实参传值给形参的拷贝过程:调用a函数->进入拷贝构造函数完成值的拷贝 -> 进入a函数内部。若拷贝构造函数的第一个参数不是自定义类型的引用,就会因为第一个函数传参而无穷调用拷贝构造函数,形成无穷递归。

        3. C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。

        编译器未进行优化时的函数传值返回的拷贝过程: a函数返回值->进入拷贝构造函数完成值的拷贝 -> 给临时对象 -> 进入拷贝构造函数完成值的拷贝 ->给接收的对象。

        4. 若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝)对自定义类型成员变量会调用他的拷贝构造,进行深拷贝(对指向的资源也进行拷贝会开辟另一块一样大小的空间,结构与值是一样的,且互不干扰:拷贝空间malloc相同大小 + 拷贝空间上的值memcpy)

        5. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显示实现拷贝构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的拷吧构造会调用Stack的拷贝构造,也不需要我们显示实现MyQueue的拷贝构造。这里还有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就需要显示写拷贝构造,否则就不需要

        6. 传值返回会产生一个临时对象调用拷贝构造;传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回。

        无穷递归的形成:在类类型对象d2通过【拷贝构造函数】拷贝d1的数据进行初始化时,若【拷贝构造函数】的第一个形参没有写该类类型的引用,就会形成值传递,而根据 C++ 传值传参就要调用【拷贝构造函数】来完成的规定,把d1的数据给第二次调用的【拷贝构造函数】完成值的拷贝再给第一次调用的【拷贝构造函数】做参数,而再调用第二次【拷贝构造函数】时也发现该函数的第一个参数不是该类类型的引用,就会继续向下调用拷贝调用函数,如此反复,无穷无尽,形成了无穷递归。

        过程图解如下:

        拷贝构造函数的两个学习出发点思维导图如下:

(二)使用例

        在Date类中,拷贝构造的使用:

                Date d1(2024, 10, 10);//初始化构造

                Date d2(d1); 或者 Date d2 = d1; //都是拷贝构造

        注意:使用赋值符号也是拷贝构造(编译器进行了优化)。

1、Date类

#define _CRT_SECURE_NO_WARNINGS 1
#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(Date* d)//普通的构造函数(有参数){_year = d->_year;_month = d->_month;_day = d->_day;}Date(const Date& d)//拷贝构造函数{_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};void Func1(Date d)
{cout << &d << endl;d.Print();
}
// Date Func2()
Date& Func2()
{Date tmp(2024, 7, 5);tmp.Print();return tmp;
}int main()
{Date d1(2024, 7, 5);// C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里传值传参要调用拷贝构造// 所以这里的d1传值传参给d要调用拷贝构造完成拷贝,传引用传参可以较少这里的拷贝Func1(d1);cout << &d1 << endl;// 这里可以完成拷贝,但是不是拷贝构造,只是⼀个普通的构造Date d2(&d1);d1.Print();d2.Print();//这样写才是拷贝构造,通过同类型的对象初始化构造,⽽不是指针Date d3(d1);d2.Print();// 也可以这样写,这里也是拷贝构造Date d4 = d1;d2.Print();// Func2返回了⼀个局部对象tmp的引⽤作为返回值// Func2函数结束,tmp对象就销毁了,相当于了⼀个野引用Date ret = Func2();ret.Print();return 0;
}

2、Stack类

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:Stack(int n = 4)//构造函数{_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}Stack(const Stack& st)//拷贝构造函数{// 需要对_a指向资源创建同样⼤的资源再拷⻉值_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);if (nullptr == _a){perror("malloc申请空间失败!!!");return;}memcpy(_a, st._a, sizeof(STDataType) * st._top);_top = st._top;_capacity = st._capacity;}void Push(STDataType x)//push方法{if (_top == _capacity){int newcapacity = _capacity * 2;STDataType* tmp = (STDataType*)realloc(_a, newcapacity *sizeof(STDataType));if (tmp == NULL){perror("realloc fail");return;}_a = tmp;_capacity = newcapacity;}_a[_top++] = x;}~Stack()//析构函数{cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}
private:STDataType* _a;size_t _capacity;size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
private:Stack pushst;Stack popst;
};
int main()
{Stack st1;//自动调用构造函数进行初始化st1.Push(1);st1.Push(2);// 若Stack不显示实现拷贝构造,用自动生成的拷贝构造完成浅拷贝// 会导致st1和st2⾥⾯的_a指针指向同⼀块资源,析构时会析构两次,程序崩溃Stack st2 = st1;MyQueue mq1;// MyQueue自动生成的拷贝构造,会自动调用Stack拷贝构造完成pushst/popst// 的拷贝,只要Stack拷贝构造自己实现了深拷贝就行MyQueue mq2 = mq1;return 0;
}

(三)总结

        ① Date类类型使用系统默认生成的拷贝构造函数没问题,当Stack类类型使用系统默认生成的拷贝构造函数也会完成浅拷贝,但这样就会把栈的数组地址也会拷贝过来,导致两个栈指向同一块空间,在析构的时候,会把第二个栈先析构,然后析构第一个栈,但是同一个空间不能被析构两次(free两次错误),抛开析构的问题不谈,他们对栈的操作会互相干扰。

        ② 为什么自定义类型(类类型)必须调用拷贝构造函数完成传值传参的拷贝:因为不调用拷贝构造函数的话,只会完成浅拷贝,就会出现上面说的问题;使用const修饰的引用就可以解决必须调用拷贝构造函数的问题,不用进行拷贝,速度快。

        ③ 拷贝构造特点中的第五点(关键点:是否指向资源)与第六点 (类类型只要形成拷贝就会调用拷贝构造函数,无论是传参还是接收返回值,都涉及;局部对象不能使用引用放回,否则会产生越界报错——出了函数后就会把对象进行析构,返回值就会把析构后的对象通过拷贝构造函数复制给接收的对象,接收的是错误值。引用传参怎么用都行,但是传引用返回值就需要谨慎

        ④ 使用:如果引用对象中的成员变量不改变,能加const就加const进行引用的修饰

五、赋值运算符重载函数

(一)运算符重载函数

1、运算符重载的特点

        当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。

        运算符重载是具有特殊名字的函数,他的名字是由operator和后面要定义的运算符共同构成。和其他函数一样,它也具有其返回类型和参数列表以及函数体

        • 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数

        • 如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个

        • 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。

        • 不能通过连接语法中没有的符号来创建新的操作符:比如operator@。

        • .*】、【::】、【sizeof】、【?:(三目运算符)】、【.】,注意以上5个运算符不能重载。(选择题里面常考,要记⼀下)

        【.*】运算符是C++的运算符,用于调用成员函数的指针。(了解,用的不多)

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;class A
{
public:void func(){cout << "A::func()" << endl;}
};typedef void(A::* PF)(); //typedef成员函数指针类型为PFint main()
{// C++规定成员函数要加&才能取到函数指针PF pf = &A::func;A obj;//定义ob类对象temp// 对象调用成员函数指针时,使用【.*】运算符(obj.*pf)();//意思为:调用(【.】)obj对象中的func函数(【*】解引用函数指针)return 0;
}

        类外定义:void (A::*pf)( );

        与正常的函数指针定义有点不一样,需要指定类域,因为成员函数的参数隐含了this指针,不加类域就说明不了是类中的函数指针(传不了参数中的this指针)类域并不是作为函数指针的名字,单纯起限定作用

        • 重载操作符至少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: int operator+(int x, int y)。

        • ⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date类重载operator-就有意义,但是重载operator+就没有意义。

        • 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。 C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分。

        • 重载<<(流输出操作符)和>>(流输入操作符)时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调用时就变成了 【对象 << cout】,不符合使用习惯和可读性。重载为全局函数把 ostream/istream 放到第一个形参位置就可以了,第二个形参位置当类类型对象。

2、使用例

(1)Date类运算符重载在全局
#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>
using namespace std;
class Date
{
public:Date(int year = 1, int month = 1, int day = 1)//默认构造{_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}//private:int _year;int _month;int _day;
};
// 重载为全局的面临对象访问私有成员变量的问题
// 有几种方法可以解决:
// 1、成员放公有
// 2、Date提供getxxx函数
// 3、友元函数
// 4、重载为成员函数
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(2024, 7, 5);Date d2(2024, 7, 6);// 运算符重载函数可以显⽰调用operator==(d1, d2);// 编译器会转换成 operator==(d1, d2);d1 == d2;return 0;
}
(2)Date类运算符重载在类中
#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>
using namespace std;
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}bool operator==(const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;}Date& operator++(){cout << "前置++" << endl;//...return *this;}Date operator++(int){Date tmp;cout << "后置++" << endl;//...return tmp;}
private:int _year;int _month;int _day;
};
int main()
{Date d1(2024, 7, 5);Date d2(2024, 7, 6);// 运算符重载函数可以显⽰调用d1.operator==(d2);// 编译器会转换成 d1.operator==(d2);d1 == d2;// 编译器会转换成 d1.operator++();++d1;// 编译器会转换成 d1.operator++(0);d1++;return 0;
}

(二)赋值运算符重载函数

        赋值运算符重载是⼀个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象

1、赋值运算符的特点

        1. 赋值运算符重载是一个运算符重载,规定必须重载为成员函数赋值运算重载的参数建议写成 const 当前类类型引用,否则会传值传参会有拷贝

        2. 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值的场景。

        3. 没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数。

        4. 像Date这样的类,成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的赋值运算符重载会调用Stack的赋值运算符重载,也不需要我们显示实现MyQueue的赋值运算符重载。这里有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要。

        赋值运算符重载函数的两个学习出发点思维导图如下:

2、使用例

(1)Date类实现赋值重载
#define _CRT_SECURE_NO_WARNINGS 1
#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(const Date& d)//拷贝{cout << " Date(const Date& d)" << endl;_year = d._year;_month = d._month;_day = d._day;}// 传引用返回减少拷贝// d1 = d2;Date& operator=(const Date& d)//赋值运算符重载{// 不要用断言检查自己给自己赋值的情况,使用if判断就行if (this != &d){_year = d._year;_month = d._month;_day = d._day;}// d1 = d2表达式的返回对象应该为d1,也就是*thisreturn *this;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{Date d1(2024, 7, 5);Date d2(d1);Date d3(2024, 7, 6);d1 = d3;//赋值重载Date d4 = d1;//拷贝构造// 需要注意这里是拷贝构造,不是赋值重载// 请牢牢记住赋值重载完成两个已经存在的对象直接的拷贝赋值// 而拷贝构造用于一个对象拷贝初始化给另一个要创建的对象return 0;
}

(三)总结

        ① 拷贝构造用于一个已经存在的对象拷贝初始化另一个要创建的对象。而赋值运算符重载函数用于两个已经存在的对象直接复制拷贝。

        ② 需要考虑自己给自己赋值的情况(this不等于引用取地址的情况,也就是地址不相等的情况),才进行拷贝。

        ③ 在没有资源申请的情况下,使用默认生成的赋值运算符重载即可(浅拷贝),否则就要自己写,完成深拷贝。

        ④ 一般重载于类中的成员函数。

        ⑤ 需要析构 = 就写拷贝构造 + 赋值运算符重载

                构造一般需要自己写,自己传参定义初始化

                析构函数:在构造时有资源申请(如malloc或者fopen等),就需要显示写析构

                拷贝构造和赋值重载(行为是类似的),显示写了析构函数(内部管理资源),就需要显示实现深拷贝

六、日期类的实现

(一)代码实现

Date.h文件

#pragma once
#include<iostream>
#include<assert.h>
using namespace std;class Date
{//使用友元使得外部的函数也能访问类中private权限的成员变量friend ostream& operator<<(ostream& cout, Date& d);friend istream& operator>>(istream& cin, Date& d);public://打印年月日void printf()const;//获取天数int getDay(int year, int month)const//写在类里面是因为会频繁调用。{assert(0 < month && month < 13);static int arr_day[] = { -1, 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;elsereturn arr_day[month];}//构造函数Date(int year = 1900, int month = 1, int day = 1);//比较运算符重载//① 小于bool operator<(Date& d)const;//② 等于bool operator==(Date& d)const;//③ 小于等于(对小于和等于的重载函数进行复用)bool operator<=(Date& d)const;//④ 大于(对小于和等于的重载函数进行复用)bool operator>(Date& d)const;//⑤ 大于等于(对大于和等于的重载函数进行复用)bool operator>=(Date& d)const;//⑥ 不等于bool operator!=(Date& d)const;//运算符重载(对象与整形的运算)//① 重载+=Date& operator+=(int x);//② 重载+(复用+=)Date operator+(int x)const;//③ 重载-=Date& operator-=(int x);//④ 重载-(复用-=)Date operator-(int x)const;//⑤ 重载前置++Date& operator++();//⑥ 重载后置++Date operator++(int);//⑦ 重载前置--Date& operator--();//⑧ 重载后置--Date operator--(int);//运算符重载(对象与对象的运算)//① 重载-int operator-(Date& d)const;bool checkDate()const;private:int _year;int _month;int _day;
};

Date.cpp文件

#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"//打印年月日
void Date::printf()const
{cout << _year << "年" << _month << "月" << _day << "日" << endl;
}//检测日期合法性
bool Date::checkDate()const
{if (_month < 1 || _month > 12 || _day < 1 || _day > getDay(_year, _month))return false;elsereturn true;
}//构造函数
Date::Date(int year, int month, int day)
{_year = year;_month = month;_day = day;if (!checkDate())cout << "非法日期" << endl;
}//运算符重载函数
//① 小于
bool Date::operator<(Date& d)const
{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;elsereturn false;
}//② 等于
bool Date::operator==(Date& d)const
{return _year == d._year && _month == d._month && _day == d._day;
}//③ 小于等于(对小于和等于的重载函数进行复用)
bool Date::operator<=(Date& d)const
{return *this < d || *this == d;
}//④ 大于等于(对小于和等于的重载函数进行复用)
bool Date::operator>(Date& d)const
{return !(*this < d) && !(*this == d);
}//⑤ 大于等于(对大于和等于的重载函数进行复用)
bool Date::operator>=(Date& d)const
{return *this > d || *this == d;
}//⑥ 不等于
bool Date::operator!=(Date& d)const
{return !(*this == d);
}//运算符重载
//① 重载+=
Date& Date::operator+=(int x)
{if (x < 0){return *this -= x;}_day += x;while (_day > getDay(_year, _month)){_day -= getDay(_year, _month);_month += 1;if (_month == 13){_month = 1;_year += 1;}}return *this;
}
//② 重载+
Date Date::operator+(int x)const
{Date tmp(*this);tmp += x;return tmp;
}//③ 重载-=
Date& Date::operator-=(int x)
{if (x < 0){return *this += x;}_day -= x;while (_day < 0){_month--;_day += getDay(_year, _month);if (_month == 1){_month = 12;_year--;}}return *this;
}
//④ 重载-(复用-=)
Date Date::operator-(int x)const
{Date tmp(*this);tmp -= x;return tmp;
}//⑤ 重载前置++
Date& Date::operator++()
{return *this += 1;
}
//⑥ 重载后置++
Date Date::operator++(int)
{Date tmp(*this);*this += 1;return tmp;
}//⑦ 重载前置--
Date& Date::operator--()
{return *this -= 1;
}
//⑧ 重载后置--
Date Date::operator--(int)
{Date tmp(*this);*this -= 1;return tmp;
}//运算符重载(对象与对象的运算)
//① 重载-
int Date::operator-(Date& d)const
{Date max = *this;Date min = d;int flag = 1;if (max < min){max = d;min = *this;int flag = -1;}int count = 0;while (max != min){min++;count++;}return flag * count;
}//重载流输入
ostream& operator<<(ostream& cout, Date& d)//操作符左右两边的操作数要对得上
{cout << d._year << "年" << d._month << "月" << d._day << "日" << endl;return cout;
}//重载流输出
istream& operator>>(istream& cin, Date& d)
{while (1){cout << "请依次输入年月日:";cin >> d._year >> d._month >> d._day;if (!d.checkDate())cout << "非法日期,请重新输入!" << endl;elsebreak;}return cin;
}

test.cpp文件

#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"int main()
{Date d1(2024, 10, 10);Date d2(2020, 10, 10);/*bool a = d1 != d2;if (a == 1)cout << "不等" << endl;elsecout << "等" << endl;*/int re =  d1 - d2;//d1.printf();cout << re << endl;return 0;
}

(二)总结

        ① 所有类的比较运算符重载对【小于 ()和等于 (==) 运算符的重载函数】进行复用会很方便。(如 <= 对 < 和 == 进行复用,不用再单独写:【return *this < 参数对象 || *this == 参数对象;】,> 对 < 和 == 进行复用,不用再单独写:【return !(*this <= 参数对象);】,因为他们的逻辑之间存在联系。

        ② 重载运算符要保证语义和基本的运算符保持一致,在实现+的重载时,对象本身是不会变化的,所以在运算符重载函数中,首先使用自动生成的拷贝构造函数创建出一个与*this相同的临时对象tmp,对tmp进行运算,最后返回的也是tmp。(若先实现了+=,则可以直接对+=进行复用,tmp += day)

        ③ 实现+=后进行复用实现+更好,还是实现+后进行复用实现+=更好?+=和+的区别:+的实现有2个拷贝(拷贝构造临时对象和返回值时的拷贝构造),+=没有拷贝,所以实现+后进行复用实现+=有四个拷贝,实现+=后进行复用实现+只有2个拷贝,开销会更低。总结:实现+=后进行复用实现+更好。

        ④ 后置++的参数可以不加变量,直接写个类型:Date operator++(int){};因为这个形参没有意义,单纯是为了区分前置++和后置++;注意:若没有必要,尽量使用前置++,因为后置++有两个拷贝,开销大。

        ⑤ 日期减日期,可以让较小的那个日期++,一直重复到较大日期为止,++了多少次就有多少天。

        ⑥ 在类中重载流输出(双目运算符),cout是ostream的一个对象,所以参数是 ostream& out (cout传给out,out是cout的别名),这个重载存在争抢位置的情况,因为 cout << d1,而c++规定第一个操作数传给第一个参数,第二个参数传给第二个参数。隐藏的this就会被cout抢占,解决:在全局(类外)进行重载即可,但又不可避免地碰到成员函数私有化的问题。

        解决:

                • 提供get函数。

                • 使用友元。(在类中进行友元函数的声明:friend + 正常函数声明,在类外的函数就可以使用类中的成员变量)

        ⑦ 输出运算符从左往右结合,其他运算符从右往左结合,为了实现连续的输出,可以使用返回引用,类型是ostream& 。(全局定义,出了函数还在)

七、取地址运算符重载

(一)const成员函数

1、const成员函数特点

        • 将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后⾯。

        • const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。const 修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this。

2、使用例

(1)Date类
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// void Print(const Date* const this) constvoid Print() const{cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};
int main()
{// 这⾥非const对象也可以调用const成员函数是⼀种权限的缩小Date d1(2024, 7, 5);d1.Print();const Date d2(2024, 8, 5);d2.Print();return 0;
}

3、总结

        ① 当const修饰类类型对象的时候,修饰的是内容,而隐藏的this指针的const修饰的是指针的指向,所以当const修饰的类类型对象使用没有const修饰的成员函数时,属于权限的放大,这是不允许的。注意:只有const修饰内容的时候才存在权限放大或缩小的问题,修饰指针指向是没有的(隐藏的this指针)。这时候就需要const放在成员函数的参数列表后面进行权限的缩小,这个const修饰的是this指针指向的内容

        ② const修饰指向的内容和非const拷贝赋值才涉及权限放大和缩小问题。

        ③ 好处:const修饰成员函数,普通对象也能调用(权限的缩小是可以的),const修饰的对象调用(权限的平移)。缺点:被const修饰成员函数中的成员变量不能再修改了。

        ④ 理论上不对成员变量进行修改的,都可以加const进行修饰;定义在类外的函数不要加,因为const修饰的是类中的成员函数,不对类外的函数起作用;若成员函数的声明和定义是分离的,都要加const

(二)取地址运算符重载

1、取地址运算符重载的特点

        取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。除非一些很特殊的场景,比如我们不想让别⼈取到当前类对象的地址,就可以自己实现一份,胡乱返回一个地址。

 

2、使用例

(1)Date类
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Date
{
public:Date * operator&(){return this;//return nullptr;}const Date * operator&()const{return this;// return nullptr;}
private:int _year; // 年int _month; // ⽉int _day; // ⽇
};

3、总结

        默认生成的重载函数有两个版本,普通版本和const版本,以日期类为例子,普通版是Date*类型的指针,const版是 const Date*类型的指针。

        编译器自己生成的就够用,不需要自己实现


        以上内容仅供分享,若有错误,请多指正。

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

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

相关文章

scrapy的xpath在控制台可以匹配,但是到了代码无法匹配(无法匹配tbody标签)

问题 使用xpath-helper可以匹配到,然后scrapy却无法 然后写入html来看看 发现根本就没有tbody,太可恶了 解决 方法1 不使用tbody就可以 方法2 使用或运算符 | big_list response.xpath("//div[classChannelClasssNavContent]/table/tbody/tr[1]/td/table/tbody/t…

Android OpenGL天空盒

在我们的项目学习过程中&#xff0c;我们从一片漆黑的虚空开始构建。为了给这个世界增添一些色彩&#xff0c;我们加入了三个粒子喷泉&#xff0c;但即便如此&#xff0c;我们的世界依然大部分被黑暗和虚无所笼罩。这些喷泉仿佛悬浮在无尽的黑暗之中&#xff0c;没有边界&#…

玫瑰花HTML源码

HTML源码 <pre id"tiresult" style"font-size: 9px; background-color: #000000; font-weight: bold; padding: 4px 5px; --fs: 9px;"><b style"color:#000000">0010000100000111101110110111100010000100000100001010111111100110…

unity学习-全局光照(GI)

在全局光照&#xff08;Lighting&#xff09;界面有两个选项 Realtime Light&#xff08;实时光照&#xff09;&#xff1a;在项目中会提前计算好光照以及阴影的程序&#xff0c;当你需要调用实时全局光照的时候会将程序调用出来使用 Mixed Light&#xff08;烘焙光照&#x…

Nova-Admin:基于Vue3、Vite、TypeScript和NaiveUI的开源简洁灵活管理模板

嗨&#xff0c;大家好&#xff0c;我是小华同学&#xff0c;关注我们获得“最新、最全、最优质”开源项目和工作学习方法 Nova Admin是一个基于Vue3、Vite、TypeScript和NaiveUI的简洁灵活的管理模板。这个项目旨在为开发者提供一个现代化、易于定制的后台管理界面解决方案。无…

什么是3D模型?如何进行3D建模?应用领域有哪些?

3D模型是在计算机图形学中&#xff0c;为某个表面或物体在专用软件中创建的数字形象&#xff0c;它代表了一个物理实体在三维空间中的形态。以下是对3D模型的详细解释及实现方式的介绍&#xff1a; 一、3D模型的定义 概念&#xff1a;3D模型&#xff0c;即三维模型&#xff0…

springboot+vue的宠物医院管理系统(源码+lunwen)

基于vuespringboot的宠物医院管理系统&#xff0c;分为前台页面和后台管理端。 前台页面&#xff1a; 用户注册与登录&#xff1a;用户可以创建账户并登录系统&#xff0c;以便预约服务、查看个人信息等。宠物信息管理&#xff1a;用户可以添加、编辑和删除自己的宠物信息&am…

数字后端实现静态时序分析STA Timing Signoff之min period violation

今天给大家分享一个在高性能数字IC后端实现timing signoff阶段经常遇到的min period violation。大部分时候出现memory min period问题基本上都是需要返工重新生成memory的。这是非常致命的错误&#xff0c;希望大家在做静态时序分析时一定要查看min period violation。 什么是…

RabbitMQ 发布确认模式

RabbitMQ 发布确认模式 一、原理 RabbitMQ 的发布确认模式&#xff08;Publisher Confirms&#xff09;是一种机制&#xff0c;用于确保消息在被 RabbitMQ 服务器成功接收后&#xff0c;发布者能够获得确认。这一机制在高可用性和可靠性场景下尤为重要&#xff0c;能够有效防止…

数据结构——顺序表的基本操作

前言 介绍 &#x1f343;数据结构专区&#xff1a;数据结构 参考 该部分知识参考于《数据结构&#xff08;C语言版 第2版&#xff09;》24~28页 补充 此处的顺序表创建是课本中采用了定义方法为SqList Q来创建&#xff0c;并没有使用顺序表指针的方法&#xff0c;具体两个…

TCL中环开工率下滑,员工集体要求解约赔偿

“ 尽管中环的市占率有所提高&#xff0c;但是高开工率也带来了巨量硅片库存&#xff0c;严重拖累了公司业绩。 ” 转载&#xff1a;科技新知 原创 作者丨依蔓 编辑丨蕨影 因大幅下调开工率&#xff0c;光伏硅片龙头TCL中环疑似遭遇员工“离职潮”&#xff1f; 近日&…

[云] 创建 Docker 镜像,将其推送到 Amazon Elastic Container Registry (ECR),并对已部署的应用程序进行负载测试

在此作业中&#xff0c;您将学习如何使用 AWS Lambda 和 API Gateway 将机器学习模型部署为无服务器应用程序。您将创建 Docker 镜像&#xff0c;将其推送到 Amazon Elastic Container Registry (ECR)&#xff0c;并对已部署的应用程序进行负载测试。此外&#xff0c;您还将分析…

【KEIL那些事 4】CMSIS缺失!!!!导致不能编译!!!!软件自带芯片下载缓慢!!!!!!快速下载芯片包!!!!!

安装了keli发现emmm&#xff0c;CMSIS缺失&#xff01;&#xff01;&#xff01;&#xff01;不能编译&#xff0c;&#xff0c;&#xff0c;自带下载芯片缓慢&#xff0c;&#xff0c;&#xff0c;官网下载emmm&#xff0c;竟然不带动的&#xff01;&#xff01;&#xff01;&…

数据库集群

主从复制 作用&#xff1a; 1.做数据的热备&#xff0c;作为后备数据库&#xff0c;主数据库服务器故障后&#xff0c;可切换到从数据库继续工作&#xff0c;避免数据丢失。 2.架构的扩展。业务量越来越大&#xff0c;I/O访问频率过高&#xff0c;单机无法满足&#xff0c;此…

基于node.js宜家宜业物业管理系统【附源码】

基于node.js宜家宜业物业管理系统 效果如下&#xff1a; 系统首页界面 业主登录界面 停车位页面 小区公告页面 管理员登录界面 管理员功能界面 物业管理员管理界面 缴费信息管理界面 物业管理员功能界面 研究背景 近年来互联网技术飞速发展&#xff0c;给人们的生活带来了极…

《云计算网络技术与应用》实训6-1:配置KVM虚拟机使用NAT网络

任务1、计算节点基础环境准备 1. 使用VMware安装CentOS 7虚拟机&#xff0c;安装时记得开启CPU虚拟化&#xff0c;命名为“KVMC6”。 2. &#xff08;网卡配置和之前的一样&#xff0c;都用100网段&#xff09;网关设置为192.168.100.1&#xff0c;地址段为192.168.100.10-25…

excel将文本型数字转变为数值型数字

问题导入&#xff1a;复制数字到excel表格中&#xff0c;但是表格中数字显示为文本&#xff0c;且无法通过常规方法转变为可进行四则运算的数字。例如&#xff1a;在i3单元格中输入常规的转换方法仍然报错。在j3单元格中输入ISTEXT(H3)显示h3单元格确实为文本。 解决办法&#…

Chrome DevTools 三: Performance 性能面板扩展—— 性能优化

Performance 性能 &#xff08;一&#xff09;性能指标 首次内容绘制 (First Contentful Paint&#xff0c;FCP)&#xff1a; 任意内容在页面上完成渲染的时间 最大内容绘制 (Largest Contentful Paint&#xff0c;LCP)&#xff1a; 最大内容在页面上完成渲染的时间 第一字节…

【经管】比特币与以太坊历史价格数据集(2014.1-2024.5)

一、数据介绍 数据名称&#xff1a;比特币与以太坊历史价格数据集 频率&#xff1a;逐日 时间范围&#xff1a; BTC&#xff1a;2014/9/18-2024/5/1 ETH&#xff1a;2017/11/10-2024/5/1 数据格式&#xff1a;面板数据 二、指标说明 共计7个指标&#xff1a;Date、Open…

天润融通大模型文本机器人,让客服迈入“无人化”的第一步

明明很着急&#xff0c;但客服机器人总是答非所问&#xff1f; 相信很多人都经历过这样的尴尬时刻&#xff0c;问题的关键&#xff0c;是传统文本机器人还在通过关键词和基础语义分析回答问题。 △传统机器人处理问题流程示意 要知道在客户咨询与服务过程中&#xff0c;用户的…