一、类的默认成员函数
类内的默认成员函数:用户不显示实现,编译器就会自动生成的成员函数,被称为类的默认成员函数。这些默认成员函数各有各存在的作用。但实际上,很多时候,需要自己写这些成员函数,而不是使用编译器生成的。
翻译一下就是,在类内有这样六个成员函数,如果你不写,编译器就会自动生成,因为是编译器生成的,使用被称为默认成员函数。但只要你写了,编译器就不会帮你生成。而这样的成员函数一共有六个。本文将会先介绍三个。
二、构造函数的初步认识
1.构造函数的特点
无返回值
函数名为类名
构造函数可以重载
对象实例化时编译器自动调用对应的构造函数
2.构造函数的作用
对象实例化时自动调用构造函数,完成对类对象的初始化。也就是对对象的成员变量初始化。
3.可以重载的构造函数
如下图,是一个用户显示实现的无参的构造函数
当用户显示实现了一个无参构造函数时,即使函数内什么都没写,但是类对象s1依旧完成了初始化。这是因为,构造函数在类对象创建的时候会自动调用。虽然说是自动调用,但实际上还是需要一些辅助条件。
语句Stack s1;其实就相当于是在调用我们显示实现的构造函数。本来应该是这样的Stack s1();该语句更符合调用无参函数时的情景,但是但是Stack s1();的写法与函数声明相同,编译器无法区分,因此写做Stack s1其实就是调用无参构造函数的写法。而不能写作Stack s1();
如下图,是一个显示实现的含参的构造函数,同无参构造函数构成重载
可以看到,即使显示实现的含参数的构造函数什么也不写,对象s2也依旧和s1一样被初始化。到这里大家可能以为s2自动调用的是含参数的构造函数,其实并不是。前文提到,构造函数的自动调用也是需要辅助条件的,这是因为构造函数支持重载,而Stack s1;的语句只是调用无参构造函数的写法,而要想自动调用含参数的构造函数,需要在实例化对象的时候传参数过去。如下图中的s2
可以看到,即使无参构造函数或者和含参数的构造函数内什么都没写,也会将对象s1,s2的成员变量初始化。但是如果我们在其中添加了赋值语句,则可以根据自己的需求任意初始化。如下图
与此同时,构造函数的参数也支持缺省参数的写法,这样我们如果在对象实例化的时候,不传递参数,也会按照缺省值进行初始化。如果传递参数,则按照传递的参数进行初始化。如下图
如果上图的无参构造函数没有被注释,那么对象实例化时不传参会造成调用歧义,编译器无法分辨是要调用无参的还是全缺省值,如下图
4.编译器生成的构造函数
上文一直说的都是显示实现的无参构造函数。实际上,如果我们不写构造函数,编译器就会自动生成一个默认的无参的构造函数,来看看二者的区别。如下图
可以看到,编译器生成的无参构造函数将对象s1的成员变量初始化,对象s1的成员变量任然是随机值。这是因为,编译器生成的无参构造函数在初始化对象的成员变量时,是分情况的,对于内置类型的成员变量,不做处理(实际上有的编译器会处理,但是我们当作不处理),而对于自定义类型则会调用该类型的默认构造函数。
补充:显示实现的无参构造函数,全缺省的构造函数,以及编译器生成的构造函数都被称为默认构造函数
补充:类内的成员变量的声明也可以有缺省值,这时候如果没有显示实现的构造函数,编译器生成的构造函数也会根据给出的缺省值进行初始化,但是如果显示传参或者显示实现的含参数构造函数有缺省值。(这其实就是对编译器生成的构造函数无法初始化内置类型打的一个补丁)。则以后者为优先如下图
5.构造函数是否需要显示实现
如果不显示实现构造函数,编译器虽然也会生成一个无参构造函数,但却并不会对内置类型的成员变量进行初始化(会是随机值)。
什么情况下不需要写构造函数
1.类内的成员变量有缺省值。因为有缺省值,在不传参时,会按照缺省值进行初始化
2.类内的成员变量都是自定义类型。因为编译器生成的构造函数会对自定义类型调用该自定义类型(其实就是类)的默认构造函数,所以本身可以不写构造函数(但是调用的默认构造函数得有初始化功能)。
大多数情况下,构造函数是需要我们自己写的,它可以更好的完成对对象初始化的工作。
三、构造函数的再深入
在对象实例化的时候,会自动调用构造函数来完成对构造函数的初始化。但实际上,这是通过构造函数内的赋值语句来完成的赋初值,这其实并不能叫做初始化,因为赋值语句可以有多个,而初始化只能有一次。
如下图
在构造函数中对成员变量_top的赋值语句有三句,最终结果以最后一次为准,但多次进行赋值很明显不符合初始化只能进行一次的含义。
1.初始化列表
赋值语句不符合构造函数初始化的定义,因此有了初始化列表的诞生。
1.1初始化列表的写法
在构造函数函数名和参数列表之后,函数内部{}之前。以冒号开始,紧接着是成员变量(),括号内是对成员变量的初始化值或者表达式,其余成员变量之间以逗号隔开。如下图
1.2初始化列表的要求
1.每个成员变量在初始化列表最多只能出现一次
在出现第二次时,会报错。如下图
2.也可以不出现,如下图
2.初始化列表的使用场景
2.1自定义类型的成员变量,且自定义类型没有默认构造函数
类 MyQueue中存在自定义类型的成员变量Stack,并且类Stack中没有默认构造函数,MyQueue又没有用初始化列表对Stack类型的成员变量push初始化,会存在报错。如下图
即使在函数体内部通过赋值语句进行赋初值,报错依然存在。如下图
使用初始化列表的方式初始化,报错消失
2.2引用类型的成员变量
引用类型的成员变量必须采取初始化列表初始化,否则会报错。如下图
初始化列表中初始化引用成员变量时,报错消失。如下图
2.3const成员变量
存在const成员变量,但未用初始化列表初始化,出现报错,如下图
函数体内部的赋值语句也会报错,如下图
采用初始化列表,报错消失。如下图
以上三种情况下,必须使用初始化列表进行初始化。但相比直接在构造函数体内部写赋值语句,还不如直接写初始化列表。
3.自定义类型成员变量的初始化
对于自定义类型的成员变量且该类型不存在默认构造函数时,必须利用初始化列表初始化。如果存在默认构造函数,就连构造函数也可以不写。
如下图,如果不写构造函数,编译器会调用自定义类型Stack的默认构造函数进行初始化
如果写了,即使初始化列表内容为空(没有使用初始化列表),也可以进行初始化(实际上也是调用自定义类型Stack的默认构造)
补充:无论是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。初始化类别是成员定义的地方,定义就是创建变量,因为是自定义类型,在定义的时候会调用该类型的构造函数
4.初始化列表的初始化顺序
按照类中成员变量的声明顺序进行初始化,和初始化列表的出现顺序无关。如下图
成员变量_a的声明在前,所以_a是在_b前初始化的,可初始化列表中却用_b初始化_a,所以运行失败
5.赋初值和初始化列表
直接在构造函数内写下语句,只是对对象的成员变量赋初值,成员变量可出现多次,进行多次赋值。而初始化列表中,成员变量最多出现一次。并且,在成员变量有自定义类型或者引用或者const变量时,必须使用初始化列表。因此,推荐使用初始化列表的方式来完成构造函数,进行正确的初始化
四、析构函数
1.析构函数的作用
作用:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
翻译一下就是:析构函数的存在是为了释放动态申请资源所占据的空间(堆区),其余的临时变量和静态变量和全局变量,在程序结束后,会由编译器自动释放。
2.析构函数的特点
析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构 函数不能重载
4.对象生命周期结束时,C++编译系统系统自动调用析构函数
3.编译器生成的析构函数
规则:编译器生成的析构函数,不会对内置类型做处理,而对于自定义类型会调用该自定义类型的析构函数。
如下图。程序运行结束后,本应释放的是类MyQueue的对象q1,但因为没有显示实现MyQueue对象的析构函数,因为MyQueue的成员变量是Stack类型作用编译器生成的析构函数又会用自定义类型Stack的析构函数,因此显示的是类Stack的析构函数内容。
4.析构函数是否需要显示实现
如果类中没有动态申请资源时,析构函数可以不写,直接使用编译器生成析构函数,因为只要不是动态申请的资源,其他都会在程序结束时被编译器释放。如下图
五、拷贝构造函数
1.编译器默认生成的拷贝构造函数
规定:,编译器生成的默认拷贝构造函数对于内置类型只会进行浅拷贝,而对于自定义类型,则会调用该自定义类型的拷贝构造。
浅拷贝也叫值拷贝,拷贝的是变量本身。例如拷贝int类型变量,就是拷贝变量本身,int*类型的指针变量,也会拷贝指针变量本身,也就是地址,而这,就可能会引起问题。
2.拷贝构造函数的特点
1.是构造函数的重载函数。
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用
这是因为,在调用拷贝构造函数的时候,也就是调用函数,如果是传值方式,会将实参拷贝一份,在传给形参,而这个过程也是在拷贝对象,也需要调用拷贝构造。也就是说,调用拷贝构造前的传参也是拷贝的过程,也需要调用拷贝构造,会引发无穷递归,或者报错。(注:对象是自定义类型)
对于存在动态内存申请的对象,正确写法如下图
3.拷贝构造函数的作用
作用:通过拷贝构造,用已经存在的对象初始化另一个对象(拷贝已存在的对象)。
如下图
4.拷贝构造函数是否需要显示实现
对于需要动态申请资源的情况,必须自己写拷贝构造,否则就是浅拷贝。
例如栈。如下图
图中注释掉的拷贝构造就是值拷贝。成员变量int* _a,也是内置类型,s2在拷贝s1时,因为是编译器生成的拷贝构造,所以s2._a拷贝的是s1._a的值,也就是说,两个对象的成员变量会指向同一块空间,这就会导致,一块空间会被析构两次,程序会崩溃。这种情况就需要自己动手写拷贝构造。
而如果没有动态申请资源的情况,拷贝构造写不写都可以
5.拷贝构造函数的调用场景
1.用已存在的对象创建(初始化)另一个对象
2.函数返回值类型为类类型对象
3.函数参数类型为类类型对象
如下图