【C++类和对象解密】面向对象编程的核心概念(下)
之前我们了解到构造函数是在对象实例化之时对对象完成初始化工作的一个函数。在我们不写时,编译器会自动生成构造函数。构造函数有一些特点,比如,他对内置类型不做处理,对自定义类型的成员会去调用其自身的构造。
我们上篇文章还提到了,默认成员函数不仅仅指我们不写编译器自动生成的函数,当我们不传参数,编译器会自动调用的函数,均为默认成员函数。
一、再探构造函数
- 之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有一种方式,就是初始化列表,初始化列表的使用方式是以一个冒号开始,接着是一个逗号分隔的数据成员列表,每个“成员变量”后边跟一个放在括号中的初始值或表达式。
- 语法上理解,初始化列表可以认为是每个成员变量定义初始化的地方。
※※每个成员变量在初始化列表中只能出现一次,这个很重要,编译会报错。
- 当然,函数体内的初始化和初始化列表初始化是可以同时存在的,这里会有同学疑惑,既然函数体内也可以进行初始化,为什么要延伸出初始化列表这个概念呢?要记住,存在即合理。
- 引用成员变量、const成员变量、没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则编译会报错。
- 首先,我们来看,引用成员变量和const成员变量有啥相似,那就是这两者必须在定义时就初始化。
如下图,当在函数体内对这两种变量进行初始化时,编译会报错。
然而,当我们试图修改这种初始化方式,试图将_ref变为year的别名,_i变为day的别名,虽然从下图可以看出,语法上是没什么问题的,可以运行成功,但是,要考虑到year、month、day均是形参,当出了函数作用域,他们均将被销毁,这时的_ref、_i就变成了野引用,相当于野指针,是极其危险的。
- 同时,第三个特殊的变量是没有默认构造的类类型变量,需要在定义时显式地调用构造函数传参给值,所以也需要在定义之时进行初始化。这里要注意一点,如果图中的Time类的构造函数有缺省值,就不需要在初始化列表进行初始化了。
- 严格来说,这些声明过的成员变量不管是否在初始化列表中出现都会走一遍初始化列表。
- 还有一点,我们可以看到_day编译器只赋了一个随机值,这也是我们之前说过的C++对内置类型的初始化是不做处理的,事实上,_day也走了初始化列表。
引用成员变量的初始化可以参考下图:
在声明处也可以这样写,相当于给成员变量缺省值:
※※※按声明顺序(与初始化列表出现顺序无关),所以建议同学们将声明顺序与初始化列表顺序保持一致。
这里要问大家一个问题:这里最终的初始化结果是什么?
答案是_i = 1。
- 首先,我们要明确,_i如果在声明处给了缺省值3,假设我们没有在初始化列表中显式对_i进行初始化,_i还是会走初始化列表,但程序是没有错误的。
- 其次,由于_i在初始化列表进行了初始化,且值为1,我们可以从上面给大家提供的逻辑分析,由于成员变量已经显式初始化,就不会再考虑未初始化的情形了。
有同学又要问,我们可不可以只用初始化列表初始化,不在函数体内部了,当然是不行的了!
还是那句话:存在即合理!像这一类,需要函数逻辑来进行初始化的成员,当然还是需要在函数体内进行初始化。
建议之后初始化将缺省值、初始化列表、函数体结合起来共同实现,因为我们的成员变量(包括未显式初始化的成员)都要走一遍初始化列表,有些初始化逻辑又必须使用函数,我们何必要浪费资源,不如都应用起来!~
二、类型转换
C语言阶段我们也曾提到过类型转换,内置类型隐式转换,前提是他们之间是有关联的,比如整型之间的转换,int可以转换为short;比如整形和浮点数之间也可以进行相互转化,int转换为double;再比如整型和指针的转换;指针和指针之间也可以相互转换。
- C++支持内置类型隐式转换为类类型对象,需要有相关内置类型为参数的构造函数。
因此,也就引申出了:
有同学会问,这种场景现实吗?由于r1是别名,这里的1隐式转换为A,为临时对象,其实是可以的。但临时对象具有常性,这是我们之前就了解过的,所以这里应该在A前加const。
在之前的学习中,我们了解到,类型转换会构造临时对象,临时对象又具有常性。
- 构造函数前面加explicit就不再支持隐式类型转换。
同时,要注意一个问题,当我们对多内置类型的对象传值时,使用(1,1)会被误认为是逗号表达式,从而只传了一个值,在此,我们可以使用{1,1}。
- 类类型的对象之间也可以隐式类型转换,需要相应的构造函数支持。
三、static成员
提出一个需求,实现一个类,计算程序中创建出了多少个类对象?最能想到的方式就是定义一个全局变量_scount,每当创建一个对象调用一次拷贝构造,_scount就加1。但我们上面提到过,当编译器同时遇到连续构造和拷贝构造,就会采用优化,变为直接构造,除非关闭优化。所以到底程序创建了多少对象,是算不明白的。
还有一个弊端,_scount作为全局变量,在任何类、函数里都可以修改,是否可以定义一个专属于一个类的全局变量呢?
答案是可以的。这个变量就叫做静态成员变量,它不属于某个对象,而是属于整个类,属于这个类的所有对象,相当于“类中的全局变量”。属于静态区,不存在对象中,并且受访问限定符的限制,不会轻易改变。
再一个问题,static静态成员变量是不可以给缺省值的,由于它不参与对象的创建,不会走初始化列表,更不会使用缺省值。
※※※静态成员变量要在类内声明,类外定义。
类中的静态成员变量怎样访问呢?
假设设置他为公有,那么只要指定类域or用对象访问就可以使用(因为这两种情况都可以让编译器识别到_scount这个静态变量是属于A类的),但是会遇到与上面提到过的全局变量一样的问题,有随时被修改的风险;
假设设置为私有,我们可以设置一个公有的成员函数,有点类似于Java中的get/set()。
除了静态成员变量,还有静态成员函数。所谓静态,就是在函数返回类型前加上static。
※很重要的一点,静态成员函数的特点是没有this指针!
也就是如果对象中有非静态的成员变量,在静态成员函数中是不能访问的!
静态成员函数应用场景:
解锁访问静态成员函数的两种姿势:
cout << A::Get() << endl;cout << aa1.Get() << endl;
四、友元
- 我们在类外面是不能访问私有或保护成员的,友元则提供了一种突破类访问限定符封装的方式。
- 友元分为:友元函数和友元类。
- 在函数声明或者类声明的前面加friend,并且把友元声明放到一个类的里面。
- 外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,他不是类的成员函数。
- 友元函数可以在类定义的任何地方声明,不受访问限定符限制。
- 一个函数可以是多个类的友元函数。
- 友元类的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员。
- 友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但B类不是A类的友元。
- 友元类不能传递,如果A是B的友元,B是C的友元,但A不是C的友元。
- 友元有时提供了便利,但是友元会增加耦合度,破坏了C++的封装特性,所以友元不宜多用。
五、内部类
- 如果一个类定义在另一个类内部,这个内部类就叫做内部类。内部类是一个独立的类,跟定义在全局相比,他只是受外部类的类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
- 如果内部类在外部类中定义为私有,那么这个内部类就是外部类的专属类。
- 内部类默认是外部类的友元。
- 这里说明,内部类在外部类的类域里面,突破类域,可以直接访问静态成员变量
- 由于B(内部类)默认是A(外部类)的友元,所以非静态成员变量需要通过调用对象进行访问
- 内部类本质也是一种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。
六、匿名对象
- 匿名对象,从他的名字可以看出,他是没有名字的对象,也就是在定义时不起名字;我们在之前定义有名对象时,曾提到调用无参构造时不要写括号,会与函数声明冲突,分不清楚,与有名对象不同,匿名对象在调用无参构造时要加上括号,如果不加,就很容易迷惑人,不知道在写什么。
- 它是由我们主动写的,并非编译器生成的。
- 匿名对象还有一个显著的特点:它的生命周期只在当前一行。即用完就被销毁掉了。
七、对象拷贝时的编译器优化
- 现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下,尽可能减少一些传参和传返回值的过程中可以省略的拷贝。
- 对于如何优化,C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流、相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新的编译器还会进行跨行跨表达式的合并优化。
(传值传参)以下两种情况,将两个构造过程合二为一,提高了程序效率:
(传值返回)
总结一下:
- 如果用对象向函数进行传值传参,尽可能用匿名对象or隐式类型转换的方式来替代有名对象(无优化)
- 如果传值返回,接收返回值更推荐使用拷贝构造的方式(也就是上上图的第一种方式)
未完待续…点个赞呗~~ (2025/7/17/20:03:19)