文章目录
构造函数
对于上述日期类来说,每次创建类对象的时候都需要调用初始化函数,那能不能再创建的时候就初始化完毕了呢。
构造函数是一个特殊的成员函数,它有以下几个特点:
- 函数名与类名相同
- 无返回值
- 对象实例化时,自动调用
- 支持重载函数
这里要注意构造函数与初始化函数功能类似,并不是开辟空间的作用。**注意:**此时我们写的只是一个构造函数,并不是默认构造函数。默认构造函数已补充
如上图,我们在d1这个对象实例化的时候自动调用的构造函数,实现了赋值操作。
那如果我们不自己定义呢?
如上图我们发现成员变量为随机值。
下面的代码运行结果是什么呢?
运行结果:
我们可以看到,我们的年份那些变量并没有初始化,但是我们的 _t 却被调用了。
这里要说一下,C++的类型分为两种:
- 内置类型:int,double等等
- 自定义类型:class/struct等等
C++标准定义了当我们没有显式定义构造函数的时候,对于内置类型是否初始化时是没有规定的,所以这完全依靠编译器决定,而自定义类型必须初始化,如上面的 _t,它会调用 _t 的构造函数来初始化_t。当然,如果我们 Time类中并没有初始化 _a变量,那么这个内置类型也不会初始化的。
如上图:C++11中添加了一个补丁,那就是在写成员变量的时候,可以使用缺省参数,这样就可以让成员变量初始化了。
更新(6_27) 默认构造函数
注意: 无参数、全缺省和没有显示定义编译器自动生成的构造函数才是默认构造函数。 也就是说,这三个才是默认调用的。
析构函数
默认生成的清理函数(主要是清理资源)
特点:
- 函数名就是类名,只不过名字前面加了个 ‘~’ 符号(按位取反符号)
- 无参数
- 不支持函数重载
- 对象声明周期结束后,会默认调用析构函数
当我们运行到return 0的时候,按下f11直接进入到析构函数。
拷贝构造函数
特性:
- 拷贝构造函数是构造函数的一个函数重载
- 拷贝构造函数只有一个参数,那必须是类的类型的引用,如果不是的话会无限递归。
- 拷贝构造函数没有显式定义的时候,会生成一个默认的拷贝构造函数,不过这里的只能进行值拷贝 - C++也叫浅拷贝。
先来谈一谈第二点为什么需要传类的引用:
如上图:语法规定:当自定义类型传值传参的时候,会默认调用拷贝构造函数,如果我们的拷贝构造函数参数是 Data 类型,那么还会继续调用拷贝构造函数,解决方法就是用类的类型引用或者参数用指针接受,但是用指针接受就不是拷贝构造函数了。
当我们显式定义了拷贝构造函数之后,我们创建d2对象之后,会默认调用我们的拷贝构造函数。那如果我们不写呢?
我们发现,即使我们不自己定义,编译器默认生成的也可以,那我们就不需要自己写了吗?来看一下我们的栈:
- 可以看到,当我们没有写拷贝构造函数的时候,编译器默认生成的为浅拷贝,它是直接把st1的地址拷贝给st2,所以当我们运行结束后调用析构函数就调用了两次,不过这里是先调用的st2的析构函数(后面再说),所以当我们的st1调用析构的时候,此时我们的st1的_a和st2的_a是同一快空间,我们再free一遍肯定要报错的,free了一片已经free得空间,就相当于下面这种情况[doeg]:
所以当我们有想 stack的 _a,这种资源类型时(动态开辟等等),我们就必须自己写一个拷贝构造函数了。(深拷贝)
深拷贝实现:
//这里加const的原因就是,防止写错,例如把类里面的值拷贝到st里面(写反了)
Stack(const Stack& st)
{
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == nullptr)
{
printf("malloc fail!\n");
exit(-1);
}
memcpy(_a,st._a,sizeof(int)*(st._size));
_size = st._size;
_capacity = st._capacity;
}
构造和析构的执行顺序
构造函数的执行顺序就是你先写谁先执行。
析构函数的执行顺序则是:先局部你后写的反而先析构,然后再是全局的,全局也是后写的先析构
析构顺序:
注意:析构的顺序,当局部有静态类的时候,它的生命周期是到程序结束的时候才结束,所以即使他存在局部域,也是在程序结束的时候才调用析构的。
运算符重载
如果现在有一个日期类,要你比较两个日期的大小,你会怎么做呢?
C++为了增强代码的可读性引入了一个新的关键字 -> operator 来对类的比较进行重载。
使用方法:
如上图: operator+运算符构成函数名,同时参数参返回值依据自己决定,参数类型由运算符的操作数决定,比如 + 的操作数有两个,但是因为我们的成员函数隐藏一个this指针,所以我们只需要显式的写一个就行了。
注意:,重载操作符参数必须有一个类类型。同时不能连接其他符号来创造操作符比如:operator@。
上图也可以这么写:
赋值运算符重载
参数要加const修饰 - 原因
格式:
o
p
e
r
a
t
o
r
=
operator=
operator= ,同时注意,赋值运算符重载只能用于两个已经示例化的对象,跟拷贝构造还是不一样的。
思考几个问题:
- 为什么返回类型要加引用?什么时候加?
先来看看不加引用的情况:
- 如上图,我们从右向左赋值,一开始 d3的值 赋值给 d2,没问题,然后这个值会给一个临时变量,这个临时变量作为结果继续赋值给d1。这就是为什么我们返回值要加引用的元素。原因同时注意: 临时变量具有常属性,这也就意味着我们的参数部分要用const修饰
进行连续赋值的话,我们每一次都是把 = 操作符的左操作数作为返回值,如果不加引用的话,会频繁调用析构函数。
补充:赋值运算符只能作为类的成员函数重载。
如果我们没有显示定义赋值重载函数,如果我们把赋值运算符重载为全局的函数,那么它会与编译器默认生成的赋值运算符产生冲突。
const修饰
好,我们接着来看下面的函数:(添加引用后)
以下图为例:
可以看到,此时我们d1和预期结果不一样啊。这时为什么呢?
我们可以看到,我们局部变量的声明周期在函数结束的时候就结束了,所以我们结束的时候会默认调用析构函数,如果返回的是引用的话,就会把析构后的结果返回,而且由于空间被收回,我们并不确定,那片空间什么时候还会再被使用,所以此时是不安全的。
当我们把引用去掉时:
- 如图我们返回值是类型的时候,会创建一个临时变量,并把d给我们的临时变量,上文讲过,由于我们拷贝构造函数必须是类的引用,那么此时如果一个临时变量传过来,他是不可以改变的,而我们参数是可以改变的,这就是权限扩大,编译都过不了,所以我们这里的拷贝构造函数的参数最好加一个const修饰。
所以即使我们这里的d析构了(这里还会调用赋值重载函数,所以我们赋值重载函数最好还是用const修饰),我们的d1却还是有临时变量把值传了过来。
这也是为什么我们返回值是类的类型却成功的原因。
总结一下:
我们返回值什么时候用类型?什么时候用引用呢?
- 函数结束后,生命周期结束时,传值返回
- 函数结束后,生命周期不结束,引用返回
两个const修饰:
- 拷贝构造函数和赋值重载函数,由于我们传值返回时,会产生临时变量,会调用默认拷贝构造函数,而由于默认拷贝构造函数定义的参数必须为类型的引用,所以我们这里要加const修饰。而我们返回之后两个自定类型赋值(包含临时变量),会调用赋值重载函数,也会传入临时变量,所以赋值重载函数也需要用const修饰。
日期类的实现
再实现日期类的时候,我们需要实现日期比大小等问题,所需函数如下:
bool operator<(const Data& d); 日期 < 日期
bool operator<=(const Data& d); 日期 <= 日期
bool operator>(const Data& d); 日期 > 日期
bool operator>=(const Data& d); 日期 >= 日期
bool operator==(const Data& d); 日期 == 日期
bool operator!=(const Data& d); 日期 != 日期
那这么多,我们都要一个一个去实现吗?肯定不是,跟着我的顺序,会面会很简单。
- 判断两个日期是否相等
代码:
bool Data::operator==(const Data& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
- 判断小于
bool Data::operator<(const Data& d)
{
if (_year < d._year)
{
return true;
}
else if (_year == d._year)
{
if (_month < d._month)
return true;
else if (_month == d._month)
{
return _day < d._day;
}
}
return false;
}
OK,上面的都可以不用再单独写了。
这里简单列举一下:
- 日期 <= 日期 = = == == 日期 < 日期 或 日期 == 日期
- 日期 > 日期 = = == == 取反(日期 <= 日期)
- …
bool Data::operator<=(const Data& d)
{
return *this < d || *this == d;
}
bool Data::operator>(const Data& d)
{
return !(*this <= d);
}
bool Data::operator>=(const Data& d)
{
return !(*this < d);
}
bool Data::operator!=(const Data& d)
{
return !(*this == d);
}
有了上面的基础后我们就可以来练习一下日期类的实现的。
主要功能介绍:
- 日期 += 天数
- 日期 + 天数
- 日期 - =天数
- 日期 - 天数
- 前置++日期
- 后置++日期
- 日期 - 日期 (相差多少天)
- 日期 +=天数
如何得出一个日期第50天后或者第50天之前的准确日期呢。
看,我们把原来的天数加上,再不断地减去当月的天数,当我们当前月份的天数 > 此时的天数的时候,那么此时就不需要再往下个月去走了,表示我们需要的日期就在当前月份。
同时我们还需要一个获取当前月份天数的函数,这个函数需要特殊判断是否是闰年,然后返回对应月份的天数,由于我们需要频繁调用这个函数,而且代码量也不是很多,我们直接把他写为内联函数,**注意:**内联函数的声明和定义必须写一块。
代码:
inline int GetMonthday(int year, int month)
{
int MonthArray[] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
if (((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) && month == 2)
return 29;
return MonthArray[month];
}
代码实现:
Data& Data::operator+=(int day)
{
_day += day; //获取总天数
while (_day > GetMonthday(_year, _month))
{
_day -= GetMonthday(_year, _month);
//要注意当前月份可能为12月
if (_month == 12)
{
_year++;
_month = 1;
}
else
_month++;
}
return *this;
}
- 日期 + 天数
类似 a + 2,我们是需要返回相加后的天数的。
Data Data::operator+(int day)
{
Data tmp = *this;
tmp += day;
return tmp; //返回局部变量用传值返回
}
这里我们最好是先写 += 的函数,然后再写 + 的函数,如果倒过来的话,我们
日期 + 天数的函数需要开辟一个变量,日期 += 天数还需要调用 日期 + 天数的函数,这样拷贝构造函数就会变多,效率也就变低了。同样下面我们也先写 -=天数的函数。
- 日期 -= 天数 (50 天之前):
我们拿上一个月的天数来补这个负数,什么时候截止呢?当这个天数 <=0的事后,我们都要补,如果等于0了,我们还要补一个满月,这不就相当于月底最后一天吗。
代码实现:
Data& Data::operator-=(int day)
{
_day -= day;
while (_day <= 0)
{
//借的是上一个月的
if (_month == 1)
{
_year -= 1;
_month = 12;
}
else
_month--;
_day += GetMonthday(_year,_month);
}
return *this;
}
- 日期 - 天数
Data Data::operator-(int day)
{
Data tmp = *this;
tmp -= day;
return tmp;
}
这里再说一下,我们现在做的是自定义类型的 加减操作,所以我们都需要用operator关键字,来构造运算符重载函数。还记得我们C++入门学的函数重载吗?那么由于前置和后置++我们都是这样的形式:
operator++()
那怎么区分呢?看如下:
//++d1
Data& operator++();
//d1++
Data operator++(int);
我们看到后置++,我们多了个参数,来和前置++构成函数重载。
- 日期后置++
创建个变量保存++之前的日期 tmp,然后++日期,返回 tmp,这样就能返回++之前的日期了,同时我们返回的是临时变量,所以我们用传值返回
代码:
Data Data::operator++(int)
{
Data tmp = *this;
*this += 1;
return tmp;
}
- 日期前置++
//++d1
Data& Data::operator++()
{
*this += 1;
return *this;
}
const成员函数
我们把const修饰的成员函数叫做const成员函数。这个可以用来做什么呢?
// this - Data* const this
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
如上图,这时我们写在日期类里面的成员函数。
我们发现,当我们初始化一个用const修饰的Data对象的时候,我们写的Prin()函数竟然不起作用了,这时为什么呢?
如上图,我们需要给this指针添加一个const修饰即可,语法规定是添加在外头的。同时加了const之后,无论有无const修饰的日期对象我们都可以正确的调用Print()函数。
使用规则:当我们权限被放大的时候不能使用const,当我们权限所缩小的时候,可以使用。
取地址和const取地址操作符重载
这个作为了解一下。
//取地址重载函数和const修饰的取地址重载函数 (默认成员函数)
Data* operator&()
{
cout <<"Data* operator&()" << endl;
return this;
}
// Data* const
const Data* operator&() const //加const的目的是为了构成重载
{
cout << "const Data* operator&() const" << endl;
return this;
}
什么时候可以用呢,不如你想要取地址的时候返回一个空指针?
友元函数
- 流插入和流提取操作符重载
直接看一下代码:
ostream& Data::operator<<(ostream& out)
{
out << _year << "年" << _month << "月" << _day << "日" << endl;
return out;
}
当我们把流插入写为成员函数的时候,虽然可以运行,但这就会出现一个问题,那就是由于成员函数会有一个隐形参数this指针,他会占据第一个参数的位置,那么我们此时访问的时候,可能需要这样访问了:
这样看着就很奇怪啊,但由于this指针,我们只能尝试把流插入定义为全局的(普通函数)
//设成全局
ostream& operator<<(ostream& out,Data& d);
//
istream& operator>>(istream& input, Data& d);
但由于类的私有变量在类外面是访问不了的,所以我们只能把私有变量屏蔽,改为公有的。
所以这里我们有了友元函数:
我们只需要在类的定义中加上这两句话就可以了:
//设为友元 - 我们是朋友所以私有的可以使用
//函数位置写公开和私有都行。
friend ostream& operator<<(ostream& out, Data& d);
friend istream& operator>>(istream& input, Data& d);
friend 是一个关键字,这就表示我们是一个朋友,那么我就可以去访问类中的私有成员变量了。
**注意:**友元函数还是普通函数,并不是成员函数。只是声明在类中。
ostream& operator<<(ostream& out, Data& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
//
istream& operator>>(istream& input, Data& d)
{
cout << "请输入年份: ";
cin >> d._year;
cout << "请输入月份: ";
cin >> d._month;
cout << "请输入日: ";
cin >> d._day;
return input;
}
同时,为了避免输入的日期有误,例如天数为0,月数 > 12等等,我们可以加一个检查函数。
bool CheckData() const
{
if (_month < 1 || _month > 12 ||
_day <= 0 || _day > GetMonthday(_year, _month))
{
return false;
}
return true;
}
istream& operator>>(istream& input, Data& d)
{
cout << "请输入日期:> ";
cin >> d._year;
cin >> d._month;
cin >> d._day;
if (!d.CheckData())
{
cout << "非法日期" << endl;
}
return input;
}
思维导图