手撕C++类和对象(中)

2024-09-23 21:13:45 浏览数 (2)

1.类的默认成员函数

默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个类,我 们不写的情况下编译器会默认⽣成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最 后两个取地址重载不重要,我们稍微了解⼀下即可。其次就是C 11以后还会增加两个默认成员函数, 移动构造和移动赋值,这个我们后⾯再讲解。默认成员函数很重要,也⽐较复杂,我们要从两个⽅⾯ 去学习:

• 第⼀:我们不写时,编译器默认⽣成的函数⾏为是什么,是否满⾜我们的需求。

• 第⼆:编译器默认⽣成的函数不满⾜我们的需求,我们需要⾃⼰实现,那么如何⾃⼰实现?

我们不写,编译器默认生成的函数即是默认成员函数

2. 构造函数

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并 不是开空间创建对象(我们常使⽤的局部对象是栈帧创建时,空间就开好了),⽽是对象实例化时初始化 对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数⾃动调⽤的 特点就完美的替代的了Init。

不能以常规的看法看待这个函数

构造函数的特点

  1. 函数名与类名相同。
  2. ⽆返回值。(返回值啥都不需要给,也不需要写void,不要纠结,C 规定如此)
  3. 对象实例化时系统会⾃动调⽤对应的构造函数。
  4. 构造函数可以重载。

构造的基础样式:

代码语言:javascript复制
class Data
{
public:

    //Data()//函数名和类名是相同的
    //{
    //  _year = 1;
    //  _month = 1; 
    //  _day = 1;
    //}
    //全缺省
    Data(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;
    int _month;
    int _day;
};
int main()
{
    //Data f1();存在歧义,不能这么写
    Data d1;//构造函数在对象实例化的时候自动调用
    Data d2(2024, 10, 8);
    Data d3(2024);//使用全缺省
    /*
    以前我们调用的时候都是函数名 参数
    这里是对象 参数
    因为函数在对象实例化的时候自动调用
    我们创建对象的时候边创建边调用

    而且没有参数的时候后面还不能加括号,加括号是存在歧义的

    */
    d1.Print();
    d2.Print();
    d3.Print();

    //只要我们实例了对象的话,那么我们是一定能调用里面的函数的
    return 0;
}
/*
我们在定义类中的函数的时候是不能将无参数的函数和全缺省的函数放在一起的
因为会有歧义的

*/
  1. 如果类中没有显式定义构造函数,则C 编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦⽤⼾显 式定义编译器将不再⽣成。
  2. ⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成 函数重载,但是调⽤时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫 默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调 ⽤的构造就叫默认构造。
  3. 我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始 化是不确定的,看编译器。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始 化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤ 初始化列表才能解决,初始化列表,我们下个章节再细细讲解

说明:C 把类型分成内置类型(基本类型)和⾃定义类型。内置类型就是语⾔提供的原⽣数据类型, 如:int/char/double/指针等,⾃定义类型就是我们使⽤class/struct等关键字⾃⼰定义的类型。

默认构造的一些介绍:

代码语言:javascript复制
class Data
{
public:
    //这个函数既不是全缺省也不是无参数
    //Data(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;
};
//栈
typedef int STDataType;
class Stack
{
public:
    Stack(int n = 4)
    {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        if (nullptr == _a)
        {
            perror("malloc申请空间失败");
            return;
        }
        _capacity = n;
        _top = 0;
    }
private:
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};

//两个栈实现一个队列
class MyQueue
{
private:
    Stack _pushst;//入栈
    Stack _popst;//出栈
};

int main()
{

    Data d1;
    //我们这里是没有写构造函数的代码
    //但是我们在这里定义了一个构造函数的对象
    //那么C  的编译器会自动生成一个构造函数
    //然后我们这里打印的就是三个随机值
    d1.Print();

    Stack st1;

    MyQueue mq;//没有写构造函数

    return 0;
}

/*
默认构造函数有三种:
无参数构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数

并且这三个函数有且只有一个存在,不能同时存在多个

无参和全缺省虽然一起调用的时候构成重载,但是会存在歧义的

反正这三个函数不可能同时存在的

总结就是不传实参就能调用的构造就是默认构造
*/
/*
存在代码中没有构造函数的情况
我们在代码中写一个上面那样的函数
这个函数既不是全缺省函数也不是无参函数
但是这里已经有了一个函数
那么编译器就不会生成额外的构造函数
那么这个类中就不会有构造函数了

所以说不传参的时候调用的构造就是默认构造函数


一般情况下我们都是要自己写构造的代码,靠编译器是靠不住的

我们如果自己不提供构造函数的话,任凭编译器发挥就有问题了
*/

默认构造函数有三种:

无参数构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数

并且这三个函数有且只有一个存在,不能同时存在多个

无参和全缺省虽然一起调用的时候构成重载,但是会存在歧义的

反正这三个函数不可能同时存在的

总结就是不传实参就能调用的构造就是默认构造

第⼀:我们不写时,编译器默认⽣成的函数⾏为是什么,是否满⾜我们的需求。

• 第⼆:编译器默认⽣成的函数不满⾜我们的需求,我们需要⾃⼰实现,那么如何⾃⼰实现?

结论:一般情况下,构造函数都要自己写

只有少数情况,默认生成就可以用,比如MyQueue

编译器默认生成MyQueue的构造函数调用了我们之前写的Stack的构造,完成了两个成员的初始化

3. 析构函数

析构函数与构造函数功能相反,析构函数不是完成对对象本⾝的销毁,⽐如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C 规定对象在销毁时会⾃动调⽤析构函数,完成对象中资源的清理释放⼯作。析构函数的功能类⽐我们之前Stack实现的Destroy功能,⽽像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。

析构函数的特点:1. 析构函数名是在类名前加上字符 ~。表达和构造函数的功能相反

  1. ⽆参数⽆返回值。 (这⾥跟构造类似,也不需要加void)

没有参数,不能重载

  1. ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。
  2. 对象⽣命周期结束时,系统会⾃动调⽤析构函数。
  3. 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数。
  4. 还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数。
  5. 如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如Date;如果默认⽣成的析构就可以⽤,也就不需要显⽰写析构,如MyQueue;但是有资源申请时,⼀定要⾃⼰写析构,否则会造成资源泄漏,如Stack。
  6. ⼀个局部域的多个对象,C 规定后定义的先析构。

析构函数不是完成对象本身的销毁的,而是完成对象中资源的清理释放工作

内置类型成员指的是int float bool等系统自带的类型的

自定义类型的成员就是自己定义的类中的成员

析构函数的讲解以及使用方法:

代码语言:javascript复制
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;

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

