再探构造函数
构造函数体赋值与初始化列表
其实之前我们实现构造函数时,初始化成员变量主要使⽤函数体内赋值,构造函数初始化还有⼀种⽅式,就是初始化列表,C++规定初始化列表的使⽤⽅式是以 ⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。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分配空间,但你怎么知道这个空间有没有分配成功呢?显然初始化列表办不到这件事,得要在函数体内进行判断。所以初始化列表与函数体内赋值是相辅相成的。
类型转换
1. C++⽀持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。 首先我们知道, 当两个类型不同的变量进行赋值会发生类型转换,如上图将一个double的变量赋值给int类型的变量,而b又不可能发生变化,所以编译器会把b先给一个临时变量,由临时变量进行转换成int类型再赋值给i。 可以看到当我们将一个常量赋值给C类的对象,编译器会先创建一个C类的临时对象,而创建对象会进行默认构造,将20值赋值给临时对象的_a,创建完后再将临时对象拷贝构造给对象c。但现在的编译器会对这种操作进行优化-->优化用2直接构造。 那么为了证实临时变量的存在,将原本C c = 20改成C &c = 20。可以看到编译发生了报错,因为临时对象具有常性(类似被const修饰),所以直接对临时对象进行引用,此时权限被放大,只有加上const C &c编译才能通过。 2. 构造函数前⾯加explicit就不再⽀持隐式类型转换。
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是一个静态对象,只会在第一次的时候进行创建,只有在程序结束的时候才会销毁。
有元
有元的概念:
友元提供了⼀种突破类访问限定符封装的⽅式,友元分为:友元函数和友元类,在函数声明或者类声明的前⾯加friend,并且把友元声明放到⼀个类的⾥⾯。有元函数:
上图代码创建了一个日期类,再日期类外面创建了一个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引用匿名对象-->生命周期在当前函数局部域
对象拷贝时的编译器优化
现代编译器会为了尽可能提⾼程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传返回值的过程中可以省略的拷贝。
如何优化C++标准并没有严格规定,各个编译器会根据情况⾃⾏处理。当前主流的相对新⼀点的编译器对于连续⼀个表达式步骤中的连续拷⻉会进⾏合并优化,有些更新更"激进"的编译器还会进行跨行跨表达式的合并优化。 对于上图代码,按照正常逻辑走, 先调用F3函数,在F3函数里会创建了aa对象,而因为F3返回的是参数,所以不会直接返回对象aa,而是会拷贝构造一个临时对象(具有常性),由拷贝构造进行返回,接着通过临时变量再对a对象进行拷贝构造。所以这里按正常来说会有两次拷贝构造。 当程序真正运行起来后,并没有和我们所分析的那样去走,编译器直接将拷贝构造通过F3函数对a对象进行了构造,将构造拷贝给优化了。
通过上图代码可以看到,如果是先进行初始化对象a,再进行对a的赋值,会产生很多步骤。
1:创建a对象进行默认构造,2:通过F3函数创建aa对象进行默认构造,3:aa对象的参数进行返回(这里编译器对拷贝构造进行优化所以看不到),4:通过赋值重载将临时变量的值赋值给对象a,后先析构临时变量,再析构对象a。
以上的两种方法虽然结果一样,但效率完全不一样。以后尽力写成第一种的形式会更好。
————那么本篇文章到这里就结束了,感谢各位大佬的观看,咱们下期再见
标签:初始化,变量,对象,成员,C++,列表,函数 From: https://blog.csdn.net/2301_80894970/article/details/141101354