【C++条记】类和对象的深入明白(三)

[复制链接]
发表于 2026-1-22 06:52:57 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
【C++条记】类和对象的深入明白(三)


🔥个人主页:明白的编程日志日志
🔥专栏:C++条记




媒介

   哈喽,各位小同伴各人好!上期我们讲了类和对象的更深入的内容。本日我们就给类和对象收尾。话不多说,咱们进入正题!向大厂冲锋!

  一.日期类的实现

为了加深我们前面临类和对象的明白。我们现在来试动手搓一个日期类。
1.1声明和界说分离

我们想让.h放函数的声明。.cpp放函数的界说。
  1. #pragma once
  2. #include<iostream>
  3. using namespace std;
  4. #include<assert.h>
  5. class Date
  6. {
  7.         friend istream& operator>>(istream& in, Date& d);
  8.         friend ostream& operator<<(ostream& out, const Date& d);//友元声明
  9. public:
  10.         Date(int year ,int month ,int day );
  11.         void Print();
  12.         // Ĭinline
  13.         int GetMonthDay(int year, int month)
  14.         {
  15.                 assert(month > 0 && month < 13);
  16.                 static int monthDayArray[13] = { -1, 31, 28, 31, 30, 31, 30,
  17. 31, 31, 30, 31, 30, 31 };
  18.                 if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
  19.                 {
  20.                         return 29;
  21.                 }
  22.                 return monthDayArray[month];
  23.         }
  24.         int GetYear();
  25.         int GetMonth();
  26.         int GetDay();
  27.         bool operator<(const Date& d) const;
  28.         bool operator<=(const Date& d) const;
  29.         bool operator>(const Date& d) const;
  30.         bool operator>=(const Date& d) const;
  31.         bool operator==(const Date& d) const;
  32.         bool operator!=(const Date& d) const;
  33.         bool CheckDate();
  34.         Date operator+(int day) const;
  35.         Date& operator+=(int day);
  36.         Date operator++(int);//后置
  37.         Date& operator++();//前置
  38.         Date operator--(int);//后置
  39.         Date& operator--();//前置
  40.         Date& operator-=(int day);
  41.         Date operator-(int day) const;
  42.         int operator-(const Date& d) const;
  43. private:
  44.         int _year;
  45.         int _month;
  46.         int _day;
  47. };
  48. istream& operator>>(istream& in,  Date& d);
  49. ostream& operator<<(ostream& out, const Date& d);
复制代码
我们日期类重要实现他的比力和日期类之间的相互运算。
1.2日期类+=整数

这里我们的思绪是先将整数加到天数上。然后如果天数不高出当月天数。直接返回即可。否则说嫡期比当月的全部日期都大。则向下月借位。天数减去当月天数之和,以此类推知道天数不高出当月的最大天数位置。如果12借位则年份+1,月份改为1即可。
  1. int GetMonthDay(int year, int month)
  2. {
  3.         assert(month > 0 && month < 13);
  4.         static int monthDayArray[13] = { -1, 31, 28, 31, 30, 31, 30,
  5. 31, 31, 30, 31, 30, 31 };
  6.         if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
  7.         {
  8.                 return 29;
  9.         }
  10.         return monthDayArray[month];
  11. }
  12. Date& Date::operator+=(int day)
  13. {
  14.         _day += day;
  15.         while (_day > GetMonthDay(_year, _month))
  16.         {
  17.                 _day -= GetMonthDay(_year, _month);
  18.                 _month++;
  19.                 if (_month == 13)
  20.                 {
  21.                         _year++;
  22.                         _month = 1;
  23.                 }
  24.         }
  25.         return *this;
  26. }
复制代码
1.3日期类+整数

现在我们实现日期类+整数,也就是不改变日期类。返回日期类+=整数的结果。那我们可以复用日期类+=整数的逻辑。为了不修改日期类。我们对日期类拷贝一份tmp。让tmp举行+=的逻辑如许改变就不会日期类。
  1. Date Date::operator+(int day) const
  2. {
  3.         Date tmp=*this;
  4.         tmp +=day;
  5.         return tmp;
  6. }
复制代码
1.4日期类-=整数

这里思绪和我们前面实现+的思绪一样。都是通过借位的头脑。我们向当天数-day。
然后当天数!>0时,向上一个月借位,天数+=上个月的天数(注意是上个月的,由于当月的天数已经被减过了)。如果是1月的话,借客岁12月的天数。让月份=12然后年份–。
  1. Date& Date::operator-=(int day)
  2. {
  3.         _day -= day;
  4.         while (_day <= 0)
  5.         {
  6.                 if (_month == 1)
  7.                 {
  8.                         _month = 12;
  9.                         _day += GetMonthDay(--_year, _month);
  10.                 }
  11.                 else
  12.                 {
  13.                         _day += GetMonthDay(_year, --_month);
  14.                 }
  15.         }
  16.         return *this;
  17. }
