【C++笔记】C++之类与对象(下)
- 1、再看构造函数
- 1.1、构造函数的初始化列表
- 1.2、C++支持单参数的构造函数的隐式类型转换
- 1.3、匿名对象
- 2、Static成员
- 2.1、为什么要有静态成员变量?
- 2.2、一个类的静态成员变量属于这个类的所有对象
- 2.3、静态成员函数
- 3、友元
- 3.1、友元函数
- 3.2、友元类
- 4、内部类
- 4.1、内部类与外部类是互相独立的
- 4.1、内部类默认是外部类的友元
1、再看构造函数
1.1、构造函数的初始化列表
为什么要引入初始化列表呢?
我们之前知道,在编译器自动生成的构造函数中,对于自定义类型会去调用其默认构造函数,但要是这个自定义类型没有可用的默认构造函数,就会出问题了,例如:
这时候先要解决这个问题,就要用到初始化列表了,因为在初始化列表中就可以对这个b进行定义。
初始化列表的语法如下图所示:
也就是初始化列表是在构造函数的大括号之前,以冒号开始以逗号分隔每个成员的值用括号定义。
初始化列表是每个成员定义的地方:
初始化列表是每个成员定义的地方,所以有了初始化列表之后,在就如构造函数的函数体之前就会去走初始化列表,然后再去走函数体。
我们通过调试就可以看出:
事实上,不管你写不写每个成员都要走初始化列表,这个我们可以通过为成员加上缺省值来验证,因为成员的缺省值就是给初始化列表用的:
可以看到,虽然我们并没有在初始化列表里面写上_month的定义,但是当走到_day的定义的时候,_month就已经定义成了我们所给的缺省值了。这就说明了_month其实也走了初始化列表。
其实在走完_year时,编译器是会自动跳到成员列表处去定义_month的:
正是因为不管怎样,每个成员都会走初始化列表,所以我们以后可以用初始化列表就尽量要用初始化列表。
初始化列表初始化的顺序和声明的顺序相同:
这个可以通过一个“反常”现象进行验证:
对于这个程序我们一般都会理解成,输出两个1,但是结果却不是这样。这其实就恰恰说明了初始化列表初始化的顺序是和声明的顺序是一样的,因为是a2先声明,所以初始化列表会先走a2的定义,但a1的只还是随机值。
所以就出现了以上的结果,只要我们改一下它们在初始化列表中的顺序,这程序就正常了:
所以为了避免出现各种问题,我们一般都要保证初始化列表初始化的顺序和声明的顺序一样。
而在吧编译器自动生成的构造函数中,其实是在初始化列表中对内置类型不作处理(假如没有给缺省参数),对于自定义类型则去调用其默认构造函数:
1.2、C++支持单参数的构造函数的隐式类型转换
C++之所以支持这个语法,主要还是能个好的应付自定义类型的一些场景,还是拿我们的栈来举例子,对于下面这个类,我们在定义对象的时候其实有两种写法:
这里的本质其实就是隐式类型转换,编译器会先用2去调用A的构造函数去生成一个临时对象,再用这个对象去拷贝构造a2。
但是编译器觉得先构造在拷贝构造太麻烦了,于是编译器就再次进行了优化,将拷贝构造省去,用2直接构造a2。
从下面的结果中我们也可以看到编译器只调用了构造函数:
1.3、匿名对象
在C语言中我们见过匿名结构体,在C++中也有一个匿名对象,即我们在定义对象的时候可以不给名字:
匿名对象的生命周期只在一行,我们可以通过加上析构函数来验证这一点:
我们会发现程序在运行下一行指令的时候,就会先去调用析构函数。
其实C++支持这个语法还是为了代码简便,例如我们现在有一个函数的参数是一个自定义类型,如果不支持匿名对象我们每次都要先定义一个有名对象再去传:
但是有了匿名对象之后,我们就可以直接传一个匿名对象了:
2、Static成员
2.1、为什么要有静态成员变量?
有时候我们可能会有这样的需求:统计一个类总共定义了对少个对象。
我们很容易会想到定义一个全局变量,然后再在构造函数和拷贝构造中让这个全局变量自加1:
但这个做法的缺点就在于全局变量的作用域太大了,很容易就会被修改,只要被外人一修改,这统计的就不对了。
所以为了解决这个问题,C++引入了静态成员变量:
2.2、一个类的静态成员变量属于这个类的所有对象
首先要说明的是静态成员变量并不在类里面,这一点可以通过计算类的大小来验证:
可以看到,A的大小为4,也就是说只计算了成员_a的大小,并没有计算N的大小。
实际上静态成员变量是存在于静态区的。
静态成员变量不能给缺省值,静态成员变量需要在类外边定义:
因为静态成员变量属于所有类,所以如果它是共有的,他就可以直接使用类作用限定符来访问,而其他成员变量就不可以:
2.3、静态成员函数
熟了静态成员变量,C++还有一个静态成员函数
静态成员函数没有this指针,所以静态成员函数不能访问非静态成员:
但是它可以自由的访问静态成员变量:
而且如上图所示,静态成员变量并没有this指针,所以在调用的时候也就不需要先有对象,直接是用来访问限定符突破类域即可。
3、友元
虽然在类里边我们可以随便访问成员变量而不受访问限定符的限制,但有些函数我们会发现将它写成成员函数会很奇怪,例如对日期我们需要使用运算符重载重载一个流插入运算符:
我们这好像写的没问题,但当我们去调用的时候却会发现问题了:
这里提示说未接收到参数,这是因为我们的顺序反了,我们知道非静态成员函数都会有一个隐藏的this指针,并且永远在第一位:
所以我们如果要调用,就需要这样:
这样就简直太奇怪了,而且使用起来也是真不习惯。
所以为了解决这个问题,我们就需要引入友元了。
3.1、友元函数
上面的这个问题主要是参数的顺序不对,所以我们可以考虑将其写成全局的,这样就可以随意安排参数的顺序了:
但是当我们在调用的时候却还是会出现问题:
因为这些成员都是私有的,我们不能够直接访问。
其实我们可以用一个简单的方法来解决,就是对应每一个成员都写一个共有的函数来返回对应成员的值:
但当成员有很多个的时候这种方法也不是很简便,所以我们就可以用到友元声明:
因为这里的友元函数仅仅只是个声明,所以写在任何地方都是可以的。
这样这个函数就可以直接访问这些成员了:
3.2、友元类
有时候我们需要在一个类里边定义另一个类的对象:
但是烦心的是我们并不能直接访问其成员,因为是私有的:
这时候我们就可以将Date类声明成A类的“友元类”,没错,友元不仅可以声明友元函数还可以声明友元类:
这样在Date类中就可以随意的访问A类中的成员了:
这其实和友元函数的作用是相同的。
但有一点需要注意:“友元”并不是相互的,就像这里只有Date类是A的友元类,但A并不是Date类的友元类,也就是说在A类中不可以直接访问Date类中的成员:
4、内部类
这就像结构体可以嵌套定义一样,类也可以嵌套定义:
4.1、内部类与外部类是互相独立的
“内部类”虽然名称叫做内部类,但事实上它本身却并不包含在外部类里,这一点我们可以通过计算类的大小来验证:
很明显这里A类的大小仅为4,如果要包含内部类的话至少也的是8才对。
但内部类受外部类的访问限定符的限制,比如内部类若是公有的我们就可以直接通过A的类作用限定符来定义对象:
如若是私有,就不能了:
4.1、内部类默认是外部类的友元
内部类的优势就是内部类默认是外部类的友元类,也就是说内部类可以直接访问到外部类的成员:
但上面也说过了,友元并不是相互的,所以外部类并不能直接访问内部类的成员: