【C++笔记】C++之类与对象(中)
1、类的构造函数
其实每个类都有6个默认成员函数:
默认成员函数指的是:用户没有显示实现编译器会自动生成的函数。
这些函数主要完成一些初始化和资源回收和复制的工作,而且有些有些是会自动调用的。有了这些成员函数,我们的类使用起来就能更省心。
而我们这个阶段只会接触到前两个“初始化和清理”和“拷贝构造”。
第一个默认成员函数就是构造函数,它主要完成的是对类的初始化工作。
1.1、构造函数的基本用法
其实构造函数,就和我们以前写的初始化函数一样,只不过构造函数更方便一些,在写法上他有两个特征:
然后函数体内的写法就和以前的初始化函数差不多。
就拿栈这个类来举例,我们就可以这样来写构造函数:
当然了,构造函数也是可以传参的,我们可以传一个参数表示要开多少空间:
而它的用法就是在创建类对象的时候,在类对象后面加一个括号,括号后面加上对应的参数即可:
而构造函数是自动调用的,为了验证这个特点,我们可以把构造函数改成无参的:
1.2、构造函数的7个特性
其实构造函数,难就难在特性太多了,不好记,构造函数总的有7个特性,前面是个是:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
前三个我们已经验证过了,我们就从第4个开始逐一验证。
4. 构造函数可以重载。
既然C++搞出了函数重载,那为什么构造函数就不能重重载呢?而且构造函数的重载为我们提供了更多的初始化方法,例如对于栈这个类,我们就可以写这样的两个构造函数,这样在初始化的时候就可以有两种初始化的方式可以选择了:
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
验证个这个特性我们就只需要保留一个有参数的构造函数,再使用不传参初始化的方式即可:
而要是我们不主动去写构造函数,编译器就不会报错了:
6.编译器生成的默认构造函数对内置类型不进行处理,对自定义类型就调用其默认构造函数。
我们可以通过调试来看一看上面的例子中的,个成员有没有被处理:
我们可以看到,所有的成员都还只是随机值,因为这里的所有成员都是内置类型的。
我们可以再随便定义一个自定义类型,来验证第二点:
7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为
是默认构造函数。
构造函数麻烦就麻烦在这里,我们大概率都会认为编译器自动生成的无参的构造函数就是默认构造函数,但是它偏不是,而是有三个。
而我们只需要证明全缺省的构造函数是默认构造函数即可:
其实构造函数可以简单理解为,不用传参就可以调用的函数,所以全缺省的构造函数也是一个默认构造函数。
但要是我们不提供默认构造函数,而只是写了一个不符合默认构造函数的构造函数,如果我们在初始化的时候以不传参的方式初始化,编译器就会报错,因为找不到默认构造函数:
这是因为一旦我们显示的写了构造函数,那不管其满不满足默认构造函数的条件,编译器都不会在生成默认的构造函数了。
2、类的析构函数
析构函数其实就类似于我们以前写的销毁函数destroy,它的主要工作就是,清理数据和释放空间。
2.1、析构函数的基本用法
析构函数在语法上有两个特点:
还是拿栈来举例,栈的析构函数可以这样写:
注意:析构函数不能显示调用!
2.2、析构函数的6个特性
与构造函数一样,析构函数也有一些让人头晕的特性,前面两个:
我们已经清楚。
那我们从第三个开始说起。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
也就是说析构函数不能重载:
至于编译器自动生成的析构函数,我不会验证,反正记住就行了。
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数
这一条上面已经验证过了。
5.编译器自动生成的析构函数对于内置类型不作处理,对于自定义类型则去调用其析构函数。
置于为什么对于内置类型不作处理,我想应该很容易理解,因为内置类型本身出了作用域就会随着内存一并被回收,所以处不处理都一样了。
而关于第二点,我们可以用以下例子来证明:
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如
Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
3、类的拷贝构造函数
就像内置类型总是需要将一个变量的数据拷贝给另一个变量一样,自定义类型也需要将一个对象的数据拷贝给另个一对象,这就需要我们的拷贝构造了。
3.1、拷贝构造的基本用法
拷贝构造其实是构造函数的一个重载样式,所以它的函数名也是也类名相同,并且拷贝构造规定只有一个参数,就是一个同类型的引用。
就拿日期类来举例,日期类的拷贝构造就可以这样写:
然后调用其实也是向初始化一样,只不过参数变成了另一个对象:
3.2、拷贝构造的“无限套娃”陷阱
为什么拷贝构造一定要传一个对象的引用,那直接穿一个对象行不行呢?
其实,C++规定,如果函数传参传的是一个类对象,那么在函数调用的第一步传参的时候,就要去调用这个类的拷贝构造去生成一个拷贝。
我们可以通过以下例子来验证:
我们可以看到,这里只是简单地调用了一下func(),而函数内并没有做任何操作,却已经调用了拷贝构造。从而我们就可以确认类的传值传参就回去调用拷贝构造。
这样我们就可就可以来解释为什么,拷贝构造规定只能传对象的引用了,如果我们拷贝构造传是对象的值拷贝,那么在调用拷贝构造之前就会先去调用拷贝构造,而调用拷贝构造就需要传值,那就会再去调用拷贝构造,如此循环往复无穷无尽。所以如果拷贝构造传的是类对象的只拷贝就会陷入这样的一一个死循环:
而我这里的编译器是进行了强制检查的。
3.3、深拷贝与浅拷贝
其他的构造函数都有编译器自动生成的版本,拷贝构造也有,但编译器自动生成的拷贝构造完成的是“浅拷贝”,而浅拷贝存在一些问题,这个问题我们可以通过下面这个例子看出:
可以看出,对于栈这个类只进行浅拷贝的话就崩了。而且我们看到调用了两次析构函数。
这是因为:
编译器自动生成的拷贝构造对内置类型会进行值拷贝,而对于自定义类型会去调用其拷贝构造。
而现在写的栈的所有成员都是内置类型,那就是只进行值拷贝了,这里的问题出就出在析构函数。因为成员函数都只进行了值拷贝,所以两个栈对象的_data都指向了同一块空间。而而当其中一个栈析构了以后,这一块空间就已经被释放了,然后后一个栈又调用了一次析构函数,这就会导致同一块空间被释放了两次,这就是报错的原因。
所以对于栈这种有额外空间申请的类,我们应该进行的是“深拷贝”:
4、运算符重载
有时候我们也需要将各个类对象进行比较,但我们不能直接运用运算符进行比较,因为只有内置类型才能这样做。因为编译器并不知道你自己定义的类型到底该怎样比较。
这就需要运算符重载出场了。
4.1、运算符重载的基本使用方法
运算符重载的函数名就是“operator”加上各种运算符,而返回值可以是布尔类型或其他各个类型。
比如我们要比较两个日期类对对象那个更小,就可以这样写:
这样我们就可以直接使用小于运算符比较两个类对象了:
而这里之所以要加上括号的原因是,流插入的优先级要高于小于运算符,如果不加上的话就会报错了:
4.2、运算符重载使用的注意事项
运算符重载的使用也有一些注意事项。
1.不能通过连接其他符号来创建新的操作符:比如operator@
当然,我们不能自己创造运算符,我们重载的运算符必须是C++已有的运算符,例如:
2.重载操作符必须有一个同类类型参数
这个必须有的同类型参数就是我们的this,因为对象调用的时候一定要将自己的指针传过去,只不过这个this指针是隐藏起来的。
剩下的三点分别是:
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
5.".* :: sizeof ?: . "注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
4.3、重载前置++和后置++怎么区分?
对于函数名相同且,不用传参数的运算符重载的函数,我们该怎么区分呢?
例如这里的前置++和后置++。
我们可以为其中一个添加一个额外的“无用”的参数:
这样便一起在调用的时候就可以看情况,是否要加上一个参数来区分了。
当然,这个参数你传任何值都是一样的,因为它的存在只是为了做一个区分,本身不是用来使用的。.
5、类的const成员函数
因为在引用的过程中,权限不能放大,所以对于const对象有些函数是不能调用的:
因为引用的底层其实就是用指针实现的,所以这里可以将引用等同于指针。因为Print隐藏的this指针不是const的,所以这里的调用实则是将const的指针转化成了非const指针,涉及到了权限的放大。
为了决绝这个问题,我们可以添加一个const修饰的Print函数:
const函数的语法就是在函数的后面加上一个const。
这里的const和非const的函数其实是构成了重载。
这样便一起在调用的时候就会自动去匹配最符合的函数,const的对象就去调用const的Print函数,非const对象就会去调用非const的Print函数。