首先我们先来看一下通过类实现对日期的一系列处理,同时给大家说一下当中存在的一些细节问题:
这个函数的作用就是我们输入一个得到某一年某个月的天数,对后续的一些函数有着非常重要的作用,但我们要记得一个特殊情况,那就是闰年,因为闰年的二月是29天,非闰年是28天,注意这种情况就可以写代码了。
// 获取某年某月的天数
int GetMonthDay(int year, int month)
{
//日期数组
int MonthDayarr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
//判断闰年
if (2 == month && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))
{
return MonthDayarr[2] + 1;
}
else
{
return MonthDayarr[month];
}
}
这里我们要注意的点就是先判断是不是二月,不是二月我们就没有必要去判断是否是闰年了,没必要了就,这样会节省很多时间。
全缺省构造:
这里我们要给大家讲两种类型的构造函数,一种是全缺省构造函数,另一种是拷贝构造函数,这两种函数不清楚是啥的话可以去看这篇博客:构造函数和全缺省构造函数
我怕们先来说全缺省构造函数:
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
//检查日期是否合法
if (!(year >= 1
&& (month >= 1 && month <= 12)
&& (day >= 1 && day <= GetMonthDay(year, month))))
{
cout << "日期非法" << endl;
}
}
这就是全缺省构造函数,如果传过来值,就赋值,否则就用默认给定的值,在平常的写代码过程中,我还是建议大家去写这种构造函数,因为这种构造函数满足的场景更加多样,不传值也可以,传值当然也可以。
Date d1(2024, 6, 23);
Date d2;
Date d3(2024, 12);
像这样的传值方式都是可以的
拷贝构造:
接下来看一下拷贝构造函数:
// 拷贝构造函数
// d2(d1)
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
这种就是我们的拷贝构造函数,其实就是传一个对象的别名,然后将这个对象的值赋给另一个对象,这就叫拷贝构造。
使用时可以这样去赋值:
Date d1(2024, 6, 23);
Date d2(d1);
比较两个日期是否相等的这种函数被称为赋值运算符重载,像这种函数的函数名,我们应该怎样去写呢,可能有些同学直接这样去写:
bool operator==(const Date& d1,const Date& d2);
但是其实这样会直接报错,我们来看一下编译器给出的错误原因:
这就是这里报错的原因,我们回想一下,是不是忽略了一个叫this指针的东西,没错,我们这里去调这个函数的时候,会有一个隐藏的this指针,所以我们应该这样去写这个函数:
// ==运算符重载
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
这样写才符合要求
调用时是这样调用;
int ret=d1==d2;
int ret=d1.operator==(d2);
赋值运算符重载就是用来为对象赋值的:
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
这就是赋值运算符重载。
调用时可以这样:
Date d1(2024,10,1);
Date d2;
d2.operator=(d1);
这里是比较两个日期的大小,我们肯定先去比较年,其次是月,最后是日,按照规则来就好了
bool operator>(const Date& d)
{
if (_year > d._year)
{
return true;
}
else if (_year == d._year)
{
if (_month > d._month)
{
return true;
}
else if (_month == d._month)
{
if (_day == d._day)
{
return true;
}
}
}
return false;
}
// >=运算符重载
bool operator>=(const Date& d)
{
return operator>(d) || operator==(d);
}
// <运算符重载
bool operator<(const Date& d)
{
if (_year < d._year)
{
return true;
}
else if (_year == d._year)
{
if (_month < d._month)
{
return true;
}
else if (_month == d._month)
{
if (_day < d._day)
{
return true;
}
}
}
return false;
}
这里我直接把两个写在一起,哪里不懂可以问我
这里我先来说一下这两个的区别,其实大致相同,不同的就是:
日期+=天数是改变了传过来的日期,在返回,而日期+天数并没有改变原来的日期,
看一下代码:
// 日期+=天数
Date& operator+=(int day)
{
//如果传过来一个负数
if (day < 0)
{
return *this -= -day;
}
_day += day; //_day加上要加的天数
while (_day > GetMonthDay(_year, _month)) //加完后,如果_day大于当月天数,进入循环
{
//_day减去当月天数,_month++
_day -= GetMonthDay(_year, _month);
_month++;
if (_month > 12)//如果_month大于12,则_year++
{
_month = 1;
_year++;
}
}
return *this;
}
// 日期+天数
Date operator+(int day)
{
Date ret(*this);
ret += day;//这里直接调用上面这个函数就可以了
return ret;
}
可以发现日期加天数就是创建一个临时的对象用来储存传过来的对象,然后返回,起到不改变原来对象的作用。
这个与上面相同,我不做过多的介绍,直接上代码:
// 日期-=天数
Date& operator-=(int day)
{
_day -= day;
while (_day <= 0)
{
_month--;
if (_month < 1)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
// 日期-天数
Date operator-(int day)
{
Date ret(*this);
ret -= day;
return ret;
}
// 前置++
Date& operator++()
{
*this += 1;
return *this;
}
这里我们如果通过函数调用来区分前置++和后置++呢,这个函数如何去写呢,这里我们有一个很妙的写法,就是前置加加正常写,然后后置加加里面加入一个int,构成重载函数:
// 前置++
Date& operator++()
{
*this += 1;
return *this;
}
// 后置++
Date operator++(int)//用int来构成重载函数,区分前置++和后置++
{
//cout << "后置++" << endl;
Date tmp(*this);
*this += 1;
return tmp;
}
传值时想要后置加加里面加上一个整形的数即可,0,1都可以
还是如上,直接上代码:
// 前置--
Date& operator--()
{
*this -= 1;
return *this;
}
// 后置--
Date operator--(int)
{
//cout << "后置--" << endl;
Date tmp(*this);
*this -= 1;
return tmp;
}
我们知道,我们调用一个对象的时候,会传递一个this指针来,传过去的this指针是这种类型的:Date* const this,也就是说明这里的this指向的对象不能改变,但是本身的值可以改变,但是如果我们传一个const对象,就是指向的对象和值都不能改变,那样会咋变呢,如果直接传,就会出现权限的放大问题,我们知道,权限可以缩小,但绝对不可以放大。 这时候我们可以这样去做,在函数定义和对象的声明的后面加上一个const:
像这样。
在 C++中,要调用一个const
对象,可以使用const
引用或const
指针。const
引用或const
指针可以绑定到const
对象,从而避免对const
对象的直接修改。
避免权限放大的方法是在调用const
对象时,使用const
引用或const
指针。这样可以确保在函数内部不会修改const
对象的值,从而避免权限放大的问题。
向我们之前讲的构造函数,以及拷贝构造函数,赋值构造函数等等,都是为了给类中的内置成员进行初始化的,但是如果出现了下面的这几种情况呢?
1、引用
2、const
3、没有默认构造自定义类型成员(必须显示传参调构造)
我们知道,引用只能在定义的时候初始化,然后const定义的变量也无法被修改自定义类型要调用它的默认构造函数, 如果没有默认构造函数就会error
所以此时C++中就给出了一种合适的解决方法,那就是初始化列表。
MyQueue(int n, int& rr)
:_pushst(n)
, _popst(n)
,_x(1)
,_y(rr)
{
_size = 0;
}
/*{
_size(0);
}*/
private:
Stack _pushst;
Stack _popst;
int _size;
//这两个必须在定义时初始化
const int _x;
int& _y;
};
就像这样,这就是初始化列表的写法,在{}上面进行操作,但是不管{}有没有值,必须带着{}才可以。写法就是:然后后面每一句前面几上,即可。不要去在乎这样写的原因是啥,主要还是要记住就是这样去写。
这里还有一点要注意,初始化列表初始化的顺序是声明的顺序,与初始化列表的顺序无关:
class A
{
public:
//初始化列表初始化的顺序是生命的顺序,不是初始化的顺序
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
如果运行这一串代码,如果传过去一个1你认为结果是啥,结果其实应该是一个随机值加上1,因为声明的顺序是_a2在前面。然后_a1在后面,所以初始化_a2,所以是一个随机值。
我们知道static的作用是:
这里我们要说的是其实static也可以在类中声明,被定义的成员被称为类的静态成员,我们知道静态成员不是谁所特有的,而是共享的,不属于某个具体的类,存放在静态区
即使声明在类中,我们依然要在外面定义:
类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
我们可以根据这个特点去定义它
class A
{
public:
A() { ++_sount; }
A(const A& t) { ++_sount; }
~A()
{
//--_sount;
}
//静态成员函数
//没有this指针,只能访问静态成员,无法访问其他的成员,访问其他成员变量要依靠this指针
static int GetCount()
{
return _sount;
}
private:
int _a1 = 1;
int _a2 = 2;
//静态区,不存在于对象中
//这里不能给缺省值,因为缺省值是给初始化列表的,
// 但是这里不会走初始化列表,因为他在静态区,所以不走初始化列表
//价值:属于所有整个类,属于所有对象
static int _sount;
};
//这是_sount的定义
int A::_sount = 0;
这里我们还需要注意一点就是静态成员函数无法调用非静态成员函数的,因为静态成员函数没有this指针的传递。
友元是一种很有效的方式,但是并不推荐使用,因为它会破坏封装
比如说 Date d1; cout << d1 << endl; 要实现这样的代码, 就只能重载 <<
如果实现成Date的成员函数, ostream& operator<<(ostream& _cout) 第一个参数是this, 所以调用的时候就成了 d1 << cout ; 这样特别奇怪,所以一般这个都会重载成全局函数,所以又引发新的问题, 类的成员变量一般是私有的 如果重载成全局函数,无法访问私有/保护成员,所以友元就派上用场了
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在 类的内部声明,声明时需要加friend关键字
像这样:
这样就可以访问类中私有成员
友元类其实就是两个类之间的友元:
class A
{
//声明 B是A的友元
//此时B就可以访问A中的私有,但是不证明A可以访问B的私有
//也就是A把B当成朋友了,但是不能说明B把A当成朋友
friend class B;
};
class B
{
};
像这样,此时B就是A的友元,B就可以访问A中的私有成员,但是A不可以访问B的私有,
也就是A把B当成朋友了,但是不能说明B把A当成朋友
内部类其实就是在一个类中再去定义一个类:
内部类,内部类是外部类的私有
在一个类中,除了定义函数和变量,还可以定义类,就叫内部类,类似于套娃
class A
{
private:
static int k;
int h;
public:
void func()
{
cout << "func" << endl;
}
private:
//内部类
//放到A里面
//仅仅受到类域限制
class B//这里的B天生就是A的友元。
{
public:
B(int b = 1)
{
_b = b;
}
void foo(const A& a)
{
//cout << k << endl;
//cout << a.h << endl;
}
private:
int _b;
};
};
这就是一个内部类,然后这里的B天生就是A的友元。
编译器对拷贝构造的优化通常有以下几种方式:
我给大家看一个例子:
#include<iostream>
using namespace std;
class A
{
public:
A()
{
cout << "A()" << endl;
}
A(const A& a)
{
cout << "A(const A& a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
A func()
{
A a;
return a;
}
int main()
{
A tmp=func();
return 0;
}
大家认为这个例子共进行了多少次的拷贝构造和析构?
所以是两次构造,一次拷贝构造,一次赋值重载,三次析构。
如果我们换个方式接收呢
func改成这样:
A func()
{
return A();
}
A
int main()
{
A tmp=func();
return 0;
}
我们再分析一下,A()是一次构造,返回是一次拷贝构造,然后拷贝构造给tmp,然后析构一次临时对象,西沟一次匿名对象,析一次tmp。 所以就是一次构造,两次拷贝构造,三次析构。可是运行一下就发现:
是一次构造一次析构。 为什么呢? 原因就是使用A()去返回的时候还返回啥临时对象啊,直接在接受返回值的地方构造就可以了,一个表达式,多次构造 +拷贝构造的 都会被优化为1次构造,这就是编译器的优化。
最后再来简单来说一下匿名对象,其实就是创建一个对象时不给他名字,