C++自制心得——类与对象中(构造+析构)
前言:
C++的类一共有六个默认(编译器自动生成)成员函数,分别是构造函数,析构函数,拷贝构造,赋值重载,取地址重载(普通对象+const对象,共两个),其中取地址重载函数编译器默认生成的就够用了,重点就是前四个函数,这篇讲构造函数和析构函数。
构造函数:
这里有一个新鲜出炉的栈,让我们测试一下...?

怎么报错了,栈这种东西我早已写的滚瓜烂熟,不可能有问题,f10,启动。

(⊙﹏⊙),野指针,等一下,为什么会有野指针......忘初始化了。我相信忘记初始化这种尴尬的事情不止一个人犯过,同样,忘记释放内存也是大坑(等我敲到这里我才发现我甚至没写释放函数)。不过在C++里我们有一个完美的解决方案,那就是构造函数。
基础版
高级版
第二个是初始化列表,后面再讲。我们看第一个
构造函数是六大默认成员函数之一,其主要功能为给已开辟好空间的对象进行初始化。
基本特点如下:
1. 函数名与类名相同
2. 无返回值类型(连void都不用写)
3. 可重载
一个对象可以拥多个初始化方式,因此构造函数应当具有重载能力(拷贝构造函数与其重名)
4. 自动调用
怎么证明这个函数是自动调用的?我给构造函数加了一个printf,打印一下

看,的确可以自动调用。
main函数(修改后)
注意,不传参的初始化不要加括号,会被认作函数声明。
具体应用这里就不展开写了,我们来看一些麻烦的东西。
5. 默认构造函数
默认构造函数有三种,编译器自动生成的,无参的,全缺省的。其余构造函数均不是构造函数。
6. 如未显式定义构造函数,编译器会自动生成默认构造函数
对于这个默认构造函数,它有三个特点,不处理内置类型(处理的是编译器行为,而非标准规定),自动调用自定义类型的默认构造函数,你写了就不会生成了。如果你真的想让编译器处理内置类型,你可以在成员变量的声明处定义缺省值,这样编译就会自动处理内置类型,用日期类举个例子

自定义类型的自动调用很有用处,还记得leetcode上的用栈实现队列吗?我用它再举个例子

看,我们什么都没做,编译器自动调了两次Stack的构造函数。所以像这种成员变量全是自定义类型的类,可以考虑不写构造函数。
如果你写了一个非默认构造函数,但是定义对象时不传参,就会出现如下情况。

如果MyQueue类中Stack类没有默认构造函数就会出现如下情况

想解决这个问题,要么写默认构造,要么......走初始化列表。

7. 初始化列表(难点)
我们知道成员变量的声明在类里面,那么它的定义在哪里?首先,我们要清楚构造函数也是有隐含的this指针,所以我们在构造函数函数体里做的工作都叫赋值操作,而非初始化,我可以证明这一点
我们在类里加入了一个const对象,它的特点是必须在初始化时给值,后面不能再改,如果构造函数的函数体里是初始化操作,那就不会报错,否则

这是一个超级巨坑,如果说像引用,const这类必须在定义时初始化的成员变量不可以被构造函数初始化,那这个构造函数就是一个失败的半成品,因此就有了初始化列表这个概念。
初始化列表是成员变量定义的地方,在初始化列表里进行的操作就是初始化操作,这样就能解决上述问题。把上面的代码用初始化列表表示,就是这样的
好,那怎样写初始化列表,函数名下方以“ : ”作为起始符号,后接成员变量,以函数调用的形式填写初始化参数(因为规则与形式几乎与函数调用完全一致,只要最后结果类型能匹配初始化参数给函数也是可以的),两个成员用“,”隔开,以函数体为结束标志({})。
下面是一些初始化列表的特性。
1. 每一个成员变量都会走一次初始化列表,无论你是否显式写出了那个成员变量的初始化。

用这个代码来举例子,首先,成员变量声明处的缺省值会在初始化列表处被应用,又因为它们是缺省值,一旦某个变量被显式初始化编译器就不会用缺省值初始化。如果某些变量没有显式初始化,那么编译器就会用缺省值进行隐式初始化。因此整个程序的输出结果就是2022/2/1
2. 变量初始化顺序只和声明有关

以这段代码为例,成员变量初始化顺序为aa,a,初始化列表就先走aa(a),再走a(x),所以aa就成了随机值
3. 编译器生成的默认初始化列表对于内置类型,用缺省值或随机值(是不是看编译器)初始化,而对于自定义类型,会调用该类型的默认构造函数(其实就是把构造函数的特性又说了一遍,不过这次更具体了)
析构函数
相比于构造函数这只怪物,析构函数简单的多。析构函数的功能与构造函数正好相反,它主要完成对对象内资源的清理工作,而非回收对象空间。
析构函数有以下特性:
1. 函数名为“~” + “类名”
2. 无返回值类型与参数
3. 如未显式定义,编译器会自动生成(特性同构造函数)
4. 对象生命周期结束后自动调用
对于一些简单的类,比如日期类,它就完全不需要写析构函数,只有像栈这种需要手动回收资源的类才需要写析构函数
MyQueue类又躺赢了,它也不需要写析构函数,编译器调Stack类现成的就行,妥妥的人生赢家。
最后补充一点,如果有多个对象同时出生命周期,根据函数栈帧的开辟与销毁规则,析构函数的调用遵循FIFO原则。
下一章专栏讲拷贝构造和赋值重载,又要掉头发咯。