再探构造函数
构造函数体赋值与初始化列表
class A
{
public:A(int n, int ret):_ret(ret),_n(1){cout << _ret << endl << _n << endl;}private:const int _n ;int& _ret;
};
通过上图代码可以看到之前我们使用构造函数的时候都是在括号里进行赋值初始化,而初始化列表是在括号之前就进行初始化。
初始化列表的特点
1.每个成员变量在初始化列表中最多只能出现一次,如果重复出现编译器则会报错,语法理解上初始化列表可以认为是每个成员变量定义初始化的地⽅。
2.对于引⽤成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进⾏初始化,否则会编译报错。
从上图可以看到,当成员变量为自定义,const,引用类型时,在构造函数体内赋值程序会进行报错,只有在初始化列表定义才不会。
自定义类型:成员变量必须在初始化列表中初始化,否则在构造函数体内赋值将导致错误,因为成员变量的生命周期还未开始。
const类型:const成员变量也必须在初始化列表中初始化,因为一旦被初始化,其值不能改变。
引用类型:引用成员变量必须在初始化列表中初始化,因为引用必须绑定到一个有效的对象或变量。
3.对于没有显⽰在初始化列表初始化的⾃定义类型成员会调⽤这个成员类型的默认构造函数,如果没有默认构造会编译错误。
通过上图可以看到,在初始化列表的时候并没有显示初始化C类的成员变量。那么在初始化完成后发现,C类的成员变量c也进行了初始化,可以看出就算不显示写自定义类型的初始化,编译器也会自动调用该类的初始化构造。
首先先要捋清什么是默认构造,无参,全缺省以及编译器自动生成的构造函数都叫做默认构造。
那么上图代码将C类的构造函数改成有参且需要传值的构造函数,如果不在A类的初始化列表进行显示传参初始化C类编译器则会进行报错。
4.初始化列表中按照成员变量在类中声明顺序进⾏初始化,跟成员在初始化列表出现的的先后顺序⽆关。建议声明顺序和初始化列表顺序保持⼀致。
观察上图代码,在声明A类的成员变量时,先声明_a2,再声明_a1。而编译器会根据声明的顺序进行初始化,可以看到再初始化列表时,会先对_a2进行初始化,而_a2里传的参数为_a1但_a1此时并没有先进行初始化所以_a1为随机值。再对_a1进行初始化。
初始化列表的缺陷:
那么有了初始化列表是不是就可以代替函数体内赋值呢?初始化列表完成了构造函数90%的操作,还要10%是初始化列表完成不了的。
上图代码创建了一个Stack类,在初始化列表时给a分配空间,但你怎么知道这个空间有没有分配成功呢?显然初始化列表办不到这件事,得要在函数体内进行判断。所以初始化列表与函数体内赋值是相辅相成的。
类型转换
static成员
static成员概念:
在 C++ 中,static成员是一种特殊的成员变量或成员函数,它们与类的实例无关,而是与整个类相关。static成员在类的所有实例之间共享,且只存在一份。
static成员特点:
1.⽤static修饰的成员变量,称之为静态成员变量,静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,属于所有对象共用的变量,不⾛构造函数初始化列表,只能再类外面进行初始化。
2.成员变量与静态成员变量的区别:
成员变量:属于每个一个类对象,存储再对象里面。
静态成员变量:属于类,属于类的每个对象共享,存储再静态区(像公交车)
3.⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
所以静态成员函数中可以访问其他的静态成员,但是不能访问⾮静态的,没有this指针,反而⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数,因为静态成员函数与变量是共享的。
4.突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。
通过静态成员函数以及静态成员变量可以很好的算出有多少个对象正在使用。
如上图,类A包含一个静态成员变量_scout,用于追踪实例数量。构造函数和拷贝构造函数增加_scout,析构函数减少_scout(表示对象销毁),
GetCount()是一个静态成员函数,用于取 _scout的值。静态成员变量 _scout在类外初始化为 0。
想要输出 _scout可以使用GetCount()成员函数,_scout属于私有不能直接访问,因为是静态成员函数突破类域就可以直接访问静态成员,所以使用A::GetCount()就可以直接获取到_scout的值。
当第一次输出时,只有一个全局对象aa0,输出1。当第二次输出时,此时此时存在三个对象,输出3,而当最后一次输出缺还是输出3,原因是Func()函数的对象aa2是一个静态对象,只会在第一次的时候进行创建,只有在程序结束的时候才会销毁。
有元
有元的概念:
有元函数:
上图代码创建了一个日期类,再日期类外面创建了一个fun函数,在主函数中建立了一个日期类的对象d,并调用fun函数输出d的成员变量值。
我们可以知道,当把成员变量限制成私有(private)或者保护(protected)时,是不允许从类外面进行访问的。而有元(friend)关键字则突破了封装限制,通过声明外部友元函数,就可访问类的私有和保护成员。这里要注意友元函数仅仅是⼀种声明,他不属于类的成员函数。
有元函数声明就像朋友一样,声明这个函数是我的朋友,我允许他来我家玩,但并不是说他来我家玩就变成我家的人了,这里要进行区分。
有元类:
1.友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。
2.有元破坏了类的封装性质,所以不宜多⽤。
3.友元类的关系是单向的,不具有交换性,⽐如A类是B类的友元,但是B类不是A类的友元,也就是说b类可以访问a类的私有和保护成员,但a不访问b类的私有以及包含成员。
有元类与有元函数类似,如上图在A类里有元声明B类,那么就可以在B类里直接访问A类私有的成员变量。
内部类
内部类的概念:
内部类的特点:
1.内部类定义在外部类的里面,不占用外部类的空间。
上图代码创建了A类与B类。而B类又被包含在A类里面,所以B类是A类的内部类。
A类的成员变量k为静态变量,不算在A类的大小里。而通过sizeof计算出A类的大小为4,只算入了一个h成员变量。所以可以得知b类定义在a类里面,但并不占a类的空间。
2.内部类受访问限定符的限制 。
从上图代码可以看到,当讲内部类的访问限定符设置为private的时,此时从类外面则使用不了内部类。
3.内部类是外部类天然的有元类
从上图代码可以看到,在B类的成员函数fun里可以直接访问A类的成员变量,并没有被private限制,所以可以证明内部类是外部类天然的有元类。
匿名对象
1.在C++中,匿名对象是指没有显式命名的对象。这种对象通常在表达式中创建,生命周期通常限于表达式的结束。它们可以用于临时操作,而无需声明单独的变量。
2.匿名对象⽣命周期只在当前一行,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名对象。
从上图代码可以看到,当创建完A(1)对象后程序一往下指向,A(1)对象就进行了析构,而有名对象A aa(0)在main函数结束时才进行析构。
通过上图可以看到,当直接引用一个匿名对象时编译发生报错。那么可以得知匿名对象具有常性(类似被const修饰)。所以想要引用匿名对象必须加上const限定。
通过上图代码输出Sum_Solution,可以观察到,当使用const引用匿名对象的时,本应在执行最后一句输出Sum_Solution之前就应该析构的匿名对象,却等到main函数结束时才进行了析构,使用const引用匿名对象演唱了,匿名对象的生命周期。
匿名对象总结:
有名对象-->生命周期在当前函数局部域
匿名对象-->生命周期在当前行。
const引用匿名对象-->生命周期在当前函数局部域
对象拷贝时的编译器优化
现代编译器会为了尽可能提⾼程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传返回值的过程中可以省略的拷贝。
通过上图代码可以看到,如果是先进行初始化对象a,再进行对a的赋值,会产生很多步骤。
1:创建a对象进行默认构造,2:通过F3函数创建aa对象进行默认构造,3:aa对象的参数进行返回(这里编译器对拷贝构造进行优化所以看不到),4:通过赋值重载将临时变量的值赋值给对象a,后先析构临时变量,再析构对象a。
以上的两种方法虽然结果一样,但效率完全不一样。以后尽力写成第一种的形式会更好。
————那么本篇文章到这里就结束了,感谢各位大佬的观看,咱们下期再见