• C++可以这么学------>类和对象(中)


    本章主要内容:

    • 初识构造函数
    • 析构函数

    1.初识构造函数
    我们还是以栈这个数据结构为例:

    //C语言里面的栈结构
    typedef int STDataType;
    typedef struct Stack{
      STDataType* _a;
      size_t _size;
      size_t _capacity;
    };
    void StackInit(Stack* ps)
    { 
       ps->_a=NULL;
       ps->_size=ps->_capacity=0;
    }
    void StackDestroy(Stack * ps){
    
      free(ps->_a);
      ps->_a=NULL;
      ps->_size=ps->_capacity=0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在C语言中,我们不仅不能直接用类的名字做类型,而且对于内部的数据类型我们不能起到很好的保护作用。在C++里面,我们不仅可以直接用类名做类型,而且还引入了访问权限保护机制来保护数据。
    在C语言里面,我们如果想要用一个栈,我们就必须写Init函数来初始化这个栈和Destroy函数来清除这个栈动态申请的资源。这样做显然是非常麻烦的。所以C++引入了两个非常特殊的机制----->构造函数和析构函数


    什么是构造函数?

    首先,我们要明确一个概念!构造函数并不是完成创建对象这个动作的!构造函数的作用是在对象创建的同时,完成对创建对象的初始化工作!

    构造函数的语法规定如下:

    1.没有返回值,函数名字和类型名相同
    2.可以支持重载

    C++规定:在对象创建的时候,自动会调用对应的构造函数进行初始化。

    //Stack类的构造函数
    class Stack{
    public:
     //定义栈的构造函数
       Stack(size_t capacity)
     {   _a=(int*)malloc(sizeof(int)*capacity);
        _size=0;
        _capacity=capacty;
     }
    
    private:
       int* _a;
       size_t size;
       size_t capacity;
    };
      int main()
    {   
       //构造了一个初始容量是5的栈
        Stack st(5);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    因为有了构造函数,所以我们对于初始化的工作就可以完全交给构造函数来进行处理了。而构造函数中还有一类非常特殊的构造函数----->默认构造函数。
    首先我们要明确什么是默认构造函数---->简单来讲,默认构造函数就是调用的时候不需要传递参数的构造函数。而我们常见的默认构造函数通常有3种

    //3种默认构造函数
    class Demo1{
      //显式定义的无参构造
      Demo1(){
        _a=0;
       }
      //传递全缺省的构造函数
      Demo1(int a=0)
      {  
         _a=a;
      }
    private:
       int _a;
    };
    //第三种,我们没有显式给出,编译器自动生成的默认构造函数
    class Demo2{
    //编译器会自动生成一个对应的默认构造函数(这个生成的默认构造函数有很多需要注意的地方)
    private:
       int _aa;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    对于前面两种构造函数没有什么特殊的地方。但是对于我们第三种由编译器生成的特殊的默认构造函数。这里面的细节就有很多了。
    首先我们来看,如果一个类只有内置类型成员的话,默认生成的构造函数会怎么处理:

    class Demo2{
    //编译器会自动生成一个对应的默认构造函数(这个生成的默认构造函数有很多需要注意的地方)
    private:
       int _aa;
    };
    int main()
    {
         Demo2 d;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    打开对应的调试窗口信息如下:
    在这里插入图片描述
    不难可以看出,这里的_aa是随机值,说明编译器生成的默认构造函数并没有对这个_aa进行任何处理。我们再来看一看,假如说这个类的成员是自定义类型的,并且这个自定义成员所属的类显式定义了默认构造函数,看一看会发生什么?

    class A
    {public:
    	A(int a = 0)
    	{
    		_a = a;
    	}
    private:
    	int _a;
    };
    class Demo2
    {
    private:
    	A _aa;
    };
    int main()
    {  
       Demo2 d;
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在这里插入图片描述
    很显然,这里的_a被初始化为0,说明_aa这个自定义类型成员调用了A类的默认构造函数来进行初始化,说明对于自定义类型成员,编译器默认生成的构造函数是会调用对应类型的默认构造函数进行初始化的。

    所以我们就得到了编译器生成的默认构造函数的工作原理:对于内置类型成员,编译器不做任何处理,而对于自定义类型成员,编译器会调用对应的类的默认构造函数进行初始化

    所以,对于只有内置类型成员的类来说,编译器默认生成的构造函数没有任何价值!而如果一个类型只有自定义类型成员并且这个类型有对应的默认构造函数,这种情况下使用编译器默认生成的构造函数就够用了。

    class stack{
    public:
       stack(size_t capacity=5)
    {
        _a=(int*)malloc(sizeof(int)*capacity);
        _size=0;
        _capacity=capacity;
    }  
    private:
      int* _a;
      size_t _size;
      size_t _capacity;
    };
    class MyQueue
    {  
    
    private:
        stack _pushst;
        stack _popst;
    };
    int main()
    {  
       //这里写的mq调用的就是编译器生成的默认构造函数
       MyQueue mq;
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    而如果这里的栈没有提供默认构造函数,那么MyQueue类也就无法生成默认构造函数。

    class stack{
    public:
       stack(size_t capacity)
    {
        _a=(int*)malloc(sizeof(int)*capacity);
        _size=0;
        _capacity=capacity;
    }  
    private:
      int* _a;
      size_t _size;
      size_t _capacity;
    };
    class MyQueue
    {  
    
    private:
        stack _pushst;
        stack _popst;
    };
    int main()
    {  
       //编译错误,这里没法生成默认构造函数
       MyQueue mq;
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    注意:只有在没有定义任何构造函数的情况下,编译器才会生成一个无参的构造函数,否则编译器都不会在生成任何构造函数!!!所以这里的stack就是相当于没有默认构造函数可用,而生成MyQueue的默认构造函数又必须使用stack的默认构造函数,因为stack没有默认构造函数可用,所以最后编译失败!
    而对于又内置类型的成员的类,编译器默认生成的构造函数又不会对内置类型处理。所以大多数情况下,我们都要自己写默认构造函数。而通常我们都会给一个全缺省的函数作为默认构造函数。

    因为默认构造函数对内置类型不处理,对自定义类型成员调用默认构造函数的机制比较奇怪。所以在C++11里面引入了一个缺省值的机制来弥补这个不足

    //c++11才支持这种方式
    class A
    {
      private:
        int _a=0;
    };
    int main()
    {  
       A a;
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述
    并不是一个类一定都要有默认构造函数,只是如果这个类没有构造函数,那么你就不能够调用默认函数来初始化!简单来说,就是你没有默认构造函数,只要你不调用这个默认构造函数就没事。
    但是在有的场景下,有的类拥有的成员又没有提供默认构造函数,无论是我们显式提供或者是编译器默认合成都无法生成对应的默认构造函数,解决的方案是除了给对应类成员所属的类提供默认构造函数以外,还有一种方式可以解决----->初始化列表

    //使用初始化列表解决对应成员没有默认构造函数
    class A
    { public:
      //A类的构造函数,
        A(int a)
        { 
           _a=a;
        } 
     private:
       int _a;
    };
    class B
    {  //使用初始化列表解决无法生成类型B默认构造函数的问题
      public:
      //初始化列表的语法如下
         B()
         : _aa(5)
         , _c('a')
         {}
      private:
        A _aa;
        char _c;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    在这里我们只是简单的提一下初始化列表的用法,关于初始化列表的更多信息我将会在后面的博客里面介绍,这里我们只要稍微有个印象就可以了。


    2.析构函数
    前面我们知道了,构造函数是对象创建以后完成初始化工作的一个函数。那么当我们不需要一个对象的时候,对象就要被销毁。而析构函数就是和对象被销毁的时候息息相关的函数。
    首先,析构函数不是负责释放对象!!!而是处理对象被销毁以后清理对象资源的函数! 那么,析构函数的语法的规定如下:

    1.函数名和类名相同,在函数名字前面加上’~’
    2.析构函数没有参数,析构函数不能重载!

    //析构函数的语法,以日期类为例:
    class Date
    {  public:
        //提供全缺省的默认构造函数
         Date(int year=1900,int month=1,int day=1)
         {      
                _year=year;
                 _month=month;
                 _day=day;
          }
         //定义日期类的析构函数
         ~Date()
         {   
             //这是日期类的析构函数
         }
      private:
        int _year;
        int _month;
        int _day;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    和构造函数一样,如果我们没有显式提供,那么编译器就会自动生成一个析构函数。这个析构函数的处理原则也是:对内置类型不做任何处理,对于自定义类型则会调用对应类型的析构函数进行处理。
    所以,如果一个类的成员都是内置类型成员,我们使用编译器默认生成的构造函数就可以了。如果这个类型有自定义类型成员,并且这个自定义类型提供了析构函数,我们使用编译器默认生成的也就够用了。但是,假设这个类直接管理了一些资源(通常是动态申请了堆上的资源),那么这个时候我们就需要自己提供析构函数!

    class stack
    {  public: 
          stack(size_t capacity=4)
            {  
                    _a=(int*)malloc(sizeof(int)*_capacity);
                    _capacity=capacity;
                    _size=0;
            } 
      //stack的成员指向堆区上的内存,所以对象销毁的时候要释放内存
      //必须我们提供析构函数,否则会有内存泄漏的问题
        ~stack()
        { 
           free(_a);
           _a=nullptr;
         }
       private:
          int* _a;
          size_t  _size;
          size_t _capacity;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    而对于MyQueue类,由于MyQueue并没有直接管理堆上的内存。所以我们只要给stack提供了正确的析构函数,然后使用编译器默认生成的析构就可以了。

    class stack
    {  public: 
          stack(size_t capacity=4)
            {  
                    _a=(int*)malloc(sizeof(int)*_capacity);
                    _capacity=capacity;
                    _size=0;
            } 
      //stack的成员指向堆区上的内存,所以对象销毁的时候要释放内存
      //必须我们提供析构函数,否则会有内存泄漏的问题
        ~stack()
        { 
           free(_a);
           _a=nullptr;
         }
       private:
          int* _a;
          size_t  _size;
          size_t _capacity;
    };
    //不用提供析构和默认构造,编译器默认生成的已经足够使用
    class MyQueue
    {  public:
      private:
        stack _pushst;
        stack _popst;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    只有这个类在直接管理资源的时候,我们才需要自己定义构造函数。否则我们使用编译器默认自动生成的就可以了。
    现在有如下的一段代码:

    class stack
    {  public: 
          stack(size_t capacity=4)
            {  
                    _a=(int*)malloc(sizeof(int)*_capacity);
                    _capacity=capacity;
                    _size=0;
            } 
      //stack的成员指向堆区上的内存,所以对象销毁的时候要释放内存
      //必须我们提供析构函数,否则会有内存泄漏的问题
        ~stack()
        { 
           free(_a);
           _a=nullptr;
         }
       private:
          int* _a;
          size_t  _size;
          size_t _capacity;
    };
    int main()
    {  //会先释放哪一个呢?
        stack s1;
        stack s2;
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    这里会先释放s2,结合函数栈帧的创建和销毁的过程我们可以知道,s2是函数栈帧的栈顶元素,所以是s2先被析构。也就是后创建的被先析构


    总结:

    • 构造函数是完成对象创建后初始化的工作的函数
    • 构造函数函数名和类名相同,支持重载,没有返回值
    • 默认构造函数:不需要传递参数就可以调用的函数,可以充当默认构造函数的函数:全缺省的构造函数,无参构造函数,编译器默认生成的构造函数
    • 析构函数:对象释放的时候负责清理资源的函数,不可重载。

    本篇文章的主要内容就到这里,若有不足指出还望指正。希望能和大家一起共同进步。

  • 相关阅读:
    ROS的程序编写流程
    python打印带下标的字母组合
    R数据分析:解决科研中的“可重复危机”,理解Rmarkdown
    Leetcode1071. 字符串的最大公因子(三种方法,带详细解析)
    SP34009 CTTC - Counting Child
    mybatis、mybatisPlus
    机器学习-特征选择:使用Lassco回归精确选择最佳特征
    [网鼎杯 2018]Comment git泄露 / 恢复 二次注入 .DS_Store bash_history文件查看
    frp使用oidc认证和搭建
    OpenMesh 获取网格面片各个顶点
  • 原文地址:https://blog.csdn.net/qq_56628506/article/details/125341770