• 【C++】————类和对象(下)


     9efbcbc3d25747719da38c01b3fa9b4f.gif

                                                          作者主页:     作者主页

                                                          本篇博客专栏:C++

                                                          创作时间 :2024年6月25日

    9efbcbc3d25747719da38c01b3fa9b4f.gif

    一、日期类

    首先我们先来看一下通过类实现对日期的一系列处理,同时给大家说一下当中存在的一些细节问题:

    1.1 GetMonthDay函数

    这个函数的作用就是我们输入一个得到某一年某个月的天数,对后续的一些函数有着非常重要的作用,但我们要记得一个特殊情况,那就是闰年,因为闰年的二月是29天,非闰年是28天,注意这种情况就可以写代码了。

    1. // 获取某年某月的天数
    2. int GetMonthDay(int year, int month)
    3. {
    4. //日期数组
    5. int MonthDayarr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
    6. //判断闰年
    7. if (2 == month && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))
    8. {
    9. return MonthDayarr[2] + 1;
    10. }
    11. else
    12. {
    13. return MonthDayarr[month];
    14. }
    15. }

    这里我们要注意的点就是先判断是不是二月,不是二月我们就没有必要去判断是否是闰年了,没必要了就,这样会节省很多时间。

    1.2日期类中的两种构造函数

    全缺省构造:

    这里我们要给大家讲两种类型的构造函数,一种是全缺省构造函数,另一种是拷贝构造函数,这两种函数不清楚是啥的话可以去看这篇博客:构造函数和全缺省构造函数

    我怕们先来说全缺省构造函数:

    1. // 全缺省的构造函数
    2. Date(int year = 1900, int month = 1, int day = 1)
    3. {
    4. _year = year;
    5. _month = month;
    6. _day = day;
    7. //检查日期是否合法
    8. if (!(year >= 1
    9. && (month >= 1 && month <= 12)
    10. && (day >= 1 && day <= GetMonthDay(year, month))))
    11. {
    12. cout << "日期非法" << endl;
    13. }
    14. }

    这就是全缺省构造函数,如果传过来值,就赋值,否则就用默认给定的值,在平常的写代码过程中,我还是建议大家去写这种构造函数,因为这种构造函数满足的场景更加多样,不传值也可以,传值当然也可以。

    1. Date d1(2024, 6, 23);
    2. Date d2;
    3. Date d3(2024, 12);

    像这样的传值方式都是可以的 

     拷贝构造

    接下来看一下拷贝构造函数

    1. // 拷贝构造函数
    2. // d2(d1)
    3. Date(const Date& d)
    4. {
    5. _year = d._year;
    6. _month = d._month;
    7. _day = d._day;
    8. }

    这种就是我们的拷贝构造函数,其实就是传一个对象的别名,然后将这个对象的值赋给另一个对象,这就叫拷贝构造。

    使用时可以这样去赋值:

    1. Date d1(2024, 6, 23);
    2. Date d2(d1);

    1.3比较两个日期是否相等

    比较两个日期是否相等的这种函数被称为赋值运算符重载,像这种函数的函数名,我们应该怎样去写呢,可能有些同学直接这样去写:

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

    但是其实这样会直接报错,我们来看一下编译器给出的错误原因:

    这就是这里报错的原因,我们回想一下,是不是忽略了一个叫this指针的东西,没错,我们这里去调这个函数的时候,会有一个隐藏的this指针,所以我们应该这样去写这个函数:

    1. // ==运算符重载
    2. bool operator==(const Date& d)
    3. {
    4. return _year == d._year
    5. && _month == d._month
    6. && _day == d._day;
    7. }

    这样写才符合要求

    调用时是这样调用;

    1. int ret=d1==d2;
    2. int ret=d1.operator==(d2);

    1.3赋值运算符重载

    赋值运算符重载就是用来为对象赋值的:

    1. Date& operator=(const Date& d)
    2. {
    3. _year = d._year;
    4. _month = d._month;
    5. _day = d._day;
    6. return *this;
    7. }

    这就是赋值运算符重载。

    调用时可以这样:

    1. Date d1(2024,10,1);
    2. Date d2;
    3. d2.operator=(d1);

    1.4>和<运算符重载

    这里是比较两个日期的大小,我们肯定先去比较年,其次是月,最后是日,按照规则来就好了

    1. bool operator>(const Date& d)
    2. {
    3. if (_year > d._year)
    4. {
    5. return true;
    6. }
    7. else if (_year == d._year)
    8. {
    9. if (_month > d._month)
    10. {
    11. return true;
    12. }
    13. else if (_month == d._month)
    14. {
    15. if (_day == d._day)
    16. {
    17. return true;
    18. }
    19. }
    20. }
    21. return false;
    22. }
    23. // >=运算符重载
    24. bool operator>=(const Date& d)
    25. {
    26. return operator>(d) || operator==(d);
    27. }
    28. // <运算符重载
    29. bool operator<(const Date& d)
    30. {
    31. if (_year < d._year)
    32. {
    33. return true;
    34. }
    35. else if (_year == d._year)
    36. {
    37. if (_month < d._month)
    38. {
    39. return true;
    40. }
    41. else if (_month == d._month)
    42. {
    43. if (_day < d._day)
    44. {
    45. return true;
    46. }
    47. }
    48. }
    49. return false;
    50. }

    这里我直接把两个写在一起,哪里不懂可以问我

    1.5日期+天数与日期+=天数

    这里我先来说一下这两个的区别,其实大致相同,不同的就是:

    日期+=天数是改变了传过来的日期,在返回,而日期+天数并没有改变原来的日期,

    看一下代码:

    1. // 日期+=天数
    2. Date& operator+=(int day)
    3. {
    4. //如果传过来一个负数
    5. if (day < 0)
    6. {
    7. return *this -= -day;
    8. }
    9. _day += day; //_day加上要加的天数
    10. while (_day > GetMonthDay(_year, _month)) //加完后,如果_day大于当月天数,进入循环
    11. {
    12. //_day减去当月天数,_month++
    13. _day -= GetMonthDay(_year, _month);
    14. _month++;
    15. if (_month > 12)//如果_month大于12,则_year++
    16. {
    17. _month = 1;
    18. _year++;
    19. }
    20. }
    21. return *this;
    22. }
    23. // 日期+天数
    24. Date operator+(int day)
    25. {
    26. Date ret(*this);
    27. ret += day;//这里直接调用上面这个函数就可以了
    28. return ret;
    29. }

    可以发现日期加天数就是创建一个临时的对象用来储存传过来的对象,然后返回,起到不改变原来对象的作用。

    1.6日期-天数与日期-=天数

    这个与上面相同,我不做过多的介绍,直接上代码:

    1. // 日期-=天数
    2. Date& operator-=(int day)
    3. {
    4. _day -= day;
    5. while (_day <= 0)
    6. {
    7. _month--;
    8. if (_month < 1)
    9. {
    10. _month = 12;
    11. _year--;
    12. }
    13. _day += GetMonthDay(_year, _month);
    14. }
    15. return *this;
    16. }
    17. // 日期-天数
    18. Date operator-(int day)
    19. {
    20. Date ret(*this);
    21. ret -= day;
    22. return ret;
    23. }
    24. // 前置++
    25. Date& operator++()
    26. {
    27. *this += 1;
    28. return *this;
    29. }

    1.7前置++与后置++

    这里我们如果通过函数调用来区分前置++和后置++呢,这个函数如何去写呢,这里我们有一个很妙的写法,就是前置加加正常写,然后后置加加里面加入一个int,构成重载函数:

    1. // 前置++
    2. Date& operator++()
    3. {
    4. *this += 1;
    5. return *this;
    6. }
    7. // 后置++
    8. Date operator++(int)//用int来构成重载函数,区分前置++和后置++
    9. {
    10. //cout << "后置++" << endl;
    11. Date tmp(*this);
    12. *this += 1;
    13. return tmp;
    14. }

    传值时想要后置加加里面加上一个整形的数即可,0,1都可以

    1.8前置--与后置--

    还是如上,直接上代码:

    1. // 前置--
    2. Date& operator--()
    3. {
    4. *this -= 1;
    5. return *this;
    6. }
    7. // 后置--
    8. Date operator--(int)
    9. {
    10. //cout << "后置--" << endl;
    11. Date tmp(*this);
    12. *this -= 1;
    13. return tmp;
    14. }

    二、const对象的调用

    我们知道,我们调用一个对象的时候,会传递一个this指针来,传过去的this指针是这种类型的:Date* const this,也就是说明这里的this指向的对象不能改变,但是本身的值可以改变,但是如果我们传一个const对象,就是指向的对象和值都不能改变,那样会咋变呢,如果直接传,就会出现权限的放大问题,我们知道,权限可以缩小,但绝对不可以放大。

    这时候我们可以这样去做,在函数定义和对象的声明的后面加上一个const:

    像这样。

    在 C++中,要调用一个const对象,可以使用const引用或const指针。const引用或const指针可以绑定到const对象,从而避免对const对象的直接修改。

     

    避免权限放大的方法是在调用const对象时,使用const引用或const指针。这样可以确保在函数内部不会修改const对象的值,从而避免权限放大的问题。

    三、初始化列表

    向我们之前讲的构造函数,以及拷贝构造函数,赋值构造函数等等,都是为了给类中的内置成员进行初始化的,但是如果出现了下面的这几种情况呢?

    1、引用

    2、const

    3、没有默认构造自定义类型成员(必须显示传参调构造)

    我们知道,引用只能在定义的时候初始化,然后const定义的变量也无法被修改自定义类型要调用它的默认构造函数, 如果没有默认构造函数就会error

    所以此时C++中就给出了一种合适的解决方法,那就是初始化列表。

    1. MyQueue(int n, int& rr)
    2. :_pushst(n)
    3. , _popst(n)
    4. ,_x(1)
    5. ,_y(rr)
    6. {
    7. _size = 0;
    8. }
    9. /*{
    10. _size(0);
    11. }*/
    12. private:
    13. Stack _pushst;
    14. Stack _popst;
    15. int _size;
    16. //这两个必须在定义时初始化
    17. const int _x;
    18. int& _y;
    19. };

    就像这样,这就是初始化列表的写法,在{}上面进行操作,但是不管{}有没有值,必须带着{}才可以。写法就是:然后后面每一句前面几上,即可。不要去在乎这样写的原因是啥,主要还是要记住就是这样去写。

    这里还有一点要注意,初始化列表初始化的顺序是声明的顺序,与初始化列表的顺序无关:

    1. class A
    2. {
    3. public:
    4. //初始化列表初始化的顺序是生命的顺序,不是初始化的顺序
    5. A(int a)
    6. :_a1(a)
    7. , _a2(_a1)
    8. {}
    9. void Print()
    10. {
    11. cout << _a1 << " " << _a2 << endl;
    12. }
    13. private:
    14. int _a2;
    15. int _a1;
    16. };

    如果运行这一串代码,如果传过去一个1你认为结果是啥,结果其实应该是一个随机值加上1,因为声明的顺序是_a2在前面。然后_a1在后面,所以初始化_a2,所以是一个随机值。

    四、static成员

    我们知道static的作用是:

    1. 静态局部变量:在函数内定义的静态局部变量,在程序执行期间只初始化一次,并且其生命周期贯穿整个程序运行过程,而不是仅在函数调用期间存在。
    2. 静态成员变量:属于整个类而不是某个具体对象,所有该类的对象共享这一个静态成员变量。
    3. 静态成员函数:可以通过类名直接调用,不依赖于具体对象,不能访问非静态成员变量(但可以通过对象访问)。
    4. 静态全局变量:限制了该变量的作用域仅在当前文件内可见,避免了命名冲突。

    这里我们要说的是其实static也可以在类中声明,被定义的成员被称为类的静态成员,我们知道静态成员不是谁所特有的,而是共享的,不属于某个具体的类,存放在静态区

    即使声明在类中,我们依然要在外面定义:

    类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问

    我们可以根据这个特点去定义它

    1. class A
    2. {
    3. public:
    4. A() { ++_sount; }
    5. A(const A& t) { ++_sount; }
    6. ~A()
    7. {
    8. //--_sount;
    9. }
    10. //静态成员函数
    11. //没有this指针,只能访问静态成员,无法访问其他的成员,访问其他成员变量要依靠this指针
    12. static int GetCount()
    13. {
    14. return _sount;
    15. }
    16. private:
    17. int _a1 = 1;
    18. int _a2 = 2;
    19. //静态区,不存在于对象中
    20. //这里不能给缺省值,因为缺省值是给初始化列表的,
    21. // 但是这里不会走初始化列表,因为他在静态区,所以不走初始化列表
    22. //价值:属于所有整个类,属于所有对象
    23. static int _sount;
    24. };
    25. //这是_sount的定义
    26. int A::_sount = 0;

    这里我们还需要注意一点就是静态成员函数无法调用非静态成员函数的,因为静态成员函数没有this指针的传递。

    五、友元函数

    友元是一种很有效的方式,但是并不推荐使用,因为它会破坏封装

    比如说
    Date d1;
    cout << d1 << endl;
    要实现这样的代码, 就只能重载 <<

    如果实现成Date的成员函数, ostream& operator<<(ostream& _cout)
    第一个参数是this, 所以调用的时候就成了 d1 << cout ;
    这样特别奇怪,所以一般这个都会重载成全局函数,所以又引发新的问题, 类的成员变量一般是私有的
    如果重载成全局函数,无法访问私有/保护成员,所以友元就派上用场了

    友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在
    类的内部声明,声明时需要加friend关键字

    像这样:

    这样就可以访问类中私有成员

    • 友元函数可访问类的私有和保护成员,但不是类的成员函数
    • 友元函数不能用const修饰
    • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
    • 一个函数可以是多个类的友元函数
    • 友元函数的调用与普通函数的调用原理相同

    六、友元类

    友元类其实就是两个类之间的友元:

    1. class A
    2. {
    3. //声明 B是A的友元
    4. //此时B就可以访问A中的私有,但是不证明A可以访问B的私有
    5. //也就是A把B当成朋友了,但是不能说明B把A当成朋友
    6. friend class B;
    7. };
    8. class B
    9. {
    10. };

    像这样,此时B就是A的友元,B就可以访问A中的私有成员,但是A不可以访问B的私有,

    也就是A把B当成朋友了,但是不能说明B把A当成朋友

    七、内部类

    内部类其实就是在一个类中再去定义一个类:

    1. 内部类,内部类是外部类的私有
    2. 在一个类中,除了定义函数和变量,还可以定义类,就叫内部类,类似于套娃
    3. class A
    4. {
    5. private:
    6. static int k;
    7. int h;
    8. public:
    9. void func()
    10. {
    11. cout << "func" << endl;
    12. }
    13. private:
    14. //内部类
    15. //放到A里面
    16. //仅仅受到类域限制
    17. class B//这里的B天生就是A的友元。
    18. {
    19. public:
    20. B(int b = 1)
    21. {
    22. _b = b;
    23. }
    24. void foo(const A& a)
    25. {
    26. //cout << k << endl;
    27. //cout << a.h << endl;
    28. }
    29. private:
    30. int _b;
    31. };
    32. };

    这就是一个内部类,然后这里的B天生就是A的友元。

     八、拷贝对象的优化

    编译器对拷贝构造的优化通常有以下几种方式:

    • 内联优化:编译器将拷贝构造函数的代码直接插入到调用处,从而避免了函数调用的开销。
    • 指针传递优化:如果传递对象的时候使用指针或引用,而不是拷贝整个对象,可以避免大量的内存拷贝操作,从而提高程序的执行效率。
    • 移动语义优化:如果拷贝对象的目的是为了将其传递给另一个函数或对象,编译器可以使用移动语义来避免不必要的拷贝操作。移动语义是一种新的语言特性,在 C++11 标准中引入,可以将对象的资源所有权从一个对象转移到另一个对象,避免了拷贝数据的开销。
    • 返回值优化(RVO):如果函数返回一个对象,编译器可以将这个对象在函数内部创建,然后直接返回给调用者,从而避免了拷贝对象的开销。这种优化方式被称为 RVO。

    我给大家看一个例子:

    1. #include
    2. using namespace std;
    3. class A
    4. {
    5. public:
    6. A()
    7. {
    8. cout << "A()" << endl;
    9. }
    10. A(const A& a)
    11. {
    12. cout << "A(const A& a)" << endl;
    13. }
    14. ~A()
    15. {
    16. cout << "~A()" << endl;
    17. }
    18. private:
    19. int _a;
    20. };
    21. A func()
    22. {
    23. A a;
    24. return a;
    25. }
    26. int main()
    27. {
    28. A tmp=func();
    29. return 0;
    30. }

    大家认为这个例子共进行了多少次的拷贝构造和析构?

    所以是两次构造,一次拷贝构造,一次赋值重载,三次析构。

    如果我们换个方式接收呢

    func改成这样:

    1. A func()
    2. {
    3. return A();
    4. }
    5. A
    6. int main()
    7. {
    8. A tmp=func();
    9. return 0;
    10. }

    我们再分析一下,A()是一次构造,返回是一次拷贝构造,然后拷贝构造给tmp,然后析构一次临时对象,西沟一次匿名对象,析一次tmp。

    所以就是一次构造,两次拷贝构造,三次析构。可是运行一下就发现:

    是一次构造一次析构。

    为什么呢?

    原因就是使用A()去返回的时候还返回啥临时对象啊,直接在接受返回值的地方构造就可以了,一个表达式,多次构造 +拷贝构造的 都会被优化为1次构造,这就是编译器的优化。

    九、匿名对象

    最后再来简单来说一下匿名对象,其实就是创建一个对象时不给他名字,

    • 有名对象的生命周期是当前作用域(main函数)
    • 匿名对象的作用域是当前这一行,即用即销毁

    最后:

    十分感谢你可以耐着性子把它读完和我可以坚持写到这里,送几句话,对你,也对我:

    1.一个冷知识:
    屏蔽力是一个人最顶级的能力,任何消耗你的人和事,多看一眼都是你的不对。

    2.你不用变得很外向,内向挺好的,但需要你发言的时候,一定要勇敢。
    正所谓:君子可内敛不可懦弱,面不公可起而论之。

    3.成年人的世界,只筛选,不教育。

    4.自律不是6点起床,7点准时学习,而是不管别人怎么说怎么看,你也会坚持去做,绝不打乱自己的节奏,是一种自我的恒心。

    5.你开始炫耀自己,往往都是灾难的开始,就像老子在《道德经》里写到:光而不耀,静水流深。

    最后如果觉得我写的还不错,请不要忘记点赞✌,收藏✌,加关注✌哦(。・ω・。)

    愿我们一起加油,奔向更美好的未来,愿我们从懵懵懂懂的一枚菜鸟逐渐成为大佬。加油,为自己点赞!

  • 相关阅读:
    最小步数
    YOLOv5,YOLOv8添加ASFF(自适应空间特征融合)
    代码随想录day2
    前端架构师之09_JavaScript_BOM
    大数据开发,Hadoop Spark太重?你试试esProc SPL
    SQL注入漏洞(绕过篇)
    小程序的使用
    京东按图搜索京东商品(拍立淘) API (.jd.item_search_img)快速抓取数据
    Linux安装MySQL8.0
    K-means和DBSCAN
  • 原文地址:https://blog.csdn.net/bhbcdxb123/article/details/139955560