目录
1.默认(缺省)成员函数:析构函数
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作由编译器完成
对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
析构函数特性
1. 析构函数名是在类名前加上字符 ~
2. 无参数无返回值类型
3.一个类只能有一个析构函数。若未定义,系统会自动生成默认的析构函数。析构函数不能重载
4.对象生命周期结束,C++自动调用析构函数
5.手动开辟的例如Stack中malloc,fopen等需要析构函数,Date日期类不需要析构函数
6.编译器生成的默认析构函数,对内置类型不做处理,对自定类型成员调用它的析构函数
- ~Stack()
- {
- free(_array);
- }
析构函数顺序
先定义的先构造,后定义后构造; 先定义的后析构,后定义的先析构(栈和栈帧里面的对象都要符合后进先出)
- class A
- {
- public:
- A(int a = 0)
- {
- _a = a;
- cout << "A(int a = 0)->" <<_a<< endl;
- }
-
- ~A()
- {
- cout << "~A()->" <<_a<
- }
- private:
- int _a;
- };
-
-
- void f()
- {
- A aa1(1);
- A aa2(2);
- }
-
- int main()
- {
- f();
- return 0;
- }
当带有static时,析构和构造函数的创建/销毁顺序是?
两个局部静态对象,一个全局对象
- class A
- {
- public:
- A(int a = 0)
- {
- _a = a;
- cout << "A(int a = 0)->" <<_a<< endl;
- }
-
- ~A()
- {
- cout << "~A()->" <<_a<
- }
- private:
- int _a;
- };
-
- A aa3(3);
-
- void f()
- {
- static A aa4(4);
- A aa1(1);
- A aa2(2);
- static A aa5(5);
- }
-
- int main()
- {
- f();
- return 0;
- }
全局变量最先被初始化(main函数之前初始化,全局和静态都在静态区),局部静态特点是第一次运行后初始化
析构是aa2和aa1中最先析构,原因在于剩余三个生命周期在程序结束后才销毁,main函数栈帧结束清理在栈帧中的aa2和aa1;main函数结束,再调用全局和静态(符合先定义后析构)
如果调用两次f()函数,结果又是如何?
静态变量在第一次执行后初始化,第一次函数调用结束,aa4和aa5不会销毁
2.拷贝构造函数
有时候我们需要对一个对象进行拷贝,就会调用拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
- int main()
- {
- Date d1(2022, 7, 31);
- Date d2(d1);//拷贝构造两个写法
- Date d3 = d1;
- return 0;
- }
拷贝构造函数也是特殊的成员函数,其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
3.拷贝构造函数:函数名和类名相同,没有返回值;同类型对象构造
错误的写法
- Date(Date d)
- {
- _year = d._year;
- _month = d._month;
- _day = d._day;
- }
传值传参:一份临时拷贝,开辟新空间
传引用传参:别名,原空间
d1实例化调用的是构造函数;用d1初始化d,对象实例化要调用拷贝构造函数,(同类型对象拷贝初始化)
如果不是引用调用,同类型调用拷贝构造要传参,传参又是一个拷贝构造,层层传值引发对象的拷贝的递归调用
正确的写法
- Date(const Date& d)
- {
- _year = d._year;
- _month = d._month;
- _day = d._day;
- }
同时建议拷贝构造函数加const (权限缩小),防止以下情况发生(逻辑写反,原数据被修改,并不是修改原数据而是进行拷贝)
- Date(const Date& d)
- {
- d._year = _year ;
- d._month = _month;
- d._day = _day;
- }
2.1 内置类型和自定义类型
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序(memcpy)完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调 用其拷贝构造函数完成拷贝的(日期类不需要写拷贝构造,默认生成够用)。
以下情况默认生成的拷贝构造函数无法使用,必须自己实现:
深浅拷贝问题
拷贝构造st2(st1)程序崩溃,原因在于两个指针指向了同一块malloc开辟的空间,结束时free释放了两次同一片空间,原因就在于浅拷贝造成的(指针地址拷贝)
解决方法:深拷贝
- typedef int DataType;
- class Stack
- {
- public:
- Stack(int capacity=4)
- {
- cout << "Stack(int capacity = 4)" << endl;
- _array = (DataType*)malloc(sizeof(DataType) * capacity);
- if (NULL == _array)
- {
- perror("malloc申请空间失败!!!");
- return;
- }
-
- _size = 0;
- _capacity = capacity;
- }
-
- void Push(DataType data)
- {
- // CheckCapacity();
- _array[_size] = data;
- _size++;
- }
- ~Stack()
- {
- free(_array);
- }
- private:
- DataType* _array;
- int _capacity;
- int _size;
- };
-
- int main()
- {
- Stack st1;
- Stack st2(st1);
- return 0;
- }
总结:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,拷贝构造函数一定要写,否则就是浅拷贝
3.运算符重载
一个类可以重载哪些运算符取,决于运算符对类有无意义
内置类型可以使用运算符运算,但当自定义类型,例如日期类想完成日期-日期、比较日期、日期加天数等操作,可以使用运算符重载
- //日期类构造函数需要写;析构和拷贝构造默认生成够用
- class Date
- {
- public:
- Date(int year = 1, int month = 1, int day = 1)
- {
- _year = year;
- _month = month;
- _day = day;
- if (!CheckDate())
- {
- print();
- cout << "日期非法" << endl;
- }
- }
- bool CheckDate()
- {
- if (_year >= 1
- &&_month >0 && _month <13
- && _day >0 && _day <=GetMonthDay(_year,_month))
- {
- return true;
- }
- else
- {
- return false;
- }
- }
- private:
- int _year = 1;
- int _month = 1;
- int _day = 1;
- };
运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数类型:返回值类型 operator需要重载的运算符符号(参数列表)
注意:返回值类型由运算符决定;参数列表由操作数决定(d1==d2,两个参数,并规定第一个参数为左操作数,第二个参数为右操作数)
比较运算符重载
技巧:任何一个类,写比较运算符重载,只需要写大于和等于或者小于和等于,剩下的比较运算符重载复用即可
在类外,成员变量访问受到限制,要么使用友元,要么取消private,但是都会破坏封装
- bool operator==(const Date& d1,const Date& d2)
- {
- return d1._year == d2._year
- && d1._month == d2._month
- && d1._day == d2._day;
- }
-
- bool operator!=(const Date& d)
- {
- return !(*this == d);
- }
-
- bool 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;
- }
- }
-
- bool operator>=(const Date& d)
- {
- return (*this > d) || (*this == d);
- }
-
- bool operator<(const Date& d)
- {
- return !(*this >= d);
- }
-
- bool operator<=(const Date& d)
- {
- return !(*this > d);
- }
-
- int main()
- {
- Date d1(2022,5,20);
- Date d2(2022,8,1);
- cout<<(d1 == d2)<
//<<优先级高 -
- return 0;
- }
在类中,提示运算符参数太多,原因在于:this指针
- operator==(d1, d2);//全局时,编译器其实是处理成这样
- d1.operator==(&d1, d2);//类中时,编译器处理成这样
- d1.operator==(d2);//类中实际情况
- //但是this指针不能显示传参,不能显示声明参数,但是类中可以使用
所以最好写成:成员函数
- bool operator==(const Date& d)
- {
- return _year == d._year
- && _month == d._month
- && _day == d._day;
- }
可以在类中只声明,定义放到另一个文件中(防止太多内联造成代码膨胀,除非频繁调用)
+ 和 +=(天数)
天满了进月,月满进年
复用的情况下,先写+=更好(+=没有看对象构造),同时类不关心上下顺序(作为一个整体,上下都搜索)
- int GetMonthDay(int year,int month)//涉及闰年
- {
- static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };//频繁调用用static
- if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
- {
- return 29;
- }
- else
- {
- return days[month];//拿到每个月天数
- }
- }
-
- Date& operator+=(int day)
- {
- if (day < 0)//+= -100
- {
- return *this -= -day;
- }
- _day += day;
- while (_day > GetMonthDay(_year, _month))//如果大于这个月天数非法,进位
- {
- _day -= GetMonthDay(_year, _month);//
- ++_month;
- if (_month == 13)
- {
- _month = 1;
- ++_year;
- }
- }
- return *this;//this指向当前对象的指针,*this就是当前对象
- }
-
- Date operator+(int day)
- {
- Date ret(*this);
- ret += day;
- return ret;
- }
-
- //不复用的+
- Date operator+(int day)
- {
- Date ret = (*this);
- ret._day += day;
- while (ret._day > GetMonthDay(ret._year, ret._month))//如果大于这个月天数非法,进位
- {
- ret._day -= GetMonthDay(ret._year, ret._month);//
- ++ret._month;
- if (ret._month == 13)
- {
- ret._month = 1;
- ++ret._year;
- }
- }
- return ret;
- }
前置++和后置++重载
前置++返回值为++后的值,后置++返回值为++前的值
运算符重载为了区分前后置++,C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
- Date operator++(int)//后置++
- {
- Date ret = *this;
- *this += 1;
- return ret;
- }
-
- Date& operator++()
- {
- *this += 1;
- return *this;
- }
前置--和后置--重载
- Date& operator--()//日期-天数.前置--
- {
- return (*this -= 1);
- }
-
- Date operator--(int)
- {
- Date tmp(*this);
- *this -= 1;
- return tmp;
- }
日期-天数;日期-=天数;
- Date operator-(int day)
- {
- Date ret = *this;
- ret -= day;
- return ret;
- }
-
-
- Date& operator-=(int day)
- {
- if (day < 0)//-= -100
- {
- return *this += -day;
- }
- _day -= day;
- while (_day <= 0)
- {
- --_month;
- if (_month == 0)
- {
- _month = 12;
- _year -= 1;
- }
- _day += GetMonthDay(_year, _month);
- }
- return *this;
- }
日期-日期
不复用思路:算出当前年月离当年1.1号差多少天,再算出年之间差距(闰年366天)
复用思路:累加
- int operator-(const Date& d)//日期-日期
- {
- int flag = 1;
- Date max = *this;//默认第一个大第二个小
- Date min = d;
- if (*this < d)
- {
- max = d;
- min = *this;
- flag = -1;
- }
- //小的不断++,加到跟大的相等为止
- int n = 0;
- while (min != max)
- {
- ++min;
- ++n;
- }
- return n*flag;
- }
<<流插入运算符重载
cout能自动识别类型在于cout写了运算符重载<<,依靠函数重载来实现自动识别类型
当我们想写以下函数时确保错,由于cout是ostream对象的成员,处理内置类型,却不处理自定义类型,我们可以重载<<来实现日期类的流插入<<
- cout << (d1 + 100);
- cout << d1;
原cout<<中一个是隐含的cout,一个是int/float等;在Date中一个是隐藏的Date,另一个传cout即可
错误的返回值写法:cout << d1;
(报错:没有找到接收Date类型的右操作数的运算符)
当使用原生的d1.operator<<(cout)却可以调的到,原因在于运算符有多个操作符,而第一个操作数为d1,第二个操作数为cout,写法其实是d1 << cout
- ostream& operator<<(ostream& out)
- {
- //支持年月日输出
- out << d._year << "年" << d._month << "月" << d._day <<"日" << endl;
- return out;
- }
解决方法:不能是成员函数(日期类对象抢占了第一个操作数),写在类外(使用友元)
返回值使用ostream做返回值的对象,用来支持连续cout等操作
>>流提取运算符重载
为什么scanf要取地址而>>不用,原因在于没有引用,cin转换成调用operator流提取,把cin和d1引用传入(默认输入多个值以空格或者换行去间隔)
- istream& operator>>(istream& in, Date& d)
- {
- in >> d._year >> d._month >> d._day;
- if (!d.CheckDate())
- cout << "日期非法" << endl;
- return in;
- }
3.3.友元
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
缺点:友元破坏了封装
friend ostream& operator<<(ostream& out, const Date& d);
运算符重载总结:
.*(matlab计算矩阵型号匹配) ::(域作用限定符) sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
2.内置类型的运算符,其含义不能改变
判断日期是星期几
- Date d1(1840,11,1);
- cin >> d1;
- Date start(1, 1, 1);
- int n = d1 - start;
- int weekDay = 5;//默认从0开始,1.1.1星期1,0相当于周天
- weekDay += n;
- cout << "周" << weekDay % 7 + 1 << endl;
4.赋值运算符重载:=
日期类初始化时d2(d1)为拷贝构造,当两个类已经定义好时,把值赋值给另一个类,就叫赋值运算符重载
参数类型:const T&,传递引用可以提高传参效率
日期类的赋值运算符重载:返回值是Date是为了支持连续赋值,&可以让赋值一次拷贝构造都没发生(并不是静态和全局才能用引用返回,只要对象除了作用域还在即可);加引用减少拷贝构造,同时加const缩小权限;if判断是防止自己给自己赋值的无意义行为(地址比较)
总结:能用就用引用传参和引用返回
- Date& operator=(const Date& d)//d1 = d3;
- {
- if (this != &d)
- {
- _year = d._year;
- _month = d._month;
- _day = d._day;
- }
-
- return *this;
- }
赋值运算符只能重载成类的成员函数不能重载成全局函数(写在类外,类中生成默认赋值重载,造成重载冲突)
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值
类似Stack需要自己写赋值运算符重载来避免浅拷贝(复制拷贝一样的问题)
其特性和复制拷贝一样
5.const成员
在对象加了const,调用print会遇到问题,原因在于隐藏的this指针权限放大,从const Date转换为Date*const this(const修饰的是this指针本身不能被改变,指针的内容可以改变)
&d1 是 Date*
&d2 是const Date*(*之前意味着指向的内容不能被修改,传给Date*是权限放大)
d1 < d2编译通过 d2 < d1编译报错,原因和上面情况一样
只有指针和引用涉及权限缩小放大问题
解决方法:让this指针变成const修饰即可,由于this指针隐含不能轻易修改,需要加在后面
变成const Date* const this
- void print() const
- {
- cout << _year << "/" << _month << "/" << _day<
- }
6.取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。只有特殊情况,才需要重载,比如想让别人获取到指定的内容
const对象取地址调用const A*,A对象调用A*
普通对象和const对象要分开处理,就需要写两个;如果不需要例如只需要打印,写一个即可
- class A
- {
- public:
- A* operator&()
- {
- return this;
- }
-
- const A* operator&()const
- {
- return this;
- }
- private:
- int _year; // 年
- int _month; // 月
- int _day; // 日
- };
-
- int main()
- {
- A a;
- const A b;
- &a;
- &b;
- return 0;
- }
特殊场景使用:不想让别人取到这个类型对象的地址,返回nullptr即可;或者转换为私有,无法取地址
-
相关阅读:
搭建 Makefile+OpenOCD+CMSIS-DAP+Vscode arm-none-eabi-gcc 工程模板
Linux命令:tr和xargs
含文档+PPT+源码等]基于ssm maven健身房俱乐部管理系统[包运行成功]Java毕业设计SSM项目源码
GraalVM java17 Windows打包
js前端条件语句优化
闭区间上连续函数的一些定理
java计算机毕业设计基于安卓Android的教务的校内人员疫情排查系统设计与实现APP
UDP和TCP两大协议的区别,让你快速高效掌握
afl-cov计算代码覆盖率
汇编-变量
-
原文地址:https://blog.csdn.net/weixin_63543274/article/details/126082126