1. 初始化列表
我们回忆上节写的MyQueue类,其中有两个栈类和一个int类型,栈类因为其特殊性,要开空间,所以我们必须手搓Stack类的构造函数。但是正常来说MyQueue自动生成的构造函数会调用自定义类型的默认构造函数,也就是说我们不必再手搓MyQueue类的构造函数了,编译器自动生成的就好用。
但是此时问题来了,默认构造函数简单点理解就是不用传参就能调用的构造函数,那么如果我们手搓的Stack类没有默认构造函数,那么MyQueue就不能自动生成构造函数了,因为它没得调了,就像这样。
光标圈出来的那一行中,我们没有给构造函数搞缺省参数,所以它必须传参才能调用,也就是说此时Stack类没有了默认构造函数,那么现在编译器是处在报错的状态,那我们如何通过写MyQueue的构造函数来解决这一问题。
此时就要说到初始化列表的概念了,初始化列表本质上可以理解成每个对象中成员定义的地方,所有成员你可以在初始化列表初始化,也可以在函数体内部初始化。
具体写法就是:以一个冒号开始,接着是一个以逗号分开的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。
ps: 这里面size其实应该前面有个下划线的,但是我忘记写了,大家凑活着看吧
_pushst 和 _popst 的初始化必须要在函数体外面写,因为他俩进了函数体就没法初始化了呀,不过size初始化可以写到函数体里头,当写在函数体里头的时候有必须要用赋值的方案进行初始化了。
虽说类中的所有成员都可以在初始化列表和函数体内初始化,但是有三类成员必须在初始化列表中初始化:1. 引用 2. const成员 3. 没有默认构造的自定义类型成员
我们知道如果在定义成员变量的时候赋值,我们管这个行为叫给成员变量缺省值,这个缺省值的含义就是为了给初始化列表的缺省值。因为初始化列表不管你有没有显式写,编译器都会自动给每个成员都走一遍,自定义类型的成员会调用其默认构造函数,内置类型就不做操作(除非有缺省值,那么编译器的行为相当于自动在括号中写那个缺省值)。这就解释了上面Stack没有默认构造函数MyQueue就不能自动生成构造函数。
既然是缺省值,我们在显式写初始化列表的时候就可以更改,同时默认构造函数那个赋的不也是缺省值嘛,因此也可以更改。
这里强调一下,缺省值是可以给表达式的,比如我们可以给一个指针成员一个malloc缺省值。
那么函数体中的语句的优先顺序比初始化列表还要高,也就是说初始化列表中已经初始化好的成员我们还可以在函数体中进一步修改。具体优先顺序就是: 缺省值 < 初始化列表 < 函数体
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,初始化顺序与其在初始化列表中的先后次序无关。我们看下面这段代码的输出就懂了
因为是先初始化的_a2所以输出的不是两个1,而是1和随机值。
最后在实践中,尽可能使用初始化列表进行初始化,不方便再用函数体初始化。
2. 对象赋值时的隐式类型转换
我们看下面这一段代码
我们知道aa1通过构造函数赋值,aa2通过拷贝构造赋值,那aa3是怎么回事?raa又是怎么回事?
aa3这里其实产生了隐式类型转换,由内置类型转换为了自定义类型。首先3先构造一个A类型的临时对象,再用这个临时对象拷贝构造aa3。所以说单参数构造函数可以支持这种直接赋内置类型的操作,它会产生隐式类型转换。现在我们在看打印出来的结果,当 3 赋给 aa3 的时候只调用了构造函数,这是因为编译器遇到 连续构造+拷贝构造 的组合时会优化成直接构造.
raa的本意是取引用,首先 3 先隐式转换出来一个临时对象,为了权限不放大,所以要给const,之后raa引用3转换出来的这个新的临时对象。所以只有 3 隐式转换的时候出现了构造函数,没有拷贝构造出现。
这个隐式类型转换的意义就是在于能够简化我们的代码量,有时还能缩减运行效率,直接赋一个内置变量,让编译器自己去类型转换。比如说我们想往栈里头压这个A类对象:
我们可以选择先构造一个对象,然后再给到压栈。或者直接不写构造了,直接压1,让编译器自己类型转换去,这不就方便多了。
上面是单参数的隐式类型转换,那多参数的写法是这样的:
给值的时候用花括号括起来,写不写等于号都行,但是推荐写上等于号,这样和普通括号有点区别。
2.1 explicit关键字
如果不想让构造能发生隐式类型转换,就在构造函数前面加上explicit关键字就行,单参数和多参数通用的。
aaa1因为隐式类型转换被拒所以无法构造了,那个aaa2没报错是因为编译器在这里把花括号当括号处理了。我们看aaa掉成员函数时的情况,传对象的时候就通过了,但是尝试传内置类型的时候隐式类型转换被拒绝了,因此编译报错。
3. static 静态成员
声明为static的类的成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员函数一定要在类外进行初始化。
静态成员有如下特性:
1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。
2. 静态成员变量必须定义在类外定义,定义时不添加static关键字,类中只是声明
3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员访问
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5. 静态成员也是类的成员,受 public,protected,private 访问限定符的限制
我们看上面一段代码。
_scount是一个静态成员变量,这里我将它开放成了public,否则无法在类外访问。它存储于静态区,并属于所有的A类和其实例(对象),也就是说它可以直接通过类 A::_scount 访问,同时从任何地方访问(通过类访问,或通过对象访问)并改变其值的操作,都会直接改变其在静态区存储的值,也就是说任何依次改变都会影响A类以及其下所有实例中的_scount值,这就是所谓的静态成员变量属于所有类对象。同时因为它存储于静态区,所以在实例化类的时候静态成员变量同成员函数一样,不占用对象的空间。
_scount在类中声明的时候不能给缺省值,给了就报错,因为给缺省值是为了在初始化列表中初始化,但是_scount不走初始化列表,它是静态变量,因此必须在类外定义。
那么这个静态成员变量可以用来干嘛呢,举个例子,它可以实时检测该类存在几个实例化对象,因为创建对象无非两种方法,构造和拷贝构造,因此我显式写每执行依次这两种构造就++_scount ,对象销毁析构的时候再 --_scount。最终打印出来的效果还是挺明显的。
我们再看下一段代码,讲解一下静态成员函数
这里我将_scount设置为private状态了,那么此时就不能再类外访问它了,也就是说无法这样 A::_scount 访问了,当然这也比较符合我们写成员变量的习惯,就是把所有成员变量都设置成private状态加以保护。那此时我们就要通过静态成员函数访问。static修饰变量和修饰函数的意义完全不一样,修饰变量时改变了变量的声明周期,修饰函数时改变了它的链接属性。那么静态成员函数的特性就是没有this指针,也就是说静态成员函数只能访问静态成员变量。
那么使用的时候跟静态成员函数很像,既可以通过类访问,也可以通过对象访问得到_scount的值
4. 友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为友元函数和友元类:
4.1 友元函数
在上节重载流插入和流提取的时候,我们为了保证传参顺序符合习惯,因此将流插入和流提取的重载函数写在了类外,从而引发了无法访问private成员变量,当时我们给出的解决方案是写Get函数,取消private状态肯定是不可取的,要不我们还封装个什么。
那么本节我们可以通过友元函数的设置来打破针对某一个函数的封装,其实上节最后实现日期类的时候我就已经搞友元了不知道大家注意到了没有。
这里我把日期类简化了一下,就保留了流插入和流提取的内容,在类刚开始那两行用friend修饰的代码就是友元函数的修饰,用一个friend关键字修饰一个外部函数的声明,就能使这个外部函数访问类内部的private信息了。
4.2 友元类
跟友元函数类似,在一个类中声明另一个类是友元的,那么就可以在另一个类中访问这个类的私有信息。形象一点来说就是:你是我的朋友,那么你就可以知道我的一些私有信息。
B中可以访问A中的私有成员,但是A不能访问B的成员。
最后还要说一下友元不能传递,就比如C是B的友元,B是A的友元,但是C不是A的友元。也就是说,A不能访问C的私有成员。
5. 内部类
如果一个类定义在另一个类的内部,这个内部的类就叫做另一个类的内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的私有成员。外部类对内部类没有任何优越的访问权限。
内部类天生是外部类的友元类,内部类可以通过外部类的对象参数来访问外部类中的所有成员,但是外部类不是内部类的友元
说白了就是外部类和内部类就是两个几乎完全平行的两个类,唯一不同的就是内部类要受到类域的限制,使用时必须在外部类类域下使用,同时内部类天生是外部类的友元。
6. 匿名对象
匿名对象就是在实例化的时候不给名字,它的生命周期只在当前这一行,即用即销毁
7. 拷贝对象时的一些编译器优化
因为我的编译器是vs2022优化开的太大了,没办法演示,就简单描述一下了。优化的话如果在Debug版本下,一定是在一行代码中进行优化的。但是如果开成release版本就有可能产生跨行优化。我下面讲的都是在Debug版本下的编译器优化方案。
隐式类型:连续构造+拷贝构造 -> 优化成直接构造
传值返回:拷贝构造+拷贝构造 -> 优化为一个拷贝构造,这里有一个值得注意的点,就是拷贝构造+赋值运算重载是无法优化的