目录
1.类的6个默认成员函数
我们前面讲到了一个空类,也就是类里面没有声明成员,但是空类里面真的什么都没有吗?不然,任何类在什么都不写时,编译器自动生成以下六个默认成员函数:
默认成员函数:默认成员函数是一种特殊的成员函数,如果我们不写,编译器就会自动生成,如果我们写了,编译器就不会生成。
对于有些类,这几个成员函数需要我们自己去实现,而对于另外一些类,编译器自动生成的就够了。这六个函数里面最重要的就是前四个,后面两个绝大多数情况下并不需要我们去实现。
2.构造函数
构造函数这个名字听起来像是用来创建对象的,但是实际上,构造函数的功能是在创建一个对象时对其初始化。
为什么C++会有构造函数呢?
我们用C语言实现数据结构时,每一个数据结构变量创建之后,第一件事就是要调用他的初始化函数,如果我们忘记初始化了,里面的值就是一些随机值,在使用的时候就会出问题。既然每一个结构体创建都要初始化,那么我们能不能把它设计成自动调用的呢?
构造函数是一个特殊的成员函数,他的名字虽然叫构造,但是他的主要任务并不是开空间创建对象,而是对创建好的对象初始化,他的功能和我们的 init 一样,但是他会在实例化对象的时候自动调用。
构造函数有以下几个特征:
1.函数名与类名相同
2.无返回值
3.对象实例化的时候编译器自动调用对应的构造函数
4.构造函数可以重载
构造函数在创建类类型对象时由编译器自动调用,以保证每一个数据成员都有一个合适的初值,并且在对象整个生命周期内只调用一次。
比如我们的日期类,我们可以自己写一个构造函数出来
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
注意,构造函数一定要设置为公有的,因为编译器是在创建对象的时候自动调用,也就是在类外调用。那么我们如何在创建对象的时候给他传参呢?
Date d(2022, 4, 23);
我们在创建对象的时候,在对象名后面加一个括号,然后在括号内传构造函数的参数,这时候我们就将对象初始化为我们传的参数了。
构造函数也是可以声明和定义分离的,这时候在函数定义的时候要指定类域。
但是我们上面写的构造函数有没有问题呢?我们有时候在创建对象的时候并不像对其传参怎么办?或者说我们并不想传三个参数,只想初始化年和月或者只想初始化年,
Date d(2022, 4);
Date d(2022);
Date d;
这时候我们只有一个构造函数,因为我们自己写了构造函数了,所以编译器也不会自动生成默认的自动构造函数了,那么这三种创建对象的方法就是错误的吗?最离谱的是, Date d 竟然也不行,这是不是很滑稽
这时候,我们可能就会想到上面讲的构造函数的特性 : 支持重载,那么我们是不是要把这几种情况都列出来呢? 这样未免实现起来过于复杂了。
在C++基础入门的时候我们讲了缺省参数,那么我们是不是能在构造函数这里也用缺省参数来实现,那么是全缺省还是版缺省呢?结合我们上面的问题,半缺省的情况下还是会出现说初始化不传参的时候报错,所以我们最好是给全缺省,这样更能满足实际需求,而缺省值我们怎么给就取决于类的功能和个人的实现了。
于是我们就能给出这样的一个改良之后的构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
这时候不管有没有传参,我们的构造函数都是能调用的。
构造函数我们也是可以 f11 进入到函数里面调试的。
这里大家可能会有一个问题,为什么上面的无参构造不用括号呢?
Date d;
//无参构造为什么不是这样呢?
Date d();
我们可以看到,带括号的形式看起来是不是就是一个函数的声明,Date 是返回类型, d是函数名,括号和里面就是参数列表。 如果无参构造也是这样写的话,不要去也会分不清到底是声明还是构造。而且,无参的时候不加括号更符合我们的逻辑。
而如果我们不自己实现构造函数,编译器自动生成的默认成员函数是怎么样的呢?
我们可以把上面的代码注释掉来观察一下
当我们走到return这一行来了,d的成员变量还是随机值,这是为什么呢?编译器自动生成的构造函数什么也没干啊,这些变量还是随机值啊,那他自动生成的构造函数有什么意义呢?
C++把类型分成内置类型(基本类型)和自定义类型,内置类型就是语言提供的 int 、char 等等,自定义类型就是我们用struct ,class,onion等自己定义的类型。
编译器自动生成的构造函数,对于内置类型不处理,但是对于自定义类型,他会去调用这个自定义类型的构造函数。
为什么编译器对内置类型不做处理呢?因为编译器也不清楚这些变量是用来做什么的,不知道要给他们什么样的初始值才合适,不管怎么样总有情况不会满意,所以编译器也不敢处理内置类型。
所以说,自动生成的默认构造函数还是会做事情的,只是对内置类型不处理。
对于自定义类型,自动生成的构造函数会去调用该类型的构造函数,这个怎么理解呢?
我们可以拿之前的用栈实现队列来举例,我们的队列使用两个栈来完成的。
class Stack
{
int* _a;
int _top;
int _capacity;
};
class MyQueue
{
Stack pushST;
Stack popST;
};
队列的成员变量是自定义类型Stack对象,那么对于MyQueue 类,编译器自动生成的构造函数他会去调用 Stack 的构造函数对这两个成员变量初始化。我们可以把栈的构造函数写出来测试一下MyQueue 编译器自动生成的构造函数是否真的会去调用Stack的构造函数
我们在Stack的构造函数中打印一行提示信息出来,我们就能发现,用MyQueue类来实例化对象的时候,它调用了两次Stack的构造函数,因为他的成员里面有两个Stack对象。通过监视也可以看到他确实使用Stack的构造函数初始化了
这时候我们就能大概想明白,什么时候需要我们自己写构造函数,什么时候编译器自动生成的构造函数就够用了,对于MyQueue这样的成员全是自定义类型变量的的类,我们就不用自己实现构造函数,因为编译器自动生成的构造函数就回去调用他们的构造函数。
那么如果我们再给MyQueu类中加一个 size 成员呢?
class MyQueue
{
Stack pushST;
Stack popST;
int _size;
};
这时候当我们再使用编译器自动生成的构造函数时,就会发生这样的情况
我们这里的 vs 编译器的情况不是很理想,因为C++对于内置标准就是对于内置类型不处理,而vs编译器有点多管闲事了。 正常情况下,q 对象应该会是 初始化了 两个栈,但是size仍然是随机值,那么这样是不是很尴尬,那么我们是不是就要自己实现MyQueue的构造函数了?MyQueue的构造函数实现起来,既要实现Stack的构造,又要初始化size是不是很麻烦?
我们有两种方法针对这种既有内置类型又有自定义类型的类
1.对于内置类型,我们可以在声明时给缺省值,也就是默认值。C++中针对内置类型不初始化的缺陷,又打了个补丁,就是内置类型成员变量在类中声明时可以给缺省值,注意不是对成员变量初始化,因为声明是没有空间的,是给成员变量一个默认值,如果初始化时对该成员变量进行操作,该成员变量就会使用在这个缺省值。我们将MyQueue修改成这种形式:
class MyQueue
{
Stack pushST;
Stack popST;
int _size=0;
};
这样一来,我们不用写构造函数,MyQueue 的自动生成的构造函数对于两个栈就回去调用他们的的构造函数初始化,而size 就会使用他的缺省值。
同时,我们上面的日期类也可以这样给缺省值,这样一来我们也不用自己写构造函数了。其实,上面栈也是可以的,这个缺省值是很牛逼的。我们不仅能给指定的缺省值,还能用缺省值 malloc开辟空间。
class Stack
{
private:
int* _a=(int*)malloc(sizeof(int)*4);
int _top=0;
int _capacity=4;
};
这里的 malloc 在类的声明中并没有开辟空间,而是相当于打了一个样,而是在创建对象的时候,构造函数如果没有对 _a 进行初始化,_a 就会用这个缺省值。 但是这个缺省值会有一个问题, 就是不能检查malloc 的返回值是否为空指针。
2.初始化列表,初始化列表我们会放在类和对象的后期去讲。
默认构造函数
无参的构造函数和全缺省的构造函数都成为默认构造函数,并且默认构造函数有且只能有一个(避免二义性,无参和全缺省调用的时候会冲突),类中如果有多个默认构造函数,编译器就会报错。
并不是说只有编译器自动生成的构造函数才是默认构造函数,我们自己实现的无参的或者全缺省的构造函数也是默认构造,编译器自动生成的就是无参的。总的来说,不传参数就能调用的构造函数就是默认构造函数。
所以我们在声明一个类的时候,最好要有一个默认构造函数,为了防止我们创建对象时不传参数的情况。
注意:如果一个类里面包含其他类类型成员,就比如上面的MyQueue 中的pushST和popST,这些自定义类型成员必须有默认构造函数也就是不传参的构造函数,因为编译器自动生成的 MyQueue 的默认构造函数在初始化 popST和pushST 的时候是去调用他们的无参的构造函数。
到这里构造函数我们就先讲这么多,其实构造函数还有一些其他的知识点我们在这里没讲,放到了后面我们来补充。
2.析构函数
析构函数:与构造函数功能相反,析构函数并不是完成对对象本身的销毁,这些对象也就是局部变量的销毁工作是由编译器完成的,当函数栈帧销毁时对象自然就跟着一起销毁了。析构函数完成的是对象中的资源的清理工作,清理工作就跟我们以前写的destroy相对应,他不是销毁对象本身,而是释放对象中的资源,我们目前所知道的资源就是 动态开辟的空间 以及 文件缓冲区,析构函数要做的就是释放动态开辟的内存以及关闭文件。析构函数是在对象销毁时由编译器自动调用的。
析构函数的特征:
1.析构函数名就是类名前面加 ~ (~就是我们C语言学的 按位取反的符号,意思是与构造函数的功能相反)。
2.析构函数是午餐无返回类型的
3.一个类只能有一个析构函数,若未显示定义,系统会自动生成默认的系统函数,所以析构函数是不能重载的。
4.对象生命周期结束时,C++编译器会自动调用析构函数
自动生成的析构函数与我们的构造函数是类似的,对于内置类型他不会处理,对于自定义类型会去调用他的析构函数。
那么上面的日期类我们就不需要自己写析构函数,因为他没有资源需要释放,可能有人会说我们不用把成员变量的值还原成初始化的样子吗?我们要知道,析构函数是在对象销毁之前的那一刻自动调用的,对象都要销毁了,对于这些临时变量它的值修补修改都是一样的。
而对于Stack类,因为我们动态开辟了一个数组,所以我们是要自己去写他的析构函数的,析构函数的功能就是释放掉动态开辟的数组,_a 是否置为nullptr 都无所谓。
~Stack()
{
free(_a);
}
而对于MyQueue类,他虽然有资源需要释放,但是资源都是在pushST和popST中的,编译器自动生成的析构函数会去调用Stack的析构函数去释放资源,所以我们是不用自己去想实现MyQueue的析构函数的。
要不要自己实现析构函数就一个原则: 如果编译器自动生成的就能释放掉资源,我们就不用自己写,免得画蛇添足。如果编译器自动生成的不能满足需求,我们就要自己写一个析构函数。
析构函数和构造函数是功能相反的一对默认成员函数,但是他们的特点是相似的。