    ~Date()//析构函数
    {
        cout << "~Date()" << endl;
    }
private:
    int _year;
    int _month;
    int _day;

};
typedef int STDataType;
class Stack
{

public:
    Stack(int n = 4)
    {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        if (nullptr == _a)
        {
            perror("malloc申请空间失败");
            return;
        }
        _capacity = n;
        _top = 0;
    }
    ~Stack()
    {
        cout << "~Stack()" << endl;
        free(_a);
        _a = nullptr;
        _top = _capacity = 0;
    }
private:
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};

//两个栈生成一个队列
class MyQueue
{
public:
private:
    //我们用之前创建的类创实例化出两个对象
    Stack _puahst;
    Stack _popst;

};
//
int main()
{
    Date d;//这个对象声明周期结束之后会自动调用析构函数

    Stack st;//一开始会自动调用构造,结束之后会自动调用析构函数进行释放
    MyQueue mq;//这里会调用两次栈的析构,因为这个类里面有两个栈对象
    return 0;
}
/*
日期类一般是不需要析构函数的
但是栈是需要析构函数的,因为栈有资源需要清理的

对于日期类的话,我们是不需要进行内存释放的
但是我们可以一些小操作
在对象的声明周期最后我们可以让对象中的值进行一系列的操作,反正声明周期之后会自动调的
*/

/*
跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,
自定义类型成员会调用编译器生成的析构函数
MyQueue mq;//这里会调用两次栈的析构,因为这个类里面有两个栈对象
这两个对象就是我们自定义类型的对象

自定义类型的对象我们是不需要管的,只要创建出来了肯定是会自动调用析构函数的

总结:一般情况下显示了申请的资源,才需要自己实现析构,其他情况基本都不需要显示写
栈需要
日期类不需要

析构函数需要将所有资源都释放掉

后定义的对象先析构

如果我们显示写了析构函数的话,自定义类型的成员也会调用它对应的析构函数

不管如何,自定义类型的成员肯定是会调用析构函数的

析构函数的作用和destory的作用相似,都是在对象的生命周期块快结束的时候进行内存空间的释放
那么我们写了析构函数之后就不用写destory函数了,而且这个析构函数是自动调用的
*/

在函数前面加上~ 表示取反,不加的话就是构造函数,加了取反的话就是说明效果不同

析构函数不需要参数

对于自定义类型的对象是会自动调用对应的析构函数的,不管这个类里面有没有其他的析构函数

都不影响自定义类型对于它自己的析构函数的调用的

C/C 两种方法做括号匹配的题目,感受析构的特点:

代码语言:javascript复制
// ⽤之前C版本Stack实现
//bool isValid(const char* s) {
//    ST st;
//    STInit(&st);
//    while (*s)
//    {
//        // 左括号⼊栈
//        if (*s == '(' || *s == '[' || *s == '{')
//        {
//            STPush(&st, *s);
//        }
//        else // 右括号取栈顶左括号尝试匹配
//        {
//            if (STEmpty(&st))
//            {
//                STDestroy(&st);
//                return false;
//            }
//            char top = STTop(&st);
//            STPop(&st);
//            // 不匹配
//            if ((top == '(' && *s != ')')
//                || (top == '{' && *s != '}')
//                || (top == '[' && *s != ']'))
//            {
//                STDestroy(&st);
//                return false;
//            }
//        }
//          s;
//    }
//    
//        // 栈不为空,说明左括号⽐右括号多,数量不匹配
//    bool ret = STEmpty(&st);
//    STDestroy(&st);
//    return ret;
//}
//
// ⽤之前C版本Stack实现
bool isValid(const char* s) {
    STack st;//对象定义的时候自动调用构造,那么自动进行初始化,就不需要再自己写初始化了
    //STInit(&st);
    while (*s)
    {
        // 左括号⼊栈
        if (*s == '(' || *s == '[' || *s == '{')
        {
            //STPush(&st, *s);
            st.push(*s);//直接调用这个对象里面的公共函数
        }
        else // 右括号取栈顶左括号尝试匹配
        {
            /*if (STEmpty(&st))
            {
                STDestroy(&st);
                return false;
            }*/
            if (!st.Empty())//判空
            {
                //我们这里是不需要进行destroy的书写的,因为对象在生命周期快结束的时候会自己调用的析构函数进行内存释放
                return false;
            }
            /*char top = STTop(&st);
            STPop(&st);*/
            char top = st.Top(&st);
            st.Pop(&st);
            // 不匹配
            if ((top == '(' && *s != ')')
                || (top == '{' && *s != '}')
                || (top == '[' && *s != ']'))
            {
                //不要要额外写销毁函数,编译器会自动调用析构函数进行内存的释放
                return false;
            }
        }
          s;
    }
            // 栈不为空,说明左括号⽐右括号多,数量不匹配
    //这里已经自动调用了析构函数,我们不需要额外写析构函数
    return st.Empty();//栈为空就返回真,因为左括号都已经匹配了

}

int main()
{


    return 0;
}

有了析构函数我们都不需要写destory函数了

4. 拷⻉构造函数

如果⼀个构造函数的第⼀个参数是⾃⾝类类型的引⽤,且任何额外的参数都有默认值,则此构造函数也叫做拷⻉构造函数,也就是说拷⻉构造是⼀个特殊的构造函数。

拷⻉构造的特点:

  1. 拷⻉构造函数是构造函数的⼀个重载。
  2. 拷⻉构造函数的第⼀个参数必须是类类型对象的引⽤,使⽤传值⽅式编译器直接报错,因为语法逻辑上会引发⽆穷递归调⽤。 拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引⽤,后⾯的参数必须有缺省值。
  3. C 规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥⾃定义类型传值传参和传值返回都会调⽤拷⻉构造完成。
  4. 若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造。
  5. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的拷⻉构造会调⽤Stack的拷⻉构造,也不需要我们显⽰实现MyQueue的拷⻉构造。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写拷⻉构造,否则就不需要。
  6. 传值返回会产⽣⼀个临时对象调⽤拷⻉构造,传引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回。

拷贝构造相关的知识点

代码语言:javascript复制
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;

//class Date
//{
//public:
//    Date(int year =1, int month=1, int day=1)
//    {
//        _year = year;
//        _month = month;
//        _day = day;
//    }
//
//    ~Date()//析构函数
//    {
//        cout << "~Date()" << endl;
//    }
//private:
//    int _year;
//    int _month;
//    int _day;
//
//};
//typedef int STDataType;
//class Stack
//{
//
//public:
//    Stack(int n = 4)
//    {
//        _a = (STDataType*)malloc(sizeof(STDataType) * n);
//        if (nullptr == _a)
//        {
//            perror("malloc申请空间失败");
//            return;
//        }
//        _capacity = n;
//        _top = 0;
//    }
//    ~Stack()
//    {
//        cout << "~Stack()" << endl;
//        free(_a);
//        _a = nullptr;
//        _top = _capacity = 0;
//    }
//private:
//    STDataType* _a;
//    size_t _capacity;
//    size_t _top;
//};
//
两个栈生成一个队列
//class MyQueue
//{
//public:
//private:
//    //我们用之前创建的类创实例化出两个对象
//    Stack _puahst;
//    Stack _popst;
//
//};
//
//int main()
//{
//    Date d;//这个对象声明周期结束之后会自动调用析构函数
//
//    Stack st;//一开始会自动调用构造,结束之后会自动调用析构函数进行释放
//    MyQueue mq;//这里会调用两次栈的析构,因为这个类里面有两个栈对象
//    return 0;
//}
/*
日期类一般是不需要析构函数的
但是栈是需要析构函数的,因为栈有资源需要清理的

对于日期类的话,我们是不需要进行内存释放的
但是我们可以一些小操作
在对象的声明周期最后我们可以让对象中的值进行一系列的操作,反正声明周期之后会自动调的
*/

