一.类的定义
1.1类定义的格式
图中class为关键字,Stack为类的名字,用{}框住类的主体,类定义完后;不能省略。
为了区分成员变量,一般习惯在成员变量前面或后面加一个特殊标识,_或者m_
1.2访问限定符
c++采用一种封装的方式,用类和对象的属性与方法结合在一起,让对象更加完善,通过访问权限选择性的将其接口提供给外部使用。
public修饰的成员在类外可以直接访问使用,private和protected修饰的成员在类外不能直接使用。
1.3类域
类定义了一个新的作用域,类的所有成员都在作用域中,在类体外定义成员时要用::操作符来指明成员。
二.实例化
类就像一个模型图纸一样,实例化之后可以实现许多的功能,但是在实例化之前,他是没有任何功能的,并且也不会占用实际的空间,只有当实例化出对象后,才能进行物理存储。
2.1对象大小
类实例化出的没一个成员都有独立的数据空间,那么类中的函数是否能进行储存呢?首先函数在编译之后是一段指令,对象无法进行存储。所以函数会以指针地址的方式,通过汇编指令call地址,找到函数的地址进行使用。
打个比方,类里面的成员变量每实例化一份就会开辟一份的空间,而类成员函数则会以一份公共的地址,放在一个代码公共区内。
三.this指针
在一个类中,若有多份成员函数,c++是如何找到访问对象的呢?
这里存在一个this指针,this指针在编译器编译后,会默认出现在第一个位置,增加一个当前类型的指针,类的成员访问成员变量,本质都是通过this指针来使用的。c++规定不能在形参和实参位置写this指针(编译器编译时会处理),但可以在函数体内使用this指针。
我们来看一串代码
它的编译结果是什么?
答案是正确通过!
p为空指针去访问类中的print函数,虽然是空指针,但是我们并没有拿去访问实体,所以不会出错。
若是这样去访问实体a所以就会运行崩溃
3.2this指针存在哪个区域内
常量区是用来存放一些字符类字符串类的全局变量。堆区是用来存放malloc手动开辟的空间变量的存储区域。静态区内存放的是static修饰的变量,以及全局变量。栈区是存放函数的形参实参的空间,以及函数内的变量。
很明显这里的this指针应当存放在栈区。
四.类的默认成员函数
4.1概念
默认成员函数就是用户没有显示实现,编译器会自动生成的成员函数称为默认成员函数。一个类,我们不写的情况下会默认生成6个成员函数。分别为,构造函数负责完成初始化工作,析构函数负责完成清理工作,拷贝构造是使用同类对象初始化创建对象,赋值重载只要是把一个对象赋值给另一个对象,取地址重载主要是将普通对象和const对象取地址。
4.2构造函数
构造函数是特殊的成员函数,虽然他的名字叫构造,但并不是以开空间创建对象为目的,而是对象实例化时初始化对象。构造函数的本质是为了替代以前stack类中写Init的功能,构造函数自动调用完美解决了此类问题。
4.2.2构造函数的特点以及实现
函数名与类名相同(在类中单独初始化,且名字为类名)。
无返回值(不需要给任何的类型,c++规定如此)。
对象实例化时会自动调用函数。如下图,d1会自动初始化三个值为1。
构造函数可以重载,我们可以根据自身需要设置变量的值,以及是否需要缺省参数。
若我们不写,编译器会默认初始化生成默认构造函数,若成员变量为基本类型(char int double 指针等)那么编译器就会报错。若成员变量为自定义类型(先前已经在类中定义过的类型)就可以正常进行初始化。
无参构造函数,全缺省构造函数,默认生成的构造函数,都叫做默认构造函数(不传实参就可以调用),这三个函数有且只能存在一个,不能同时存在。
4.2.3构造函数实现法2
除了用函数体内赋值的方法,构造函数初始化还有一种方法就是初始化列表,初始化列表从一个冒号开始,用逗号分隔数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。(在{}和()中只能初始化一次)
当面对引用成员,const成员变量,没有默认构造的类类型变量,必须在:下进行定义。因为这些变量已经在主函数中定义过了,无法在构造函数中重新定义,所以只需要在:下定义即可使用。
4.2.4成员变量初始化逻辑
显示在初始化列表的成员变量就按这个值进行初始化。未显示在初始化列表的成员,如果有缺省值或者就按缺省值初始化,若没有缺省值,内置类型成员可能会初始化为随机值,自定义类型成员若有对应的构造函数那么就可以调用若没有则会报错。引用,const,没有默认构造函数的成员必须在初始化列表进行初始化。成员变量会根据声明的顺序进行初始化,所以尽量按照声明顺序进行定义。
4.3析构函数
析构函数与构造函数相反,析构函数是完成对对象本身的销毁,例如我们需要在函数内申请空间,当栈帧销毁时要先做清理资源使得资源释放。
4.3.2析构函数的特点
要在析构函数前加上~。(析构函数要与类同名)
参数无返回值,且一个类只能有一个析构函数,若未显示定义,系统会自动生成。
对象生命周期结束时,系统会自动调用析构函数。
跟构造函数类似,若我们不写编译器自动生成的析构函数对内置类型不做处理,自定类型成员则调用他们的析构函数。
若类中没有资源申请,析构函数可以不写,直接使用编译器生成的默认析构函数。但是当有资源申请时,一定要自己写析构函数,否则就会造成资源泄露。
4.3.3构造函数和析构函数的顺序
构造函数遵循,先全局变量,再局部变量,然后是局部静态变量。全局变量指在main主函数外的的变量,静态变量指static修饰的变量,而局部变量就指主函数内的变量。遵循从上到下的顺序进行构造。
析构时,遵循先局部再全局的思路,先析构局部的简单变量,再析构静态变量,最后是全局变量
4.4拷贝构造函数
类似于键盘的复制粘贴,用于复制类类型的函数。拷贝构造需要接受拷贝的必须是未存在的对象,而拷贝的必须是存在的对象。
4.4.1拷贝构造函数特点
1.拷贝构造函数是构造函数的一个重载。将函数内部的值copy到拷贝构造函数中。
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用(可以有其他参数,但必须是缺省参数),使用传值方式编译器会报错,因为会在语言逻辑上引发无穷递归调用。
所以在拷贝时必须使用引用直接指向原先的类。
通过加上Const来防止拷贝的值被轻易修改。
若没有使用&符号,每当我们需要拷贝时,就要重新开辟一块空间,调用一次构造函数,此时通过拷贝函数将其copy进来,但是当我们调用拷贝函数时又会重新开辟一块新的空间,反复上述步骤。
3.若未显示定义拷贝构造,那么系统会调用默认的拷贝构造。这里的拷贝构造是浅拷贝,若变量为int,char这些类型的变量,拷贝时会单独开辟一块空间储存他们的值。若变量为malloc出的一块空间,或者指针文件类的指向型,浅拷贝后,还会指向原先的地址。若是这样一旦函数析构,或者变量改变,就会影响到拷贝的值。所以我们需要在拷贝函数上重新单独为其开辟一块新的空间储存。
五.赋值运算符重载
5.1运算符重载
当运算符被用于类类型的对象时,编译器无法直接识别运算符定义,所以我们需要重新进行运算符定义。我们用operator和后面要定义的运算符共同构成,他相当于一个函数,拥有返回类型和参数列表以及函数体。重载的运算符函数的参数个数要与运算对象个数一样多,并且按照顺序进行排列。若重载运算函数是成员函数,则它的第一个运算对象默认传给隐形的this指针,所以运算符重载作为成员函数后,参数少一个。运算符重载后,其优先级和结合性与对应的内置类型运算符保持一致。当然也并不是所有的运算符都能重载,像.* :: sizeof ?: .不能进行重载
如图其中的.*是调用成员函数指针的符号。
一个类需要重载哪些运算符是看哪些运算符重载之后有意义,比如日期减日期可以得到天数,但日期加日期就无意义。
重载运算前置++和后置++时,重载名都是++,无法区分,所以c++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载用于区分。
5.2赋值运算符重载
赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接拷贝赋值,这里需要跟拷贝构造进行区分,拷贝构造是一个对象拷贝初始化给另一个要创建的对象。
5.2.1特点
1.赋值运算符应当写成const当前类类型的引用,否则传参会有拷贝降低效率。
2.没有显示实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载为浅拷贝,对于动态开辟空间的变量会同时使用同一块空间,会导致错误发生。也就是说,当成员内有多个对象享用同一块空间时,需要自行手动实现。
3.手动实现时,需要先释放当前对象已有的资源,以避免内存泄露,对于(int,double)这类型的直接进行赋值,对于指针这类的需要重新开辟空间,进行深拷贝。最后返回当前对象的引用。
4.注意当出现a=a的情况,直接释放资源会导致错误,所以开头先判断两者地址是否重叠再进行拷贝。
代码参考
5.3日期实现
5.3.1 Date.h
其中对于自身有改变的函数需要传递引用值,对于this指针不存在改变的要在函数后添加const修饰。
5.3.2 Date.cpp
对于代码相似的函数实现尽可能的使用函数调用,这样可以减少代码量并且使得程序更加可观。
ostream&和&istream是c++库中定义的一个流输入输出重载的函数名。我们在类中定义友元函数,因为函数调用时是按照严格的左右位置调用的。
六.类型转换
当我们使用=来赋值类类型时,会存在一个隐式的类型转换,开辟一块空间将其进行拷贝函数赋值。
若是遇到引用符号时,因为临时对象具有常性,所以在传值时,左值必须使用const将变量转变为常性。若等号传参时有多个变量则用{}来包含值,若是有多组则以这样的形式传值{{},{}}。若是自定义类型转换则需要相应的构造来支持。
七.static成员
7.1static成员特点
用static修饰的成员变量称之为静态成员变量,静态成员变量一定要在类的外部进行初始化。静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。用静态成员修饰的函数没有this指针,意味着函数内部不能访问非静态的成员,只能访问静态成员。而非静态的成员函数可以访问任意的静态成员变量和静态成员函数,只需要进行突破类域。突破类域有两种方法,类名::静态成员或者对象.静态成员。静态成员不走初始化列表,因为他不属于某个对象,也不能进行声明位置的初始化,但需要在声明处进行声明。
八.内部类
内部类就是在一个类里面再封装一个类,当这两个类之间有着紧密联系或者a类是为b类专门实现的时候可以使用(内部类默认是外部类的友元类)。内部类是一个独立的类,与定义在全局相比,它只受外部类类域限制和访问限定符限制,所以外部类的对象中不包含内部类。
九.匿名对象
用类型(实参)定义出来的对象叫做匿名对象,匿名对象的生命周期只在当前一行。
若我们想延长匿名对象的生命周期,可以使用const a& b = A()的方法来延长生命。
匿名对象的用法主要服务于,当函数调用时给类类型给缺省值,无法直接给出时。
十.对象拷贝时的编译器优化
编译器为了提高程序的运行效率,在不影响正确性的情况下会尽可能减少一些传参过程中可以省略的拷贝。
在不优化的版本下,f2中的aa传值过程中,会预先开辟一块空间接收return aa的值,再将值进行一次拷贝构造给回aa2中。在vs2019的debug版本下,则会省略掉中间值的拷贝构造,而是直接将return的返回值拷贝构造给aa2。而在release版本下,则会更加的激进,return返回的aa直接就是aa2的引用返回值,直接省略了空间的开辟。