【C++】类和对象(二)

其他类和对象文章:
类和对象(一)

类和对象(三)

六个默认成员函数


如果我们写一个空类,里面并不是什么都没有,而是自动生成六个默认成员函数

class A{};

用户没有显式实现,而是由编译器隐式生成的函数就是默认成员函数

  1. 构造函数:不是构造对象,而是对对象的数据进行初始化
  2. 析构函数:不是销毁对象,而是清理对象申请的资源
  3. 拷贝构造:用一个已经存在的对象来初始化创建一个对象
  4. 赋值重载:把一个已存在对象赋值给另一个已存在对象

5 & 6. 取地址重载:普通对象和 const 对象取地址重载,这两个很少自己实现

构造函数


当我们创建一个对象时,会自动调用构造函数,初始化对象中的数据。这样就不需要我们手动初始化了。构造函数是一个特殊的成员函数,以下是它的一些特性:

  1. 函数名与类名相同
  2. 无返回值
class Date
{
public:
        // 构造函数
        Date(){}
private:
        int _year;
        int _month;
        int _day;
};

Date d1; // 实例化时,自动调用构造函数
  1. 实例化对象自动调用

  2. 可以重载,以满足不同的初始化需求

class Date
{
public:
        Date(){}
        Date(int year, int month, int day)
        {
                _year = year;
                _month = month;
                _day = day;
        }
private:
        int _year;
        int _month;
        int _day;
};

Date d1;
Date d2(2024, 1, 1);

在这里插入图片描述

注意,当创建对象不需要传数据时,就不需要加括号了

Date d1;
Date d1(); // 这种形式是不行的

因为加了括号,却没有参数,编译器就会认为这是函数声明

在这里插入图片描述

  1. 用户没有显示实现,编译器自动生成默认构造函数;如果用户自己实现了,编译器不再自动生成
class Date
{
public:
    // 不写就会自动生成一个默认构造
private:
        int _year;
        int _month;
        int _day;
};
Date d1;

在这里插入图片描述

  1. 默认生成的构造函数,对内置类型不做处理,对自定义类型调用它的构造函数

通过上图我们可以看到,编译器自动生成的构造函数,并不会对数据进行处理,那它有什么用呢?

C++ 的数据类型分为内置类型自定义类型,内置类型是语言提供的类型,如 int/char/long 等,自定义类型是我们使用 class/struct/union 定义的类型

如果一个类中嵌套了另一个类,如下:

class A
{
public:
        A()
        {
                cout << "A()" << endl;
        }
};

class Date
{
public:
    // 默认构造
private:
        int _year;
        int _month;
        int _day;
        A _a;
};

Date d1;

在我们实例化一个 Date 对象时,默认生成的构造函数不会对 _year/_month/_day 等内置类型做处理,但是会调用自定义类型 A 的构造函数来初始化 A

在这里插入图片描述

在编译器默认生成构造函数时,如果要初始化内置数据,可以在类的数据声明阶段,给数据设置缺省值

class Date
{
public:
    // 默认构造
private:
    // 这是声明,不是定义
        int _year = 1;
        int _month = 1;
        int _day = 1;
        A _a;
};
  1. 默认构造函数不只是编译器自动生成的,还有无参构造函数、全缺省构造函数。简单来说,不用传参的构造函数就是默认构造函数

先来看一下代码:

class Date
{
public:
        Date(int year, int month, int day)
        {
                _year = year;
                _month = month;
                _day = day;
        }
private:
        int _year = 1;
        int _month = 1;
        int _day = 1;
};

Date d1(2024, 1, 1);

此时运行代码,就会出现问题:

在这里插入图片描述

这里的报错是:没有合适的默认构造函数可用。虽然我们写了一个构造函数,但它不是默认构造函数,而一个类必须要有一个默认构造函数。只要我们写了构造函数,无论算不算默认构造,编译器都不会再自动生成默认构造函数了

默认构造函数有以下三种

  1. 用户没写,编译器自动生成的默认构造函数
  2. 用户写了,无参构造函数
  3. 用户写了,全缺省构造函数

