类的成员函数
类的成员函数(简称类函数)是函数的一种,可以把它看为一个出现在类体中的函数。与一般的函数不同,它是独属于这个类的。
当一个类中没有任何成员的时候,我们就称它为空类。但它却并不是什么都没,因为编译器在类创建的时候会自动生成6个函数,我们称这函数为默认成员函数。
按照这6个默认函数的功能,我们有可以把他们分为三种。
对于每一个类来说,即使是空类他也会有这6个默认成员函数。当我们没有去定义这6个成员函数的时候,编译器就会自动生成对应的成员函数。
1.构造函数
在C++中,我们通过一个类去实例化一个对象的时候,当对象实例化的同时,编译器就会自动调用构造函数来对成员变量进行初始化。所以构造函数虽然名称叫构造,但是构造函数的主要功能并不是开空间创建对象,而是初始化对象。
以Stack类为例,我们先定义一个Stack类的构造函数。
其中我们可以发现构造函数它没有写返回值,因为他就根本不会有返回值,所以为了提高代码效率就干脆直接省略了,甚至连void都不用写了。同时它的名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
同时构造函数也是可以实现重载的。
不过很多时候我们可以使用全缺省的形式来定义构造函数,这样即使我们在实例化对象的时候不输入参数也能够成功实例化。不过要避免出现歧义的情况。
例如这种情况,当我们在实例化的时候如果不输入参数,编译器就会不知道使用哪一个构造函数,从而导致报错。
需要注意的是,一旦你手动定义了构造函数之后,编译器就不会再单独生成默认的构造函数了。但如果你不去定义构造函数,编译器就会生成一个无参的构造函数。
但在使用编译器默认的构造函数的同时,要注意函数的定义一定不能带 ( ) !!!
举个例子,当你定义一个Stack类的对象st时如果输入Stack st();
就会导致编译器产生歧义,编译器不能分辨这是在定义函数还是在创建一个对象。
所以在使用默认构造函数的时候一定要注意这一点。
Tip:对于编译器自己生成的默认构造函数,该构造函数对于对象中的内置类型成员变量不进行处理,但是对于自定义类型的成员变量则去调用该自定义类型的默认构造函数来进行初始化。
什么意思呢?
就是int,char,int*等的这种内置类型(基本类型)的成员变量编译器不会去进行初始化处理,但是如果是自定义变量,编译器就会对其进行初始化处理。
所以说对于内置类型的我们一般要手动对其进行处理,但是对于自定义类型的变量就不用对其进行单独处理,默认构造函数足以。
构造函数的特性
1.函数名与类名相同。
2.无返回值。
3.对象实例化时编译器自动调用对应的构造函数。
4.构造函数可以重载。
5.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
用户显式定义编译器将不再生成。
6.内置类型成员变量在类中声明时可以给默认值。
7.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
2.析构函数
在C语言的时候我们就知道malloc了一块空间之后,使用完了要通过free来释放空间,否则就会造成空间的浪费也就是内存泄漏。
对于C++一样,对与一个对象来说,在经过构造函数对其进行初始化之后,我们还要通过一种手段来把这个对象的空间释放。而这就是析构函数。
与构造函数相似,析构函数也不需要进行手动调用,对象在销毁的时候编译器会自动调用析构函数。
同样以Stack为例,我们来写一个对应的析构函数。
Tip:对象销毁的时候一定会调用析构函数,但调用析构函数不代表对象销毁!!!
同时类似与构造函数,析构函数也不会对内置类型的成员变量进行处理,但对自定义类型的成员变量会调用对应的析构函数。所以我们的类里如果含有需要开辟空间的内置类型的成员变量我们一般都需要自己手动定义构造函数和析构函数。
析构函数的特性
1.析构函数名是在类名前加上字符 ~。
2.无参数且无返回值类型。
3.一个类只能有一个析构函数,析构函数不能重载。若未显式定义,系统会自动生成默认的析构函数。注意析构函数不能重载。
4.对象生命周期结束时,C++编译系统系统自动调用析构函数。
5.内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可,如果是new的一块空间则需要对其进行释放。
6.调用析构函数不代表对象销毁。
7.创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数。
8.如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
3.拷贝构造函数
顾名思义,拷贝构造函数的功能就是创建一个与本类对象一模一样的同类对象。它是一种特殊的构造函数,在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。
拷贝构造函数只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
还是以Stack为例,先手动定义一个拷贝构造函数。
对于类的拷贝,一般有两种调用方法。
再仔细一看,我们可以发现这样拷贝下来,三个对象的_a都是指向同一块地址,这种把数据原封不动地拷贝过去的方式我们称之为浅拷贝。
但这种方式有很大的弊端,比如你如果更改st3里面的_a指向的空间里的数据,那么st1和st2的也会跟着一起变动,这是我们不想要的。
所以我们对于这种拥有指针类型的成员变量的类我们可以采用深拷贝。但编译器自动生成的默认拷贝构造函数是浅拷贝,所以对于需要使用深拷贝的场景我们需要自行定义拷贝构造函数。
经过更改后这样调用构造时我们就会额外开辟一块新的空间,然后把原来的东西复制一份过去,这样不同对象指向的空间不是同一块但内容相同,这种拷贝构造方式就是深拷贝。
Tip:拷贝构造函数的参数有且仅有一个,且类型必须是该类类型对象的引用,如果不采用引用的方式进行传参,则会导致无穷递归调用。
我们如果使用值传递的方式来实现拷贝构造函数:
这里编译器就已经会自动报错了,告诉你不能这种拷贝构造函数不能带有Stack类型的参数。
这是因为这种值传递的方式实现的拷贝构造函数会一直递归下去。
我们知道传参的时候是会把实参拷贝到形参,这是一个拷贝动作。当我们采用值传递的方式定义拷贝构造函数的时候,我们调用它的时候由于是值传递,这个时候编译器就会自动调用Stack类的拷贝构造函数,然后拷贝构造函数又是值传递,然后又会调用Stack类的拷贝构造函数。如此往复,就会导致无穷递归调用,所以说编译器是不允许值传递的。
拷贝构造函数的特性
1.拷贝构造函数是构造函数的一个重载形式。
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
3.若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
4.在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
5.类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
6.拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
7.为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用
尽量使用引用。
运算符重载
在C++中,运算符的操作对象类型必须是内置类型,对于自定义类型却无法使用。但对于自定义类型来说,如果能够使用这些运算符进行操作,那么在很大程度上能够增加代码的可读性,提高程序的编写效率。
于是C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
对于运算符重载来说,函数名字为关键字operator后面接需要重载的运算符符号。
函数原型为返回值类型 operator操作符(参数列表)。
其中需要注意:
1.不能通过连接其他符号来创建新的操作符,比如operator@ 。
2.重载操作符必须有一个类类型参数。
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义。
4.作为类成员函数重载时,其形参看起来比操作数数目少一个,因为成员函数的第一个参数为隐藏的this。
5..*
,::
,sizeof
,.
和?
这五个操作符不能利用operator重载。
这里以运算符==
为例:
上面的函数我是直接放在Date类里面的,也可以把函数定义为全局函数。但这个时候就面临着不能访问私有成员的问题。
对于不能访问私有成员的问题,我们一般通过申明友元函数或者通过公共函数来获取私有成员变量的值,这两种方法来解决。
所谓友元函数就是指即使申明的这个函数即使不属于这个类,也可以访问这个类里面的私有成员。
这里采取申明友元函数的方法来解决。
同时用于定义运算符重载的函数也是可以重载的。就比如类里的和类外面的定义是可以同时存在的。但要注意的是会优先使用类里面的定义。
除了运算符==
之外还有>=
,<=
,>
,<
和!=
等,大家有兴趣的话可以自己试一试。
Tip:友元函数一定程度上提高了效率,是代码编写更加方便。但也破坏了类的封装性,所以对于友元函数的使用一定要谨慎。
赋值运算符重载
C++在以拷贝的方式初始化一个对象时,会调用拷贝构造函数。当给一个对象赋值时,会调用重载过的赋值运算符。
即使我们没有显式的重载赋值运算符,编译器也会以默认地方式重载它。默认重载的赋值运算符功能很简单,就是将原有对象的所有成员变量一一赋值给新对象,这和默认拷贝构造函数的功能类似,是编译器会默认生成的。
- 赋值运算符重载格式:
参数类型:const T&,传递引用可以提高传参效率。
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值。
返回*this:要复合连续赋值的含义
其中赋值运算符重载的定义不能在全局,这是因为作为六个默认函数里面的其中之一,即使我们没有显式的重载赋值运算符,编译器也会以默认地方式重载它。也就是说即使我们不去重载赋值运算符,编译器也会自己默认定义一个。
由于编译器重新定义的是在类里面的,此时如果你再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
其中,用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
Tip:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
前置++和后置++重载
在运算符中还有一个非常常用的运算符,那就是++
了。对于++
这个运算符,我们都知道有两种用法,其中一个是前置++的用法,还有一个是后置++的用法。
但对于编译器来说,它是不知道你是前置还是后置的,那么编译器是如何区分呢?
我们先直接实现两种++的重载:
取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器默认会生成。只有特殊情况,才需要重载,比如想让别人获取到指定的内容。
比如这种: