目录
一、类的默认成员函数
二、构造函数
(一)构造函数的特点
(二)使用例
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*类型的指针。
编译器自己生成的就够用,不需要自己实现。
以上内容仅供分享,若有错误,请多指正。