/*
跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,
自定义类型成员会调用编译器生成的析构函数
MyQueue mq;//这里会调用两次栈的析构,因为这个类里面有两个栈对象
这两个对象就是我们自定义类型的对象

自定义类型的对象我们是不需要管的,只要创建出来了肯定是会自动调用析构函数的

总结:一般情况下显示了申请的资源,才需要自己实现析构,其他情况基本都不需要显示写
栈需要
日期类不需要

析构函数需要将所有资源都释放掉

后定义的对象先析构

如果我们显示写了析构函数的话,自定义类型的成员也会调用它对应的析构函数

不管如何,自定义类型的成员肯定是会调用析构函数的

析构函数的作用和destory的作用相似,都是在对象的生命周期块快结束的时候进行内存空间的释放
那么我们写了析构函数之后就不用写destory函数了,而且这个析构函数是自动调用的
*/



// ?之前C版本Stack实现
//bool isValid(const char* s) {
//    ST st;
//    STInit(&st);
//    while (*s)
//    {
//        // 左括号?栈
//        if (*s == '(' || *s == '[' || *s == '{')
//        {
//            STPush(&st, *s);
//        }
//        else // 右括号取栈顶左括号尝试匹配
//        {
//            if (STEmpty(&st))
//            {
//                STDestroy(&st);
//                return false;
//            }
//            char top = STTop(&st);
//            STPop(&st);
//            // 不匹配
//            if ((top == '(' && *s != ')')
//                || (top == '{' && *s != '}')
//                || (top == '[' && *s != ']'))
//            {
//                STDestroy(&st);
//                return false;
//            }
//        }
//          s;
//    }
//    
//        // 栈不为空,说明左括号?右括号多,数量不匹配
//    bool ret = STEmpty(&st);
//    STDestroy(&st);
//    return ret;
//}
//
// ?之前C版本Stack实现
//bool isValid(const char* s) {
//    STack st;//对象定义的时候自动调用构造,那么自动进行初始化,就不需要再自己写初始化了
//    //STInit(&st);
//    while (*s)
//    {
//        // 左括号?栈
//        if (*s == '(' || *s == '[' || *s == '{')
//        {
//            //STPush(&st, *s);
//            st.push(*s);//直接调用这个对象里面的公共函数
//        }
//        else // 右括号取栈顶左括号尝试匹配
//        {
//            /*if (STEmpty(&st))
//            {
//                STDestroy(&st);
//                return false;
//            }*/
//            if (!st.Empty())//判空
//            {
//                //我们这里是不需要进行destroy的书写的,因为对象在生命周期快结束的时候会自己调用的析构函数进行内存释放
//                return false;
//            }
//            /*char top = STTop(&st);
//            STPop(&st);*/
//            char top = st.Top(&st);
//            st.Pop(&st);
//            // 不匹配
//            if ((top == '(' && *s != ')')
//                || (top == '{' && *s != '}')
//                || (top == '[' && *s != ']'))
//            {
//                //不要要额外写销毁函数,编译器会自动调用析构函数进行内存的释放
//                return false;
//            }
//        }
//          s;
//    }
//            // 栈不为空,说明左括号?右括号多,数量不匹配
//    //这里已经自动调用了析构函数,我们不需要额外写析构函数
//    return st.Empty();//栈为空就返回真,因为左括号都已经匹配了
//
//}
//
//int main()
//{
//
//
//    return 0;
//}


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

    //Date(const Date& d)//参数是引用,就是给传过来的对象起别名
    //  //那么这里就是d是d1的别名
    //  //this指针指向的是d2
    //{
    //  _year = d._year;
    //  _month = d._month;
    //  _day = d._day;
    //}
    //用指针是可以的,但是这里不是拷贝构造,就是一个普通构造
    /*Date(Date* p)
    {
        _year = p->_year;
        _month = p->_month;
        _day = p->_day;
    }*/

    ~Date()//析构函数
    {
        cout << "~Date()" << endl;
    }
private:
    int _year;
    int _month;
    int _day;

};
typedef int STDataType;
class Stack
{

public:
    Stack(int n = 4)
    {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        if (nullptr == _a)
        {
            perror("malloc申请空间失败");
            return;
        }
        _capacity = n;
        _top = 0;
    }

    void Push(STDataType x)
    {
        if (_top == _capacity)
        {
            int newcapacity = _capacity * 2;
            STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
                sizeof(STDataType));
            if (tmp == NULL)
            {
                perror("realloc fail");
                return;
            } 
            _a = tmp;
            _capacity = newcapacity;
        } 
        _a[_top  ] = x;
    }

    STDataType Top()//获取栈顶元素
    {
        return _a[_top - 1];
    }

    Stack(const Stack& st)//这里的this指针指向的就是st2,st就是st1
    {
        //期望有一样大的空间和一样的值
        _a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
        if (nullptr == _a)
        {
            perror("malloc申请空间失败");
            return;
        }
        //空间拷贝完我们要将数据拷贝过去
        //top是多大,那么实际的数据个数就是多少
        memcpy(_a, st._a, sizeof(STDataType) * st._top);
        //从st1这个空间里面的数据拷贝到我们刚创建的空间,这个空间是st2指向的
        _top = st._top;
        _capacity = st._capacity;

        //拷贝空间以及空间内的值
        //一样大的空间,地址不同

    }

    ~Stack()
    {
        cout << "~Stack()" << endl;
        free(_a);
        _a = nullptr;
        _top = _capacity = 0;
    }
private:
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};
//void func(Date d)
//void func(Date& d)
//{
//
//}

//两个栈生成一个队列
class MyQueue
{
public:
private:
    //我们用之前创建的类创实例化出两个对象
    Stack _puahst;
    Stack _popst;

};

//class MyQueue
//{
//public:
//private:
//    Stack pushst;
//    Stack popst;
//};

void func(Stack &st1)
{

}