复制代码
1.5日期类-日期

日期类-日期我们就复用-=即可。
  1. Date Date::operator-(int day) const
  2. {
  3.         Date tmp=*this;
  4.         tmp -= day;
  5.         return tmp;
  6. }
复制代码
1.6复用对比


1.7日期类==日期类

判定两个日期类很简朴。我们只必要判定是否他们的年代日都相称即可。
  1. bool Date::operator==(const Date& d) const
  2. {
  3.         if (_year == d._year && _month == d._month && _day == d._day)
  4.         {
  5.                 return true;
  6.         }
  7.         else
  8.         {
  9.                 return false;
  10.         }
  11. }
复制代码
1.8日期类<日期类

判定两个日期类<。
先比力年。
<返回true。
==继续比力月。月<返回true。 ==返回比力天。
剩下的情况都是false的情况。直接返回false.
  1. bool Date::operator<(const Date& d) const
  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.                         return _day < d._day; //判断天
  16.                 }
  17.         }
  18.         return false;//年不相等
  19. }
复制代码
1.9比力复用

剩下的我们通过前面实现的<和==复用即可。
  1. bool Date::operator!=(const Date& d) const
  2. {
  3.         return !(*this == d);
  4. }
  5. bool Date::operator<=(const Date& d) const
  6. {
  7.         return *this < d || *this == d;
  8. }
  9. bool Date::operator>(const Date& d) const
  10. {
  11.         return !(*this < d)&&!(*this==d);
  12. }
  13. bool Date::operator>=(const Date& d) const
  14. {
  15.         return !(*this < d);
  16. }
复制代码
1.10前置和后置

前置我们拷贝一份*this,在复用+=或-=即可。
  1. Date Date::operator++(int)
  2. {
  3.         Date tmp = *this;
  4.         *this += 1;
  5.         return tmp;
  6. }
  7. Date& Date::operator++()
  8. {
  9.         *this += 1;
  10.         return *this;
  11. }
  12. Date Date::operator--(int)
  13. {
  14.         Date tmp=*this;
  15.         *this-= 1;
  16.         return tmp;
  17. }
  18. Date& Date::operator--()
  19. {
  20.         *this -= 1;
  21.         return *this;
  22. }
复制代码
1.11日期类-日期类

两个日期类相减得出他们之间相差的天数。
我们的思绪就是直接让小的天数不停相加,直到相加到小的日期和大的日期相称即可。每越日期++就用一个变量记载天数即可。
  1. int Date::operator-(const Date& d) const
  2. {
  3.         int sum = 0;
  4.         int flag = 1;
  5.         Date max, min;
  6.         max = *this;
  7.         min = d;
  8.         if (*this < d)//假设法
  9.         {
  10.                 swap(max, min);
  11.                 flag = -1;
  12.         }
  13.         while (min!=max)
  14.         {
  15.                 sum++;
  16.                 min++;
  17.         }
  18.         sum *=flag;
  19.         return sum;
  20. }
复制代码
1.12日期类IO

由于类的成员函数默认*this抢占了第一个位置。和我们平常的写法不一样。以是我们把日期类的输出和输出放在类外貌。那放在类外貌我们怎样访问类的成员变量呢?那我们就可以加一个友元函数声明。


  1. istream& operator>>(istream& in, Date& d)
  2. {
  3.         cout<<"请依次输入年月日:"<<endl;
  4.         in >>d._year>> d._month >> d._day;
  5.         return in;
  6. }
  7. ostream& operator<<(ostream& out, const Date& d)
  8. {
  9.         out << d._year <<"年" << d._month<<"月" << d._day<<"日";
  10.         return out;
  11. }
复制代码
为了实现一连输入或输出。我们必要返回istrem或ostrem对象。

同时注意流对象不支持拷贝。以是istrem或ostrem必须用引用。
二.取地点运算符重载

2.1const成员函数



  • 界说
    将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的背面。
  • const修饰
    const现实修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员举行修改。const 修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为const Date* const this。

    不写加const。

加上const。

同时非const成员也可以调用const成员函数。由于权限可以缩小。以是加上const可以防止我们的步伐不警惕窜改(原来不应该修改却不警惕修改会报错),同时也让我们的的成员函数传参更宽泛,const成员也可以调用。以是能加尽加。

