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

一、类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。但是空类中并不是真的什么都没有,任何类在什么都不写的时候,编译器会自动生成以下 6 个默认成员函数。
默认成员函数用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
class Date{
    // 空类
};
【C++】类和对象(中)_第1张图片

二、构造函数

1、概念

class Date
{
public:
    void Init(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void Print()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1;
    d1.Init(2023, 1, 26);
    d1.Print();

    Date d2;
    d2.Init(2023, 8, 9);
    d2.Print();

    return 0;
}
对于 Date 类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,或者有时候忘记初始化,那能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数 名字与类名相同 ,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内 只调用一次

2、特性

构造函数是特殊的成员函数,不能以普通函数的定义和调用规则去理解,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是 初始化对象

特征:

  1. 函数名类名相同
  2. 无返回值不用写 void
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载
class Date
{
public:
    // 1、无参构造函数
    Date()
    {
        _year = 1;
        _month = 1;
        _day = 1;
    }

    // 写法相同
    /*Date(int year = 1, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }*/

  
    // 2、带参构造函数
    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};
  
void TestDate()
{
    Date d1; // 调用无参构造函数
    Date d2(2023, 9, 12); // 调用带参的构造函数
  
    Date d3(); // 声明了d3函数,该函数无参,返回一个日期类型的对象
    // warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义呢?)
}

注意:如果通过无参构造函数创建对象时对象后面不用跟括号,否则就成了函数声明。


5、如果类中没有显式定义构造函数,则 C++ 编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。 

class Date
{
public:
/*
    // 如果用户显式定义了构造函数,编译器将不再生成
    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
*/
    void Print()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
    }
  
private:
    int _year;
    int _month;
    int _day;
};
  
int main()
{ 
    Date d1;
    // 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用

    return 0;
}

将 Date 类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数。

将 Date 类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成。


6、关于编译器生成的默认成员函数,在不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d 对象调用了编译器生成的默认构造函数,但是 d 对象 _year / _month / _day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用?

C++ 把类型分成内置类型(基本类型)和自定义类型
  • 内置类型就是语言提供的数据类型,如:int / double / char / 指针等。
  • 自定义类型就是我们使用 class / struct / union 等自己定义的类型。

从下面的代码中可以发现编译器生成默认的构造函数会对自定类型成员 _t 调用的它的默认成员函数。也就是说,默认生成的构造函数对内置类型成员不作处理,对自定义类型成员去调用它的默认构造函数这个设计是 C++ 早期设计的一个缺陷,本来应该内置类型也一并处理。

  1. 对于类中的内置类型成员 —> 不处理(为随机值,除非声明时给了缺省值 - C++11)
  2. 对于类中的自定义类型成员 —> 自动调用它的默认构造函数(不要参数就可以调用的,比如 无参构造函数 或 全缺省构造函数
class Time
{
public:
    Time()
    {
        cout << "Time()" << endl;
        _hour = 0;
        _minute = 0;
        _second = 0;
    }
private:
    int _hour;
    int _minute;
    int _second;
};

class Date
{
private:
    // 基本类型(内置类型)
    int _year;
    int _month;
    int _day;

    // 自定义类型
    Time _t;
};

int main()
{
    Date d;
    return 0;
}
注意 :C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在
类中声明时可以给默认值,注意这里不是初始化,是声明,给缺省值。
class Time
{
public:
    Time()
    {
        cout << "Time()" << endl;
        _hour = 0;
        _minute = 0;
        _second = 0;
    }
private:
    int _hour;
    int _minute;
    int _second;
};

class Date
{
private:
    // 基本类型(内置类型)
    // 给缺省值
    int _year = 1; // 声明
    int _month = 1;
    int _day = 1;
 
    // 自定义类型
    Time _t;
};

int main()
{
    Date d;
    return 0;
}

7、无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。

注意 :默认构造函数:(不传参数就可以调用的)
  1. 无参构造函数。
  2. 全缺省构造函数。
  3. 没写,编译器默认生成的构造函数
class Date
{
public:
    // 1、无参构造函数
    Date()
    {
        _year = 1;
        _month = 1;
        _day = 1;
    }

    // 2、带参构造函数
    Date(int year = 1, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1; // 无法通过编译 - 调用存在二义性
    return 0;
}

构造函数特点不用传参数就可以调用。

  1. 一般的类都不会让编译器默认生成构造函数,大部分都会自己去写。显示写一个全缺省,很好用。
  2. 特殊情况才会默认生成构造函数(例如 Myqueue 这样的类)。
  3. 每个类最好都要提供默认构造函数。

三、析构函数

1、概念

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没的呢?
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

构造函数是为了替代 Init,析构函数是为了替代 Destroy。 


2、特性 

析构函数是特殊的成员函数,其特征如下:

  1. 析构函数名是在类名前加上字符 ~
  2. 无参数无返回值类型
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意析构函数不能重载
  4. 对象生命周期结束时,C++ 编译系统系统自动调用析构函数。
  5. 后定义的先析构,跟数据结构中的栈性质相同。
typedef int DataType;
class Stack
{
public:
    Stack(size_t capacity = 4)
    {
        _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 s;
    s.Push(1);
    s.Push(2);
}

6、关于编译器自动生成的析构函数,从下面的程序可以看到,编译器生成的默认析构函数,对自定义类型成员调用它的析构函数。

  1. 对于类中的内置类型成员 —> 不处理。

  2. 对于类中的自定义类型成员 —> 调用它的析构函数完成清理工作。

class Time
{
public:
    ~Time()
    {
        cout << "~Time()" << endl;
    }
private:
    int _hour;
    int _minute;
    int _second;
};

class Date
{
private:
    // 基本类型(内置类型)
    int _year = 1970;
    int _month = 1;
    int _day = 1;

    // 自定义类型
    Time _t;
};

int main()
{
    Date d;
    return 0;
}
// 程序运行结束后输出:~Time()
在 main 方法中根本没有直接创建 Time 类的对象,为什么最后会调用Time类的析构函数?


因为 main 方法中创建了 Date 对象 d,而 d 中包含 4 个成员变量,其中 _year,  _month, _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而 _t 是 Time 类对象,所以在 d 销毁时,要将其内部包含的 Time 类的 _t 对象销毁,所以要调用 Time 类的析构函数。但是 main 函数中不能直接调用 Time 类的析构函数,实际要释放的是 Date 类对象,所以编译器会调用 Date 类的析构函数,而 Date 没有显式提供,则编译器会给 Date 类生成一个默认的析构函数,目的是在其内部调用 Time 类的析构函数,即当 Date 对象销毁时,要保证其内部每个自定义对象都可以正确销毁。main 函数中并没有直接调用 Time 类析构函数,而是显式调用编译器为 Date 类生成的默认析构函数。


注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数。


7、如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date 类;有资源申请时,一定要写,否则会造成资源泄漏,比如 Stack 类。

默认生成析构函数的特点(跟构造函数类似):

  1. 一些内置类型的类不作处理。比如 Date这样的类,没有资源需要清理;比如 MyQueue 也可以不写,默认生成的就可以。
  2. 自定义类型成员回去调用它的析构函数,比如:Stack、Queue...

四、拷贝构造函数

1、概念

在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
拷贝构造函数 只有单个形参,该形参是对本类类型对象的引用(一般常用 const 修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

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) // 正确写法
    //Date(const Date d)    // 错误写法:编译报错,会引发无穷递归
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

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

int main()
{
    Date d1;
    Date d2(d1);

    return 0;
}

【C++】类和对象(中)_第2张图片

使用引用作为拷贝构造函数的参数可以有效避免无穷递归调用的问题。因为引用在初始化时不会调用拷贝构造函数,而是直接将引用绑定到已经存在的对象上。

tips:建议拷贝构造函数的参数类型前加上 const,防止其值误被修改。


3、若未显示定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

浅拷贝:

  1. 一个对象修改会影响另一个对象。
  2. 会析构两次,程序崩溃。

解决方法:自己实现深拷贝。 

如果没有显式定义,编译器自动生成的拷贝构造函数,它会做哪些事情呢?

  1. 对于类中的内置类型成员 —> 值拷贝
  2. 对于类中的自定义类型成员 —> 自动调用它的拷贝构造函数来完成拷贝初始化。
class Time
{
public:
    Time()
    {
        _hour = 1;
        _minute = 1;
        _second = 1
    }

    Time(const Time& t)
    {
        _hour = t._hour;
        _minute = t._minute;
        _second = t._second;
        cout << "Time::Time(const Time&)" << endl;
    }
private:
    int _hour;
    int _minute;
    int _second;
};

class Date
{
private:
    // 基本类型(内置类型)
    int _year = 1;
    int _month = 1;
    int _day = 1;
 
    // 自定义类型
    Time _t;
};

int main()
{
    Date d1;
    Date d2(d1);

    return 0;
}

用已经存在的 d1 拷贝构造 d2,此处会调用 Date 类的拷贝构造函数。但 Date 类并没有显式定义拷贝构造函数,则编译器会给 Date 类生成一个默认的拷贝构造函数。

注意 :在编译器生成的默认拷贝构造函数中,内置类型 按照字节方式直接拷贝 的,而自定
义类型 调用其拷贝构造函数 完成拷贝的。

4、编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?

像日期类这样的类是没必要的。但有些需要深拷贝的类,其内部往往是很复杂的,是需要用户显式定义拷贝构造函数来完成深拷贝的。

// 下面这个程序会崩溃掉,因为这里就需要深拷贝去解决。
typedef int DataType;

class Stack
{
public:
    Stack(size_t capacity = 10)
    {
        _array = (DataType*)malloc(capacity * sizeof(DataType));
        if (nullptr == _array)
        {
            perror("malloc申请空间失败");
            return;
        }
        size = 0;
        _capacity = capacity;
    }

    void Push(const DataType& data)
    {
        // CheckCapacity();
        _array[_size] = data;
        _size++;
    }

    ~Stack()
    {
        if (_array)
        {
            free(_array);
            _array = nullptr;
            _capacity = 0;
            _size = 0;
        }
    }

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

int main()
{
    Stack s1;
    s1.Push(1);
    s1.Push(2);
    s1.Push(3);
    s1.Push(4);

    Stack s2(s1); // 会造成指向的同一块区域的内容被两次释放
    return 0;
}
【C++】类和对象(中)_第3张图片
注意 :类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

如图:指向了同一块空间。

【C++】类和对象(中)_第4张图片

那么会引发什么问题呢?会导致 _str 指向的空间被释放两次,引发程序崩溃

【C++】类和对象(中)_第5张图片


5、拷贝构造函数典型调用场景:

  1. 使用已存在对象创建新对象。
  2. 函数参数类型为类类型对象。
  3. 函数返回值类型为类类型对象。
class Date
{
public:
    Date(int year, int minute, int day)
    {
        cout << "Date(int, int, int):" << this << endl;
    }
    Date(const Date& d)
    {
        cout << "Date(const Date& d):" << this << endl;
    }
    ~Date()
    {
        cout << "~Date():" << this << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};

Date Test(Date d)
{
    Date temp(d);
    return temp;
}

int main()
{
    Date d1(2023 ,9, 13);
    Test(d1);

    return 0;
}

【C++】类和对象(中)_第6张图片

为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用
尽量使用引用。

一些类需要显示写拷贝和赋值,比如:Stack、Queue...

一些类不需要显示写拷贝和赋值。比如 Date 这样的类,默认生成就会完成值拷贝 / 浅拷贝,比如 MyQueue 这样的类,默认生成就会调用它的自定义类型成员 Stack 的拷贝和赋值。


五、赋值运算符重载

1、运算符重载

C++ 为了增强代码的可读性引入了 运算符重载 运算符重载是具有特殊函数名的函数,也具有其返回值类型函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
  1. 函数名字:关键字 operator 后面接需要重载的运算符符号
  2. 函数原型:返回值类型 operator 操作符(参数列表)

内置类型可以直接使用运算符运算,编译器知道要如何运算。

自定义类型无法直接使用运算符,编译器不知道要如何运算。

  注意
  • 不能通过连接其他符号来创建新的操作符:比如 operator@。
  • 重载操作符必须有一个类类型参数。
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型 +,不能改变其含义。
  • 作为类成员函数重载时,其形参看起来比操作数数目少 1,因为成员函数的第一个参数为隐藏的 this
  • .*   ::   sizeof   ?:   .  注意以上 5 个运算符不能重载 
// 全局的operator==
class Date
{ 
public:
    Date(int year = 1, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

//private:
    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;
}

void Test ()
{
    Date d1(2023, 9, 13);
    Date d2(2023, 9, 14);
    cout << (d1 == d2) << endl;
}

运算符重载,一般有两种方式

  • 重载成类的成员函数(形参数目看起来比该运算符需要的参数少一个,因为成员函数有隐含的 this 指针,且函数的第一个形参就是 this 指针)。
  • 重载成类的友元函数(必须有一个参数要是类的对象)(一般不这样做,而是重载成成员函数)。

下面这种写法更好: 

class Date
{ 
public:
    Date(int year = 1, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
   }
    
    // bool operator==(Date* this, const Date& d2)
    bool operator==(const Date& d2) // 注意左操作数是隐藏的this,指向调用函数的对象
    {
        return _year == d2._year && _month == d2._month && _day == d2._day;
    }

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

void Test ()
{
    Date d1(2023, 9, 13);
    Date d2(2023, 9, 14);
    cout << d1.operator==(d2) << endl; // d1.operator==(&d1, d2);
}

2、赋值运算符重载

(1)赋值运算符重载格式
  • 参数类型const T&传递引用可以提高传参效率。
  • 返回值类型T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  • 检测是否自己给自己赋值
  • 返回 *this :要复合连续赋值的含义。
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;

        return *this; // 支持连续赋值
    }
 
    Date& operator=(const Date& d) // 传引用返回
    {
        if(this != &d) // 检测是否自己给自己赋值
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }   
        return *this;
}
private:
    int _year ;
    int _month ;
    int _day ;
};

void TestDate()
{
    Date d1(2023, 9, 13);
    Date d2(d1);

    Date d3(2023, 11, 28);
    d2 = d1 = d3;
}

(2)赋值运算符只能重载成类的成员函数不能重载成全局函数
class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    int _year;
    int _month;
    int _day;
};

// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
    if (&left != &right)
    {
        left._year = right._year;
        left._month = right._month;
        left._day = right._day;
    }
    return left;
}

编译失败:error C2801: “operator =”必须是非静态成员

原因 :赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现
一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值
运算符重载只能是 类的成员函数

(3)用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝
注意 :内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
class Time
{
public:
    Time()
    {
        _hour = 1;
        _minute = 1;
        _second = 1;
    }
    Time& operator=(const Time& t)
    {
        if (this != &t)
        {
            _hour = t._hour;
            _minute = t._minute;
            _second = t._second;
        }
        return *this;
    }
private:
    int _hour;
    int _minute;
    int _second;
};

class Date
{
private:
    // 基本类型(内置类型)
    int _year = 1;
    int _month = 1;
    int _day = 1;
    
    // 自定义类型
    Time _t;
};

int main()
{
    Date d1;
    Date d2;
    d1 = d2;
    return 0;
}

既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?

像日期类这样的类是没必要的。但有些需要深拷贝的类,其内部往往是很复杂的,是需要用户显式定义赋值运算符重载函数来完成深拷贝的。

// 下面这个程序会崩溃,这里需要用深拷贝去解决。
typedef int DataType;
class Stack
{
public:
    Stack(size_t capacity = 10)
    {
        _array = (DataType*)malloc(capacity * sizeof(DataType));
        if (nullptr == _array)
        {
            perror("malloc申请空间失败");
            return;
        }
        _size = 0;
        _capacity = capacity;
    }

    void Push(const DataType& data)
    {
        // CheckCapacity();
        _array[_size] = data;
        _size++;
    }

    ~Stack()
    {
        if (_array)
        {
            free(_array);
            _array = nullptr;
            _capacity = 0;
            _size = 0;
        }
    }

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

int main()
{
    Stack s1;
    s1.Push(1);
    s1.Push(2);
    s1.Push(3);
    s1.Push(4);

    Stack s2;
    s2 = s1;
    return 0;
}

注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。

【C++】类和对象(中)_第7张图片


3、前置++和后置++重载

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

    Date& operator++() // 前置++:返回+1之后的结果。
    {
        _day += 1;
        return *this; // this 指向的对象函数结束后不会销毁,故以引用的方式返回提高效率。
    }
 
    Date operator++(int) // 后置++是先使用后+1,因此需要返回+1之前的旧值
    {
        Date temp(*this); // 在实现时需要先将this保存一份,然后给this+1
        _day += 1;
        return temp; // 因为temp是临时对象,因此只能以值的方式返回,不能返回引用
    }

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

int main()
{
    Date d;
    Date d1(2023, 9, 13);
    d = d1++; // d: 2023,9,13  d1:2023,9,14
    d = ++d1; // d: 2023,9,14  d1:2023,9,14
    return 0;
}

前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载,C++ 规定:后置++重载时多增加一个 int 类型的参数,但调用函数时该参数不用传递,编译器自动传递。前置返回的是引用后置返回的是


六、日期类的实现

1、Date.h

// Date.h
#pragma once

#include
#include
#include
using namespace std;

class Date
{
public:
    // 获取某年某月的天数
    int GetMonthDay(int year, int month)
    {
        assert(month >= 1 && month <= 12);

        // 每月的天数(这个函数会被频繁调用,每次进来都要重新定义数组,所以将其定义为静态的)
		// 默认是平年
        static int days[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

        int day = days[month];
        if (month == 2 &&((year % 4 == 0 && year % 100 != 0) || (year%400 == 0)))
        // 把month == 2写在前面可以直接筛选出更少的内容
        {
            day += 1; // 闰年的二月是29天
        }
        return day;
    }
 
    Date(int year = 1, int month = 1, int day = 1) // 全缺省的构造函数
	{
		_year = year;
		_month = month;
		_day = day;

		//判断日期是否合法
		if (_year < 0 || _month <= 0 || _month >= 13 || _day <= 0 || _day > GetMonthDay(_year, _month))
		{
			cout << _year << "/" << _month << "/" << _day << "->";
			cout << "非法日期" << endl;
		}
	}

    // 打印日期
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
    
    // 拷贝构造、赋值运算符、析构函数用编译器自动生成的就可以了(因为Date类是浅拷贝)
    

    // 日期 += 天数 --> d1 += 100
	Date& operator+=(int day);
    
    // 日期 + 天数 --> d1 + 100
	Date operator+(int day);

	// 日期 -= 天数 --> d1 -= 100
	Date& operator-=(int day);
    
    // 日期 - 天数 --> d1 - 100
	Date operator-(int day);

    // 前置++
	Date& operator++(); // 编译器会解释为:Date& operator++(Date* const this);

	// 后置++
	Date operator++(int); // 编译器会解释为:Date& operator++(Date* const this, int);

	// 前置--
	Date& operator--();

	// 后置--
	Date operator--(int);

    // >运算符重载
	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 _year == d._year && _month == d._month && _day == d._day;
	}

    // 这里我们只需要把>和==运算符重载了,下面的运算符都可以复用其代码了
    
	// >=运算符重载
	bool operator>=(const Date& d)
	{
		return *this > d || *this == d; // 复用operator>、operator==
	}

	// <运算符重载
	bool operator<(const Date& d)
	{
		return !(*this >= d); // 复用operator>=,再取反
	}

	// <=运算符重载
	bool operator<=(const Date& d)
	{
		return !(*this > d); // 复用operator>,再取反
	}

	// !=运算符重载
	bool operator!=(const Date& d)
	{
		return !(*this == d); // 复用operator==,再取反
	}

    // 日期 - 日期(返回相差天数) --> d1 - d2
	int operator-(const Date& d);

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

2、Date.cpp

(1)日期 += 天数(返回累加天数后的日期)

比如:d1 += 100

注意:d1本身要被更改,天数累加到 d1 上面去。

Date& Date::operator+=(int day)
{
	if (day < 0) // 如果day是负数,就向前计算,相当于 -=
	{
		return *this -= -day; // 调用-=运算符重载函数
	}

	_day += day; // 累加天数

    // 日期不合法,需要进位
	while (_day > GetDays(_year, _month)) // 表示当前月的天数已经过完了
	{
		_day -= GetDays(_year, _month);   // 减去当前月的天数
		_month++; // 月进位

		if (_month == 13) // 判断当前月份是否合法
		{
			_year++; // 年进位
			_month = 1; // 更新为1月
		}
	}

	return *this;

	/* 写法二:复用+运算符重载函数的代码
	*this = *this + day; // d1等价于*this,对d1进行+天数操作,再赋值给d1
	return *this;        // 返回d1
	*/
}

 (2)日期 + 天数(返回累加天数后的日期)

比如 :d1 + 100

注意:d1本身不能被更改,天数累加到一个临时对象上面去。

// 写法一:
Date Date::operator+(int day)
{
	Date tmp(*this); // 拷贝构造一份临时对象,防止调用本函数的对象被更改

	tmp._day += day; // 累加天数
	while (tmp._day > GetDays(tmp._year, tmp._month)) // 表示当前月的天数已经过完了
	{
		tmp._day -= GetDays(tmp._year, tmp._month);   // 减去当前月的天数

		tmp._month++; // 月进位

		if (tmp._month == 13) // 判断当前月份是否合法
		{
			tmp._year++;      // 年进位
			tmp._month = 1;   // 更新为1月
		}
	}

	return tmp; // 返回临时对象
}

// 写法二:
Date Date::operator+(int day)
{
	/* 复用 += 运算符重载函数的代码 */

	Date tmp(*this); // 拷贝构造一份临时对象
	tmp += day;      // 对临时对象进行 += 天数操作
	return tmp;      // 返回临时对象
}

(3)日期 -= 天数(返回累减天数后的日期)

比如:d1 -= 100

Date& Date::operator-=(int day)
{
	if (day < 0) // 如果day小于0,就往后计算,相当于 +=
	{
		return *this += -day; // 调用+=运算符重载函数
	}

	_day -= day; // 累减天数

	while (_day <= 0) // 说明天数不够减了,需要向上一个月去借
	{
		_month--; // 月份-1
		if (_month == 0)
		{
			_year--;
			_month = 12;
		}
		_day += GetDays(_year, _month); // 借上一个月的天数
	}

	return *this;
}

 (4)日期 - 天数(返回累减天数后的日期)

比如:d1 - 100

Date Date::operator-(int day)
{
	// 复用 -= 运算符重载函数的代码
	Date tmp(*this); // 拷贝构造一份临时对象
	tmp -= day;      // 对临时对象进行 -= 天数操作
	return tmp;      // 返回临时对象
}

 (5)前置++ 和 后置++

注意:按正常的运算符重载规则,无法区分 前置++ 和 后置++,为了区分,这里做了一个特殊处理,给 后置++ 增加了一个 int 参数,这个参数仅仅是为了区分,使 前置++ 和 后置++ 构成重载。

// 前置++
// ++d1
Date& Date::operator++()
{
	// 复用 += 运算符重载函数的代码
	*this += 1;
	return *this;
}

// 后置++
// d1++
Date Date::operator++(int)
{
	Date tmp(*this); // 保存当前对象自减前的值
	*this += 1; // 复用 += 运算符重载函数的代码
	return tmp; // 返回当前对象自减前的值
}

(6)前置-- 和 后置– 
// 前置--
// --d1
Date& Date::operator--()
{
	// 复用 -= 运算符重载函数的代码 
	*this -= 1;
	return *this;
}

// 后置--
// d1--
Date Date::operator--(int)
{
	Date tmp(*this); // 保存当前对象自减前的值
	*this -= 1; // 复用 -= 运算符重载函数的代码
	return tmp; // 返回当前对象自减前的值
}

(7)日期 - 日期(返回相差的天数,有正负之分)

比如:d1 - d2 

思路:让小的日期不断往后++,直到等于大的日期,统计加了多少次,就相差多少天。

  • 大的日期 - 小的日期 = 正的天数
  • 小的日期 - 大的日期 = 负的天数
int Date::operator-(const Date& d)
{
	// 判断出大的日期和小的日期
	Date max = *this;
	Date min = d;
	int flag = 1; // 加一个flag变量来控制天数的正负

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

	// 让小的日期累加天数,加了多少次,说明就相差了多少天
	int count = 0;
	while (min != max)
	{
		++min;
		++count;
	}

	return flag * count;
}

【总结】 
// Date.cpp
#include "Date.h"

// 日期 += 天数
Date& Date::operator+=(int day)
{
	if (day < 0) // 如果day是负数,就向前计算,相当于 -=
	{
		return *this -= -day; // 调用-=运算符重载函数
	}

	_day += day; // 累加天数

    // 日期不合法,需要进位
	while (_day > GetDays(_year, _month)) // 表示当前月的天数已经过完了
	{
		_day -= GetDays(_year, _month);   // 减去当前月的天数
		_month++; // 月进位

		if (_month == 13) // 判断当前月份是否合法
		{
			_year++; // 年进位
			_month = 1; // 更新为1月
		}
	}
	return *this;
}

// 日期 + 天数
Date Date::operator+(int day)
{
	Date tmp(*this); // 拷贝构造一份临时对象,防止调用本函数的对象被更改

	tmp._day += day; // 累加天数
	while (tmp._day > GetDays(tmp._year, tmp._month)) // 表示当前月的天数已经过完了
	{
		tmp._day -= GetDays(tmp._year, tmp._month);   // 减去当前月的天数

		tmp._month++; // 月进位

		if (tmp._month == 13) // 判断当前月份是否合法
		{
			tmp._year++;      // 年进位
			tmp._month = 1;   // 更新为1月
		}
	}

	return tmp; // 返回临时对象
}

// 日期 -= 天数
Date& Date::operator-=(int day)
{
	if (day < 0) // 如果day小于0,就往后计算,相当于 +=
	{
		return *this += -day; // 调用+=运算符重载函数
	}

	_day -= day; // 累减天数

	while (_day <= 0) // 说明天数不够减了,需要向上一个月去借
	{
		_month--; // 月份-1
		if (_month == 0)
		{
			_year--;
			_month = 12;
		}
		_day += GetDays(_year, _month); // 借上一个月的天数
	}

	return *this;
}

// 日期 -= 天数
Date Date::operator-(int day)
{
	// 复用 -= 运算符重载函数的代码
	Date tmp(*this); // 拷贝构造一份临时对象
	tmp -= day;      // 对临时对象进行 -= 天数操作
	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; // 返回当前对象自减前的值
}

// 日期 - 日期
int Date::operator-(const Date& d)
{
	// 判断出大的日期和小的日期
	Date max = *this;
	Date min = d;
	int flag = 1; // 加一个flag变量来控制天数的正负

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

	// 让小的日期累加天数,加了多少次,说明就相差了多少天
	int count = 0;
	while (min != max)
	{
		++min;
		++count;
	}
	return flag * count;
}

七、const 成员

将 const 修饰的 “ 成员函数 ” 称之为 const 成员函数,const 修饰类成员函数,实际修饰该成员函数 隐含的 this 指针 ,表明在该成员函数中不能对类的任何成员进行修改。

【C++】类和对象(中)_第8张图片

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

    void Print() // void Print(Date* const this)
    {
        cout << "Print()" << endl;
        cout << "year:" << _year << endl;
        cout << "month:" << _month << endl;
        cout << "day:" << _day << endl << endl;
    }

    void Print() const // void Print(const Date* const this)
    {
        cout << "Print() const" << endl;
        cout << "year:" << _year << endl;
        cout << "month:" << _month << endl;
        cout << "day:" << _day << endl << endl;
    }
private:
    int _year;  // 年
    int _month; // 月
    int _day;   // 日
};

void Test()
{
    Date d1(2022,1,1);
    d1.Print();

    const Date d2(2023,1,1); // const修饰
    d2.Print(); // d2.Print(&d2); // &d2的类型是const Date*,只能读不能写
    // 传给第一个Print会导致权限放大为可读可写
}
  1. const 修饰成员函数是有好处的,这样 const 对象可以调用,非 const 对象也可以调用。
  2. 但并不是说所有的成员函数都要加 const ,具体得看成员函数的功能,如果成员函数是修改型(比如:operrato+=、Push),那就不能加;如果是只读型(比如:Print、operator+),那就最好加上 const。
  3. const 成员(只读)函数内不可以调用其它的非 const 成员(可读可写)函数(权限放大),非 const 成员(可读可写)函数内可以调用其它的 const 成员(只读)函数(权限缩小)。

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

下面这两个是默认成员函数,一般不用重新定义,不写编译器默认会自动生成。
class Date
{ 
public:
    Date* operator&()// Date* operator&(Date* const this)
    {
        return this;
    }

    const Date* operator&()const // const Date* operator&(const Date* const this)
    {
        return this;
    }
private:
    int _year;  // 年
    int _month; // 月
    int _day;   // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如不想让别人获取到这个类型对象的地址
class Date {
public:
    Date* operator&()
    {
		return nullptr;
	}
    
	const Date* operator&() const
    {
		return nullptr;
	}  
private:
	int _year;
	int _month;
	int _day;
};

你可能感兴趣的:(C++,学习,c++,开发语言,学习)