//int main()
//{
//    //对于日期类的拷贝我们不写拷贝的话也是会进行值拷贝的
//    //一个字节一个字节的进行拷贝
//    //日期类做浅拷贝/值拷贝是没有任何问题的
//    Date d1(2024, 8, 15);
//    Date d2 = d1;
//
//    //对于栈这个类型我们没有写拷贝函数
//    //但是这里我们还是能完好的进行拷贝的操作
//    Stack st1(10);
//    Stack st2(st1);
//    /*
//    我们的栈里面有一个指针,一个top 一个capacity
//    我们进行指针的拷贝,一个字节一个字节的拷贝
//    那么st1拷贝st2
//    那么这两个指针都指向栈的同一个位置
//    在这里我们的st2优先进行析构
//    然后st1再进行析构的操作
//    但是想想看:一块空间能够进行析构两次吗?
//    
//    一块空间只能进行释放一次
//
//    而且在插入数据的时候可能会进行干扰了
//    数据的覆盖
//    期望的是两个有各自的空间
//    但是因为这个指针的拷贝
//    两个栈公用同一块空间
//    两者会互相干扰
//    
//    在这里我们没有写对应的拷贝构造函数
//    那么就是用的是系统的默认的浅拷贝行为
//    默认行为他会将这些数据的字节一个一个进行拷贝
//    包括指针,就会使两个栈公用同一块空间
//
//    所以这里栈是会生成默认拷贝构造的
//    但是不符合我们的需求的
//
//    那么栈得怎么做才能解决问题呢?
//    我们得额外开辟一块空间,一样大,将数据拷贝过去,而且指st2针指向新的空间
//    那么这种就是深拷贝
//
//    深拷贝不仅仅是拷贝一个值
//
//    如果我们的栈用默认的浅拷贝的话编译器是会进行报错的
//    */
//    func(st1);//我们这里如果仅仅将st1的值拷贝过去的话就是浅拷贝
//    //所以说自定义类型在拷贝的时候一定要调用拷贝构造
//    //就会造成两次析构函数以及其他的隐藏问题
//
//    //所以C  在进行拷贝的时候要调用拷贝构造
//
//    //而且这里我们也不能用传值传参,我们这里用引用
//    //可以减少拷贝
//
//
//    MyQueue m1;
//    MyQueue m2(m1);
//    //因为内部是两个栈,那么我们这里的MyQueue是不用写拷贝构造的
//    //栈有自己的拷贝构造
//    return 0;
//}
//
//Stack  Func()//这里如果用引用返回的话,那么这个就会出现越界的情况
//{
//
//    Stack st;
//    //往栈内插入数据
//    st.Push(1);
//    st.Push(2);
//    st.Push(3);
//    return st;//返回这个栈
//    /*
//    假如我们返回的是st的别名的话
//    我们在调用完Func这个函数之后栈帧就销毁了
//    那么st指向的那块空间已经销毁了
//    那么就是野引用,类似野指针
//    我们返回st的别名
//    那么就会出现问题了
//    如果去访问这块空间就是扯淡了
//    
//    所以引用传参怎么用都可以
//    但是返回值是引用的话就要谨慎了
//    万一返回的是个空的呢
//    如果在st前面加个static
//    那么就可以
//    因为此时的st在静态区里面
//    Func结束,st是不销毁的
//    那么返回别名就没问题的
//    */
//}
//int main()
//{
//    Stack ret = Func();
//    cout << ret.Top() << endl;
//    return 0;
//}
//int main()
//{
//    Date d1(2024, 8, 9);//同数字年月日进行初始化
//    Date d2(d1);//用d1进行初始化 这就是拷贝构造
//    //拷贝d1去初始化,d2的值和d1的值是一样的
//    Date d3 = d1;//这个也是拷贝构造
//
//    func(d1);
//
//    /*Date d3(&d1);*/
//    return 0;
//}
//我们我们在进行拷贝构造的时候是传值传参的话那么编译器是会报错的
//我们这里最好使用引用

/*当函数是这个的话void func(Date d)
我们在这里调试的时候
跑到了func(d1)这里
我们按F11
然后就直接跳转到了我们这个拷贝函数构造那里了
我们进行一系列的赋值操作
然后就到了func的函数内部

因为我们的func的实参是d1
形参是Data d
那么我们将d1给d
我么进行完传值传参之后我们进行了一个拷贝构造
为什么会自动拷贝呢
因为这个拷贝构造是特别的构造函数
只要创建了对象,那么就能进行拷贝构造的操作了

进行完拷贝构造之后我们就返回func函数进行剩下的操作了

其实上面的说法可能不专业
C  规定:类类型的传值传参必须调用拷贝构造

这里的Date d就是类类型的传参

上面的是传值传参

每次调用拷贝构造函数之前要先传值传参,传值传参时一种拷贝,又形成了一个新的
拷贝构造,就形成了无穷递归

传值传参的话会进行拷贝的循环
那么这里我们采用引用


用指针的话就是普通的构造,并不是拷贝构造
因为在逻辑上就是不同的两种构造方式


拷贝构造有两种写法的
Date d2(d1);    
Date d3 = d1;
是可以用赋值来进行写

最终要的拷贝构造函数参数一定要是引用的写法
因为引用传参就是减少拷贝
建议在函数中加上const
因为不能因为形参的改变导致实参的改变

拷贝构造函数的第一个参数必须是类类型对象的引用
而且后面的参数可以有,而且是缺省的
后面的参数一定要有缺省值
如果后面写个int x那么这个函数就不是一个拷贝构造函数
后面一定要是缺省值


如果不加const就是属于权限的放大了
因为你只是我的别名
我自己都不能进行修改,你还修改上了
那不是权限放大了嘛


对于自定义类型的成员变量的话会调用他们自己对应的拷贝构造
对于内置类型的拷贝构造的话就是一个字节一个字节的拷贝

构造和析构函数是不管内置类型的
但是拷贝构造是要管的


内置类型就是日期类这种,直接就是完成值拷贝

但是栈这种带有指针的,就需要即兴深拷贝就是调用他们自己的拷贝函数

MyQueue是不用写拷贝构造的


对于日期类这种的类以及MyQueue这种类我们是不需要自己写拷贝构造函数的
用系统默认的浅拷贝就行了

如果一个类显示实现了析构函数实现了空间的释放,那么他就需要显示拷贝构造,否则的话就是不需要的
日期类我们没写析构的,
我们显示写了析构函数的话,那么就说明我们是需要资源释放的,我们之前申请了空间的
那么我们就不能使用但单纯的浅拷贝

那么我们以后就可以用是否写了析构函数来判断是否需要进行拷贝构造的编写
*/ 

总结:

我们在使用引用值返回拷贝的话,有风险的,因为假如说我们的引用值所在的空间销毁了

那么这个拷贝就是有问题的

对于栈这种,我们需要自己写拷贝构造函数

但是日期类这种我们就可以不用写,让编译器自身去实现浅拷贝的操作,一个字节一个字节进行拷贝

拷贝函数后面的参数一定要是缺省的,不然就不是拷贝函数了