2.2取地点运算符重载

   取地点运算符重载分为平凡取地点运算符重载和const取地点运算符重载,⼀般这两个函数编译器自动天生的就可以够我们⽤了,不必要去表现实现。除非⼀些很特别的场景,比如我们不想让别⼈取到当前类对象的地点,就可以本身实现⼀份,胡乱返回⼀个地点。
  1. class Date
  2. {
  3. public:
  4.         Date* operator&()
  5.         {
  6.                 return this;
  7.                 // return nullptr;
  8.         }
  9.         const Date* operator&()const
  10.         {
  11.                 return this;
  12.                 // return nullptr;
  13.         }
  14. private:
  15.         int _year; // 年
  16.         int _month; // ⽉
  17.         int _day; // ⽇
  18. };
复制代码
一样平常这两个成员函数都不必要我们表现的写。由于他们两个是默认成员函数。

按理说我们只写const成员函数即可。由于岂论是const成员还是非const成员都可以调用。但是const成员调用返回的是const this*。以是还必要写两份。而且编译器调用时会调用最匹配的。
三.再探构造函数



  • 初始化列表
    之前我们实现构造函数时,初始化成员变量重要使用函数体内赋值,构造函数初始化尚有⼀种方式,就是初始化列表,初始化列表的使用方式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"背面跟⼀个放在括号中的初始值或表达式。
大概情势如下:
  1. Date(int& x, int year = 1, int month = 1, int day = 1)
  2. :_year(year)
  3. ,_month(month)
  4. ,_day(day)
  5. ,_t(12)
  6. ,_ref(x)
  7. ,_n(1)
  8. {
  9. // error C2512: “Time”: 没有合适的默认构造函数可⽤
  10. // error C2530 : “Date::_ref” : 必须初始化引⽤
  11. // error C2789 : “Date::_n” : 必须初始化常量限定类型的对象
  12. }
复制代码


  • 界说的位置
    每个成员变量在初始化列表中只能出现⼀次,语法明白上初始化列表可以以为是每个成员变量界说初始化的地方。由于初始化列表是界说的地方以是,只能初始化一次。否则就会变成多次界说。
  • 界说初始化
    引用成员变量,const成员变量,没有默认构造的类范例变量,必须放在初始化列表位置举行初始化,否则会编译报错。
  1. class Time
  2. {
  3. public:
  4.         Time(int hour=0)
  5.                 :_hour(hour)
  6.         {
  7.                 cout << "Time()" << endl;
  8.         }
  9. private:
  10.         int _hour;
  11. };
  12. class Date
  13. {
  14. public:
  15.         Date(int& x,int year = 1, int month = 1, int day = 1)
  16.                 :_year(year)
  17.                 , _month(month)
  18.                 , _day(day)
  19.                 ,a(1)
  20.                 ,_ref(x)//成员定义
  21.         {
  22.                 // error C2512: “Time”: 没有合适的默认构造函数可⽤
  23.                 // error C2530 : “Date::_ref” : 必须初始化引⽤
  24.                 // error C2789 : “Date::_n” : 必须初始化常量限定类型的对象
  25.         }
  26.         void Print() const
  27.         {
  28.                 cout << _year << "-" << _month << "-" << _day << endl;
  29.         }
  30. private:
  31.         //声明
  32.         int _year;
  33.         int _month;
  34.         int _day; int main();
  35.         int& _ref;
  36.         const int a;
  37.         Time _t;
  38. };
复制代码

引用成员变量,const成员变量必须在初始化列表初始化。由于他们必须在界说的地方初始化。

没有默认构造的类范例也必须在初始化列表初始化,由于他必须手动传参调用构造函数。
那初始化列表和函数体内赋值可以混着用吗?
可以,由于有些场景必须混着用。
  1. Date(int& x,int year = 1, int month = 1, int day = 1)
  2.         :_year(year)
  3.         , _month(month)
  4.         , _day(day)
  5.         ,a(1)
  6.         ,_ref(x)
  7.         ,_ptr((int*)malloc(size(int)//成员定义
  8. {
  9.         if (_ptr == nullptr)
  10.         {
  11.                 perror(malloc fail!);
  12.         }
  13. }
复制代码
比方有个指针,我们malloc以后必要查抄是否失败。那就必须在函数体内查抄。


  • 缺省值
    C++11支持在成员变量声明的位置给缺省值,这个缺省值重要是给没有表现在初始化列表初始化的成员使用的。
  1. private:
  2.         //声明
  3.         int _year=1;
  4.         int _month=1;
  5.         int _day=1;
  6.         int* _ptr=((int*)malloc(sizeof(int)));
复制代码
在声明的位置给一个缺省值。



  • 总结
    只管使用初始化列表初始化,由于那些你不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显⽰在初始化列表初始化的内置范例成员是否初始化取决于编译器,C++并没有规定。对于没有表现在初始化列表初始化的⾃界说范例成员会调⽤这个成员范例的默认构造函数,如果没有默认构造会编译错误。
    以是初始化列表能写尽写,由于就算你不写他也会走初始化列表。


  • 初始化序次
    初始化列表中按照成员变量在类中声明序次举行初始化,跟成员在初始化列表出现的的先后序次无关。发起声明序次和初始化列表序次保持⼀致。

各人看一下这道题选什么。

大概许多同砚都会以为选A。但现实不是如许的。由于初始化列表初始化的序次是声明的序次。

声明是序次是他们在内存存放的序次。初始化是按照内存中的序次初始化。
四.范例转换

  1. class A
  2. {
  3. public:
  4.         //构造函数explicit就不再⽀持隐式类型转换
  5.         // explicit A(int a1)
  6.         A(int a1)
  7.                 :_a1(a1)
  8.         {}
  9.         //explicit A(int a1, int a2)
  10.         A(int a1, int a2)
  11.                 :_a1(a1)
  12.                 , _a2(a2)
  13.         {}
  14.         void Print()
  15.         {
  16.                 cout << _a1 << " " << _a2 << endl;
  17.         }
  18. private:
  19.         int _a1 = 1;
  20.         int _a2 = 2;
  21. };
  22. int main()
  23. {
  24.         // 1构造⼀个A的临时对象,再⽤这个临时对象拷⻉构造aa3
  25.         //编译器遇到连续构造+拷⻉构造->优化为直接构造
  26.     A aa1 = 1;
  27.         aa1.Print();
  28.         const A& aa2 = 1;
  29.         // C++11之后才⽀持多参数转化
  30.     A aa3 = { 2,2 };
  31.         return 0;
  32. }
复制代码


  • 隐式范例转化
    C++支持内置范例隐式范例转换为类范例对象,必要有相干内置范例为参数的构造函数。

    这里我们可以验证一下。

这里报错是为啥?
由于隐式范例转化天生是临时对象,临时对象具有常性。以是要加上const引用。

那隐式范例转化有什么用?
  1. class Stack
  2. {
  3. public:
  4.         void Push(const A& aa)
  5.         {
  6.         }
  7. private:
  8.         int top;
  9. };
  10. int main()
  11. {
  12.         Stack s;
  13.         A aa = 1;
  14.         s.Push(aa);
  15.         s.Push(1);
  16.         return 0;
  17. }
复制代码

可以发现隐式范例转化可以让我们写代码更加方便轻便。


  • explicit
    如果不想发生隐式范例转化, 构造函数前面加explicit就不再支持隐式范例转换。

五.static成员



  • 静态成员变量
    用static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外举行初始化。
  • 共享
    静态成员变量为全部类对象所共享,不属于某个详细的对象,不存在对象中,存放在静态区。



  • 静态成员函数
    用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
  • 访问权限
    静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,由于没有this指针。非静态的成员函数,可以访问恣意的静态成员变量和静态成员函数。

  • 指定类域
    突破类域就可以访问静态成员,可以通过类名::静态成员大概对象.静态成员来访问静态成员变量和静态成员函数。

  • 访问限定符
    静态成员也是类的成员,受public、protected、private访问限定符的限定。
  • 缺省值
    静态成员变量不能在声明位置给缺省值初始化,由于缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。
  • 练习一
    标题:求1+2+3…+n

这道标题把通例的方法都限定了。但是这道题可以用我们的静态成员办理。
这道题重要修饰构造出循环这个条件。以是我们可以用一个类数组来构造这个条件。
界说一个n巨细的类数组。那就会调用n次构造。我们只必要界说两个静态成员。然后每次构造把当前的值累加即可。
  1. class Sum {
  2.   public:
  3.     Sum() {
  4.         _ret += _i;
  5.         ++_i;
  6.     }
  7.     static int GetRet() {
  8.         return _ret;
  9.     }
  10.   private:
  11.     static int _i;
  12.     static int _ret;
  13. };
  14. int Sum::_i = 1;
  15. int Sum::_ret = 0;
  16. class Solution {
  17.   public:
  18.     int Sum_Solution(int n) {
  19.         //变⻓数组
  20.         Sum arr[n];
  21.         return Sum::GetRet();
  22.     }
  23. };
复制代码



  • 练习二

  1. C c;
  2. int main()
  3. {
  4.         A a;
  5.         B b;
  6.         static D d;
  7.         return 0;
  8. }
复制代码


六.友元



  • 界说
    友元提供了⼀种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明大概类声明的前面加friend,而且把友元声明放到⼀个类的内里。
  1. class A
  2. {
  3.         // 友元声明
  4.     friend void func(const A& aa, const B& bb);
  5. private:
  6.         int _a1 = 1;
  7.         int _a2 = 2;
  8. };
  9. void func(const A& aa, const B& bb)
  10. {
  11.    cout << aa._a1 << endl;
  12.    cout << bb._b1 << endl;
  13. }
复制代码
比方这里func是A的好朋侪,func就可以访问A的成员。


  • 声明
    外部友元函数可访问类的私有和掩护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。
  • 声明位置
    友元函数可以在类界说的任何地方声明,不受类访问限定符限定。
  • 多个友元
    ⼀个函数可以是多个类的友元函数。
  1. class A
  2. {
  3.         // 友元声明
  4.     friend void func(const A& aa, const B& bb);
  5. private:
  6.         int _a1 = 1;
  7.         int _a2 = 2;
  8. };
  9. class B
  10. {
  11.         // 友元声明
  12.     friend void func(const A& aa, const B& bb);
  13. private:
  14.         int _b1 = 3;
  15.         int _b2 = 4;
  16. };
复制代码
func既是A的友元又是B的友元。


  • 友元类
    友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和掩护成员
  1. class A
  2. {
  3.         // 友元声明
  4.     friend class B;
  5. private:
  6.         int _a1 = 1;
  7.         int _a2 = 2;
  8. };
  9. class B
  10. {
  11. public:
  12.         void func1(const A& aa)
  13.         {
  14.                 cout << aa._a1 << endl;
  15.                 cout << _b1 << endl;
  16.         }
  17.         void func2(const A& aa)
  18.         {
  19.                 cout << aa._a2 << endl;
  20.                 cout << _b2 << endl;
  21.         }
  22. private:
  23.         int _b1 = 3;
  24.         int _b2 = 4;
  25. };
  26. int main()
  27. {
  28.         A aa;
  29.         B bb;
  30.         bb.func1(aa);
  31.         bb.func1(aa);
  32.         return 0;
  33. }
复制代码
B是A的友元类。B的全部成员函数都可以访问A的成员变量。



  • 单向性
    友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。
  • 通报性
    友元类关系不能通报,如果A是B的友元,B是C的友元,但是A不是C的友元。
  • 使用
    偶然提供了便利。但是友元会增长耦合度,粉碎了封装,以是友元不宜多用。
七.内部类



  • 界说
    如果⼀个类界说在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独立的类,跟界说在全局相比,他只是受外部类类域限定和访问限定符限定,以是外部类界说的对象中不包罗内部类。
  1. class A
  2. {
  3. private:
  4.         static int _k;
  5.         int _h = 1;
  6. public:
  7.         class B // B默认就是A的友元
  8.         {
  9.         public:
  10.         void foo(const A& a)
  11.         {
  12.             cout << _k << endl; //OK
  13.             cout << a._h << endl; //OK
  14.         }
  15.      private:
  16.          int _b;
  17.     };
  18. };
  19. int A::_k = 1;
  20. int main()
  21. {
  22.     cout << sizeof(A) << endl;
  23.     A::B b;
  24.     A aa;
  25.     b.foo(aa);
  26. }
复制代码

以是刚刚OJ的代码也可以改成内部类
  1. class Solution {
  2.     // 内部类
  3.     class Sum
  4.     {
  5.     public:
  6.         Sum()
  7.         {
  8.             _ret += _i;
  9.             ++_i;
  10.         }
  11.     };
  12.     static int _i;
  13.     static int _ret;
  14. public:
  15.     int Sum_Solution(int n) {
  16.         // 变⻓数组
  17.         Sum arr[n];
  18.         return _ret;
  19.     }
复制代码


  • 默认友元
    内部类默认是外部类的友元类。
  • 封装
    内部类本质也是⼀种封装,当A类跟B类精密关联,A类实现出来重要就是给B类使用,那么可以思量把A类操持为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。
八.匿名对象



  • 界说
    用范例(实参)界说出来的对象叫做匿名对象,相比之前我们界说的范例对象名(实参)界说出来的叫著名对象。

匿名对象让我们写代码更方便。


  • 生命周期
    匿名对象的生命周期只在当前⼀行,⼀般临时界说⼀个对象当前用⼀下即可,就可以界说匿名对象。

    可以看到未打印1之前,A就析构了。由于匿名对象声明周期只在当前行。
后言

   这就是类和对象的深入明白。本日讲了许多内容。各人本身好好消化。感谢各位的耐烦垂阅!咱们下期见!拜拜~
  

回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表