【C++】——类和对象(中)——默认成员函数
一、类的默认成员函数
默认成员函数就是用户没有显示实现,不过编译器会自动生成的成员函数,称为默认成员函数。一个类默认成员函数一共有6个,在我们不写的情况下,编译器就会自动生成这6个成员函数,不过我们重点要学习的是前面四个,后面两个了解即可。还有就是在C++11后还增加了两个默认成员函数,移动构造和移动赋值,这个我们后续讲解C++11的时候也会进行讲解。
默认成员函数比较复杂,我们主要从下面两个方面进行学习理解:
1、我们不写的时候,编译器默认生成的函数行为是什么?是否可以满足我们的需求?
2、编译器默认生成的函数不满足我们的需求的时候,那么我们就要自己去实现,那么要如何进行 实现呢?
下面我们就从几种默认成员函数来入手:
二、构造函数
构造函数是一种特殊的成员函数,它的功能类似我们在前面实现链表、栈、队列等的数据结构的初始化函数一样,其是用来实现实例化对象初始化始化对象。还要注意的是,虽然名字叫做构造函数,但是其不会去开空间,其是局部变量进行初始化的。
下面是构造函数要注意的点:
1、构造函数的函数名和类的名字一样
2、构造函数无返回值,而且我们在定义和声明的时候连void都不需要写
3、对象实例化的时候系统就会自动调用构造函数
4、构造函数可以重载
下面我们通过代码来深入学习一下:
可以看到我们上面没有去调用这个构造函数,那么我们看看其运行结果是否会自动调用这个构造函数使得我们的成员变量初始化:
可以看到我们的成员变量被初始化成功了。所以说我们在实例化对象的时候编译器会自动调用我 们的构造函数。
我们上面还提到了,我们的构造函数还可以重载,也就是说我们的构造函数可以不止一个,可以 有多个,然后编译器会根据我们传的参数来进行判断该调用那个:
运行结果如下:
我们可以看到当我们要调用的是需要进行传参的构造函数,那么我们在实例化的时候,就需要 在后面加上括号然后进行传参。
那么我们对于没有参数的那个构造函数,我们在实例化对象的时候加个括号是否可以呢?
答案是不行滴:
可以看到编译器直接就报错了。
构造函数的使用要求还有以下几个:
5、如果类中没有显示定义构造函数,那么C++编译器会自动生成一个无参的默认构造函数,一旦 显示定义那么编译器不再生成。
6、无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成构造函数这三种构造函数都 叫默认构造函数。但是我们前面学习函数重载的时候就知道,这三个函数就只能存在一个,不 可以共存。无参构造函数和全缺省构造函数虽然构造函数重载,但是不传参调用时会有歧义。
可以看到错误信息中提示到,函数的调用不明确。
7、我们不写构造函数,那么编译器就会自动生成构造函数,对于内置类型成员变量的初始化是不 确定的,那么具体被初始化成什么就由编译器来确定了。对于自定义类型变量要求调用这个成 员变量的默认构造函数进行初始化。如果这个成员变量没有默认构造函数,那么编译器就会报 错吗,那么我们要初始化这个变量就需要用到初始化列表,初始化列表我们后续再进行讲解。
在C++中将数据类型分为内置类型和自定义类型
内置类型:int、char、double等等
自定义类型:我们使用class和struct等关键字进行定义的类型
下面我们就来看看编译器自动生成的构造函数是如何的:
我们可以看到我们在定义类的时候我们没有写构造函数的,那么我们在实例化对象的时候,我们的编译器就会自动帮我们默认构造函数,但是其具体是初始化什么内容我们就不知道了,我们上面的代码对成员变量的初始化就没啥有要求了。
还有一种情况就是我们的成员变量也是一个类,然后我们的成员变量有构造函数,那么我们这个类就可以不写构造函数了,那么其就会使用成员变量那个类的构造函数。
比如我们前面学习的数据结构中,使用栈实现队列,那么我们的队列类中的成员变量就是两个栈:
可以看到两个栈确定被初始化为了4。
要是我们的栈中的构造函数,其参数修改成需要进行传参的函数,那么就需要用来初始化列表了,
不过我们可以知道的是,对于大部分的类,我们都需要自己去写构造函数,因为编译器默认的很多情况都不能满足需求。
三、析构函数
析构函数的功能和构造函数是相反的,有点类似于我们在实现栈的时候的Destroy函数的功能,用来完成对对象中的清理释放工作。要注意的是其不是完成对对象的销毁,比如局部对象是有栈帧的,函数结束的时候会销毁栈帧,那么这个局部对象也就跟着销毁了,所以对于局部对象我们是不要理的,其也不需要析构函数。但是C++中规定了对象在销毁时会自动调用析构函数,用于完成资源的清理。如果对于一个类没有资源要进行释放的,那么理论上就不需要析构函数。
下面是析构函数使用上的一些要求:
1、析构函数名是在类名前加上字符~。
2、析构函数和构造函数一样是无参无返回值的,而且也不需要写void。
3、一个类中只能有一个析构函数只能有一个析构函数。若未显示定义,那么系统就会自动生成默 认的析构函数。
4、对象生命周期结束时,系统就会自动调用析构函数。
5、一个局部域有多个实例化对象的时候,后定义的会先进行析构。
下面我们通过代码来感受一下:
可以看到st1和st2都已经按照我们给的参数进行初始化了,然后我们继续往下运行:
可以看到我们是先将st2的销毁了,然后再对st1进行销毁的。
6、和构造函数一样,我们不写的话,那么编译器就会自动去生成这个函数,其不对内置类型的成 员变量操作,自定义类的成员变量就会进行处理。
7、还需要注意的是,我们显示写析构的时候,对于自定义类型的成员其也会调用它的析构函数, 不会受到在这个类中写的析构函数的影响。也就是说自定义类型不论什么情况下都会去自动调 用析构函数。
我们前面学习数据结构的时候,我们写了道使用栈实现队列的题目,下面我们通过这个例子来看看,我们可以在析构函数中打印一些东西来看这个函数被调用了多少次:
首先就是,我们知道的是,我们的队列自定义类型中,其有两个成员变量,编译器会其调用这个成员变量的析构函数,那么就会其栈类中调用,那么我们一共两个栈,所以其调用两次。
还有就是我们要是没有去向操作系统申请空间,那么我们的析构函数其实是可以不写的,直接使用编译器默认生成的析构函数即可,所以当我们需要进行空间申请的时候,一定要写析构函数,不然就会造成内存泄漏。
四、拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,而且任何地方的参数都有默认值,那么此函数就叫做拷贝构造函数。
下面是拷贝构造函数使用的时候的特点:
1、拷贝构造函数是构造函数的一个重载。
2、拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式的话编译器会直接报错, 这是因为在语法逻辑上会导致无穷递归调用。拷贝构造函数也可以有多个参数,但是第一个参 数一定要保证是类类型对象的引用,而且后面的参数一定要有缺省值。
我们上面的代码,将a1作为参数传给a2的拷贝构造函数,那么a2的值就会和a1的一样了。
还有就是我们的拷贝构造函数的参数部分可以加一个const关键字,因为我们不想被拷贝的对象被改变,还有就是对于被const关键字修饰的对象我们也可将其拷贝。
那么为啥不可以传值呢?这是因为传值的话,其会导致无穷递归调用拷贝构造函数,那是为啥会导致这个问题呢?
这是因为我们传值的时候,是一种浅拷贝,那么会创建一个临时变量先将参数的进行拷贝,但是我们在创建这个临时变量的时候,因为其也是类类型,那么其进行拷贝那么就会去调用拷贝构造函数,那么就造成了无穷递归。
引用传参的话,那么其是使用的实参的别名,那么在传参的过程中是不会产生拷贝的,其实际上就直接对这个传入的参数的内容进行拷贝了,所以不存在浅拷贝,就直接进入到函数中了,所以不会导致无穷递归。
3、C++中规定了自定义类型对象进行拷贝的行为必须调用拷贝构造,所以自定义类型传参和传值 都会调用拷贝构造函数来完成。
4、要是未显示定义拷贝构造函数,那么编译器就会自动生成拷贝构造函数,那么自动生成的拷贝 构造函数对内置类型的成员变量会完成值拷贝,即一个字节一个字节的拷贝,和我们前面学习 C语言的时候对字符串进行拷贝一样,对于自定义类型的成员变量那么就会调用其拷贝构造函 数。
不过对于一些比较复杂的成员变量,要是使用编译器自动生成的拷贝构造函数,会造成不好的效果,比如我们的栈成员变量:
可以看到我们的程序就直接运行不起来了,这是因为st1初始化的时候,_a会被分配一块空间,但是st2就是st1的浅拷贝,但是此时st2中的_arr和st1中的_arr使用的是一块空间了。
所以这种情况下我们一定要自己去写拷贝构造函数来实现深拷贝。
5、像我们上面的Date类中,其成员变量全是内置类型而且没有什么指向资源,那么编译器自动生 成的拷贝构造函数就可以完成了,所以我们可以不去写,但是像Stack类这样的,虽然其也是 内置类型,但是_arr其指向了资源,编译器自动生成的拷贝构造函数完成的是值拷贝,达不到 我们的需求,所以我们要自己实现一个深拷贝。还有就是很前面的使用栈实现队列中的队列类 也是一样,其成员变量的栈的类其有显示的拷贝构造函数,那么我们的队列类中可以不写,其 会去调用栈的拷贝构造函数。
下面有个技巧:
如果一个类显示的实现了析构函数,而且释放了资源,那么其就需要显示的写拷贝构造。
6、传值返回会产生一个临时对象的拷贝构造,传值引用返回,返回的是返回对象的别名,那么就没产生拷贝。那么如果返回对象是一个当前函数的局部域的局部对象,那么函数结束就销毁了,那么使用引用返回是有问题的,此时的引用就是野引用了。