如果用指针的话进行拷贝的话是构造函数,但是不是拷贝构造函数

因为这个用指针来进行拷贝的话和拷贝构造函数有本质上面的区别的,是不同的

我们建议在实现拷贝构造函数的时候使用上引用传参,可以减少拷贝

如果是传值传参的话可能会出现死循环了,就是无穷递归

C 规定:类类型的传值传参必须调用拷贝构造

内置类型可以用系统默认的浅拷贝,我们没必要自己写

5. 赋值运算符重载

运算符重载

• 当运算符被⽤于类类型的对象时,C 语⾔允许我们通过运算符重载的形式指定新的含义。C 规定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编译报错。

• 运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体。

• 重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数。

• 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数⽐运算对象少⼀个。

• 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。

• 不能通过连接语法中没有的符号来创建新的操作符:⽐如operator@。

•.* :: sizeof ?: . 注意以上5个运算符不能重载。

.*这个操作符的使用方法,对类中函数,函数指针:

代码语言:javascript复制
class A
{
public:
    void func()
    {
        cout << "A::func()" << endl;
    }
};
typedef void(A::* PF)();//进行一个改名
//将函数指针进行改变,改名都是在里面改的
int main()
{
    //A::*pf这么写是为了对应起来与func对应起来
    //就是成员函数的指针
    //void(A::*pf)();
    PF pf=&A::func;//函数指针

    A obj;//创建A类型的对象
    //对象调用函数指针那么就用.*  我们需要先用*对函数指针进行解引用
    //然后再利用对象加.进行调用
    (obj.*pf)();
    return 0;
}

•重载操作符⾄少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: intoperator (int x, int y)

• ⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义,⽐如Date类重载operator-就有意义,但是重载operator 就没有意义。

• 重载 运算符时,有前置 和后置 ,运算符重载函数名都是operator ,⽆法很好的区分。C 规定,后置 重载时,增加⼀个int形参,跟前置 构成函数重载,⽅便区分。

代码语言:javascript复制
    //  d1;
    //d1.operator  ()
    Date& operator  ()//前置  就是返回  以后的
    {
        //返回的是  之后的值
        *this  = 1;
        return *this;
        //这里是引用返回,出了这个函数的话d1是仍然存在的,那么我们取个别名保存数据
    }
    //d1  
    //d1.operator  (0 )
    //这里传什么是无所谓的,最主要是构成函数重载
    //运算符重载和函数重载没有什么关系的
    //函数重载是允许函数名相同的函数存在,但是参数是不同的
    //运算符重载是能够让类变量能使用运算符
    Date operator  (int i)//后置  就是返回  之前的
    {
        Date tmp(*this);//拷贝构造,将原先的*this进行拷贝
        *this  = 1;
        return tmp;//我们这里使用的是传值返回,返回的是tmp的一个拷贝
        //因为出了这个函数的话这个空间就会销毁了
    }

• 重载>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调⽤时就变成了 对象<<cout,不符合使⽤习惯和可读性。重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象

代码语言:javascript复制
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>

using namespace std;

class Date
{
public:
    Date(int year =1, int month=1, int day=1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    bool operator<(const Date& d)//这里只用写一个参数,因为另外一个参数是隐含的this指针
    {
        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;
        }
        //剩下的情况
        return false;
    }
    //日期相减
    int operator-(const Date& d);

    //单独写个函数去获取天数
    int GetMonthDay(int year, int month)
    {
        assert(month > 0 && month < 13);//检查这个月份违法没有
        //将天数罗列出来
        //将这个数组直接放到静态区,那么我们就不用每次都创建数组了
        static int monthDayArray[13] = { -1, 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;
        }
        else//不是闰年的话就按照数组进行返回就行了
        {
            //数组之所以有个-1这个元素是因为下标能和月份进行对应
            return monthDayArray[month];
        }
    }

    //日期 天数
    Date operator (int day)
    {


    }
    Date operator =(int day)
    {

    }


    //  d1;
    //d1.operator  ()
    Date& operator  ()//前置  就是返回  以后的
    {
        //返回的是  之后的值
        *this  = 1;
        return *this;
        //这里是引用返回,出了这个函数的话d1是仍然存在的,那么我们取个别名保存数据
    }
    //d1  
    //d1.operator  (0 )
    //这里传什么是无所谓的,最主要是构成函数重载
    //运算符重载和函数重载没有什么关系的
    //函数重载是允许函数名相同的函数存在,但是参数是不同的
    //运算符重载是能够让类变量能使用运算符
    Date operator  (int i)//后置  就是返回  之前的
    {
        Date tmp(*this);//拷贝构造,将原先的*this进行拷贝
        *this  = 1;
        return tmp;//我们这里使用的是传值返回,返回的是tmp的一个拷贝
        //因为出了这个函数的话这个空间就会销毁了
    }

private:
    int _year;
    int _month;
    int _day;

};

/*因为上面的成员变量是私有的,那么我们下面就无法进行访问了,下面是几种解决方法
1.提供getxx函数
2.友元(现在没学)
3.重载为成员函数,将这个函数放到类里面
但是这个时候就会出现问题了
当这个函数变成成员函数了,那么这个函数的参数就会多出一个this指针了
则它的第一个对象默认传给了隐含的this指针了,
那么现在的函数就只能存在一个对象了
但是和之前的函数相差无几
因为this指针一直在指向第一个参数的成员变量,this指针就是第一个参数




*/

//函数名是operator 运算符   参数个数看是几元的操作符
//左侧的传给第一个参数,右侧的传给第二个参数
//bool operator<(const Date& x1, const Date& x2)
//{
//    if (x1._year < x2._year)
//    {
//        return true;
//    }
//    else if (x1._year == x2._year&& x1._month < x2._month)//年相等就比较月份
//    {
//        return true;
//    }
//    else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
//    {
//        return true;
//    }
//    //剩下的情况
//    return false;
//}
//

class A
{
public:
    void func()
    {
        cout << "A::func()" << endl;
    }
};
typedef void(A::* PF)();//进行一个改名
//将函数指针进行改变,改名都是在里面改的
int main()
{
    //A::*pf这么写是为了对应起来与func对应起来
    //就是成员函数的指针
    //void(A::*pf)();
    PF pf=&A::func;//函数指针

    A obj;//创建A类型的对象
    //对象调用函数指针那么就用.*  我们需要先用*对函数指针进行解引用
    //然后再利用对象加.进行调用
    (obj.*pf)();


    Date d1(2024, 8, 9);
    Date d2(2024, 8, 10);
    //自定义类型是我们自己定义的,编译器不知道怎么进行比较
    //显示调用
    //将运算符重载当成普通函数
    //bool ret2 = operator<(d1, d2);//显示
    //cout << ret2 << endl;
    转换成调用对应的运算符重载函数
    非显示调用
    //bool ret3 = d1<d2;//编译器遇到这个的话会转换成和上面一样的
    //cout << ret3 << endl;

    //如果将函数放到类中,那么这个函数就是成员函数了,那么我们就不能按照上面那样写了
    //显示函数进行调用,可以不这么写,按照下面的写也是ok的
    bool ret2 = d1.operator<(d2);
    cout << ret2 << endl;

    //不显示调用,系统会自动调用
    bool ret3 = d1 < d2;
    cout << ret3 << endl;
    //内置类型比较大小是简单的
    int i = 0, j = 1;
    bool ret1 = i < j;

    //日期和日期进行相减是有意义的,可以算出中间差多少天
    //int n = d1 - d2;

    //日期和日期相加是没有意义的,但是日期 天数是有意义的

    d1  ;
      d1;
    return 0;
}

