写一个日期的类

1.构造函数,写一个类最重要的是先写一个构造函数出来

#include <iostream>
using namespace std;
class Date
{
public:
    Date(int year = 0, 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(2021, 5, 30); Date d2(d1); // 用已存在的对象d1创建对象

d2 d1.Print(); d2.Print(); return 0;

}

打印结果:

代码中,我们自己并没有定义拷贝构造函数,但编译器自动生成的拷贝构造函数最终还是完成了对象的拷贝构造。
编译器自动生成的拷贝构造函数机制:
 1、编译器自动生成的拷贝构造函数对内置类型会完成浅拷贝(值拷贝)。
 2、对于自定义类型,编译器会再去调用它们自己的默认拷贝构造函数。

四、编译器自动生成的拷贝构造函数不能实现深拷贝
 上面说到,编译器自动生成的拷贝构造函数会对内置类型完成浅拷贝。对于以下这句代码,浅拷贝实际上就是将d1的内容完完全全的复制了一份拷贝给d2,所以说浅拷贝也叫做值拷贝。

Date d2(d1);// 用已存在的对象d1创建对象d2
但某些场景下浅拷贝并不能达到我们想要的效果。例如,栈(Stack)这样的类,编译器自动生成的拷贝构造函数就不能满足我们的需求了:

Stack s1;
Stack s2(s1);// 用已存在的对象s1创建对象s2
代码中,我们的本意是用已存在的对象s1创建对象s2,但编译器自动生成的拷贝构造函数,完成的是浅拷贝,拷贝出来的对象s2将不能满足我们的要求。
举个例子,现有以下栈(Stack)类:

class Stack
{
public:
    Stack(int capacity = 4)
    {
        _ps = (int*)malloc(sizeof(int)* capacity);
        _size = 0;
        _capacity = capacity;
    }
    void Print()
    {
        cout << _ps << endl;// 打印栈空间地址
    }
private:
    int* _ps;
    int _size;
    int _capacity;
};
我们可以看到,类中没有自己定义拷贝构造函数,那么当我们用已存在的对象来创建另一个对象时,将调用编译器自动生成的拷贝构造函数。看看以下代码运行结果:

int main()
{
    Stack s1;
    s1.Print();// 打印s1栈空间的地址
    Stack s2(s1);// 用已存在的对象s1创建对象s2
    s2.Print();// 打印s2栈空间的地址
    return 0;
}
 结果打印s1栈和s2栈空间的地址相同,这就意味着,就算在创建完s2栈后,我们对s1栈做的任何操作都会直接影响到s2栈

如果对需要深拷贝的数据进行浅拷贝,析构的时候会析构两次,这样是很严重的

总结一下
 1、像Date这样的类,需要的就是浅拷贝,那么编译器自动生成的拷贝构造函数就够用了,我们不需要自己写。
 2、像Stack这样的类,浅拷贝会导致析构两次、程序崩溃等问题,需要我们自己写对应的拷贝构造函数。

赋值运算符重载

运算符重载

 C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,其目的就是让自定义类型可以像内置类型一样可以直接使用运算符进行操作。

d1 == d2;// 可读性高(书写简单)
IsSame(d1, d2);// 可读性差(书写麻烦)

