• C++ 类和对象 (查漏补缺)


    目录

    成员变量的命名风格

    类的实例化

    拷贝构造

    解决方法

    引用

    Data类拷贝构造

    指针

    Stack的拷贝构造

    ps

    运算符重载

    ps

    operator==  operator>=  operator!=

    题目

    用运算符重载函数写一个日期相加

    那我们如何正确实现一个operator+呢?

    空容器

    复用+=


    成员变量的命名风格

    下文这样来初始化一个日期类的成员变量

    1. #include
    2. using namespace std;
    3. class Data
    4. {
    5. public:
    6. void Init(int year,int month,int day)
    7. {
    8. year = year;
    9. month = month;
    10. day = day;
    11. }
    12. private:
    13. int year;
    14. int month;
    15. int day;
    16. };
    17. int main()
    18. {
    19. Data T;
    20. T.Init(2003,11,29);
    21. return 0;
    22. }

    我们发现并没有初始化上:

     原因:

    这样写加以区分:

    1. #include
    2. using namespace std;
    3. class Data
    4. {
    5. public:
    6. void Init(int year,int month,int day)
    7. {
    8. _year = year;
    9. _month = month;
    10. _day = day;
    11. }
    12. private:
    13. int _year;
    14. int _month;
    15. int _day;
    16. };
    17. int main()
    18. {
    19. Data T;
    20. T.Init(2003,11,29);
    21. return 0;
    22. }

    类的实例化

    有这样一个日期类:

    1. class Data
    2. {
    3. public:
    4. void Init(int year,int month,int day)
    5. {
    6. _year = year;
    7. _month = month;
    8. _day = day;
    9. }
    10. private:
    11. int _year;
    12. int _month;
    13. int _day;
    14. };

    即然有类域,那我们可以这样访问吗?
     

    1. Data::_year;
    2. Data._year;

    不可以,因为我们私有的成员变量只是一个声明:

     只有声明是不能直接拿来用的,我们需要定义:

    	Data T;

    现在定义了一个变量T,就开辟了一块空间,_year,_month,_day的空间也被一把开出来了。

    拷贝构造

    有如下这么两个类,一个日期类,一个栈类

    1. typedef int DataType;
    2. class Stack
    3. {
    4. public:
    5. Stack(size_t capacity = 3)
    6. {
    7. cout << "Stack()" << endl;
    8. }
    9. // s1(s)
    10. Stack(const Stack& s)
    11. {
    12. cout << "Stack(Stack& s)" << endl;
    13. }
    14. void Push(DataType data)
    15. {
    16. // CheckCapacity();
    17. _array[_size] = data;
    18. _size++;
    19. }
    20. ~Stack()
    21. {
    22. cout << "~Stack()" << endl;
    23. }
    24. private:
    25. // 内置类型
    26. DataType* _array;
    27. int _capacity;
    28. int _size;
    29. };
    1. class Data
    2. {
    3. public:
    4. void Init(int year,int month,int day)
    5. {
    6. _year = year;
    7. _month = month;
    8. _day = day;
    9. }
    10. private:
    11. int _year;int _month;int _day;
    12. };

    现在还有一个fun函数,我们把栈类的值拷贝给这个fun函数:

    1. void fun(Data a)
    2. {
    3. }
    4. int main()
    5. {
    6. Data T;
    7. T.Init(2003,11,29);
    8. fun(T);
    9. return 0;
    10. }

    如果我们把日期类拷贝给fun函数就不会蹦:

    1. void fun1(Data s)
    2. {
    3. }
    4. int main()
    5. {
    6. Data T;
    7. T.Init(2003,11,29);
    8. fun1(T);
    9. return 0;
    10. }

    如果我们把栈来拷贝给fun函数就会蹦:

    1. void fun2(Stack s)
    2. {
    3. }
    4. int main()
    5. {
    6. Stack s1;
    7. fun2(s1);
    8. return 0;
    9. }

    我们可以画图解析一下:

    如图,stack的int*_a是在堆上面开辟的空间,内容存在堆上的,_a指向堆。

    fun是stack的浅拷贝,fun的_a也指向同一块地址。

     fun后调用,先结束生命周期,先调用析构。此时就把这块空间释放掉了,那么轮到stack的时候会再次调用析构,会再把这块空间释放一次。

    看下面这个图,a和s1的_array都指向堆上的同一块地址:

    (ps:调试技巧:先给fun2()调用打断点,跳到fun2()调用之后再f11跳fun2()定义)

    fun函数先析构:

    stack再去析构的时候就会报错,因为stack的_a此时是一个野指针,指向了一块已经被释放了的空间:

    解决方法

    引用

    1. void fun2(Stack& a)
    2. {
    3. }
    4. int main()
    5. {
    6. Stack s1;
    7. fun2(s1);
    8. return 0;
    9. }

    引用的话我就是你,也就不存在析构两次的问题了。

    但是这个方法仍然有弊端,那就是如果我想通过a的值的改变但是不影响s1呢?

    下面这段代码 apush值肯定会改变s1

    1. void fun2(Stack& a)
    2. {
    3. a.Push(1);
    4. a.Push(2);
    5. }
    6. int main()
    7. {
    8. Stack s1;
    9. fun2(s1);
    10. return 0;
    11. }

    c++提供了拷贝构造函数来解决这个问题

    Data类拷贝构造

    拷贝构造函数和函数名和类名一致,参数类型也和类名一致,就是自己拷贝自己。

    如下,我们现在就可以不用fun了,直接d2拷贝d1,通过拷贝构造函数:

    1. Data(Data d)
    2. {
    3. _year = d._year;
    4. _month = d._month;
    5. _day = d._day;
    6. }

    调拷贝构造有两种方法

    d2,d3分别用不同方法拷贝d1:

    1. Data d1(2003,11,29);
    2. Data d2(d1);
    3. Data d3=d1;
    4. //fun1(d2);

    但是会有报错:

    也就是说拷贝构造函数参数必须为引用,这是因为如果不引用就会无穷无尽的拷贝。

    怎么理解这句话呢?

    内置类型可以直接传参,但是c++有自定义类型,列入栈这种,如果直接传参会面临析构两次的问题

    于是c++规定自定义类型传参需要调用拷贝构造,如果直接传参会造成无限拷贝:

     所以我们要通过引用传参:

    1. Data(Data& d)
    2. {
    3. _year = d._year;
    4. _month = d._month;
    5. _day = d._day;
    6. }

    引用是一种别名,所以引用没有拷贝。

    指针

    同样的,用指针也可以解决这个问题,因为指针只是传个地址嘛,并没有拷贝值

    1. void fun1(Data* s)
    2. {
    3. }
    4. int main()
    5. {
    6. fun1(&d1);
    7. return 0;
    8. }

    Stack的拷贝构造

    栈类的拷贝构造就没有日期类的拷贝构造那么简单了,因为涉及到了扩容的问题。

    栈对拷贝构造实际上就是把栈的构造函数给copy一份,包括空间和内容,这个也叫做深拷贝。

    1. //构造函数
    2. Stack(size_t capacity = 3)
    3. {
    4. cout << "Stack()" << endl;
    5. _array = (DataType*)malloc(sizeof(DataType) * capacity);
    6. if (NULL == _array)
    7. {
    8. perror("malloc申请空间失败!!!");
    9. return;
    10. }
    11. _capacity = capacity;
    12. _size = 0;
    13. }
    1. //拷贝构造
    2. Stack(const Stack& s)
    3. {
    4. //cout << "Stack(Stack& s)" << endl;
    5. // 深拷贝
    6. _array = (DataType*)malloc(sizeof(DataType) * s._capacity);
    7. if (NULL == _array)
    8. {
    9. perror("malloc申请空间失败!!!");
    10. return;
    11. }
    12. memcpy(_array, s._array, sizeof(DataType) * s._size);
    13. _size = s._size;
    14. _capacity = s._capacity;
    15. }

     没写拷贝构造时的:

    写了拷贝构造之后的:

     小结:

    比如MYqueue类,我们什么都不写,直接让编译器用默认生成的,全去拿栈的用。

    构造函数:默认生成,析构函数:默认生成,拷贝构造函数:默认生成。

    1. class MYqueue
    2. {
    3. Stack pushst;
    4. Stack Popst;
    5. };

    1. typedef int DataType;
    2. class Stack
    3. {
    4. public:
    5. Stack(size_t capacity = 3)
    6. {
    7. cout << "Stack()" << endl;
    8. _array = (DataType*)malloc(sizeof(DataType) * capacity);
    9. if (NULL == _array)
    10. {
    11. perror("malloc申请空间失败!!!");
    12. return;
    13. }
    14. _capacity = capacity;
    15. _size = 0;
    16. }
    17. //s1(s)
    18. Stack( Stack& s)
    19. {
    20. cout << "Stack(Stack& s)" << endl;
    21. // 深拷贝
    22. _array = (DataType*)malloc(sizeof(DataType) * s._capacity);
    23. if (NULL == _array)
    24. {
    25. perror("malloc申请空间失败!!!");
    26. return;
    27. }
    28. memcpy(_array, s._array, sizeof(DataType) * s._size);
    29. _size = s._size;
    30. _capacity = s._capacity;
    31. }
    32. void Push(DataType data)
    33. {
    34. // CheckCapacity();
    35. _array[_size] = data;
    36. _size++;
    37. }
    38. ~Stack()
    39. {
    40. cout << "~Stack()" << endl;
    41. free(_array);
    42. _array = nullptr;
    43. _size = _capacity = 0;
    44. }
    45. private:
    46. // 内置类型
    47. DataType* _array;
    48. int _capacity;
    49. int _size;
    50. };
    1. int main()
    2. {
    3. MYqueue a;
    4. MYqueue d = a;
    5. return 0
    6. }

    ps

    拷贝构造我们一般加const。

    例如:

    1. Data(const Data& d)
    2. {
    3. _year = d._year;
    4. _month = d._month;
    5. _day = d._day;
    6. cout << "拷贝构造" << endl;
    7. }

    这是因为万一有人写反了,把拷贝构造变成被别的成员变量赋值,编译器还看不出来错误:
    而加个const就是权力的缩小,拷贝构造不能被修改,它只能修改别人:

    运算符重载

    比如我们想定义两个日期类的大小,我们像下面这样比较:

     编译器就会报错,因为Data是自定义类型,比较复杂,编译器没与办法直接比较。

    我们只能自己手动比较:

    C++给给所有的运算符比较的函数起了个统一的名称:运算符重载函数,就好比给所有初始化函数起名为构造函数,所有destory函数起名为析构函数一样。运算符重载函数用operator来表示.

    运算符重载是什么意思呢?

    就是说我们两个自定义类型不能直接比较,只能通过函数比较,再通过函数的返回值得到比较结果。

    但是我们可以把这个比较函数重载为一个操作符,我们直接拿这个操作符用就可以实现这个函数的功能。

    如下,重载为“>"操作符:

    operator<

    bool operator>(Data  d1, Data d2)

     这个时候我们就可显示调用operato>函数,或者直接比较d1,d2的值,都可以:

    加括号之后:

    如果我们不用运算符重载,而就用函数名的话就不能直接比较,只能通过调用函数的方式来比较:

    原理:

    ps

    如果我们把operator>写为Data内部成员函数需要注意几点,如果我们直接放进去会报错:

     C++规定运算符重载函数作为类内部成员函数时,看起来的参数要比实际用到的参数少一个。

    什么意思呢?但是这样写就不能这样显示调用operator>了只能这样调用:

     当然,使用运算重载后的操作符照样可以:

    复用 operator<实现operator==  operator>=  operator!=

    ok,接下来我们再写operator==

    1. bool operator==(Data d2)
    2. {
    3. return _year == d2._year
    4. && _month == d2._month
    5. && _day == d2._day;
    6. }

    再来写operator>=

    我们把将才写的operator<=和operator==改一下符号吗?变成下面这样:

    1. bool operator<=( Data d2)
    2. {
    3. if (_year <=d2._year)
    4. {
    5. return true;
    6. }
    7. if (_year<=d2._year && _month <=d2._month)
    8. {
    9. return true;
    10. }
    11. if (_year<= d2._year && _month <= d2._month && _day <= d2._day)
    12. {
    13. return true;
    14. }
    15. else
    16. {
    17. return false;
    18. }
    19. }

    可以时可以,但是太麻烦,下面还要operator!= ,operator>,每个都这样拿代码就太冗余了。

    我们可以复用,

    我这里要写operator>=,我直接调用operator<和operator==就可以了。

    怎么调用呢?用this指针,this是省略值的地址,解引用得到省略值,我们可以看下列代码:

    1. bool operator>=(Data d2)
    2. {
    3. return *this > d2 || *this == d2;
    4. }

    同样,写operator<,只需要对operator<=取反就行了:

    1. bool operator<(Data d2)
    2. {
    3. return !(*this >= d2);
    4. }

    写operator!=,对operator==取反即可:

    1. bool operator!=(Data d2)
    2. {
    3. return !(*this == d2);
    4. }

    赋值运算符重载

    如下,d1的日期为非法日期,d3为合法日期,我把d3的日期赋值给d1,让d1也成为合法日期:

    1. Data d1(2003,11,33);
    2. Data d3(2003,11,30);
    3. d1 = d3;
    4. d3.print();
    5. d1.print();

    但是两个类之家是不能之间进行赋值的,这个可以运行是因为编译器默认生成了赋值运算符重载,将d3的成员变量的值逐个复制给d1的成员变量。需要注意的是,当类中存在指针类型的成员变量时,使用默认的赋值运算符重载函数可能会导致浅拷贝问题。在这种情况下,你需要自己编写赋值运算符重载函数,以确保进行深拷贝操作,避免出现内存错误。

    我们自己可以写一下赋值运算符重载:

     赋值运算符重载和拷贝构造区别

    内置类型我们可以像这样进行赋值:它的原理实际上是这样:注意:优先级问题,还需要再加个括号:但是自定义类型就不可以了:

    那我们可以按照内置类型的思路:先让d4复制给d3,再返回一个值,把这个值再赋值给d1.

    返回的这个值也要是Data类型,因为要返回一个日期,把这个日期再赋值给d1. 

    1. Data& Data::operator=(const Data& d3)
    2. {
    3. this->_year = d3._year;
    4. this->_month = d3._month;
    5. this->_day = d3._day;
    6. return *this;
    7. }
    1. Data d1(2003, 11, 33);
    2. Data d3(2003, 11, 22);
    3. Data d4(2000, 12, 3);
    4. d1 = d3 = d4;
    5. d1.print();
    6. d3.print();
    7. d4.print();

    解析:

    题目

    用运算符重载函数写一个日期相加

    思路:日期类相加,先day和day相加,满28/29/30/31就往月进,月满了往下个月进,月满13了往年进。

    闰年二月是29填,需要特殊处理:

    1. bool operator+()
    2. {
    3. }
    4. int Getmonth(int year,int month)
    5. {
    6. int GetArry[13] = {0, 31,28,30,31,30,31,30,31,30,31,30,31 };
    7. if (month==2&&(month % 4 == 0 && month % 100 != 0 || month % 400 == 0))
    8. {
    9. return 29;
    10. }
    11. return GetArry[month];
    12. }

     看一下operator+怎么写:

    1. Data d1(2003,11,29);
    2. d1+100;
    1. Data(int year, int month, int day)
    2. {
    3. _year = year;
    4. _month = month;
    5. _day = day;
    6. }

     所以此时_year=2003,_month=11,_day=29.

    1. bool operator+(int day)
    2. {
    3. //天数相加
    4. _day += day;
    5. //如果月满了
    6. while (_day > Getmonth(_year, _month))
    7. {
    8. //天数减去满月的天数
    9. _day -= Getmonth(_year, _month);
    10. //从当前月进入下一个月
    11. ++_month;
    12. }
    13. //如果12个月都满了
    14. if (_month == 13)
    15. {
    16. //当前年进入下一年
    17. ++_year;
    18. //把月置为下一年的一月
    19. _month = 1;
    20. }
    21. }

    把我们的传参再拿过来看看:

    1. Data d1(2003,11,29);
    2. d1+100;

    d1+100是一个什么类型,日期+日期还是一个日期

    所以:

    Data ret=d1+100;

    那operator+也应该用Data类型:

    1. Data operator+(int day)
    2. {
    3. _day += day;
    4. while (_day > Getmonth(_year, _month))
    5. {
    6. _day -= Getmonth(_year, _month);
    7. ++_month;
    8. }
    9. if (_month == 13)
    10. {
    11. ++_year;
    12. _month = 1;
    13. }
    14. }

    operator+该返回什么呢?

    返回*this

    1. Data d1(2003,11,29);
    2. Data ret = d1 + 50;
    3. d1.Print();
    4. ret.Print();

     我们发现结果虽然运算正确,但是d1的值也被改为了ret的值,这说明

    这是一个operator+=,而不是一个operator+

    那我们如何正确实现一个operator+呢?

    空容器

    我们可以用用拷贝构造拷贝一份到空类里面,改变这个空容器,最后返回这个空容器就可以了。

    1. Data operator+(int day)
    2. {
    3. Data temp(*this);
    4. temp._day += day;
    5. while (temp._day > Getmonth(_year, _month))
    6. {
    7. temp._day -= Getmonth(_year, _month);
    8. ++temp._month;
    9. if (temp._month == 13)
    10. {
    11. ++temp._year;
    12. temp._month = 1;
    13. }
    14. }
    15. return temp;
    16. }

    复用+=

    1. Data operator+=(int day)
    2. {
    3. _day += day;
    4. while (_day > Getmonth(_year, _month))
    5. {
    6. _day -= Getmonth(_year, _month);
    7. _month++;
    8. if (_month == 13)
    9. {
    10. _year++;
    11. _month = 1;
    12. }
    13. }
    14. return *this;
    15. }
    16. Data operator+(int day)
    17. {
    18. Data temp(*this);
    19. temp += day;
    20. return temp;
    21. }

    operator-=

    实现两个日期类相减:

    -=是一种借位思想,如果2020 7 27-27还好,如果day-200呢,那不成了-173,欠债了。

     这个时候就往前一个月借,当前月的已经光了,还欠了债,需要上个月来补,--_month.

    当day<=0时都是不合法日期,day<0不合法是欠债了,需要往上一个月借,=0不合法是因为一个月没有0号。

    当month=0时说明月借光了,再问年借,--_year;

    再把month重置为去年的12月份。

    _day要借的是上个月的day,借到_day.value不为负值,说明还完债了。

    这时候把还完债后的年月日返回。

    1. Date& Date:: operator-=(int day)
    2. {
    3. _day -= day;
    4. while (_day <=0)
    5. {
    6. --_month;
    7. if (_month == 0)
    8. {
    9. _year;
    10. _month = 12;
    11. }
    12. _day += Getmonth(_year, _month);
    13. }
    14. return *this;
    15. }
    1. Date d1(2003, 11, 29);
    2. d1 -= 100;
    3. d1.print();

     把数变大一点再测:

    1. Date d1(2003, 11, 29);
    2. d1 -= 20000;
    3. d1.print();

    这肯定错了,20000多天都50多年了

     肯定是_year没有--,改一下:

    看一下计算器算的:

  • 相关阅读:
    基于Spring Boot的个人博客系统(源码+数据库)
    8.0、C语言——操作符
    北京地标-自动驾驶高精度地图特征定位数据技术规范
    websocket逆向
    计算机网络安全技术与应用
    ChatGPT发展报告:原理、技术架构详解和产业未来(附下载)
    MySQL数据库管理及用户管理以及数据库用户授权
    Spring MVC面试题
    CSDN竞赛4期题解
    JAVA-中国矿业大学作业-计算阶乘毫秒时间
  • 原文地址:https://blog.csdn.net/m0_65143871/article/details/134089720