也就是实例化对象时不需要传参,这种构造就是默认构造函数。以上三种默认构造互斥,也就是只能有一个默认构造。一般都推荐使用全缺省

互斥:用户写了,编译器不再自动生成;而用户写的无参和全缺省也是互斥的,如果调用时没有参数,编译器不知道要调用哪个

析构函数


当对象的生命周期结束时,析构函数自动调用,释放对象申请的资源,如 malloc、new 出来的空间。析构函数也是一个特殊的成员函数,特性如下:

  1. 函数名是在类名前加一个~,例如~Date
  2. 无参数,无返回值
  3. 一个类只能有一个析构函数,不可重载
  4. 用户没有显式定义析构函数,编译器就会自动生成一个默认析构函数
  5. 默认析构函数对内置类型不做处理,对于自定义类型则去调用它的析构函数
class A
{
public:
        A()
        {
                cout << "A()" << endl;
        }

        ~A()
        {
                cout << "~A()" << endl;
        }
};
class Date
{
public:
        Date(int year = 1, int month = 1, int day = 1)
        {
                _year = year;
                _month = month;
                _day = day;
        }
private:
        int _year = 1;
        int _month = 1;
        int _day = 1;
        A _a;
};

Date d1;

在这里插入图片描述

对于日期类这种,没有申请资源,数据都放在栈上,等对象的生命周期结束,数据自然会被销毁,不用自己写析构函数,使用编译器自动生成的析构函数即可

但是如果有资源申请,例如 Stack 类,默认析构函数只会销毁指向申请空间的指针,并不会清理额外申请的空间,会造成资源泄露问题

在这里插入图片描述

所以如果类中有申请空间,就需要我们手动显示实现析构函数,清理空间

typedef int DataType;
class Stack
{
public:
        Stack(size_t capacity = 3)
        {
                _array = (DataType*)malloc(sizeof(DataType) * capacity);
                if (NULL == _array)
                {
                        perror("malloc申请空间失败!!!");
                        return;
                }
                _capacity = capacity;
                _size = 0;
        }
        void Push(DataType data)
        {
                // CheckCapacity();
                _array[_size] = data;
                _size++;
        }
        // 其他方法...
        ~Stack() // 显示定义析构函数,释放申请的资源
        {
                if (_array)
                {
                        free(_array);
                        _array = NULL;
                        _capacity = 0;
                        _size = 0;

                }
        }
private:
        DataType* _array;
        int _capacity;
        int _size;
};

void TestStack()
{
        Stack s1;
        s1.Push(1);
        s1.Push(2);
}

拷贝构造


拷贝构造是一个特殊的构造函数,可以创建一个与已存在对象相同的新对象。在我们用已存在对象创建新对象时,会自动调用

拷贝构造是一个特殊的构造函数,也就是说在我们显式实现一个拷贝构造的同时,也必须实现一个默认构造函数。以下是拷贝构造的一些特性:

  1. 拷贝构造是构造函数的一个重载
  2. 拷贝构造的参数是一个类类型对象的引用,如果不是引用则会引发无穷递归问题

这种是正确的写法,参数是引用

class Date
{
public:
        Date(int year = 1, int month = 1, int day = 1)
        {
                _year = year;
                _month = month;
                _day = day;
        }

        // 拷贝构造
        Date(const Date& d)
        {
                _year = d._year;
                _month = d._month;
                _day = d._day;
        }
        
        void Print()
        {
                cout << _year << "/" << _month << "/" << _day << endl;
        }
private:
        int _year = 1;
        int _month = 1;
        int _day = 1;
        
};

Date d1(2024, 1, 1);
Date d2(d1);
d1.Print();
d2.Print();

在这里插入图片描述

而如果拷贝构造的参数不是引用,就会引发无穷递归。因为传值传的是形参,也就是实参的拷贝。既然是拷贝,就会触发新的拷贝构造,这样就会无限递归下去

