• 【类与对象 -2】学习类的6个默认成员函数中的构造函数与析构函数


    目录

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

    2.构造函数

    2.1概念

    2.2特性

    3.析构函数

    3.1析构函数的概念

    3.2特性


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

    如果一个类中什么成员都没有,简称为空类。
    空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员
    函数。
    默认成员函数: 用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

    下面我们来分别学习一下这6个函数~

    2.构造函数

    2.1概念

    1. class Date
    2. {
    3. public:
    4. void Init(int year, int month, int day)
    5. {
    6. _year = year;
    7. _month = month;
    8. _day = day;
    9. }
    10. void Print()
    11. {
    12. cout << _year << "-" << _month << "-" << _day << endl;
    13. }
    14. private:
    15. int _year;
    16. int _month;
    17. int _day;
    18. };
    19. int main()
    20. {
    21. Date d1;
    22. d1.Init(2022, 7, 5);
    23. d1.Print();
    24. Date d2;
    25. d2.Init(2022, 7, 6);
    26. d2.Print();
    27. return 0;
    28. }

    在日常使用时,我们可能会忘记对对象进行初始化,程序报错后反而检查半天才发现问题。在为了避免这种失误出现,C++中引入了构造函数。

    构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次

    2.2特性

    构造函数在函数名上看起来好像是创建对象,实际上是完成对象的初始化

    构造函数特征如下:

    ①函数名和类名相同;

    ②没有返回值,不是在前加void,而是压根不需要写;

    ③对象实例化时编译器自动调用对应的构造函数;

    ④构造函数可以重载;

    上面代码的构造函数形式如下:

    (1)不带参数的构造函数,由于编译器自动调用构造函数,在定义对象时就已经初始化了,

    (2)带参数的构造函数,在定义对象的时候就要将初始化的值加在后面。

    调用带参的构造函数时在括号内加入要初始化的值跟在对象后,那调用无参的构造函数时可以直接跟括号在对象后吗?(为什么调用无参的构造函数时后面不用加括号)

    不能。这种形式就成了函数声明。

    如下图,声明了一个d3函数,该函数没有参数,返回一个Date类型对象。

    以理论上两种形式可以同时存在,构成了函数重载,但是一般情况下我们不会同时写出这两个,因为调用存在歧义。

    这两种形式可以合为一种,利用我们之前学过的缺省参数来修改,如下面图中的形式:

    ⑤如果在类中忘记显式定义构造函数也不用担心,C++编译器会自动生成一个无参的默认构造函数。

    我们忘记定义构造函数时执行程序,发现可以正常运行,这是因为编译器会自动生成一个默认构造函数,但是看运行结果截图我们可以发现编译器没有对参数进行初始化,_year、_month、_day都是随机值。那么编译器生成的默认构造函数有什么用呢?

    C++中把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,像int/char/double/指针等;自定义类型就是我们使用的struct、class等自己定义的类型。

    C++98中规定对默认生成的构造函数对内置类型不作处理,对自定义类型则调用它的默认构造函数。如果自定义类型没有默认构造函数就会报错。

    其实自定义类型的尽头就是内置类型。

    以上Date类中的_year、_month、_day就是int类型,所以不作处理,是随机值,接下来我们验证一下自定义类型。

    •有些编译器可能会对内置类型进行处理(int初始化为0,double初始化为0.0,指针初始化为nullptr),但C++标准并没有规定内置类型要处理。

    C++11中对内置类型不处理的缺陷又打了补丁,即:内置类型成员变量在类中声明时可以给默认值

    分析一个类型成员和初始化要求,如果需要些构造函数我们就自己写,不需要时就靠编译器自动生成。结论:大多情况下都需要自己实现构造函数

    ⑥无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
    注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为
    是默认构造函数。

    不止是编译器生成的构造函数叫默认构造函数,我们自己显式定义的无参构造函数,全缺省构造函数都是默认构造函数。即不需要传参就能调用的构造函数,就叫默认构造函数

    默认生成的构造函数不能与其他两种同时存在;无参构造函数不能与全缺省构造函数同时存在,会产生调用歧义。

    一般情况下,我们建议使用全缺省构造函数

    3.析构函数

    3.1析构函数的概念

    前面我们学习了对对象进行初始化构造函数(类似于栈中的初始化函数Init),那么怎样清理资源呢,这就要提到我们接下来要学习的析构函数(栈中的销毁函数Destory)。

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

    3.2特性

    析构函数特征:

    ①析构函数名是在类名前加上字符 ~。(C语言中“~”作用是按位取反,析构函数前加表示该函数与构造函数作用相反)
    ②无参数无返回值类型。
    ③一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构
    函数不能重载
    默认生成的析构函数与构造函数类似,对内置类型不作处理,自定义类型则调用它的析构函数。
    (Date类没有显式定义析构函数,系统将自动生成,Date类中定义了自定义类型Time类型的对象t,调用它的析构函数~Time()。)
    对象生命周期结束时,C++编译系统系统自动调用析构函数。
    以下面的日期类为例,定义一个析构函数~Date()。(其实日期类的析构函数没什么有用的,因为它的_year等都是属于对象,并没有额外开辟空间,也就没有什么资源需要清理,此处只是用来验证它的特性)
    1. class Date
    2. {
    3. public:
    4. Date(int year = 1, int month = 1, int day = 1)
    5. {
    6. _year = year;
    7. _month = month;
    8. _day = day;
    9. }
    10. void Print()
    11. {
    12. cout << _year << "-" << _month << "-" << _day << endl;
    13. }
    14. ~Date()
    15. {
    16. cout << this << endl;
    17. cout << "~Date()" << endl;
    18. }
    19. private:
    20. int _year;
    21. int _month;
    22. int _day;
    23. };
    24. void func()
    25. {
    26. Date d2;
    27. }
    28. int main()
    29. {
    30. func();
    31. Date d1;
    32. d1.Print();
    33. return 0;
    34. }

    如果类中没有申请资源时,析构函数可以不写 ,直接使用编译器生成的默认析构函数,比如
    Date类; 有资源申请时,一定要写, 否则会造成资源泄漏,比如Stack类。

    如果我们写了一个栈,忘记写析构函数会发生内存泄漏。虽然函数结束栈帧销毁,但是在堆上额外开辟了一块空间没有释放。C语言中避免出现内存泄漏需要在函数结束前调用Destory函数来销毁,但是这种方法在使用时极有可能忘记,因而C++中引入了析构函数。

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

    对以上代码加上析构函数,结果如下

    **先定义的先调用构造函数,后定义的先调用析构函数

    1. class Date
    2. {
    3. public:
    4. Date(int year)
    5. {
    6. _year = year;
    7. cout << "Date()->" << _year << endl;
    8. }
    9. ~Date()
    10. {
    11. cout << "~Date()->" << _year << endl;
    12. }
    13. private:
    14. int _year = 1;
    15. int _month = 1;
    16. int _day = 1;
    17. };
    18. Date d5(5);
    19. static Date d6(6);
    20. void func()
    21. {
    22. Date d4(4);
    23. }
    24. int main()
    25. {
    26. Date d1(1);
    27. static Date d2(2);
    28. Date d3(3);
    29. func();
    30. return 0;
    31. }

    分析上面代码,猜测结果是什么?

    我们先分析一下各个对象是全局还是局部变量。d1、d2、d3、d4是局部变量,d4是在函数 func() 内定义,函数 func() 在main函数内调用,所以d4的生命周期先结束。d2又被static修饰(称为静态局部变量),改变了它的存储位置,使得它的生命周期变长,直至程序结束,生命周期才结束,在销毁之前应先将main函数内的局部变量先销毁(后定义先析构)再销毁该静态局部变量。d5、d6是全局变量,虽然d6被static修饰(静态全局变量),但它与全局变量的存储位置没有区别,生命周期仍是整个程序的执行期间,也是后定义的先析构。

    所以调用析构函数的顺序是d4-d3-d1-d2-d6-d5

    结果如下:

    总结:

    调用析构函数的顺序:局部对象(后定义的先析构)->静态局部对象->全局对象(后定义的先析构)

            构造函数和析构函数的学习就到这了,下篇我们将一起学习其他的默认成员函数,谢谢观看!

  • 相关阅读:
    数据库扩容也可以如此丝滑,MySQL千亿级数据生产环境扩容实战
    TCP协议
    pytorch中的model.eval()与volatile=True与requires_grad=False
    Netlogo 简化版Scatter 分散
    《爆肝整理》保姆级系列教程-玩转Charles抓包神器教程(8)-Charles如何进行断点调试
    高可用系统有哪些设计原则
    vue基础语法01
    30个CSS选择器
    C++ 指向数组的指针
    vue设置页面背景及背景图片
  • 原文地址:https://blog.csdn.net/m0_70496383/article/details/136117674