• 关于类成员的构造和析构函数调用的进一步理解


    上篇博客讲解了构造函数、析构函数以及拷贝构造函数,本文主要表达一些我对类成员的构造和析构函数的调用的理解和关于编译器默认生成的成员函数的思考。

            如果类中没有显式定义构造/析构函数,那么编译器会默认生成构造/析构函数。对于默认的构造/析构函数,它们是一个空函数体,很自然的想到它们并没有实际的作用。不过,上篇博客中,我介绍了编译器生成的默认的构造/析构函数对于内置类型的成员变量不做处理,对于自定义类型的成员变量,则会调用该类型的默认构造函数来初始化它和调用析构函数以清理该变量的资源。例如:

    1. class A
    2. {
    3. public:
    4. A()
    5. {
    6. cout << "A()" << endl;
    7. }
    8. ~A()
    9. {
    10. cout << "~A()" << endl;
    11. }
    12. };
    13. class B
    14. {
    15. public:
    16. // 默认生成的构造函数
    17. /*B()
    18. {
    19. }*/
    20. // 默认生成的析构函数
    21. /*~B()
    22. {
    23. }*/
    24. private:
    25. int _b;
    26. A _aa;
    27. };
    28. int main()
    29. {
    30. B b;
    31. return 0;
    32. }

    结果:

    实例化B类的对象b时,对于成员_b是内置类型没有构造函数可能被初始化成随机值,对于A类型的成员_aa则调用了它的构造函数,当b的生命周期结束时,成员_b没有析构函数直接销毁,成员_aa则调用了它的析构函数。

            之前我们认为这是B类的默认的构造函数和析构函数的作用,基于这种理解,我们能深刻地认识到每个类都有构造/析构函数,如果没有显式定义,编译器会默认生成它们,默认生成的构造/析构也有一定的作用,就是调用自定义类型成员的默认构造函数和析构函数。

            现在我们看另一种情况,即B类显式定义了构造和析构函数:

    1. class A
    2. {
    3. public:
    4. A()
    5. {
    6. cout << "A()" << endl;
    7. }
    8. ~A()
    9. {
    10. cout << "~A()" << endl;
    11. }
    12. };
    13. class B
    14. {
    15. public:
    16. B()
    17. {
    18. }
    19. ~B()
    20. {
    21. }
    22. private:
    23. int _b = 1;
    24. A _aa;
    25. };
    26. int main()
    27. {
    28. B b;
    29. return 0;
    30. }

    结果:

    可以看到,当我们显式定义了B类的构造和析构函数时,对于自定义类型_aa也会调用它的构造/析构函数,这并没有什么问题,我们可以理解为显式定义的构造/析构函数也会调用自定义类型成员的构造/析构函数,尽管它们是空函数体,对于内置类型成员_b使用默认值1也可以认为是B类的构造函数的作用。

            接下了,我将从三个方面表达自己对类的成员变量的构造/析构函数的调用机制以及编译器生成的默认的构造/析构函数的作用的理解。

    1 构造/析构函数的功能

            构造函数的功能是初始化类的成员变量,我们可以在函数体内对变量赋初值,也可以为指针类型动态申请空间。对于自定义类型成员,我们无法显式调用它的构造函数来初始化它,那么类的构造函数来会调用自定义类型成员变量的默认构造函数。对于未处理的内置类型,它没有构造函数,也可以认为是类的构造函数初始化了它,尽管它可能是随机值,这由编译器决定。

            同样,析构函数的功能是清理成员变量申请的资源,我们可以在函数体内完成这个工作,比如free一个指针类型成员变量申请的空间。对于自定义类型的成员变量,我们无法显式调用它的析构函数来清理它的成员的资源,那么类的析构函数会调用自定义类型成员变量的析构函数。同时,所有成员变量的销毁也可以认为是析构函数的作用。

            基于以上理解,如果没有显式定义类的构造/析构函数,那么编译器必须生成它们,以完成构造/析构函数的功能。

    2 变量创建/销毁时自动调用

            我们实例化一个类的对象时,该对象的成员变量也会跟着创建,假设这个类的构造函数的函数体是空的,可以是我们显式定义或编译器自动生成的。现在我们认为空的构造函数不会执行任何功能,但是当对象被创建时会调用它,那么对于自定义类型成员变量的构造函数的调用,也就可以认为是该变量创建后自动调用了它的构造函数,而非类的构造函数的作用。

            同样,一个空的析构函数不会执行任何功能,但是当对象的生命周期结束时就会调用它,那么对于自定义类型成员变量的析构函数的调用,也就可以认为是该变量被销毁时自动调用了它的析构函数,而非类的析构函数的作用。

            内置类型的成员变量没有构造和析构函数,当它们随着对象的产生而创建时就会被初始化,如同创建一个全局变量一样,我们没有在定义时初始化它,仍可以输出它的值,它们的初始值自然是由编译器提供的。如果类的构造函数内提供了为成员变量初始化的操作,我们可以认为构造函数的执行在编译器“初始化”变量之后。当对象的生命周期结束时,内置类型的成员变量跟着销毁即可,如同任何一个其它的变量离开了作用域就会被销毁。

    3 语法的统一性以及编译器的支持

            基于第2点理解,如果我们不需要在构造函数或析构函数内执行功能,不定义它们就行了,为什么编译器还会生成默认的构造/析构函数呢?我认为这是语法的要求,C++认为类的构造/析构函数以及其它的一些成员函数非常重要,每个类都应该有这些成员函数,所以即便构造/析构函数是空的函数体也必须存在,编译器则支持了我们可以不定义空的构造/析构函数,在编译时会自动生成它们。那么,编译器生成的默认的构造/析构函数是否有实际的意义就无关紧要了,我们可以认为它们没有任何作用,也可以在名义上认为调用自定义类型的成员变量的构造/析构函数是它们的作用,以表示它们的存在。

    本文内容主要是记录个人的一些思考和理解,不一定符合真实的语法设计以及编译器的工作方式,我目前还在学习C++的过程中,一些理解难免存在局限性和不足甚至错误,望各位指出。如果你也是一名初学者,希望本文内容对你的学习有所帮助。

  • 相关阅读:
    AI-Gateway:一款整合了OpenAI、Anthropic、LLama2等大语言模型的统一API接口
    17基于matlab卡尔曼滤波的行人跟踪算法,并给出算法估计误差结果,判断算法的跟踪精确性,程序已调通,可直接运行,基于MATLAB平台,可直接拍下。
    如果你项目使用了MyBatis-Plus你一定要用它
    JavaScript DOM中获取元素、事件基础、操作元素、节点操作
    使用PCA9685控制多个舵机
    DBeaver manual
    节省时间的分层测试,到底怎么做?
    Android 10.0 禁用adb remount功能的实现
    Spring Cloud笔记---客户端负载均衡 spring cloud ribbon
    环境分析检测小剂量移液用耐受硝酸盐酸PFA材质吸管特氟龙移液枪枪头
  • 原文地址:https://blog.csdn.net/2301_79391308/article/details/132620048