• 类和对象(中)(构造函数、析构函数和拷贝构造函数)


    1.类的六个默认成员函数

    任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。

    1. //空类
    2. class Date{};

    默认成员函数:用户没有显示实现,编译器会自动生成的成员函数称为默认成员函数

    2.构造函数

    构造函数 是一个 特殊的成员函数,名字与类名相同 , 创建类类型对象时由编译器自动调用 ,以保证 每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次
    构造函数主要任务不是开空间创建对象,而是初始化对象。
    其特征如下:
            ① 函数名与类名相同。
            ② 无返回值。//不需要写void
            ③ 对象实例化时编译器 自动调用 对应的构造函数。
            ④ 构造函数可以重载。
    多个构造函数,有多种初始化方式,一般情况,建议每个类,都可以写一个全缺省的构造(好用)
    1. class Date
    2. {
    3.  public:
    4. //他们俩构成函数重载,但是无参调用时会存在歧义
    5.      // 1.无参构造函数
    6.      Date()
    7.     {
    8. _year = 1;
    9. _month = 1;
    10. _day = 1;
    11. }
    12.  
    13.      // 2.带参构造函数
    14. // 一般情况,建议每个类,都可以写一个全缺省的构造(好用)
    15.      Date(int year=1, int month=1, int day=1)
    16.     {
    17.          _year = year;
    18.       _month = month;
    19.          _day = day;
    20.     }
    21. void Print()
    22. {
    23. cout << _year << "-" << _month << "-" << _day << endl;
    24. }
    25. private:
    26.      int _year;
    27.      int _month;
    28.      int _day;
    29. };
    30. int main()
    31. {
    32. //Date d1(); //d1后面不能带括号否则定义对象无法跟函数声明区分开
    33. //Date func();//这就是d1为什么不能带括号
    34. Date d1;
    35. d1.Print();
    36. //类型 对象(202442)
    37. Date d2(2024, 4, 2);//这里调用构造函数是对象名加参数列表
    38. //这里可以和函数声明进行区分,如下一行的函数声明所示
    39. //Date func(int x, int y, int z);
    40. d2.Print();
    41. Date d3(2024);
    42. d3.Print();
    43. Date d4(2024, 4);
    44. d4.Print();
    45. return 0;
    46. }

            ⑤如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦            用户显式定义编译器将不再生成。

    1. #include<iostream>
    2. using namespace std;
    3. class Date
    4. {
    5. public:
    6. /*
    7. // 如果用户显式定义了构造函数,编译器将不再生成
    8. Date(int year, int month, int day)
    9. {
    10. _year = year;
    11. _month = month;
    12. _day = day;
    13. }
    14. */
    15. void Print()
    16. {
    17. cout << _year << "-" << _month << "-" << _day << endl;
    18. }
    19. private:
    20. int _year;
    21. int _month;
    22. int _day;
    23. };
    24. int main()
    25. {
    26. //Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
    27. //Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
    28. // 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
    29. Date d1;//对象实例化的时候自动调用对应的构造函数
    30. return 0;
    31. }

    如果用户显示定义了构造函数,编译器将不会生成无参的默认构造函数,这时候如果定义一个无参的类如Date d1,会编译失败。

    ⑥C++ 把类型分成内置类型 ( 基本类型 ) 和自定义类型。内置类型就是语言提供的数据类型,如:int/char.../任意类型指针 ,自定义类型就是我们使用 class/struct/union 等自己定义的类型。
    如果我们没写构造函数,编译器自动生成构造函数,对于编译器自动生成的构造函数
    对于内置类型的成员变量,编译器没有规定要不要做处理!(有些编译器会处理成0,但是C++标准并没有规定)
    对于自定义类型的成员变量,才会调用他的默认成员函数即无参构造,如果没有无参构造会报错。(不传参就可以调用的那个构造,全缺省构造)
    1. #include<iostream>
    2. using namespace std;
    3. class Time
    4. {
    5. public:
    6. Time()
    7. {
    8. cout << "Time()" << endl;
    9. _hour = 0;
    10. _minute = 0;
    11. _second = 0;
    12. }
    13. private:
    14. int _hour;
    15. int _minute;
    16. int _second;
    17. };
    18. class Date
    19. {
    20. private:
    21. // 基本类型(内置类型)
    22. int _year;
    23. int _month;
    24. int _day;
    25. // 自定义类型
    26. Time _t;
    27. };
    28. int main()
    29. {
    30. Date d;
    31. return 0;
    32. }

    这里我们发现编译器自动生成的构造函数对于自定义类型调用了它的默认构造函数。

    注意: C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即: 内置类型成员变量在
    类中声明时可以给默认值
    1. class Time
    2. {
    3. public:
    4. Time()
    5. {
    6. cout << "Time()" << endl;
    7. _hour = 0;
    8. _minute = 0;
    9. _second = 0;
    10. }
    11. private:
    12. int _hour;
    13. int _minute;
    14. int _second;
    15. };
    16. class Date
    17. {
    18. private:
    19. // 基本类型(内置类型)
    20. int _year = 1970;
    21. int _month = 1;
    22. int _day = 1;
    23. // 自定义类型
    24. Time _t;
    25. };
    26. int main()
    27. {
    28. Date d;
    29. return 0;
    30. }

    无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。

    注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。

    总结:不传参数就可以调用的函数就是默认构造

    Ⅰ一般情况构造函数都需要我们自己显式去实现

    Ⅱ只有少数情况下可以让编译器自动生成构造函数类似MyQueue,成员全是自定义类型

    3.析构函数

    析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由
    编译器完成的。而 对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

    析构函数是特殊的成员函数,其特征如下:

    ①析构函数名是在类名前加上字符 ~
    ②无参数无返回值类型。
    ③一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构
    函数不能重载
    ④对象生命周期结束时,C++ 编译系统系统自动调用析构函数

    ⑤对于编译器自动生成的默认析构函数,对于自定义类型成员会调用它的析构函数。

    跟构造函数类似:

    a、内置类型不做处理       

    b、自定义类型去调用他的析构

    1. class Time
    2. {
    3. public:
    4. ~Time()
    5. {
    6. cout << "~Time()" << endl;
    7. }
    8. private:
    9. int _hour;
    10. int _minute;
    11. int _second;
    12. };
    13. class Date
    14. {
    15. private:
    16. // 基本类型(内置类型)
    17. int _year = 1970;
    18. int _month = 1;
    19. int _day = 1;
    20. // 自定义类型
    21. Time _t;
    22. };
    23. int main()
    24. {
    25. Date d;
    26. return 0;
    27. }//对象在这里被销毁后,这里会自动调用析构函数

    程序运行结束后输出: ~Time() ,在main 方法中根本没有直接创建 Time 类的对象,为什么最后会调用 Time 类的析构函数?
    因为: main 方法中创建了 Date 对象 d ,而 d 中包含 4 个成员变量,其中 _year, _month,
    _day 三个是 内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t Time 类对 象,所以在d销毁时,要将其内部包含的 Time 类的 _t 对象销毁,所以要调用 Time 类的析构函数。但是:main函数 中不能直接调用Time 类的析构函数,实际要释放的是 Date 类对象,所以编译器会调用 Date 类的析构函数,而Date 没有显式提供,则编译器会给 Date 类生成一个默认的析构函数,目的是在其内部调用Time 类的析构函数,即当Date 对象销毁时,要保证其内部每个自定义对象都可以正确销毁
     main函数中并没有直接调用 Time 类析构函数,而是显式调用编译器为 Date 类生成的默认析 构函数
    注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
    如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如
    Date 类;有资源申请时,一定要写,否则会造成资源泄漏,比如 Stack
    自动生成的构造函数和析构意义何在?两个栈实现一个队列
    Stack.h如下:
    1. #pragma once
    2. #include
    3. #include
    4. using namespace std;
    5. class Stack
    6. {
    7. public:
    8. Stack(int n = 4);
    9. ~Stack();
    10. //void Init();
    11. //void Destroy();
    12. void Push(int x);
    13. bool Empty();
    14. void Pop();
    15. int Top();
    16. private:
    17. // 成员变量
    18. int* _a;
    19. int _top;
    20. int _capacity;
    21. };
    22. class Queue
    23. {
    24. public:
    25. void Init();
    26. void Push(int x);
    27. };

    Stack.cpp如下:

    1. #include"Stack.h"
    2. Stack::Stack(int n)//缺省参数声明和定义不能同时给,规定了只在声明时候给,定义的时候不给
    3. {
    4. _a = (int*)malloc(sizeof(int)*n);
    5. _top = 0;
    6. _capacity = n;
    7. }
    8. Stack::~Stack()
    9. {
    10. cout << "~Stack()" << endl;
    11. free(_a);
    12. _a = nullptr;
    13. _top = 0;
    14. _capacity = 0;
    15. }
    16. //void Stack::Init()//指明类的作用域就指明了类的出处
    17. //{
    18. // _a = nullptr;
    19. // _top = 0;
    20. // _capacity = 0;
    21. //}//任何一个变量都得先定义再使用,不符合语法报语法错误
    22. //void Destroy()
    23. //{
    24. // //...
    25. //}
    26. void Stack::Push(int x)
    27. {
    28. // ...
    29. _a[_top++] = x;
    30. }
    31. bool Stack::Empty()
    32. {
    33. return _top == 0;
    34. }
    35. void Stack::Pop()
    36. {
    37. --_top;
    38. }
    39. int Stack::Top()
    40. {
    41. return _a[_top - 1];
    42. }
    43. //void Queue::Push(int x)
    44. //{
    45. //
    46. //}

    test.cpp
    1. #icnlude
    2. class MyQueue
    3. {
    4. private:
    5. Stack _pushst;
    6. Stack _popst;
    7. };
    8. int main()
    9. {
    10. MyQueue q;
    11. return 0;
    12. }

    这里创建MyQuque类q时,会自动调用调用Stack类的默认构造函数,q销毁时,也会自动调用Stack类的默认析构函数。

    下面是一个括号匹配问题,利用c和c++实现的比较,可以发现,利用c++的构造和析构特性后,会方便很多

    实践中总结:
    1、有资源需要显示清理,就需要写析构。如:Stack List
    2、有两种场景不需要显示写析构,默认生成就可以了
    a、没有资源需要清理,如:Date
    b、内置类型成员没有资源需要清理。剩下的都是自定义类型成员
    4.拷贝构造函数
    拷贝构造函数: 只有单个形参 ,该形参是对本 类类型对象的引用 ( 一般常用 const 修饰 ) ,在用 已存 在的类类型对象创建新对象时由编译器自动调用
    特征:
    ①拷贝构造函数 是构造函数的一个重载形式
    拷贝构造函数的 参数只有一个 必须是类类型对象的引用 ,使用 传值方式编译器直接报错 因为会引发无穷递归调用。
    1. class Date
    2. {
    3. public:
    4. Date(int year = 1970, int month =1, int day=1)
    5. {
    6. _year = year;
    7. _month = month;
    8. _day = day;
    9. }
    10. //Date(Date d)错误写法
    11. //Date d2(d1);//d1传给了d,d2就是this
    12. Date(const Date& d)//加了const之后d的内容就无法修改
    13. {
    14. cout << "const Date& d" << endl;
    15. //this->_year = d._year;
    16. _year = d._year;
    17. _month = d._month;
    18. _day = d._day;
    19. }
    20. // Date(Date* d)//规定这里不是拷贝构造,就是一个普通构造,编译器会自动生成一个默认的拷贝构造
    21. // {
    22. // _year = d->_year;
    23. // _month = d->_month;
    24. // _day = d->_day;
    25. // }
    26. void Print()
    27. {
    28. cout << _year << "-" << _month << "-" << _day << endl;
    29. }
    30. private:
    31. // 基本类型(内置类型)
    32. int _year ;
    33. int _month ;
    34. int _day ;
    35. };
    36. void func(Date d)
    37. {
    38. d.Print();
    39. }
    40. int main()
    41. {
    42. Date d1(2024,4,18);
    43. Date d2(d1);
    44. func(d2);
    45. return 0;
    46. }

    //Date(Date d)是错误写法,

    原因:对于自定义类型传值传参要调用拷贝构造完成,这是一种规定,自定义类型的拷贝都要调用拷贝构造才能完成。这里用d1去构建d2,规定需要调用拷贝构造,调用拷贝构造得先传参,先传参形成了一个新的拷贝构造,新的拷贝构造假设去调用,去调用这个拷贝构造,又要先传参,从逻辑上来说就是一个无穷递归

    对于
    1. func(d2);
    2. void func(Date d)
    3. {
    4. d.Print();
    5. }
    6. Date(const Date& d)
    7. {
    8. cout << "const Date& d" << endl;
    9. _year = d._year;
    10. _month = d._month;
    11. _day = d._day;
    12. }

    与构造和析构函数一样,内置类型就直接传,自定义类型传值传参就要调用拷贝构造完成,这里调用func之前先传参,传参就会形成一个拷贝构造,d是d2的别名,函数结束回来,传参完成,参数的传递就是完成拷贝构造调用,最后调用调用func函数,结束

    也可以从建立函数栈帧的方式去看函数func(d2)的调用

    当然也可以采用指针或者引用的方式,这种方式不会调用拷贝构造。

    1. func(d2);
    2. void func(Date& d)
    3. {
    4. d.Print();
    5. }
    6. //这种方式也不会调用拷贝构造,d是d2的别名
    7. func(&d2);
    8. void func(Date* d)
    9. {
    10. d->Print();
    11. }
    12. //这种情况下没有拷贝构造,因为传的是内置类型,把d2地址进行传递,用指针进行接收

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

    1. class Time
    2. {
    3. public:
    4. Time()
    5. {
    6. _hour = 1;
    7. _minute = 1;
    8. _second = 1;
    9. }
    10. Time(const Time& t)
    11. {
    12. _hour = t._hour;
    13. _minute = t._minute;
    14. _second = t._second;
    15. cout << "Time::Time(const Time&)" << endl;
    16. }
    17. private:
    18. int _hour;
    19. int _minute;
    20. int _second;
    21. };
    22. class Date
    23. {
    24. private:
    25. // 基本类型(内置类型)
    26. int _year = 1970;
    27. int _month = 1;
    28. int _day = 1;
    29. // 自定义类型
    30. Time _t;
    31. };
    32. int main()
    33. {
    34. Date d1;
    35. // 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
    36. //Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
    37. Date d2(d1);
    38. return 0;
    39. }

    注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
    ④深拷贝
    编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,这是浅拷贝或者值拷贝,对于日期类不需要自己显示实现,值拷贝就是将一块空间里面的值按照字节一个一个的拷贝过来,有点像memcpy拷贝—样,memcpy就是按字节拷贝
    但是如果内部有指针或者一些值指向资源,需要显示写析构释放,通常就需要显示写构造完成深拷贝,如:Stack Queue List等
    1. //这里如何去掉Stack的拷贝构造会发现下面的程序会崩溃掉,这里就需要深拷贝去解决。
    2. typedef int DataType;
    3. class Stack
    4. {
    5. public:
    6. Stack(size_t capacity = 10)
    7. {
    8. _array = (DataType*)malloc(capacity * sizeof(DataType));
    9. if (nullptr == _array)
    10. {
    11. perror("malloc申请空间失败");
    12. return;
    13. }
    14. _size = 0;
    15. _capacity = capacity;
    16. }
    17. Stack(const Stack& st)
    18. {
    19. _array = (DataType*)malloc(st._capacity * sizeof(DataType));
    20. if (nullptr == _array)
    21. {
    22. perror("malloc申请空间失败");
    23. return;
    24. }
    25. memcpy(_array, st._array, st._size*sizeof(DataType));
    26. _size = st._size;
    27. _capacity = st._capacity;
    28. }
    29. void Push(const DataType& data)
    30. {
    31. // CheckCapacity();
    32. _array[_size] = data;
    33. _size++;
    34. }
    35. bool Empty()
    36. {
    37. return _size == 0;
    38. }
    39. DataType Top()
    40. {
    41. return _array[_size - 1];
    42. }
    43. void Pop()
    44. {
    45. --_size;
    46. }
    47. ~Stack()
    48. {
    49. if (_array)
    50. {
    51. free(_array);
    52. _array = nullptr;
    53. _capacity = 0;
    54. _size = 0;
    55. }
    56. }
    57. private:
    58. DataType* _array;
    59. size_t _size;
    60. size_t _capacity;
    61. };
    62. class MyQueue
    63. {
    64. private:
    65. Stack _st1;
    66. Stack _st2;
    67. int _size = 0;
    68. };
    69. int main()
    70. {
    71. Stack st1(10);
    72. st1.Push(1);
    73. st1.Push(1);
    74. st1.Push(1);
    75. Stack st2 = st1;
    76. st2.Push(2);
    77. st2.Push(2);
    78. while (!st2.Empty())
    79. {
    80. cout << st2.Top() << " ";
    81. st2.Pop();
    82. }
    83. cout << endl;
    84. //输出1 1 1
    85. while (!st1.Empty())
    86. {
    87. cout << st1.Top() << " ";
    88. st1.Pop();
    89. }
    90. cout << endl;
    91. //输出2 2 1 1 1
    92. MyQueue q1;
    93. MyQueue q2(q1);
    94. //这里自定义类型会去调用栈的拷贝构造(栈的拷贝构造是深拷贝),内置类型完成值拷贝
    95. return 0;
    96. }

    注意:这里_array按照字节拷贝,相当于然s2的_array指向的空间和s1的_array指向的空间一样,但是这样会导致两个问题:(只要指针指向资源的都会有问题)
    1.s1 push后s2上也能看见,因为s1改变的是和s2同一块空间的值,同时s1的size会改变,但是s2的size不会改变
    ⒉析构的时候,s1会free一次,s2也会free—次,相当于对一块空间free两次,也即析构两次
    这里可以用深拷贝来解决,深拷贝就是你的形状和你的空间是什么样子就去开和你—样的空间,放一样的值,复制和你一样的出来。
    在类里面加上如下的拷贝构造即可
    1. Stack(const Stack& st)
    2. {
    3. _array = (DataType*)malloc(st._capacity * sizeof(DataType));
    4. if (nullptr == _array)
    5. {
    6. perror("malloc申请空间失败");
    7. return;
    8. }
    9. memcpy(_array, st._array, st._size*sizeof(DataType));
    10. _size = st._size;
    11. _capacity = st._capacity;
    12. }

    实践中总结:
    1、如果没有管理资源,一般情况不需要写拷贝构造,默认生成的拷贝构造就可以。如:Date
    2、如果都是自定义类型成员,内置类型成员没有指向资源,也类似默认生成的拷贝构造就可以。如:MyQueue3、一般情况下,不需要显示写析构函数,就不需要写拷贝构造
    4、如果内部有指针或者一些值指向资源,需要显示写析构释放,通常就需要显示写构造完成深拷贝。如:Stack Queue List等

  • 相关阅读:
    Linux初始化mysql后外网无限制访问
    【链表】设计跳表
    【Docker】Docker的应用包含Sandbox、PaaS、Open Solution以及IT运维概念的详细讲解
    2022年9月2号(常用matlab图像处理函数)
    P6607 [Code+#7] 蚂蚁 题解
    Redis的主从复制
    第9期ThreadX视频教程:自制个微秒分辨率任务调度实现方案(2023-10-11)
    如何在 .NET MAUI 中加载 json 文件?
    释放数据的潜力:用梯度上升法解锁主成分分析(PCA)的神奇
    oracle登录及基本操作
  • 原文地址:https://blog.csdn.net/qq_45755259/article/details/137832710