// 拷贝构造
// Date(const Date& d) // 正确写法
Date(const Date d) // 错误写法
{
        _year = d._year;
        _month = d._month;
        _day = d._day;
}

在这里插入图片描述

  1. 若用户未显式定义拷贝构造,编译器就会自动生成一个默认拷贝构造函数。默认拷贝构造函数会按照字节序完成拷贝,这种拷贝叫做浅拷贝,也叫值拷贝
class Date
{
public:
        Date(int year = 1, int month = 1, int day = 1)
        {
                _year = year;
                _month = month;
                _day = day;
        }
        
        void Print()
        {
                cout << _year << "/" << _month << "/" << _day << endl;
        }
private:
        int _year = 1;
        int _month = 1;
        int _day = 1;
        // A _a;
};

Date d1(2024, 1, 1);
Date d2(d1);

d1.Print();
d2.Print();

在这里插入图片描述

  1. 默认构造函数会对内置类型做浅拷贝,对自定义类型则调用它的拷贝构造
class A
{
public:
        A()
        {
                cout << "A()" << endl;
        }


        A(const A& a)
        {
                cout << "A(const A& a)" << endl;
        }

};
class Date
{
public:
        Date(int year = 1, int month = 1, int day = 1)
        {
                _year = year;
                _month = month;
                _day = day;
        }

        void Print()
        {
                cout << _year << "/" << _month << "/" << _day << endl;
        }
private:
        int _year = 1;
        int _month = 1;
        int _day = 1;
         A _a;
};

Date d1(2024, 1, 1);
Date d2(d1);

d1.Print();
d2.Print();

在这里插入图片描述

  1. 和析构函数一样,日期类这种不用申请资源的类,默认拷贝构造就够用了。但是一旦涉及到资源申请,就必须要我们自己实现拷贝构造了

还是用之前的 Stack 类测试一下,看看用自动生成的默认拷贝构造会怎样

void TestStack()
{
    Stack s1;
    s1.Push(1);
    s1.Push(2);
    s1.Push(3);
    s1.Push(4);
    Stack s2(s1);
}

程序崩溃了:

在这里插入图片描述

这是因为默认拷贝构造使用的是浅拷贝,s2 只是把 s1 中的 _array 指针拷贝了,并没有拷贝空间

在这里插入图片描述

到了程序结束时,s1 和 s2 都是要销毁的。s2 后创建,所以先销毁,调用析构函数,将申请的空间资源释放。然后 s1 销毁,调用析构函数,释放同样的空间,对一块空间释放两次是非法操作,所以会造成程序崩溃

所以当类中有资源申请时,就要手动实现一个拷贝构造,对申请的空间中的数据也进行拷贝,就是深拷贝

在这里插入图片描述

Stack(const Stack& s)
{
        DataType* tmp = (DataType*)malloc(sizeof(DataType) * s._capacity);
        if (tmp == nullptr)
        {
                perror("malloc fail");
                exit(-1);
        }

        memcpy(tmp, s._array, s._size * sizeof(DataType));
        _array = tmp;
        _size = s._size;
        _capacity = s._capacity;
}

赋值运算符重载


运算符重载

内置类型可以使用运算符,例如+、-、++等,那么自定义类型可不可以使用这些运算符呢?

在这里插入图片描述

显然是不可以的,因为编译器并不认识这些自定义类型,这时就需要我们进行运算符重载

运算符重载方法:返回值 operator操作符(参数列表),例如

bool operator==(const Date& d1, const Date& d2)

注意:

  1. 只能重载已经存在的操作符,不能自己创造新的操作符,例如 operator@
  2. 重载操作符不能改变操作符原本的含义,例如 operator++ 只能实现 ++ 功能
  3. 不能重载的 5 个运算符:.* . :: sizeof ?:
  4. 重载操作符必须有一个类类型参数
  5. 重载为成员函数时,形参的数目是少一个的,因为有一个参数是隐函的 this 指针,例如:
bool operator==(const Date& d); // 第一个参数是 this 指针,*this == d

下面我们尝试为 Date 重载一个全局函数 operator==