 运算符重载函数也具有自己的返回值类型,函数名字以及参数列表。其返回值类型和参数列表与普通函数类似。
 运算符重载函数名为:关键字operator后面接需要重载的操作符符号。
函数原型:返回值 operator运算符(参数列表

注意

 1.不能通过连接其他符号来创建新的操作符:比如operator@。
 2.重载操作符必须有一个类类型或枚举类型的操作数。
 3.用于内置类型的操作符,重载后其含义不能改变。
 4.作为类成员的重载函数时,函数有一个默认的形参this,限定为第一个形参。
 5.sizeof 、:: 、.* 、?: 、. 这5个运算符不能重载

这里以重载 == 运算符作为例子:
 我们可以将该运算符重载函数作为类的一个成员函数,此时该函数的第一个形参默认为this指针。

class Date
{
public:
    Date(int year = 0, 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;
    }
private:
    int _year;
    int _month;
    int _day;
};
 我们也可以将该运算符重载函数放在类外面,但此时外部无法访问类中的成员变量,这时我们可以将类中的成员变量设置为共有(public),这样外部就可以访问该类的成员变量了(也可以用友元函数解决该问题)。并且在类外没有this指针,所以此时函数的形参我们必须显示的设置两个。

class Date
{
public:
    Date(int year = 0, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void Print()
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
    int _year;
    int _month;
    int _day;
};
bool operator==(const Date& d1, const Date& d2)// 运算符重载函数
{
    return d1._year == d2._year
        &&d1._month == d2._month
        &&d1._day == d2._day;
}

重载赋值运算符需要注意以下几点:

1.参数类型设置为引用,并用const进行修饰

赋值运算符重载函数的第一个形参默认是this指针,第二个形参是我们赋值运算符的右操作数。

由于是自定义类型传参,我们若是使用传值传参,会额外调用一次拷贝构造函数,所以函数的第二个参数最好使用引用传参(第一个参数是默认的this指针,我们管不了)。

其次,第二个参数,即赋值运算符的右操作数,我们在函数体内不会对其进行修改,所以最好加上const进行修饰。
二、函数的返回值使用引用返回

 实际上,我们若是只以d2 = d1这种方式使用赋值运算符,赋值运算符重载函数就没必要有返回值,因为在函数体内已经通过this指针对d2进行了修改。但是为了支持连续赋值,即d3 = d2 = d1,因为如果没有返回值,操作符=是从右进行连续赋值的,这个时候需要一个临时变量来进行赋值,返回值就是那个临时变量

三、赋值前检查是否是给自己赋值
 若是出现d1 = d1,我们不必进行赋值操作,因为自己赋值给自己是没有必要进行的。所以在进行赋值操作前可以先判断是否是给自己赋值,避免不必要的赋值操作。
四、引用返回的是*this
 赋值操作进行完毕时,我们应该返回赋值运算符的左操作数,而在函数体内我们只能通过this指针访问到左操作数,所以要返回左操作数就只能返回*this。
五、一个类如果没有显示定义赋值运算符重载,编译器也会自动生成一个,完成对象按字节序的值拷贝
 没错,赋值运算符重载编译器也可以自动生成,并且也是支持连续赋值的。但是编译器自动生成的赋值运算符重载完成的是对象按字节序的值拷贝,例如d2 = d1,编译器会将d1所占内存空间的值完完全全地拷贝到d2的内存空间中去,类似于memcpy。
 对于日期类,编译器自动生成的赋值运算符重载函数就可以满足我们的需求,我们可以不用自己写。但是这也不意味着所有的类都不用我们自己写赋值运算符重载函数,当遇到一些特殊的类,我们还是得自己动手写赋值运算符函数的。
    Date d1(2021, 6, 1);
    Date d2(d1);
    Date d3 = d1;

这里一个三句代码,我们现在都知道第二句代码调用的是拷贝构造函数,那么第三句代码呢?调用的是哪一个函数?是赋值运算符重载函数吗?
 其实第三句代码调用的也是拷贝构造函数,注意区分拷贝构造函数和赋值运算符重载函数的使用场景:
 拷贝构造函数:用一个已经存在的对象去构造初始化另一个即将创建的对象。
 赋值运算符重载函数:在两个对象都已经存在的情况下,将一个对象赋值给另一个对象。

const成员
const修饰类的成员函数
 我们将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰的是类成员函数隐含的this指针,表明在该成员函数中不能对this指针指向的对象进行修改。

例如,我们可以对类成员函数中的打印函数进行const修饰,避免在函数体内不小心修改了对象:

    void Print()const// cosnt修饰的打印函数
    {
        cout << _year << "年" << _month << "月" << _day << "日" << endl;
    }
日期类的实现

在学习了C++的6个默认成员函数后,我们现在动手实现一个完整的日期类,来加强对这6个默认成员函数的认识。
 这是日期类中所包含的成员函数和成员变量:

class Date
{
public:
    // 构造函数
    Date(int year = 0, int month = 1, int day = 1);
    // 打印函数
    void Print() const;
    // 日期+=天数
    Date& operator+=(int day);
    // 日期+天数
    Date operator+(int day) const;
    // 日期-=天数
    Date& operator-=(int day);
    // 日期-天数
    Date operator-(int day) const;
    // 前置++
    Date& operator++();
    // 后置++
    Date operator++(int);
    // 前置--
    Date& operator--();
    // 后置--
    Date operator--(int);
    // 日期的大小关系比较
    bool operator>(const Date& d) const;
    bool operator>=(const Date& d) const;
    bool operator<(const Date& d) const;
    bool operator<=(const Date& d) const;
    bool operator==(const Date& d) const;
    bool operator!=(const Date& d) const;
    // 日期-日期
    int operator-(const Date& d) const;

    // 析构,拷贝构造,赋值重载可以不写,使用默认生成的即可

private:
    int _year;
    int _month;
    int _day;
};

1
// 获取某年某月的天数
inline int GetMonthDay(int year, int month)
{
    // 数组存储平年每个月的天数
    static int dayArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
    int day = dayArray[month];
    if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
    {
        //闰年2月的天数
        day = 29;
    }
    return day;
}
// 构造函数
Date::Date(int year, int month, int day)
{
    // 检查日期的合法性
    if (year >= 0
    && month >= 1 && month <= 12
    && day >= 1 && day <= GetMonthDay(year, month))
    {
        _year = year;
        _month = month;
        _day = day;
    }
    else
    {
        // 严格来说抛异常更好
        cout << "非法日期" << endl;
        cout << year << "年" << month << "月" << day << "日" << endl;
    }
}

GetMonthDay函数中的三个细节:
 1.该函数可能被多次调用,所以我们最好将其设置为内联函数。
 2.函数中存储每月天数的数组最好是用static修饰,存储在静态区,避免每次调用该函数都需要重新开辟数组。
 3.逻辑与应该先判断month == 2是否为真,因为当不是2月的时候我们不必判断是不是闰年。

日期 += 天数

 对于+=运算符,我们先将需要加的天数加到日上面,然后判断日期是否合法,若不合法,则通过不断调整,直到日期合法为止。
调整日期的思路:
 1.若日已满,则日减去当前月的天数,月加一。
 2.若月已满,则将年加一,月置为1。
反复执行1和2,直到日期合法为止。

// 日期+=天数
Date& Date::operator+=(int day)
{
    if (day<0)
    {
        // 复用operator-=
        *this -= -day;
    }
    else
    {
        _day += day;
        // 日期不合法,通过不断调整,直到最后日期合法为止
        while (_day > GetMonthDay(_year, _month))
        {
            _day -= GetMonthDay(_year, _month);
            _month++;
            if (_month > 12)
            {
                _year++;
                _month = 1;
            }
        }
    }
    return *this;
}
        注:当需要加的天数为负数时,转而调用-=运算符重载函数。

日期 + 天数
 +运算符的重载,我们可以复用上面已经实现的+=运算符的重载函数。但是要注意:虽然我们返回的是加了之后的值,但是对象本身的值并没有改变。就像a = b + 1,b + 1的返回值是b + 1,但是b的值并没有改变。所以我们还可以用const对该函数进行修饰,防止函数内部改变了this指针指向的对象。

// 日期+天数
Date Date::operator+(int day) const
{
    Date tmp(*this);// 拷贝构造tmp,用于返回
    // 复用operator+=
    tmp += day;

    return tmp;
}

注意:+=运算符的重载函数采用的是引用返回,因为出了函数作用域,this指针指向的对象没有被销毁。但+运算符的重载函数的返回值只能是传值返回,因为出了函数作用域,对象tmp就被销毁了,不能使用引用返回。

日期 -= 天数
 对于-=运算符,我们先用日减去需要减的天数,然后判断日期是否合法,若不合法,则通过不断调整,直到日期合法为止。
调整日期的思路:
 1.若日为负数,则月减一。
 2.若月为0,则年减一,月置为12。
 3.日加上当前月的天数。
反复执行1、2和3,直到日期合法为止。
// 日期-=天数
Date& Date::operator-=(int day)
{
    if (day < 0)
    {
        // 复用operator+=
        *this += -day;
    }
    else
    {
        _day -= day;
        // 日期不合法,通过不断调整,直到最后日期合法为止
        while (_day <= 0)
        {
            _month--;
            if (_month == 0)
            {
                _year--;
                _month = 12;
            }
            _day += GetMonthDay(_year, _month);
        }
    }
    return *this;
}
注:当需要减的天数为负数时,转而调用+=运算符重载函数。

日期 - 天数

 和+运算符的重载类似,我们可以复用上面已经实现的-=运算符的重载函数,而且最好用const对该函数进行修饰,防止函数内部改变了this指针指向的对象。

// 日期-天数
Date Date::operator-(int day) const
{
    Date tmp(*this);// 拷贝构造tmp,用于返回
    // 复用operator-=
    tmp -= day;

    return tmp;
}
注意:-=运算符的重载函数采用的是引用返回,但-运算符的重载函数的返回值只能是传值返回,也是由于-运算符重载函数中的tmp对象出了函数作用域被销毁了,所以不能使用引用返回。

前置 ++

 前置++,我们可以复用+=运算符的重载函数。

// 前置++
Date& Date::operator++()
{
    // 复用operator+=
    *this += 1;
    return *this;
}

后置 ++

 由于前置++和后置++的运算符均为++,为了区分它们的运算符重载,我们给后置++的运算符重载的参数加上一个int型参数,使用后置++时不需要给这个int参数传入实参,因为这里int参数的作用只是为了跟前置++构成重载。

// 后置++
Date Date::operator++(int)
{
    Date tmp(*this);// 拷贝构造tmp,用于返回
    // 复用operator+=
    *this += 1;
    return tmp;
}

注意:后置++也是需要返回加了之前的值,只能先用对象tmp保存之前的值,然后再然对象加一,最后返回tmp对象。由于tmp对象出了该函数作用域就被销毁了,所以后置++只能使用传值返回,而前置++可以使用引用返回。

前置 –

 前置–,我们也是可以复用前面的-=运算符的重载函数。

// 前置--
Date& Date::operator--()
{
    // 复用operator-=
    *this -= 1;
    return *this;
}

后置–
 后置–需要注意的事项和后置++是一样的,我这里就不过多阐述了。

// 后置--
Date Date::operator--(int)
{
    Date tmp(*this);// 拷贝构造tmp,用于返回
    // 复用operator-=
    *this -= 1;
    return tmp;
}

日期类的大小关系比较
 日期类的大小关系比较需要重载的运算符看起来有6个,实际上我们只用实现两个就可以了,然后其他的通过复用这两个就可以实现。

注意:进行日期的大小比较,我们并不会改变传入对象的值,所以这6个运算符重载函数都应该被const所修饰。

>运算符的重载
 >运算符的重载很简单,先判断年是否大于,再判断月是否大于,最后判断日是否大于,这其中有一者为真则函数返回true,否则返回false。
bool Date::operator>(const Date& d) const
{
    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 Date::operator==(const Date& d) const
{
    return _year == d._year
        &&_month == d._month
        &&_day == d._day;
}

>=运算符的重载
 >=,即大于或者等于,满足其中之一即可。

bool Date::operator>=(const Date& d) const
{
    return *this > d || *this == d;
}

<运算符的重载
 <,大于等于的反面即是小于。

bool Date::operator<(const Date& d) const
{
    return !(*this >= d);
}

<=运算符的重载
 <=,大于的返回即是小于等于。

bool Date::operator<=(const Date& d) const
{
    return !(*this > d);
}
1
2
3
4
!=运算符的重载
 !=,等于的反面即是不等于。

bool Date::operator!=(const Date& d) const
{
    return !(*this == d);
}

日期 - 日期
 日期 - 日期,即计算传入的两个日期相差的天数。我们只需要让较小的日期的天数一直加一,直到最后和较大的日期相等即可,这个过程中较小日期所加的总天数便是这两个日期之间差值的绝对值。若是第一个日期大于第二个日期,则返回这个差值的正值,若第一个日期小于第二个日期,则返回这个差值的负值。

// 日期-日期
int Date::operator-(const Date& d) const
{
    Date max = *this;// 假设第一个日期较大
    Date min = d;// 假设第二个日期较小
    int flag = 1;// 此时结果应该为正值
    if (*this < d)
    {
        // 假设错误,更正
        max = d;
        min = *this;
        flag = -1;// 此时结果应该为负值
    }
    int n = 0;// 记录所加的总天数
    while (min != max)
    {
        min++;// 较小的日期++
        n++;// 总天数++
    }
    return n*flag;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值