//如果我们比较的是内置类型的函数的话,编译器会自己进行比较大小
//但是如果是自定义类型的话,编译器会调用我们写的比较函数
//有两种写法:一种是将这个运算符重载函数当做函数进行调用
//另一种写法就是直接比较大小,返回值是bool类型的
//编译器会自己进行调用的

赋值运算符的重载

赋值运算符重载是⼀个默认成员函数,⽤于完成两个已经存在的对象直接的拷⻉赋值,这⾥要注意跟

拷⻉构造区分,拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象。

赋值运算符重载的特点:

  1. 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成

const 当前类类型引⽤,否则会传值传参会有拷⻉

  1. 有返回值,且建议写成当前类类型引⽤,引⽤返回可以提⾼效率,有返回值⽬的是为了⽀持连续赋值场景。
  2. 没有显式实现时,编译器会⾃动⽣成⼀个默认赋值运算符重载,默认赋值运算符重载⾏为跟默认拷⻉构造函数类似,对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的赋值重载函数。
  3. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的赋值运算符重载就可以完成需要的拷⻉,所以不需要我们显⽰实现赋值运算符重载。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器⾃动⽣成的赋值运算符重载完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的赋值运算符重载会调⽤Stack的赋值运算符重载,也不需要我们显⽰实现MyQueue的赋值运算符重载。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写赋值运算符重载,否则就不需要。

拷贝构造是将一个已经存在的对象拷贝初始化给另一个要创建的对象

而赋值运算符的重载是两个已经存在的对象,一个对象将他的值拷贝赋值给另一个对象

operator=

赋值运算符的重载的具体使用说明以及注意事项:

代码语言:javascript复制
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>

using namespace std;

class Date
{
public:
    Date(int year = 1, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    //我们这里还是使用引用传参,可以减少拷贝的流程
    // 下面是转换的过程
    //d1=d3;
    //d1.operator=(d3)---但是我们不这么写
    // 两种写法的效果是一样的
    //这里的d1传给了this  d3传给了d
    //Date& operator=(const Date& d)//第一个参数传给了this指针,我们这里只写一个参数进行接收
    //{
    //  //对象是一个的话就不会进入这个代码的
    //  if (this != &d)//就是赋值对象不是自己的话就进行赋值拷贝的操作
    //  {
    //      //我们现在将d3的值给d1
    //      _year = d._year;//前面的_year前面还有一个隐含的this指针指向的是d1
    //      _month = d._month;
    //      _day = d._day;

    //      //d3赋值给d2,那么我们的返回值就是d2 this指向的就是d2的地址,那么我们要返回this的解引用
    //      return *this;//因为this指向的是d2,那么出了这个作用域的话d2还是存在的
    //      //那么我们就使用引用返回,返回别名,那么我们就不用进行额外的拷贝的操作

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


private:
    int _year;
    int _month;
    int _day;

};

int main()
{
    Date d1(2024, 9, 28);
    //对一个已经存在的值拷贝生成一个新的对象
    Date d2(d1);//拷贝构造  拿d1拷贝构造出,初始化d2

    /*Date d3(2024, 9, 11);*/
    //两个存在的值,将其中一个值拷贝给另一个

    /*d1 = d3;*///将d3的值拷贝给d1
    //赋值运算符的重载
    //用于两个已经存在的连个对象直接的拷贝赋值

    //这里要和拷贝构造进行区分
    //拷贝构造是用于一个对象拷贝初始化给另一个要创建的对象
    Date d4 = d1;//拷贝构造,因为这里的d4是不存在的,我们将d1进行拷贝初始化生成另一个对象
    Date d3(2024, 9, 11);


    //两种写法的效果是一样的
    /*d1=d3;*///但是我们还是喜欢这么写简单些
    //d1.operator=(d3);


    d1 = d2 = d3;
    //d3赋值给d2,返回值是d2,d2再赋值给d1
    // 那么我们就需要对赋值运算符的重载的函数进行调整,需要返回对应的值
    //这就是连续赋值
    //int i, j, k;
    //i = j = k = 1;//赋值是从右往左进行赋值的
    //d1 = d1;//自己给自己赋值

    return 0;
}

/*
日期类严格来说是不需要写运算符重载的
因为成员都是内置类型的
默认赋值运算符重载会对内置类型成员变量完成值拷贝/浅拷贝(一个字节一个字节的进行拷贝)

对自定义类型的成员变量会调用他的对应的赋值重载函数的

栈是需要我们自己写的
对于两个栈实现队列的话
如果用默认的这个赋值运算符重载的话,那边就会导致这连个栈共用同一块空间的

指针也被拷贝过去了,就会出问题

所以MyQueue我们也是不许要自己写的
因为这个内部就是两个栈
两个栈调用他们的赋值运算符重载就行了

*/

/*‘

如果一个类显示实现了析构函数并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要
就是看析构函数
*/

/*总结:
* 构造--一般都需要自己写,自己传参定义初始化
* 析构---构造时有资源申请(如内存,malloc或者fopen),就要写析构函数
* 拷贝构造和赋值重载,如果显示写了析构,内部管理资源,就需要显示实现神拷贝,不能是浅拷贝,浅拷贝会出现问题的
* 


*/

日期类的实现

存在的情况下实现 =操作符的重载:

代码语言:javascript复制
Date Date::operator (int day)//我们不能改变原先的对象,就是this指针指向的对象
{
    Date tmp = *this;//调用拷贝构造
    tmp._day  = day;
    while (tmp._day > GetMonthDay(tmp._year, tmp._month))//如果当前天数大于这个月的天数,那么我们就进行下面的操作
    {
        tmp._day -= GetMonthDay(tmp._year, tmp._month);
        //将当前这个月的天数减掉
        //减掉之后我们进行月份的增加
          tmp._month;
        if (tmp._month == 13)//那么就说明当前这一年已经过完了
        {
            tmp._year  ;
            tmp._month = 1;//月份变为来年的一月

        }
    }
    return tmp;//返回的是最开始拷贝的值,原先的对象的大小是不变的
}这里使用传值返回,因为出了这个作用域的话tmp就会别销毁了
//所以我们用传值返回的操作,不能用引用返回

//在 存在的情况下实现 =的操作符重载
Date& Date::operator =(int day)
{
    *this = *this   day;//存在 拷贝
    return *this;
}
//因为这里是存在拷贝的,而且这下面的先 再 =的话拷贝次数比上面的多,
//所以我们选择先 =再 

=存在的情况下实现 操作符的重载:

代码语言:javascript复制
Date& Date::operator =(int day)//日期加整型是有意义的,帮助我们算多少天以后的日期
{
    //d1 =-100    d1=d1-100
    if (day < 0)//这里的day<0
    {
        return *this -= -day;
    }
    _day  = day;
    while (_day > GetMonthDay(_year, _month))//如果当前天数大于这个月的天数,那么我们就进行下面的操作
    {
        _day -= GetMonthDay(_year, _month);
        //将当前这个月的天数减掉
        //减掉之后我们进行月份的增加
          _month;
        if (_month == 13)//那么就说明当前这一年已经过完了
        {
            _year  ;
            _month = 1;//月份变为来年的一月

        }
    }
    return *this;//反正这个结果出了函数是没有销毁的,那么我们直接用引用返回就行了
    /*
    Date dateObj(2024, 8, 20);
    dateObj = dateObj   10; // 将日期增加10天,并且更新 dateObj
    我们返回的的就是这个天数增加的对象
    */

}

//这个的前提是有 =的操作符重载的函数
Date Date::operator (int day)//我们不能改变原先的对象,就是this指针指向的对象
{
    Date tmp = *this;//调用拷贝构造,拷贝一个对象出来

    tmp  = day;//我们上面已经写了 =的操作了,我们直接在这里赋值运用 =的操作

    return tmp;//返回的是最开始拷贝的值,原先的对象的大小是不变的
}这里使用传值返回,因为出了这个作用域的话tmp就会别销毁了
//所以我们用传值返回的操作,不能用引用返回

因为这里是存在拷贝的,而且这下面的先 再 =的话拷贝次数比上面的多,

所以我们选择先 =再

那么我们选择第二种方法,先 =再

日期类实现的具体代码:

Date.h

代码语言:javascript复制
#pragma once
#include<iostream>
#include<assert.h>

using namespace std;


class Date
{


public:

    //流提取
    friend istream& operator>>(istream& in, Date& d);
    //这里是不能加const的,因为我们提取的值要写到对象d里面去


    //友元函数的声明
    friend ostream& operator<<(ostream& out,const Date& d);
    //拷贝构造以及赋值重载是不需要写的,析构函数也不需要写

    //构造函数
    Date(int year = 1900, int month = 1, int day = 1);
    void Print();//打印

    //检查日期是否合法的函数
    bool CheckDate()
    {
        if (_month < 1 || _month>12
            || _day<1 || _day>GetMonthDay(_year, _month))
        {
            return false;
        }
    }


    //获取每个月的天数---这个函数会使用的很频繁,我们直接放到类里面,就是内联函数
    int GetMonthDay(int year, int month)
    {
        assert(month > 0 && month<13);//保证月份在正确的区间
        static int monthDayArray[13] = { 1,31,28,31,30,31,30,
        31,31,30,31,30,31 };//将第0个位置空出来
        //将这个数组放到静态区里面
        //因为我们会很高频的应用这个代码,那么我们每次都要进行创建,那么就会造成浪费的现象
        if ( month == 2&&((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
        {
            //我们将这个month==2放在前面,更好的提升了效率
            return 29;
        }
        return monthDayArray[month];


    }

    //比较大小的运算符重载
    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);

    //我们这里实现的是 =,将加的数值赋值到对象的身上,对象也会因此发生改变
    Date& operator =(int day);//日期加整型是有意义的,帮助我们算多少天以后的日期
    Date operator (int day);

    Date& operator-=(int day);
    Date operator-(int day);

    //  d1  前置    返回  后的值
    Date& operator  ();

    //d1    后置     
    Date operator  (int);


    //--d1  前置--  返回--后的值
    Date& operator--();

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


    //日期-日期=两个日期中间相差的天数
    int  operator-(const Date& d);

    //流插入---输出
    //要放到全局函数
    // void operator<<(ostream& out);
    //d1对应的就是隐含的this指针,cout对应的就是后面的参数
    //但是这种写法很怪
    //那么我们是否有方法将这个进行改变呢

    /*
    重载<<和>>时,需要重载为全局函数
    然后我们的ostream/iostream就能放到第一个参数了
    然后第二个参数就是我们的对象了*/

private:
    int _year;
    int _month;
    int _day;
};
//流插入
ostream& operator<<(ostream& out, const Date& d);

//流提取
istream& operator>>(istream& in,  Date& d);
//这里是不能加const的,因为我们提取的值要写到对象d里面去

Date.cpp

代码语言:javascript复制
#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"

//为什么这里没有缺省值呢?
//因为缺省值在声明和定义都存在的时候,我们只能在声明的时候给
Date::Date(int year, int month, int day)//类外面定义,我们要制定类域的,不然是无法区分成员函数的
{
    _year = year;
    _month = month;
    _day = day;
    if(!CheckDate())//如果这个日期不合法的话
    {
        cout << "日期非法,请重新输入->" ;
        cout << *this;
    }
}

void Date::Print()
{
    cout << _year << "-" << _month << "-" << _day << endl;
}

bool Date::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;
    }
    //剩下的条件就是大于了
    return false;
}

bool Date::operator>(const Date& d)
{
    //我们已经将小于和等于的操作符重载写出来了,那么我们就直接进行取反,那么剩下的就是大于
    return !(*this <= d);
}
//d1<=d2  那么this就是d1的地址,d就是d2
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 _year == d._year 
        && _month == d._month 
        && _day == d._day;
}

bool Date::operator!=(const Date& d)//等于取反
{
    return !(*this == d);
}
/*
这些操作符重载是有关系的
我们将小于和等于写出来就能进行剩下的操作符重载的代码书写了

*/

//d1 100天
Date& Date::operator =(int day)//日期加整型是有意义的,帮助我们算多少天以后的日期
{
    //d1 =-100    d1=d1-100
    if (day < 0)//这里的day<0
    {
        return *this -= -day;
    }
    _day  = day;
    while (_day > GetMonthDay(_year, _month))//如果当前天数大于这个月的天数,那么我们就进行下面的操作
    {
        _day -= GetMonthDay(_year, _month);
        //将当前这个月的天数减掉
        //减掉之后我们进行月份的增加
          _month;
        if (_month == 13)//那么就说明当前这一年已经过完了
        {
            _year  ;
            _month = 1;//月份变为来年的一月

        }
    }
    return *this;//反正这个结果出了函数是没有销毁的,那么我们直接用引用返回就行了
    /*
    Date dateObj(2024, 8, 20);
    dateObj = dateObj   10; // 将日期增加10天,并且更新 dateObj
    我们返回的的就是这个天数增加的对象
    */

}

