1.类的六个默认成员函数
在一个空类中真的什么都没有吗,错!在创建类的时候,编译器自动生成六个函数,这六个函数叫默认成员函数。但是,如果我们自己实现六个同名函数(依旧有默认成员函数的特性,只是把编译器的替换成了我们的),这样就不会编译器就不会自己生成函数,而是利用我们写的。
---------------------------------------------------------------------------------------------------------------------------------
1.构造函数
概念:我们经常忘记对一个对象的初始化,这会导致一些未知的问题。构造函数也可以叫做初始化函数,帮助我们对对象初始化。
特性:
对内置类型数据:没有定义要不要去初始化,有可能处理,也有可能不处理。
对自定义类型数据:会调用这个自定义类型里面编译器生成的默认构造函数去处理这个自定义类型数据。这里有两种情况,一种是我们自己在这个这个自定义类型里定义了不同于编译器生成的构造函数(在这种情况下编译不会生成默认构造函数),而找不到默认构造函数就会报错。第二种是我们没有自己实现构造函数(这时编译器自动生成默认构造函数),调用了默认构造函数之后,对这个自定义类型里的内置类型不处理,遇到自定义类型重复以上步骤,最终自定义类型都是由内置类型组成,所以都是不处理,这样就没意义了,如下:
这里我要强调一下:并非是编译器自动生成的函数才叫做默认构造函数,无参构造函数,全缺省参数构造函数都可以叫做默认构造函数,也就是说,虽然我自定义了构造函数导致编译器没有自动生成构造函数,只要我定义的构造函数是无参,或者全缺省参数,我也不报错(上面说了,只调用默认成员函数,没有就会报错),直接调用我定义的函数去初始化。
换句话说,无参构造函数,全缺省参数构造函数伪装成了默认构造函数。严格来讲,无参构造函数,全缺省参数构造函数,编译器生成函数只能存在一个,否则可能应为传参相同的问题产生歧义
下面是两个栈实现队列的部分代码:首先,创建了xx这个队列时,自动调用了默认构造函数,因为st1和st2是自定义类型,所以转而调用Stack中的构造函数去初始化这两个栈,而栈里面的构造函数是我们自己实现的,但它伪装成了默认构造函数,所以调用了这个函数去初始化st1和st2中的变量,所以有了下面的结果,感觉不错。
---------------------------------------------------------------------------------------------------------------------------------
除了上面这样处理以外c++11还为这个缺陷打上了补丁:
在创建类的时候就可以赋予内置类型初始值,这样我们创建对象时内置类型就有了初始值,相当于初始化了,我们把赋予的值叫做缺省值
注意:如果默认构造函数对内置类型做了相关初始化,就以默认构造函数的初始化为准,这也是为
什么叫缺省值的原因,可用可不用嘛。
---------------------------------------------------------------------------------------------------------------------------------
2.析构函数
概念:我们经常忘记对数据进行销毁,比如销毁开辟的动态内存,析构函数帮助我们销毁数据。
特性:
1.无论是析构函数还是构造函数,大概只有两种情况不需要自己定义:
1).没有资源需要清理(局部变量会自动销毁,资源是指动态内存等不会销毁的数据) 或 在类里面用了缺省值初始化且没有自定义类型 。
2).内置类型没有资源需要初始化或清理,剩下的自定义类型可以正确初始化或清理(自定义类型里面有自己定义的默认成员函数)比如两个上文中两个栈实现队列就是这种情况。
2.在同一生命周期内,按定义的顺序对类构造,但是析构时顺序和构造相反。
---------------------------------------------------------------------------------------------------------------------------------
3.拷贝构造
概念:拷贝构造是指在创建对象的时候将已有对象的内容拷贝到该对象中。实际上拷贝构造就是构造函数的一个特殊分支,本质上还是初始化。
特性:
1.拷贝构造是构造函数的一种重载形式。所以拷贝构造无返回值。
2.拷贝构造的参数只有一个,且必须是类的类型的引用(用指针也可以,只是相对引用来说比较麻烦),如果直接传值的话会报错因为会引发无穷递归:
想要把d1拷贝给d2,先得把d1拷贝给形参date,然后在用date拷贝d2。而想把d1拷贝给date就得先把d1拷贝给新的形参date,这样一直循环发生无穷递归。
3.拷贝构造的使用方法是在创建对象的同A st2(st1)或者A st2 = st1,其中Stack是类名,st1是要拷贝的对象。如下:
4.若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数按字节拷贝对象,类似于memcpy,这种拷贝叫做浅拷贝,或者值拷贝。
5.如果对象中有指向一块空间的指针的时候还使用浅拷贝,那这两个指针就会指向同一块空间,这样两个对象就不是独立的了,这不是我们想要的。这时候就要深拷贝,你可以定义一个拷贝构造函数,在里面开创新的空间,然后把里面内容改成和要拷贝的对象里指针所指向空间一样的。
总结:
1.如果对象内没有指向空间的指针,那么不需要自己写拷贝构造函数,使用默认拷贝构造函数即可,如果有的话就自己写一个深拷贝。
2.一般需要写析构函数就需要写拷贝构造函数。
4.赋值运算符重载
1)运算符重载
两个类如何比较?定义一个compare函数?那么大于返回真,还是小于返回真?无论如何这样代码可读性会较低。
c++为了增强代码的可读性引入了运算符重载。运算符重载是具有特殊函数名的函数。
对于这个函数有如下规则:
1.函数名为关键字operator加上运算符,如:operator<。
2.函数参数个数与操作符的操作数个数保持一致。且参数中必须有至少一个类类型参数。
3.函数的返回值由运算符决定,比如operator==的返回值是bool,operator+的返回值为int。
4.不能用其他符号链接operator构成函数名,只能用内置运算符。而且,在内置运算符中,.*,::,?:,. ,sizeof也不能用来链接。
5.传参数的顺序和操作数顺序一致
6.编译器的规则是当遇到一个类使用运算符时自动调用运算符重载(这也是为什么可读性会增高),先在类里面找,再到全局中找,找不到报错。如下:
st1>st2比较日期是不是非常直观,比调用个compare函数可读性高不少。实际上st1>st2会被转换成st1.opeartor>(st2),是函数调用。可以直接写成转换后的样子,但是说显式写成这样可读性不高,失去了运算符重载的意义。
这里注意一下,如果把opeartor>定义在类外,需要增加一个参数,而在类里隐式的给opeartor> 传入了this指针,实际上也是两个参数。
2)赋值运算符重载
赋值运算符重载同样拥有以上所有特性,这里是对赋值运算符重载的一些补充:
1. 和其他运算符重载不同,赋值重载必须是成员函数,不能定义在全局。同构造,析构,拷贝函数一样,如果没有在类里实现赋值重载,类中会自动生成默认成员函数(该默认成员函数的行为与拷贝构造类似),这样会和全局函数发生冲突。
2.赋值重载和拷贝构造区分:
Date st1 = st2;拷贝构造
st3= st4;赋值重载
简单来说,拷贝构造针对的是刚要创建的对象,赋值重载针对的是两个已存在的对象。
3.赋值重载必须有返回值,因为有连续赋值的情况。有两种返回方式,一种是返回引用。一种是返回拷贝。
总的来说返回引用效率高一点。出了作用域后,如果返回对象的生命周期在对象返回后没有结束,并且没有析构,那就返回引用,除此外返回拷贝(使用已经销毁对象的引用会导致不可预知的错误)。
3)前置++和后置++
前置++和后置++的函数名相同,但是前置++应该返回的是++后的值,而后置++返回++之前的值,他们的实现不同,函数名相同,这就需要函数重载。然而,之前规定了运算符的参数个数与操作数个数一致,所以前置和后置++函数的参数都只有this指针。迫不得已,又规定给后置++的参数中多加一个int来区分这两个函数。
这个int参数不需要使用,仅仅为了区分前置和后置++
4)const修饰成员函数
const Date st1;
st1++;
我们用const修饰了st1,但是调用运算符重载函数时编译器传入的this指针是Date*const类型,导致权限放大,这是编译器不允许的。
所以我们需要把this指针类型改为const Date * const,这样无论st1受不受const限制,权限只可能缩小或不变,不可能放大,这是正确的。
this指针是隐式的,要想修饰它,规定在函数后面加const,如:
int Func()const;//声明
int Func()const //定义
{
}
其实const都是修饰的隐含的this指针
注意:
1.只有成员函数能用const修饰。
2.构造函数和析构函数不能使用const修饰。
3.如果要对this指针指向的对象做改动就不能用const修饰成员函数。
5.取地址操作符重载和取const修饰的对象地址的操作符重载
这两个重载使用编译器生成的默认成员函数即可。