【C++】类和对象(中)-CSDN博客

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

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

如果一个类中什么成员都没有简称为空类。但是空类中并不是真的什么都没有任何类在什么都不写的时候编译器会自动生成以下 6 个默认成员函数。
默认成员函数用户没有显式实现编译器会生成的成员函数称为默认成员函数。
class Date{
    // 空类
};

二、构造函数

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;
}

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

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;
}
注意 类中如果没有涉及资源申请拷贝构造函数是否写都可以一旦涉及到资源申请 时则拷贝构造函数是一定要写的否则就是浅拷贝。

如图指向了同一块空间。

image-20220417221717913

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

image-20220417221809176


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;
}

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

一些类需要显示写拷贝和赋值比如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;
}

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


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<iostream>
#include<assert>
#include<stdbool.h>
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 指针 表明在该成员函数中不能对类的任何成员进行修改。

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;
};
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++