//这个的前提是有 =的操作符重载的函数
Date Date::operator (int day)//我们不能改变原先的对象,就是this指针指向的对象
{
    Date tmp = *this;//调用拷贝构造,拷贝一个对象出来

    tmp  = day;//我们上面已经写了 =的操作了,我们直接在这里赋值运用 =的操作

    return tmp;//返回的是最开始拷贝的值,原先的对象的大小是不变的
}这里使用传值返回,因为出了这个作用域的话tmp就会别销毁了
//所以我们用传值返回的操作,不能用引用返回



//Date Date::operator (int day)//我们不能改变原先的对象,就是this指针指向的对象
//{
//    Date tmp = *this;//调用拷贝构造
//    tmp._day  = day;
//    while (tmp._day > GetMonthDay(tmp._year, tmp._month))//如果当前天数大于这个月的天数,那么我们就进行下面的操作
//    {
//        tmp._day -= GetMonthDay(tmp._year, tmp._month);
//        //将当前这个月的天数减掉
//        //减掉之后我们进行月份的增加
//          tmp._month;
//        if (tmp._month == 13)//那么就说明当前这一年已经过完了
//        {
//            tmp._year  ;
//            tmp._month = 1;//月份变为来年的一月
//
//        }
//    }
//    return tmp;//返回的是最开始拷贝的值,原先的对象的大小是不变的
//}这里使用传值返回,因为出了这个作用域的话tmp就会别销毁了
所以我们用传值返回的操作,不能用引用返回
//
在 存在的情况下实现 =的操作符重载
//Date& Date::operator =(int day)
//{
//    *this = *this   day;//存在 拷贝
//    return *this;
//}
//因为这里是存在拷贝的,而且这下面的先 再 =的话拷贝次数比上面的多,
//所以我们选择先 =再 


Date& Date::operator-=(int day)
{

    if (day < 0)//这里的day是负数
    {
        return *this  = -day;
    }
    _day -= day;//原先的天数-我们想要减去的天数
    while (_day <= 0)
    {
        --_month;
        if (_month == 0)//如果现在是1月的话,我们--就到0了。实际应该是12月
        {
            //上一年的12月
            _month = 12;
            --_year;
        }
        _day  = GetMonthDay(_year,_month);//我们再加上这个月的天数
    }
    return *this;
}
Date Date::operator-(int day)
{
    Date tmp(*this);//我们进行拷贝构造
    tmp -= day;//利用上面我们已经实现的-=

    return tmp;
}



//  d1  前置    返回  后的值
Date& Date::operator  ()
{
    *this  = 1;
    return *this;
}

//d1    后置     返回  前的值
Date Date::operator  (int)
{
    Date tmp(*this);//先拷贝构造出一个对象
    *this  = 1;
    return tmp;//返回  以前的值

}

//--d1  前置--  返回--后的值
Date& Date::operator--()
{
    *this -= 1;
    return *this;
}

//d1--  后置--  
Date Date::operator--(int)
{
    Date tmp(*this);//先拷贝构造出一个对象
    *this -= 1;
    return tmp;//返回  以前的值
}



//两个日期相减就是得到的之间相差的天数
int  Date::operator-(const Date& d)
{
    /*
    思路一:我们算出两个日期距离当年的的1月1号有多少天
    如果是同一年的话那么直接就是大的减小的
    但是如果不是一年的话,我们直接就算出距离多少年*/
    /*
    思路二:让小的日期进行  ,直到走到了大的日期
    加了多少次就是多少天,不需要考虑其他的条件*/

    Date max = *this;//假设d1大
    Date min = d;//d假设d2小
    //上面的是拷贝构造
    int flag = 1;

    if (*this < d)//运用这个小于操作符重载就能看出谁小
    {
        //这里的是赋值,都是存在的对象
        max = d;
        min=*this ;
        flag = -1;
    }
    int count = 0;
    while (min != max)
    {
          min;
          count;
    }
    return count * flag;//这个存在第一个小第二个大,第二个小,第一个大的两种情况
    //返回值可能是负数

}



//流插入---输出
//void Date::operator<<(ostream& out)
//{
//    out << _year << "年" << _month << "月" << _day << "日" << endl;
//}

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

//流提取
istream& operator>>(istream& in,Date& d)
//这里是不能加const的,因为我们提取的值要写到对象d里面去
{
    while (1)//直到我们输入了一个正确的日期我们就能退出
    {
        cout << "请一次输入年月日:";
        in >> d._year >> d._month >> d._day;

        if (d.CheckDate())
        {
            break;
        }
        else
        {
            cout << "日期非法,请重新输入"<<endl;
        }
    }
    return in;
}

test.cpp

代码语言:javascript复制
#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"


int main()
{
    //Date d1(2024, 8, 10);
    //d1.Print();//2024-8-10
    ///*d1  = 100;
    //d1.Print();*///2024-11-18

    //Date d2 = d1   100;
    //d2.Print();
    //d1.Print();//那么这里的d1就没有被改变了
    ///*int i = 1;
    //i   100;*/
    ///*
    //如果是单纯的 的话,原先的对象是不会被改变的,但是现在我们的对象已经被改变了
    //由此可知我们在这里实现的是 =的操作符
    //*/
    //d1  = 100; 
    //d1.Print();


    //d1 -= 100;
    //d1.Print();

    //Date ret1=  d1;
    //d1.Print();
    //Date ret2 = d1  ;
    //d1.Print();

    //ret1.Print();
    //ret2.Print();
    Date d1(2024, 2, 29);
    Date d2(2024, 8, 20);
    cout << d1 - d2 << endl;
    cout << d2 - d1 << endl;

    /*cout << d1;*/
    /*d1 << cout;*///我们只有这么写才能正确打印出来日期
    //d1对应的就是隐含的this指针,cout对应的就是后面的参数
    //但是这种写法很怪
    //那么我们是否有方法将这个进行改变呢

    /*
    重载<<和>>时,需要重载为全局函数
    然后我们的ostream/iostream就能放到第一个参数了
    然后第二个参数就是我们的对象了*/
    cout << d1; 
    operator<<(cout, d1);//本质
    //那么到这里我们就不支持了
    cout << d1<<d2<<endl;
    //我们这里是从左往右进行结合
    //对于cout << d1这个我们的返回值应该是cout 然后cout和d2
    //再次返回cout 和endl

    //所以流插入是需要返回cout的

    cin >> d1 >> d2;
    cout << d1 << d2 << endl;
    return 0;
}

总结:

我们具体实现了一下的一些方法

构造函数(包含了一个检查我们输入的日期是否合法)

获取每个月的天数

操作符的重载:< > ≤ ≥ == ≠

前置 后置 前置-- 后置--

计算出两个日期之间的天数

流插入

流提取

0 人点赞