1.深入了解构造函数
1.1初始化列表
引入:我们首先要知道,在类中我们是声明变量,在实例化的时候才是开空间,在调用构造函数的时候又分两段,一段是定义(所有的成员变量进行定义),一段是赋值(构造函数体内,也就是{}包含的地方)。
而我们所说的初始化列表就是可以实现在构造函数的定义区进行赋值
补充:说开空间也不是很准确。准确来说,我们在开空间的时候是编译器预估好函数大概需要占用的空间去开辟,然后我们的对象再从已经开了的空间中占用一定的空间,如果不够我们才会接着开。
(1)格式:初始化列表的使⽤⽅式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。而这种方式也叫显式实现初始化列表
(2)重点:每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义的地⽅。
这也是我在引入的时候说的,初始化列表的位置就是所有成员变量集体定义的地方,若用了初始化列表就相当于可以顺便初始化了。
(3)引⽤成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进⾏初始化,否则会编译报错。
1.引用:因为引用必须在定义的时候初始化(语法要求),所以我们必须在初始化列表这个所有变量定义的地方对它初始化。
就像这里,我们没有在初始化列表给引用c初始化,所以报错了。
我们没实现引用的初始化列表的时候相当于下面代码所示
也就是说没有实现初始化列表的时候,引用他也会自动在这里定义,但是因为他自动的定义没有指定引用对象,所以会报错。
2.const成员变量:和引用类似,const成员变量也需要在定义的时候初始化。这是因为语法规定const变量的值只有一次改变的机会,就是定义的时候。那如果我们在定义的时候没有给它值,他就没有意义了,所以报错。
这里我们的const变量_a没有在初始化列表实现给初始值,所以报错
3.没有默认构造的类类型变量:没有默认构造的类类型也就相当于它的构造函数必须传参数。如果不实现类类型初始化列表,由于编译器自动实现的定义不会传参数,所以调用构造失败,就会报错
这里我们就是因为没有实现初始化列表传参去调用构造函数,所以报错了
(4)C++11⽀持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显式在初始化列表初始化的成员使⽤的。也叫隐式实现初始化列表
格式就是在声明后面利用赋值符号给值(只能给一个缺省值)
其实就和缺省参数一个道理,我们在这里给了缺省值,相当于编译器就能在定义的时候回来获取这个值去进行初始化。
(5)尽量使⽤初始化列表初始化,因为那些你不在初始化列表初始化的成员也会⾛初始化列表(因为所有成员变量都会在初始化列表的地方定义)。不过要注意要么我们全部显式实现,要么我们全部隐式实现,不要混着用。
(6)初始化列表中按照成员变量在类中声明顺序进⾏初始化,跟成员在初始化列表出现的的先后顺序⽆关。建议声明顺序和初始化列表顺序保持⼀致这里我们的预期是_a = 1,然后_a又给_c赋值,所以_c = 1。那么最后输出的答案应该是11。这里为什么_c是随机值?因为我们初始化的顺序并不是按照初始化列表的顺序,而是按照声明的顺序。声明的顺序=开空间顺序=初始化顺序所以我们先给_c初始化,_c=_a,可是_a此时还没有初始化,还是个随机值,所以_a将随机值就给了_c。然后我们再给_a初始化,由于我们传了一个参数1,所以_a的值就是1.
(7)总结:
我们总共有三条初始化路线:
1.显式实现初始化列表:就按照显式的来初始化
2.隐式实现初始化列表:如果写了显式就按照显式,没有就按照隐式
3.没实现初始化列表:按照我们写的构造函数体走
总共有两大种属性:
1.必须用初始化列表初始化的属性:const修饰的变量,引用,需要显式传参的自定义类型
2.可以不用初始化列表初始化的属性:其他的属性
2.类型转换
(1)C++⽀持内置类型隐式类型转换为类类型对象,但是需要有相关内置类型为参数的构造函数。
其实就是需要我们去写一个构造函数,用内置类型构造出一个自定义类型的临时对象,然后通过拷贝构造函数,把临时对象的值给新创建的对象初始化。
(2)类类型的对象之间也可以隐式转换,需要相应的构造函数支持
(3)传递多参数的类型转换
我们多参数的传递方式就是使用花括号去包含参数。
注意:构造函数前⾯加explicit就不再⽀持隐式类型转换。这时就说明这个构造不支持隐式类型转换了
(4)隐式类型转换的实际过程
首先,内置类型通过构造函数创建了一个新的临时对象,然后这个临时对象通过拷贝构造函数给要类型转换成的对象初始化
3.友元函数
友元提供了⼀种突破类访问限定符封装的⽅式(也就是可以访问类中的私有成员,但不是类的成员函数。)
友元分为:友元函数和友元类
(1)格式:在函数声明或者类声明的前⾯加friend,并且把友元声明放到⼀个类的⾥⾯。
这里我们声明了一个友元类和一个友元函数,他们的函数/类声明前面加了个friend。并且都在类a里面
(2)⼀个函数可以是多个类的友元函数。也就类似于,一个人可以获得不同的国家的签证,然后去进入这些国家。一个函数可以是多个类的友元函数,然后访问他们的私有成员(3)友元类的关系是单向的,友元类关系不能传递。你获得了他的身份令牌,不代表他获得了你的身份令牌(单向)你获得的身份令牌不能传递给其他人(不能传递)友元会增加耦合度,破坏了封装,所以友元不宜多⽤。
总结:所以与其说友元函数是当好朋友,不如说是获取了身份令牌更合适,有了这个类给你认证了身份令牌,你就可以访问他的私有成员了。
但是他没在你的类里面声明为友元函数,也就是没有获取你的身份令牌,所以无法访问你的私有成员
4.static成员
(1)⽤static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进⾏初始化。
(2)突破类域就可以访问静态成员,可以通过类名::静态成员(只有静态类型可以这样用) 或者 对象.静态成员(和访问类中的普通属性一样的访问方法) 来访问静态成员变量和静态成员函数。
疑问:为什么这里我们无法访问static修饰的 _c呢?
这是因为静态成员也是类的成员,受public、protected、private 访问限定符的限制。如果我们把它放到public部分就可以访问了
接下来我们看看静态变量特有的访问方式
这种类名::的访问方式是static类型的变量特有的,类中的其他成员变量都不能这样访问
这是因为其他成员变量是属于每个不同的对象的,所以一定要利用对应的对象来调用。
而我们的static类型的成员变量是一个类中所有对象公用的,并不属于某个对象,不存放在对象中,存放在静态区。所以可以用类名直接调用
(3)静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不⾛构造函数初始化列表。
这里报错就是因为static变量不会走初始化列表,所以给初始化列表传值的缺省值就不能用在static变量上
⽤static修饰的成员函数,称之为静态成员函数不过和其他成员函数不同的是,静态成员函数没有this指针。这是因为静态成员函数和对象的实例化无关,他不属于任何一个对象,所以不需要默认有一个this指针
(1)也正是因为没有默认一个this指针,所以静态成员函数不能访问非静态的成员,之所以可以访问静态的,是因为静态的不属于任何一个对象,可以脱离this去访问。
这里我们看到可以直接访问静态成员变量(2)⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数。因为静态成员函数和变量是该类的所有对象共用的这里我们就是普通的成员函数,可以直接调用静态成员函数或者变量。
接下来我们看看一道题
1.限制了循环语句:所以我们不能用循环的方法来做
2.限制了判断语句:所以我们不能用递归做
3.限制了乘除法:所以不能用公式来做
现在我们只剩一种方法:利用n个数据的构造函数,每次进入构造函数就进行一次累加运算.
要实现这个有个前提是我们要有静态成员变量
第一步:写一个专门负责运算的类(Sum类),并设置其成员变量。
其中一个成员变量是sum,负责保存总和。
还有一个i,负责根据调用次数改变自己的值。
由于n>0,所以我们设置i为1
且因为他们生命周期要求是整个程序的生命周期,所以设置成静态成员变量。
第二步:在Sum构造函数中写运算逻辑
因为n=1的时候我们的总和是1,所以我们先进行运算,然后再让i++
第三步:写返回Sum的函数
之所以写成静态的,是因为我们的返回函数和具体的某个对象没有关系,我们这里只需要返回一次全体的就行。
第四步:调用
先写一个变长数组,实现n次调用构造函数(自定义类型的数组的每个元素都会依次调用构造函数初始化,如果要实现初始化也可以利用静态变量)。然后利用GetSum函数返回总和的值
构造函数的调用顺序为:c a b d
析构函数的调用顺序为; b a d c
这是因为a和b都是局部变量,生命周期局限于局部。而c是全局变量,d是静态变量,他们的生命周期都是整个程序的生命周期。所以a和b一定比c和d先析构。
又因为b比a后定义,所以b比a先析构
又因为d比c后定义,所以d比c先析构
5.匿名对象
(1)⽤ 类型(实参) 定义出来的对象叫做匿名对象,之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象
我们看到匿名对象把对象名给省略掉了,不过如果不传参是需要写个空的小括号上去的
(2)匿名对象⽣命周期只在当前⼀⾏,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名对象。
通过调试我们看到,匿名对象的构造函数调用完,他那一行一结束析构函数就调用了,说明它的生命周期确实只有一行。
思考:那我们有没有办法延长他的生命周期呢?
只要用加上const修饰的引用就可以
我们看到这里虽然已经走完那一行代码了,但是我们没有调用析构函数去销毁匿名对象,这说明它的生命周期不再是一行了。而是和引用的生命周期一致。
(3)匿名对象的两个应用场景
1.做缺省参数
这里我们如果要给对象缺省参数,有两个方法
方法一:传一个内置类型,然后进行类型转换。
但是这个方法比较麻烦,因为我们还要单独写一个构造函数去帮他类型转换,并且看起来并不直观
方法二:给他一个匿名对象,看起来就更加直观了
2.临时调用成员函数时可以减少一行代码
不过这个调用的函数是有要求的,他最好是临时需要使用一次就行的那种函数,比如输入输出之类的。
6.内部类
(1)如果⼀个类定义在另⼀个类的内部,这个类就叫做内部类。内部类是⼀个独⽴的类,跟定义在全局相⽐,他只是受外部类类域限制和访问限定符限制,而外部类定义的对象中不包含内部类。
这里我们的inner类就是test类的内部类。注意内部类和外部类之间不是君臣关系,内部类并不属于外部类,只是相当于内部类的类域处于外部类的类域之中而已
1.受外部类类域限制
我们需要在inner类前面使用域作用符去突破外部类类域的限制。(放在public才能实例化)
2.访问限定符限制
我现在把inner类放在了private部分,接下来我们看看是否还能初始化
报错了,这是因为inner类被test类中private给修饰了,也就是我们无法使用这个类。
有一种方法可以让我们在外部类中访问内部类的成员函数
方法:在外部类成员函数中实例化内部类对象
相当于在main函数中实例化一个类,然后访问这个对象的public函数
反思:实际上我们在外部类的成员函数进行内部类的实例化这个过程,类似于我们在main函数中实例化一个普通的类的过程。
(2)内部类默认是外部类的友元类。
也就是说内部类的成员函数可以访问外部类的私有成员。
(3)内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地⽅都⽤不了。比如我们前面的sum类,就是写出来给solution用的,那我们就可以把sum写成solution的内部类