• C++ 【类和对象: 析构函数,拷贝构造函数,运算符重载 --2】


    目录

    1.默认(缺省)成员函数:析构函数

    当带有static时,析构和构造函数的创建/销毁顺序是?

    2.拷贝构造函数

    2.1 内置类型和自定义类型

    3.运算符重载

    前置++和后置++重载

    友元

    4.赋值运算符重载:=

    5.const成员

    6.取地址及const取地址操作符重载


    1.默认(缺省)成员函数:析构函数

    析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作由编译器完成

    对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

    析构函数特性

    1. 析构函数名是在类名前加上字符 ~

    2. 无参数无返回值类型

    3.一个类只能有一个析构函数。若未定义,系统会自动生成默认的析构函数。析构函数不能重载

    4.对象生命周期结束,C++自动调用析构函数

    5.手动开辟的例如Stack中malloc,fopen等需要析构函数,Date日期类不需要析构函数

    6.编译器生成的默认析构函数,对内置类型不做处理,对自定类型成员调用它的析构函数

    1. ~Stack()
    2. {
    3. free(_array);
    4. }

    析构函数顺序

    先定义的先构造,后定义后构造; 先定义的后析构,后定义的先析构(栈和栈帧里面的对象都要符合后进先出)

    1. class A
    2. {
    3. public:
    4. A(int a = 0)
    5. {
    6. _a = a;
    7. cout << "A(int a = 0)->" <<_a<< endl;
    8. }
    9. ~A()
    10. {
    11. cout << "~A()->" <<_a<
    12. }
    13. private:
    14. int _a;
    15. };
    16. void f()
    17. {
    18. A aa1(1);
    19. A aa2(2);
    20. }
    21. int main()
    22. {
    23. f();
    24. return 0;
    25. }

    当带有static时,析构和构造函数的创建/销毁顺序是?

    两个局部静态对象,一个全局对象

    1. class A
    2. {
    3. public:
    4. A(int a = 0)
    5. {
    6. _a = a;
    7. cout << "A(int a = 0)->" <<_a<< endl;
    8. }
    9. ~A()
    10. {
    11. cout << "~A()->" <<_a<
    12. }
    13. private:
    14. int _a;
    15. };
    16. A aa3(3);
    17. void f()
    18. {
    19. static A aa4(4);
    20. A aa1(1);
    21. A aa2(2);
    22. static A aa5(5);
    23. }
    24. int main()
    25. {
    26. f();
    27. return 0;
    28. }

    全局变量最先被初始化(main函数之前初始化,全局和静态都在静态区),局部静态特点是第一次运行后初始化

    析构是aa2和aa1中最先析构,原因在于剩余三个生命周期在程序结束后才销毁,main函数栈帧结束清理在栈帧中的aa2和aa1;main函数结束,再调用全局和静态(符合先定义后析构)

     如果调用两次f()函数,结果又是如何?

     

    静态变量在第一次执行后初始化,第一次函数调用结束,aa4和aa5不会销毁


    2.拷贝构造函数

    有时候我们需要对一个对象进行拷贝,就会调用拷贝构造函数

    拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

    1. int main()
    2. {
    3. Date d1(2022, 7, 31);
    4. Date d2(d1);//拷贝构造两个写法
    5. Date d3 = d1;
    6. return 0;
    7. }

    拷贝构造函数也是特殊的成员函数,其特征如下:

    1. 拷贝构造函数是构造函数的一个重载形式。

    2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

    3.拷贝构造函数:函数名和类名相同,没有返回值;同类型对象构造

     错误的写法

    1. Date(Date d)
    2. {
    3. _year = d._year;
    4. _month = d._month;
    5. _day = d._day;
    6. }
    非法的复制构造函数

    传值传参:一份临时拷贝,开辟新空间

    传引用传参:别名,原空间

    d1实例化调用的是构造函数;用d1初始化d,对象实例化要调用拷贝构造函数,(同类型对象拷贝初始化)

    如果不是引用调用,同类型调用拷贝构造要传参,传参又是一个拷贝构造,层层传值引发对象的拷贝的递归调用

    正确的写法

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

    同时建议拷贝构造函数加const (权限缩小),防止以下情况发生(逻辑写反,原数据被修改,并不是修改原数据而是进行拷贝)

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


    2.1 内置类型和自定义类型

    若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序(memcpy)完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

    在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调 用其拷贝构造函数完成拷贝的(日期类不需要写拷贝构造,默认生成够用)。

    以下情况默认生成的拷贝构造函数无法使用,必须自己实现:

    深浅拷贝问题

    拷贝构造st2(st1)程序崩溃,原因在于两个指针指向了同一块malloc开辟的空间,结束时free释放了两次同一片空间,原因就在于浅拷贝造成的(指针地址拷贝)

     解决方法:深拷贝

    1. typedef int DataType;
    2. class Stack
    3. {
    4. public:
    5. Stack(int capacity=4)
    6. {
    7. cout << "Stack(int capacity = 4)" << endl;
    8. _array = (DataType*)malloc(sizeof(DataType) * capacity);
    9. if (NULL == _array)
    10. {
    11. perror("malloc申请空间失败!!!");
    12. return;
    13. }
    14. _size = 0;
    15. _capacity = capacity;
    16. }
    17. void Push(DataType data)
    18. {
    19. // CheckCapacity();
    20. _array[_size] = data;
    21. _size++;
    22. }
    23. ~Stack()
    24. {
    25. free(_array);
    26. }
    27. private:
    28. DataType* _array;
    29. int _capacity;
    30. int _size;
    31. };
    32. int main()
    33. {
    34. Stack st1;
    35. Stack st2(st1);
    36. return 0;
    37. }

    总结:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,拷贝构造函数一定要写,否则就是浅拷贝


    3.运算符重载

     一个类可以重载哪些运算符取,决于运算符对类有无意义

    内置类型可以使用运算符运算,但当自定义类型,例如日期类想完成日期-日期、比较日期、日期加天数等操作,可以使用运算符重载

    1. //日期类构造函数需要写;析构和拷贝构造默认生成够用
    2. class Date
    3. {
    4. public:
    5. Date(int year = 1, int month = 1, int day = 1)
    6. {
    7. _year = year;
    8. _month = month;
    9. _day = day;
    10. if (!CheckDate())
    11. {
    12. print();
    13. cout << "日期非法" << endl;
    14. }
    15. }
    16. bool CheckDate()
    17. {
    18. if (_year >= 1
    19. &&_month >0 && _month <13
    20. && _day >0 && _day <=GetMonthDay(_year,_month))
    21. {
    22. return true;
    23. }
    24. else
    25. {
    26. return false;
    27. }
    28. }
    29. private:
    30. int _year = 1;
    31. int _month = 1;
    32. int _day = 1;
    33. };

    运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

    函数类型:返回值类型 operator需要重载的运算符符号(参数列表)

    注意:返回值类型由运算符决定;参数列表由操作数决定(d1==d2,两个参数,并规定第一个参数为左操作数,第二个参数为右操作数)

    比较运算符重载

    技巧:任何一个类,写比较运算符重载,只需要写大于和等于或者小于和等于,剩下的比较运算符重载复用即可

    在类外,成员变量访问受到限制,要么使用友元,要么取消private,但是都会破坏封装

    1. bool operator==(const Date& d1,const Date& d2)
    2. {
    3. return d1._year == d2._year
    4. && d1._month == d2._month
    5. && d1._day == d2._day;
    6. }
    7. bool operator!=(const Date& d)
    8. {
    9. return !(*this == d);
    10. }
    11. bool operator>(const Date& d)
    12. {
    13. if (_year > d._year)
    14. {
    15. return true;
    16. }
    17. else if(_year == d._year && _month > d._month)
    18. {
    19. return true;
    20. }
    21. else if (_year == d._year && _month == d._month && _day > d._day)
    22. {
    23. return true;
    24. }
    25. else
    26. {
    27. return false;
    28. }
    29. }
    30. bool operator>=(const Date& d)
    31. {
    32. return (*this > d) || (*this == d);
    33. }
    34. bool operator<(const Date& d)
    35. {
    36. return !(*this >= d);
    37. }
    38. bool operator<=(const Date& d)
    39. {
    40. return !(*this > d);
    41. }
    42. int main()
    43. {
    44. Date d1(2022,5,20);
    45. Date d2(2022,8,1);
    46. cout<<(d1 == d2)<//<<优先级高
    47. return 0;
    48. }

    在类中,提示运算符参数太多,原因在于:this指针

    1. operator==(d1, d2);//全局时,编译器其实是处理成这样
    2. d1.operator==(&d1, d2);//类中时,编译器处理成这样
    3. d1.operator==(d2);//类中实际情况
    4. //但是this指针不能显示传参,不能显示声明参数,但是类中可以使用

    所以最好写成:成员函数

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

    可以在类中只声明,定义放到另一个文件中(防止太多内联造成代码膨胀,除非频繁调用)


    + 和 +=(天数)

    天满了进月,月满进年

    复用的情况下,先写+=更好(+=没有看对象构造),同时类不关心上下顺序(作为一个整体,上下都搜索)

    1. int GetMonthDay(int year,int month)//涉及闰年
    2. {
    3. static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };//频繁调用用static
    4. if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
    5. {
    6. return 29;
    7. }
    8. else
    9. {
    10. return days[month];//拿到每个月天数
    11. }
    12. }
    13. Date& operator+=(int day)
    14. {
    15. if (day < 0)//+= -100
    16. {
    17. return *this -= -day;
    18. }
    19. _day += day;
    20. while (_day > GetMonthDay(_year, _month))//如果大于这个月天数非法,进位
    21. {
    22. _day -= GetMonthDay(_year, _month);//
    23. ++_month;
    24. if (_month == 13)
    25. {
    26. _month = 1;
    27. ++_year;
    28. }
    29. }
    30. return *this;//this指向当前对象的指针,*this就是当前对象
    31. }
    32. Date operator+(int day)
    33. {
    34. Date ret(*this);
    35. ret += day;
    36. return ret;
    37. }
    38. //不复用的+
    39. Date operator+(int day)
    40. {
    41. Date ret = (*this);
    42. ret._day += day;
    43. while (ret._day > GetMonthDay(ret._year, ret._month))//如果大于这个月天数非法,进位
    44. {
    45. ret._day -= GetMonthDay(ret._year, ret._month);//
    46. ++ret._month;
    47. if (ret._month == 13)
    48. {
    49. ret._month = 1;
    50. ++ret._year;
    51. }
    52. }
    53. return ret;
    54. }

    前置++和后置++重载

    前置++返回值为++后的值,后置++返回值为++前的值

    运算符重载为了区分前后置++,C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递

    1. Date operator++(int)//后置++
    2. {
    3. Date ret = *this;
    4. *this += 1;
    5. return ret;
    6. }
    7. Date& operator++()
    8. {
    9. *this += 1;
    10. return *this;
    11. }

    前置--和后置--重载

    1. Date& operator--()//日期-天数.前置--
    2. {
    3. return (*this -= 1);
    4. }
    5. Date operator--(int)
    6. {
    7. Date tmp(*this);
    8. *this -= 1;
    9. return tmp;
    10. }

    日期-天数;日期-=天数;

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

    日期-日期

    不复用思路:算出当前年月离当年1.1号差多少天,再算出年之间差距(闰年366天)

    复用思路:累加

    1. int operator-(const Date& d)//日期-日期
    2. {
    3. int flag = 1;
    4. Date max = *this;//默认第一个大第二个小
    5. Date min = d;
    6. if (*this < d)
    7. {
    8. max = d;
    9. min = *this;
    10. flag = -1;
    11. }
    12. //小的不断++,加到跟大的相等为止
    13. int n = 0;
    14. while (min != max)
    15. {
    16. ++min;
    17. ++n;
    18. }
    19. return n*flag;
    20. }

    <<流插入运算符重载

    cout能自动识别类型在于cout写了运算符重载<<,依靠函数重载来实现自动识别类型

    当我们想写以下函数时确保错,由于cout是ostream对象的成员,处理内置类型,却不处理自定义类型,我们可以重载<<来实现日期类的流插入<<

     

    1. cout << (d1 + 100);
    2. cout << d1;

    原cout<<中一个是隐含的cout,一个是int/float等;在Date中一个是隐藏的Date,另一个传cout即可

    错误的返回值写法:cout << d1;

    (报错:没有找到接收Date类型的右操作数的运算符)

    当使用原生的d1.operator<<(cout)却可以调的到,原因在于运算符有多个操作符,而第一个操作数为d1,第二个操作数为cout,写法其实是d1 << cout

    1. ostream& operator<<(ostream& out)
    2. {
    3. //支持年月日输出
    4. out << d._year << "年" << d._month << "月" << d._day <<"日" << endl;
    5. return out;
    6. }

    解决方法:不能是成员函数(日期类对象抢占了第一个操作数),写在类外(使用友元)

    返回值使用ostream做返回值的对象,用来支持连续cout等操作

    >>流提取运算符重载

    为什么scanf要取地址而>>不用,原因在于没有引用,cin转换成调用operator流提取,把cin和d1引用传入(默认输入多个值以空格或者换行去间隔)

    1. istream& operator>>(istream& in, Date& d)
    2. {
    3. in >> d._year >> d._month >> d._day;
    4. if (!d.CheckDate())
    5. cout << "日期非法" << endl;
    6. return in;
    7. }

     

    3.3.友元

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

    缺点:友元破坏了封装

    friend ostream& operator<<(ostream& out, const Date& d);
    

    运算符重载总结:

     .*(matlab计算矩阵型号匹配)    ::(域作用限定符)    sizeof   ?:    .  注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

    2.内置类型的运算符,其含义不能改变

    判断日期是星期几

    1. Date d1(1840,11,1);
    2. cin >> d1;
    3. Date start(1, 1, 1);
    4. int n = d1 - start;
    5. int weekDay = 5;//默认从0开始,1.1.1星期1,0相当于周天
    6. weekDay += n;
    7. cout << "周" << weekDay % 7 + 1 << endl;


    4.赋值运算符重载:=

    日期类初始化时d2(d1)为拷贝构造,当两个类已经定义好时,把值赋值给另一个类,就叫赋值运算符重载

    参数类型:const T&,传递引用可以提高传参效率

    日期类的赋值运算符重载:返回值是Date是为了支持连续赋值,&可以让赋值一次拷贝构造都没发生(并不是静态和全局才能用引用返回,只要对象除了作用域还在即可);加引用减少拷贝构造,同时加const缩小权限;if判断是防止自己给自己赋值的无意义行为(地址比较)

    总结:能用就用引用传参和引用返回

    1. Date& operator=(const Date& d)//d1 = d3;
    2. {
    3. if (this != &d)
    4. {
    5. _year = d._year;
    6. _month = d._month;
    7. _day = d._day;
    8. }
    9. return *this;
    10. }

    赋值运算符只能重载成类的成员函数不能重载成全局函数(写在类外,类中生成默认赋值重载,造成重载冲突)

    用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值

    类似Stack需要自己写赋值运算符重载来避免浅拷贝(复制拷贝一样的问题)

    其特性和复制拷贝一样


     

    5.const成员

    在对象加了const,调用print会遇到问题,原因在于隐藏的this指针权限放大,从const Date转换为Date*const this(const修饰的是this指针本身不能被改变,指针的内容可以改变)

    &d1 是 Date*

    &d2 是const Date*(*之前意味着指向的内容不能被修改,传给Date*是权限放大)

    d1 < d2编译通过  d2 < d1编译报错,原因和上面情况一样

    只有指针和引用涉及权限缩小放大问题

     解决方法:让this指针变成const修饰即可,由于this指针隐含不能轻易修改,需要加在后面

    变成const Date* const this

    1. void print() const
    2. {
    3. cout << _year << "/" << _month << "/" << _day<
    4. }


    6.取地址及const取地址操作符重载

    这两个默认成员函数一般不用重新定义 ,编译器默认会生成。只有特殊情况,才需要重载,比如想让别人获取到指定的内容

    const对象取地址调用const A*,A对象调用A*

    普通对象和const对象要分开处理,就需要写两个;如果不需要例如只需要打印,写一个即可

    1. class A
    2. {
    3. public:
    4. A* operator&()
    5. {
    6. return this;
    7. }
    8. const A* operator&()const
    9. {
    10. return this;
    11. }
    12. private:
    13. int _year; // 年
    14. int _month; // 月
    15. int _day; // 日
    16. };
    17. int main()
    18. {
    19. A a;
    20. const A b;
    21. &a;
    22. &b;
    23. return 0;
    24. }

    特殊场景使用:不想让别人取到这个类型对象的地址,返回nullptr即可;或者转换为私有,无法取地址

  • 相关阅读:
    搭建 Makefile+OpenOCD+CMSIS-DAP+Vscode arm-none-eabi-gcc 工程模板
    Linux命令:tr和xargs
    含文档+PPT+源码等]基于ssm maven健身房俱乐部管理系统[包运行成功]Java毕业设计SSM项目源码
    GraalVM java17 Windows打包
    js前端条件语句优化
    闭区间上连续函数的一些定理
    java计算机毕业设计基于安卓Android的教务的校内人员疫情排查系统设计与实现APP
    UDP和TCP两大协议的区别,让你快速高效掌握
    afl-cov计算代码覆盖率
    汇编-变量
  • 原文地址:https://blog.csdn.net/weixin_63543274/article/details/126082126