目录
1.默认(缺省)成员函数:析构函数
当带有static时,析构和构造函数的创建/销毁顺序是?
在成员函数中调用delete this会出现什么问题?对象还可以使用吗?
如果在类的析构函数中调用delete this,会发生什么?
2.拷贝构造函数
2.1 内置类型和自定义类型
3.运算符重载
前置++和后置++重载
3.3.友元
4.赋值运算符重载:=
5.const成员
6.取地址及const取地址操作符重载
1.默认(缺省)成员函数:析构函数
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作由编译器完成
对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
析构函数特性
1. 析构函数名是在类名前加上字符 ~
2. 无参数无返回值类型
3.一个类只能有一个析构函数。若未定义,系统会自动生成默认的析构函数。析构函数不能重载
4.对象生命周期结束,C++自动调用析构函数
5.手动开辟的例如Stack中malloc,fopen等需要析构函数,Date日期类不需要析构函数
6.编译器生成的默认析构函数,对内置类型不做处理,对自定类型成员调用它的析构函数
7.析构函数可以定义在类外
~Stack()
{
free(...);
}
析构函数顺序
先定义的先构造,后定义后构造; 先定义的后析构,后定义的先析构(栈和栈帧里面的对象都要符合后进先出)
class A
{
public:
A(int a = 0)
{
_a = a;
cout << "A(int a = 0)->" <<_a<< endl;
}
~A()
{
cout << "~A()->" <<_a<
当带有static时,析构和构造函数的创建/销毁顺序是?
两个局部静态对象,一个全局对象
class A
{
public:
A(int a = 0)
{
_a = a;
cout << "A(int a = 0)->" <<_a<< endl;
}
~A()
{
cout << "~A()->" <<_a<
全局变量最先被初始化(main函数之前初始化,全局和静态都在静态区),局部静态特点是第一次运行后初始化
析构是aa2和aa1中最先析构,原因在于剩余三个生命周期在程序结束后才销毁,main函数栈帧结束清理在栈帧中的aa2和aa1;main函数结束,再调用全局和静态(符合先定义后析构)
如果调用两次f()函数,结果又是如何?
静态变量在第一次执行后初始化,第一次函数调用结束,aa4和aa5不会销毁
在成员函数中调用delete this会出现什么问题?对象还可以使用吗?
在类对象的内存空间中,只有数据成员和虚函数表指针,并不包含代码内容,类的成员函数单独放 在代码段中。在调用成员函数时,隐含传递一个this指针,让成员函数知道当前是哪个对象在调用它。 当调用delete this时,类对象的内存空间被释放。在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,就会出现不可预期的问题。
为什么是不可预期的问题?
delete this之后不是释放了类对象的内存空间了么,那么这段内存应该已经还给系统,不再属于这个进程。照这个逻辑来看,应该发生野指针之类的令崩溃的问题。这个问题牵涉到操作系统的内存管理策略。delete this释放了类对象的内存空间,但是内存空间却并不是马上被回收到系统中,可能是缓冲或者其他什么原因,导致这段内存空间暂时并没有被系统收回。此时这段内存是可以访问的,你可以加上100,加上200,但是其中的值却是不确定的。当你获取数据成员,可能得到的是未初始化的随机数;访问虚函数表,指针无效的可能性非常高,造成系统崩溃。
如果在类的析构函数中调用delete this,会发生什么?
会导致堆栈溢出。delete的本质是“为将被释放的内存调用一个或多个析构函数,然后,释放内存”。显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。
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;
}
private:
DataType* _array;
int _capacity;
int _size;
};
int main()
{
Stack st1;
Stack st2(st1);
return 0;
}
解决方法:深拷贝,具体在string模拟实现中
总结:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,拷贝构造函数一定要写,否则就是浅拷贝
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)<
在类中,提示运算符参数太多,原因在于: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,const Date& d)
{
//支持年月日输出
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;
}
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;
3.3.友元
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
缺点:友元破坏了封装
friend ostream& operator<<(ostream& out, const Date& d);
友元函数不能用const修饰
友元类:
1.友元关系是单向的,不具有交换性
2.友元关系不能传递(A=B B=C A!=C)
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即可;或者转换为私有,无法取地址