深度探索C++对象模型 笔记
封装
1. 对象持有自己的数据,等价于C语言的结构体,保持了访问速度。我的意思是,C++中,class对象就是struct,而不是像Java中的对象那样均是引用;C++的对象就是对象本身,不存在间接性,而引用另有语法表示。之所以能提升效率,在于struct被分配在栈上,而栈帧的扩张要比动态申请内存要快得多。甚至,这也是in place new存在的原因:存在先于本质,不论对象空间被分配在堆上还是栈上,之后,均可以通过该重载的new操作符来刷新其中的数据。只不过,栈上的临时对象生命周期要短一些,频繁的构造与析构是一个问题;但本质上,单纯就对象封装特性来看,C++的Plain Old Data在效率上与C结构体是等价的,而因封装特性引入C++的仅是一些类似数据可访问性的微小差异。这里的要点是,C/C++的struct是能够利用函数栈比堆要快的优势的(函数栈可以理解为一个给足了大小,单向增长缩短的空间,更重要的是函数体中的局部数据是以栈空间为基础的,也就是栈上的数据是直接访问的,除非有的参数被放在充足的寄存器上才会更快,而堆是一种动态地寻找剩余空间的、间接访问的空间,但理论上比栈的空间要大,所以,大的数据块,比如超长的数组等,可能还是得分配到堆上去,以免stack overflow)。
2. 对象还持有方法表指针,以提供一种基于方法表下标的形式统一的虚方法的获取方式(这是C++实现OOP的方式,像Java这样的语言其实也类似)。而且,如果一个类没有虚方法,其对象也不会有方法表指针,而类也没有方法表,是完全兼容C结构体的。所以,虚方法触发类的方法表及其对象的方法表指针的产生,为的必须是继承体系下函数调用的动态绑定能力,否则是完全没有意义的。
3. 其他数据均是全局的,即,除了对象的数据成员和可能存在的虚(方法表)指针外,其他一切静态数据与方法、构造器都是静态的。了解过一点JVM,可见Java与C++之间的相似性:普通函数对应Java中的static或special调用,均是编译期就被决议定的,而通过指针指向方法表里的方法调用对应Java的dynamic调用,是动态决议的。
继承
1. 扩展类对象完全包含了基类对象的数据(大小);而扩展类的方法表包含了基类的方法表,且按顺序将基类的方法(指针)排在前面,使得虚方法调用有一个统一的按下标获取的方式,实现动态绑定的效果。万恶之源是多继承的出现,而虚拟继承这个大魔王也是为了解决多继承共基类的数据冗余而出现的;Java就没有多继承这些烦恼了。在介绍它们导致的麻烦之前,可以认为基类数据排在扩展对象的前面(可能更前面还有虚指针),而在扩展类的方法表上,未被覆盖的虚方法中,对基类成员数据的引用依然可以不加调整地像它在基类方法中那样基于一致的this指针的偏移量(仅就数据部分,而不包括虚指针)来实现。
p.s. 传递指针,即一个地址(类型的)值,不会改变对象;但是,直接传递对象被保留为C语言那种按结构体类型做值拷贝的语义。所以,虚方法调用这种动态绑定的语义就只能用指针了(或引用,即已做判空的指针,更确切地说是从诞生源头上就不为空的指针)来表达了(否则,按基类对象拷贝就把后面的子类数据丢了)。
2. 在兼容C的情况下,C++的拷贝保持了结构体数据在函数栈上非间接引用的效率,但它既是直接寻址的,就像数组,对象成员的访问是通过对象首地址加偏移量来表示(翻译)的,也是静态的;即,先是数据类型(大小)被静态地指定了,然后才有将一个扩展类对象直接拷贝到基类上的情况,因为拷贝对象意味着编译期要翻译出类的大小。动态绑定付出了间接访问的代价,按基类做静态拷贝的代价则是它仅能保留了基类对象的大小,表现为扩展类对象看上去被截断了。而这个截断除了是数据上的,还是OOP语义上的:即,不要忘了虚指针,否则,扩展类中被重载的函数可能继续去访问已经被截掉的扩展类数据成员。于是,除了拷贝数据部分,还需要将方法表指针覆盖为基类方法表的地址。对C++程序员来说,可以自定义拷贝语义(默认浅拷贝,无移动语义)以免浅拷贝句柄成员不足以满足需求,但填写方法表指针的代码只是编译器实现的(因为类型已定,不存在可以发挥的可能性)。
p.s. 在一块能容纳扩展对象(即子类没有自己独有的数据成员)的基类对象空间上是可以执行扩展对象的in place构造的。但是,大概in place构造只能影响数据部分,方法表指针依然还会是基类的那个方法表指针。
3. 引入多继承与虚拟继承后,因为多继承中的第二继承对象的起始位置不再与单一继承相一致,所以,所涉及到的相关指针就需要调整了。首先是虚方法中的this指针需要调整,由于扩展对象仍然可以与第一继承对象共享方法表指针,所以,扩展对象中的第二及后续其他继承对象的this指针将需要往后调整到对应对应继承对象的位置,而继承自第二即后续对象的虚方法将成为扩展对象自己的方法;而第二及后续继承对象的虚拟方法表中,涉及到所有数据的方法,包括虚析构函数,将需要调整到扩展类对象的起始处。同时,在动态地在多个继承对象类型之间切换时,可能需要用到方法表第0槽中的type_info信息,以将扩展类与继承类的方法表区分开来。b.t.w. type_info原本是为了支持try catch中对动态抛出的异常类型的决议的,却也新引入了一个在动态绑定之外的动态决议的能力,就是凡是调用dynamic_cast的地方就会生成对应类型的type_info以支持动态类型切换;具体做的事情,关键就包括方法表指针的重写吧。而为了解决dynamic_cast转换引用类型失败会抛出异常的效率问题,可以用typeid替换它,本质上,它的判断也是基于type_info的。
4. 最后,为了支持多继承中虚拟基类只有一份数据,可以为子类增加一个虚基类的指针,也可以扩展一下方法表,在反向的方法表槽中填充虚基类对象在扩展类对象中的偏移量,从而维持对象整体性。同时,为了使虚基类对象仅被构造一次,需要为所有扩展类的构造函数扩展一个参数以区别是否需要构造基类,并仅在最终扩展类中对虚基类对象进行构造。
构造
1. 对象的数据成员是按照声明顺序初始化(区别于赋值,e.g. 初始化列表属于构造,但是构造函数体内就认为对象已经构造完成,能做的只有赋值了)的;如果未初始化,但成员定义有默认构造函数(i.e. 无参数构造,不论是编译器合成,还是人为定义的),则数据成员的默认构造函数会被调用,该安插行为是编译器的工作。加上继承与方法表指针后,构造的顺序变成:基类(包括虚基类)构造,方法表指针赋值,然后扩展类中的成员按照声明的顺序进行初始化。 p.s. 概括起来,构造中,有四种需要编译器安插某些行为的场景,除了安插基类对象的默认构造、成员数据的默认构造,方法表指针的重填,以及虚基类对象指针的安排也需要编译器安插初始化操作,这就是C++因为引入继承、虚方法和虚继承所产生的一些关键成本。
2. 对象传递的方式有两种:引用(包括指针)与拷贝。引用是拷贝构造函数的基础,即,拷贝构造函数只能以引用为入参类型,否则,就是一场死循环。拷贝构造又是函数的基石,即,拷贝构造是函数调用中非拷贝的参数与返回值的实现方式;且,注意是构造而非拷贝复制运算符。拷贝赋值运算符,虽然也只能返回this对象的引用,但它的入参,如果是引用类型的,就类似于拷贝构造,如果是非引用类型的,就相当于在入参处先执行了一次入参对象的拷贝构造,后者可以搭配swap函数实现原对象持有的资源以自动类型的形式在析构中释放,毕竟,拷贝构造的入参本质就是临时对象,而前者则需要先手动释放自己原来持有的资源,即先手动调用析构再加上拷贝构造的动作。
3. 与const类似,在现代C++定义的移动语境中,右值引用是一个用于重载的类型标志,暗示函数可以由自己一方来持有入参对象的指向资源的指针,并将入参对象的资源指针置空,表现为将这样的入参对象所持有的资源窃取了;而move仅仅做了一次强制类型转换,即,加上了一个暗示可以窃取资源的标志,以触发移动语义优先于拷贝被匹配。有移动语义后,具名返回值NRV优化就没有必要了,后者本来就是为了解决拷贝构造在普通函数调用过程中所造成的效率问题的,而移动语义替代了拷贝。与作为基本能力在任何时候都是要合成的拷贝不同,在未自定义任何拷贝操作的情况下,且所有数据成员都是可以移动的,即,是基本类型,或者本身定义了移动行为,编译器才会合成移动操作;移动操作不可用时,对应的拷贝操作就会代替移动操作被调用。而只有自定义了拷贝构造(非拷贝赋值运算符),NRV优化才会被触发,因为,与合成拷贝中的memset相比,一个真实存在的拷贝构造函数调用显然会有不可避免地有损于执行效率,而NRV优化也可能导致具名局部对象的析构函数调用机制被连带抹掉倒是其次,本来反复构造析构临时对象就是要被避免的;可见,二者均是为了解决在函数调用过程中拷贝构造可能造成的重大效率问题,而移动的语义也的确更加清晰了。
其他
1. 编译器会为代码的执行插入很多初始化与清理操作。除了临时对象外,全局对象在main函数的入口和出口的exit处进行构造与析构,且,C++中未初始化的全局对象不会像C那样放入BSS段,而仍然是当作初始化过的全局对象看待的。而在new失败的地方,在catch和re-throw的地方,均会插入一些析构临时对象的操作。
2. 在Template中的名称模板决议法一节中,有一个有趣的例子,从中可以看出,模板中的方法是按照方法级别分模板定义与模板实例化两个阶段进行决议的,如果方法不涉及模板类型,决议就是在定义阶段,如果涉及到模板参数,就是在实例化的时候决议。并且,为了保证我们想要的方法被实例化,可以显式地指出全部按某个类型实例化或选择个别方法实例化。
用例与示意图: https://www.cnblogs.com/qqiwei/p/17066823.html 继承:https://img2023.cnblogs.com/blog/1382440/202303/1382440-20230318144339278-1721819877.jpg 多继承&虚拟继承:https://img2023.cnblogs.com/blog/1382440/202303/1382440-20230318143231142-99387741.jpg https://img2023.cnblogs.com/blog/1382440/202303/1382440-20230318143302516-1309577535.jpg
说明 · · · · · ·
表示其中内容是对原文的摘抄