class Date
{
public:
        Date(int year = 1, int month = 1, int day = 1)
        {
                _year = year;
                _month = month;
                _day = day;
        }

        void Print()
        {
                cout << _year << "/" << _month << "/" << _day << endl;
        }
private:
        int _year = 1;
        int _month = 1;
        int _day = 1;
};
bool operator== (const Date & d1, const Date & d2)
{
        return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}

问题来了,全局的 operator== 无法访问成员变量,有三个措施:

  1. 将成员变量设为公有,为了让不破坏类的封装性,一般不会这么做
  2. 将 operator 声明为 Date 的友元函数,如下
class Date
{
public:
        Date(int year = 1, int month = 1, int day = 1)
        {
                _year = year;
                _month = month;
                _day = day;
        }

        void Print()
        {
                cout << _year << "/" << _month << "/" << _day << endl;
        }
        
        // 友元函数
        friend bool operator== (const Date & d1, const Date & d2);
private:
        int _year = 1;
        int _month = 1;
        int _day = 1;
};
bool operator== (const Date & d1, const Date & d2)
{
        return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
  1. 将 operator== 重载为Date的成员函数
class Date
{
public:
        Date(int year = 1, int month = 1, int day = 1)
        {
                _year = year;
                _month = month;
                _day = day;
        }

        bool operator== (const Date& d)
        {
                return _year == d._year && _month == d._month && _day == d._day;
        }
        void Print()
        {
                cout << _year << "/" << _month << "/" << _day << endl;
        }

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

重载为成员函数时,虽然看起来形参少一个,但是实际 this 指针是第一个参数

赋值重载


赋值重载也是默认成员函数之一,可以让我们将一个已存在的对象赋值给另一个已存在的对象

  1. 参数:类类型的引用,这样传参可以避免传值拷贝,提升效率
  2. 返回值:类类型的引用,不仅可以提高效率,可以支持连续赋值,例如:
int i = 1, j = 2;
i = j = 3; // 连续赋值

尝试重载 Date 类型的 =

Date& operator=(const Date& d)
{
        // 检查自己给自己赋值
        if (*this == d)
                return *this;
        _year = d._year;
        _month = d._month;
        _day = d._day;

        return *this;
}

Date d1(2024, 1, 1);
Date d2(2025, 2, 2);
Date d3(2026, 3, 3);
d1 = d2 = d3;
d1.Print();
d2.Print();
d3.Print();

在这里插入图片描述

此外,赋值重载同样有以下特性:

  1. 如果用户没有显式实现赋值重载,编译器就会自动实现一个默认的
class Date
{
public:
        Date(int year = 1, int month = 1, int day = 1)
        {
                _year = year;
                _month = month;
                _day = day;
        }

        bool operator== (const Date& d)
        {
                return _year == d._year && _month == d._month && _day == d._day;
        }
        void Print()
        {
                cout << _year << "/" << _month << "/" << _day << endl;
        }

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

在这里插入图片描述

  1. 同拷贝构造一样,默认的赋值重载只会进行浅拷贝,如果类中存在资源申请,就必须手动实现一个赋值重载

  2. 赋值重载不可以重载为全局函数,只能是成员函数。因为如果类中不写,就会自动生成一个,此时就会与全局的赋值重载发生冲突

前置++ 和 后置++ 重载

前置++:先加1,后使用。因此可以这样实现:

Date& operator++()
{
        _day++;
        return *this;
}

返回引用可以使效率更高

后置++:先使用,后加1。这就要将加1之前的值临时拷贝一份,然后加1,再返回加1之前的值。这样就不可以返回引用了

前置++的重载已经用了operator++,那么后置++的重载该如何呢?那只能重载

为了构成重载,C++规定,后置++的重载要这样写:参数中加一个 int 类型,但是使用时不用传参

Date operator++(int)
{
        Date tmp(*this);
        _day++;
        return tmp;
}

日期类的实现


学习了上述四个默认成员函数之后,就可以进行日期类的编写了

模块化

  • test.cpp 用于测试
  • Date.h 包含需要的头文件与类的定义、成员函数的声明
  • Date.cpp 成员函数的定义

成员变量

我们这里的日期类不要求多么精细,只要能表达清楚最基础的年月日即可。为了后续测试,这里写一个 Print 用来打印年月日

class Date
{
public:
        void Print()
        {
                cout << _year << "/" << _month << "/" << _day << endl;
        }
private:
        int _year = 1;
        int _month = 1;
        int _day = 1;
};

构造函数

通常我们都是把构造函数写为全缺省形式,这样创建对象时传不传参都可以

Date::Date(int year, int month, int day)
{
        _year = year;
        _month = month;
        _day = day;
}

class Date
{
public:
        Date(int year = 1, int month = 1, int day = 1);

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

注意函数声明定义分离时,缺省值不能同时都给,要写在声明中

析构函数,拷贝构造,赋值重载对于日期类来说只用自动生成的就够了,接下来我们来看日期类的运算符重载

运算符重载

< 和 ==

先实现 < 和 ==,然后其他的运算符 != > >= 等等就可以复用了

在实现 < 时,我们可以把小于的情况都列出来,其他情况不管

  • 年小,就是小于;年相等,比较月
  • 月小,就是小于;月相等,比较日
  • 日小,就是小于

其他情况就不管了,代码如下

bool Date::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;
}

而 == 就比较好实现了,只有当两个对象的年月日严格相等时,就返回true

bool Date::operator==(const Date& d)
{
        return _year == d._year && _month == d._month && _day == d._day;
}

测试:

在这里插入图片描述

目前来看没有问题,下面就复用这两个代码,实现其他运算符重载

bool Date::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 Date::operator==(const Date& d)
{
        return _year == d._year && _month == d._month && _day == d._day;
}

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

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

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

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

在这里插入图片描述

+= 和 +

日期加日期是没有意义的,而日期加天数x可以算出x天后的日期

我们这里先实现 += ,然后实现 + 时就可以复用了。这里先留个问题:为什么不先实现 +,再复用实现 += 呢?

首先要明确返回值:+= 支持连续赋值,所以我们要将 += 过后的数返回,返回类型可以使用引用

再来看实现:

  • 先让日期加上要加的天数x
Date& Date::operator+=(int x)
{
        _day += x;
}
  • 然后处理日期越界的情况,例如 2024/1/10 += 50 结果为 2024/1/60,显然是不合理的,因此需要做进位处理:_day超过当月天数,就减去当月天数, _month进一;
  • 如果_month满了,就将 _month 设为1,_year++。
  • 循环上述操作,直到_day小于当月天数

在这里插入图片描述

因为要用到当月的天数,这里就写一个成员函数,用于获取当月的天数,同时还要考虑到闰年的 2 月有 29 天。如下:

int GetMonthDays(int year, int month)
{
        static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

        // 判断闰年
        if (month == 2 &&((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))
                return 29;
        return days[month];
}

然后是 operator+= 函数

Date& Date::operator+=(int x)
{
        _day += x;
        // 进位处理
        while (_day > GetMonthDays(_year, _month))
        {
                _day -= GetMonthDays(_year, _month);
                _month++;
                if (_month == 13)
                {
                        _month = 1;
                        _year++;
                }
        }
        return *this;
}

这里就可以测试了,为了验证代码写的对不对,可以随便找一个日期计算网站验证一下计算结果

Date d1(2024, 1, 1);
d1 += 100;
d1.Print();

测试结果:

在这里插入图片描述

初步测试应该是没问题的,接下来直接复用 += 实现 +

+ 运算符不能改变原数据,又要返回 原数据+x 的值,所以只能拷贝一份原数据,再 += x,并返回原数据。这里返回值就不可以是引用

Date Date::operator+(int x)
{
        Date tmp(*this); // 拷贝构造
        tmp += x;
        return tmp;
}

测试代码

Date d1(2024, 1, 1);
Date d2;
d2 = d1 + 1000;
d2.Print();

测试结果:

在这里插入图片描述

到这里,就算完成了 += 和 + 的重载了。这时我们就要解决开始提出的问题了:为什么不先实现 +,然后复用实现 += 呢?

以下是先实现 +,然后复用实现 += 的代码

Date Date::operator+(int x)
{
        Date tmp(*this); // 拷贝消耗
        tmp._day += x;

        // 进位处理
        while (tmp._day > GetMonthDays(tmp._year, tmp._month))
        {
                tmp._day -= GetMonthDays(tmp._year, tmp._month);
                tmp._month++;
                if (tmp._month == 13)
                {
                        tmp._month = 1;
                        tmp._year++;
                }
        }
        return tmp;
}

Date& Date::operator+=(int x)
{
        *this = *this + x; // 调用+,而+有拷贝消耗
        return *this;
}

经过对比两种写法,我们可以看出来,它们的拷贝消耗是不同的:

  • 先实现 +=,再复用实现 +。+=没有拷贝构造的消耗,而 + 拷贝构造的消耗
  • 先实现 +,再复用实现 +=。+=拷贝消耗,+拷贝消耗

在这里插入图片描述

所以我们选择消耗少的一种

-= 和 -

日期可以加天数,也就有日期减天数。这里我们依然是先实现 -=,再实现 -,不再解释

实现:

  • 先将 _day 减去天数 x
Date& Date::operator-=(int x)
{
        _day -= x;
}
  • 处理越界情况,例如 2024/1/10 -= 20 结果为 2024/1/-10,这是不合理的,需要向_month借位
  • _month–,如果 _month == 0,就需要向 _year 借位,将 _month 设为 12
  • _day 加上当月天数
  • 循环上述操作,直到 _day > 0

代码:

Date& Date::operator-=(int x)
{
        _day -= x;
        while (_day <= 0)
        {
                _month--;
                if (_month == 0)
                {
                        _month = 12;
                        _year--;
                }
                _day += GetMonthDays(_year, _month);
        }

        return *this;
}

Date Date::operator-(int x)
{
        Date tmp(*this);
        tmp -= x;
        return tmp;
}

测试:

在这里插入图片描述

在这里插入图片描述

++ 和 –

不管是前置++、–,还是后置++、–,可以复用 += 和 -= 来实现,代码如下:

Date& Date::operator++()
{
        *this += 1;
        return *this;
}
Date Date::operator++(int)
{
        Date tmp(*this);
        *this += 1;
        return tmp;
}

测试:

在这里插入图片描述

以下是前置–和后置–

Date& Date::operator--()
{
        *this -= 1;
        return *this;
}
Date Date::operator--(int)
{
        Date tmp(*this);
        *this -= 1;
        return tmp;
}

测试:

在这里插入图片描述

日期减日期

日期加日期没有意义,但是日期减日期可以算出两个日期的天数差

直接让两个日期相减不好处理,但是我们可以这样实现:

  • 区分出两个日期的大小
  • 小日期循环++,直到小日期==大日期;设置一个变量 cnt,每次循环 cnt++
  • 假设第一个参数大,设置标记 flag = 1;如果是第二个参数大,设置标记 flag = -1
  • 最后返回 cnt*flag

这样虽然效率比较慢,但是实现很简单。代码如下:

int operator-(Date& d)
{
        Date max(*this);
        Date min(d);
        int flag = 1;

        if (d > *this)
        {
                max = d;
                min = *this;
                flag = -1;
        }

        int cnt = 0;
        while (min != max)
        {
            min++;
            cnt++;
        }

        return cnt * flag;
}

测试:

在这里插入图片描述

在这里插入图片描述

流插入<<

默认的流插入识别不出自定义类型,所以如果我们想直接打印Date对象就需要重载流插入运算符<<

因为流插入操作符支持连续操作,所以返回值要返回流的引用,参数则是流对象的引用

ostream& Date::operator<<(ostream& out)
{
        out << _year << "/" << _month << "/" << _day << endl;
        return out;
}

但是这样写是有问题的:

如果重载为成员函数,那么参数列表的第一个参数就是隐含的 this 指针。调用时是这样的顺序:输出的数据 << 流

ostream& Date::operator<<(Date* this, ostream& out)

在这里插入图片描述

而我们正常使用流插入应该是这样的: 流 << 输出的数据,第一个参数是流,第二个参数是类类型

所以流插入和流提取应该重载为全局函数,在类中声明为友元函数

class Date
{
public:
    friend ostream& operator<<(ostream& out, const Date& d);
}

ostream& operator<<(ostream& out, const Date& d)
{
        out << d._year << "/" << d._month << "/" << d._day;
        return out;
}

测试:

在这里插入图片描述

流提取>>

流提取也是重载为全局函数,之后声明为Date的友元函数

class Date
{
public:
    friend istream& operator>>(istream& in, Date& d);
}

istream& operator>>(istream& in, Date& d)
{
        cout << "请输入年月日:";
        in >> d._year >> d._month >> d._day;
        return in;
}

测试:

在这里插入图片描述

为了确保用户不会输入一些不合理的日期,例如 2023/29,我们可以写一个成员函数,检查日期是否有效

bool CheckInvalid()
{
        if (_year < 0 || _month < 0 || _month > 12 || _day < 0 || _day > GetMonthDays(_year, _month))
                return false;
        return true;
}

将 CheckInvalid 加到流提取重载中,用于检测用户输入

istream& operator>>(istream& in, Date& d)
{
        while (1)
        {
                cout << "请输入年月日:";
                in >> d._year >> d._month >> d._day;
                if (d.CheckInvalid())
                        break;
                else
                        cout << "输入日期无效,请重新输入日期" << endl;
        }
        return in;
}

测试:

在这里插入图片描述

代码

Date.h

#pragma once

#include <iostream>
using namespace std;

class Date
{
public:
        Date(int year = 1, int month = 1, int day = 1);
        Date* operator&()
        {
                return (Date*)0x11223344;
        }
        // < ==  > != <= >=
        bool operator<(const Date& d);
        bool operator==(const Date& d);
        bool operator!=(const Date& d);
        bool operator<=(const Date& d);
        bool operator>(const Date& d);
        bool operator>=(const Date& d);

        // d+10
        Date& operator+=(int x);
        Date operator+(int x);

        // d-10
        Date& operator-=(int x);
        Date operator-(int x);

        // ++
        Date& operator++();
        Date operator++(int);

        // --
        Date& operator--();
        Date operator--(int);

        // d1 - d2
        int operator-(Date& d);

        // <<
        friend ostream& operator<<(ostream& out, const Date& d);
        // >>
        friend istream& operator>>(istream& out, Date& d);

        int GetMonthDays(int year, int month)
        {
                static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

                // 判断闰年
                if (month == 2 &&((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))
                        return 29;
                return days[month];
        }

        bool CheckInvalid()
        {
                if (_year < 0 || _month < 0 || _month > 12 || _day < 0 || _day > GetMonthDays(_year, _month))
                        return false;
                return true;
        }

        void Print() const
        {
                cout << _year << "/" << _month << "/" << _day << endl;
        }
private:
        int _year = 1;
        int _month = 1;
        int _day = 1;
};

// <<
ostream& operator<<(ostream& out, const Date& d);
// >>
istream& operator>>(istream& out, Date& d);

Date.cpp

#include "Date.h"

Date::Date(int year, int month, int day)
{
        _year = year;
        _month = month;
        _day = day;
}

// < == <= != > >+
bool Date::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 Date::operator==(const Date& d)
{
        return _year == d._year && _month == d._month && _day == d._day;
}

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

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

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

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


// d+10
Date& Date::operator+=(int x)
{
        _day += x;
        // 进位处理
        while (_day > GetMonthDays(_year, _month))
        {
                _day -= GetMonthDays(_year, _month);
                _month++;
                if (_month == 13)
                {
                        _month = 1;
                        _year++;
                }
        }
        return *this;
}

Date Date::operator+(int x)
{
        Date tmp(*this); // 拷贝构造
        tmp += x;
        return tmp;
}

//Date Date::operator+(int x)
//{
//        Date tmp(*this); // 拷贝消耗
//        tmp._day += x;
//
//        // 进位处理
//        while (tmp._day > GetMonthDays(tmp._year, tmp._month))
//        {
//                tmp._day -= GetMonthDays(tmp._year, tmp._month);
//                tmp._month++;
//                if (tmp._month == 13)
//                {
//                        tmp._month = 1;
//                        tmp._year++;
//                }
//        }
//        return tmp;
//}
//
//Date& Date::operator+=(int x)
//{
//        *this = *this + x; // 调用+,而+有拷贝消耗
//        return *this;
//}

// d-10
Date& Date::operator-=(int x)
{
        _day -= x;
        while (_day <= 0)
        {
                _month--;
                if (_month == 0)
                {
                        _month = 12;
                        _year--;
                }
                _day += GetMonthDays(_year, _month);
        }

        return *this;
}

Date Date::operator-(int x)
{
        Date tmp(*this);
        tmp -= x;
        return tmp;
}

// ++
Date& Date::operator++()
{
        *this += 1;
        return *this;
}
Date Date::operator++(int)
{
        Date tmp(*this);
        *this += 1;
        return tmp;
}

//--
Date& Date::operator--()
{
        *this -= 1;
        return *this;
}
Date Date::operator--(int)
{
        Date tmp(*this);
        *this -= 1;
        return tmp;
}

// d1-d2
int Date::operator-(Date& d)
{
        Date max(*this);
        Date min(d);
        int flag = 1;

        if (d > *this)
        {
                max = d;
                min = *this;
                flag = -1;
        }

        int cnt = 0;
        while (min != max)
        {
                min++;
                cnt++;
        }

        return cnt * flag;
}

// <<
ostream& operator<<(ostream& out, const Date& d)
{
        out << d._year << "/" << d._month << "/" << d._day;
        return out;
}
// >>
istream& operator>>(istream& in, Date& d)
{
        while (1)
        {
                cout << "请输入年月日:";
                in >> d._year >> d._month >> d._day;
                if (d.CheckInvalid())
                        break;
                else
                        cout << "输入日期无效,请重新输入日期" << endl;
        }
        return in;
}

const成员

当我们使用 const 修饰一个对象时,表示这个对象不可修改,当 const 对象调用成员函数时,成员函数也必须是 const 修饰的成员函数,不然会报错

const Date d1(2024, 1, 1);
d1.Print();

在这里插入图片描述

原因如下:

  • d1 调用 Print() 时会自动传 this 指针,类型是 const Date*
  • 而 Print() 是普通成员函数,参数类型是 Date*
  • const Date* 表示指向的数据不可写,只可读;而 Date* 表示数据可读可写,这就造成了权限的放大
  • 权限只可平移、缩小,不可放大

什么是权限的平移、缩小、放大呢?

在这里插入图片描述

所以 const 对象只能调用 const 成员函数,如下:

void Print() const
{
        cout << _year << "/" << _month << "/" << _day << endl;
}

被 const 修饰的成员函数就是const成员函数,实际上 const 修饰的是this 指针,表示在该函数中不可以对类的成员进行修改

思考:

  1. const对象可以调用非const成员函数吗?
  2. 非const对象可以调用const成员函数吗?
  3. const成员函数中可以调用其他非const成员函数吗?
  4. 非const成员函数中可以调用其他const成员函数吗?

回答:

  1. 不可,权限放大
  2. 可以,权限缩小
  3. 不可,权限放大
  4. 可以,权限缩小

总结

  • 对于不需要修改成员变量的成员函数,建议 const 修饰,这样 const对象和非const对象 都可以使用
  • 对于需要修改成员变量的成员函数,不用加 const,否则不能修改成员变量

取地址及const取地址操作符重载


这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容

class Date
{
public:
        Date* operator&()
        {
                return (Date*)0x11223344; // 假地址
        }
}

        return out;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿洵Rain

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值