重读深度探索C++对象模型:函数
成员函数的调用:
1.非静态成员函数:
对静态函数的调用实际上经历了以下的过程:
1.改写函数原型,安插一个this指针作为额外的参数
float Point3d::magnitude3d() const{};
float magituded3d(const Point3d*_this){};
2.将函数内对非静态成员的存取改为经由this指针来存取(静态成员不属于特定的某个实例,属于整个类,不应被绑定在this指针上)
3.将成员函数重新写成一个新的外部函数,对函数名称进行改写,使这个函数在程序中独一无二
4.NRV优化:将返回值作为一个参数,以引用的形式放置在参数列表中,返回时也直接返回这个值。
X bar()--------->void bar(this,X&_result)
2.虚拟成员函数
如果normalize()是一个虚拟成员函数,则ptr->normalize();被转换为:
(*ptr->vptr[1])(ptr)
ptr经由虚函数指针vptr,找到对应的虚函数表,再在其中找到对应的函数,跳转到该函数位置,传递一个this指针进行调用。
对于非多态的情况,函数调用如obj.normalize()不会经由vptr去调用,这样与普通的非静态函数的效率一致。
3.静态成员函数
静态成员函数不能直接存取非静态成员,也不能调用非静态成员函数。如果要调用非静态成员函数,必须在参数中传递一个该非静态成员函数所属的实例,通过该实例来调用非静态成员函数。这是因为非静态成员函数是不与this相绑定的,因此必须要指明所调用的非静态成员函数到底属于哪个对象。
静态成员函数的特点在于:
1.不能直接存取非静态成员与调用非静态成员函数。
2.不能被声明为const,volatile或virtual。(因为不能被this指针绑定)
虚拟成员函数:
对于ptr->z()
需要哪些信息才能使在运行期调用正确的z()实体:
1.ptr所指向的真实类型
2.z()的位置
在编译期,虚函数地址被确定下来,这些函数和地址是不会在运行期改变的。
另外,在类中安插一个新的指针vptr,用于指向虚函数表,通过虚函数表来调用虚函数。
单一继承下:P157
这图也太他妈糊了
多重继承下:
多重继承下的虚成员函数会变得复杂,主要出现在第二个及第三个或第四个...的基类身上,因为必须在执行期调整this指针
设:
class A{};
class B{};
class C:public A,public B{};
当使用基类指针时:
A* a=new C;
此时与单一继承没有什么区别,a指向C的起始地址。
而在第二个base class时:
B* b=new C;
此时b指向的地址必须调整,指向B subobject的起始处而不是对象的起始地址。
如果不这样调整,通过b来调用B类(不是C)的成员就会出错:b->data_B;
这是因为A和B是平级的关系,因此对C来说,它也是相当于直接继承自B,而C++中对成员的存取实际上是通过起始地址+类中的偏移量来实现的,如果不考虑A,B中的成员的的地址就是相对于B的偏移,而现在如果不调整b的指向,b->data_B将变为A的起始地址+B的偏移,则存取的不知道是什么数据成员。
同时,如果调用b的析构函数:delete b;
此时又要调整b的指针指向到C的起始处。
这是因为,我们要保证delete b与delete a达到相同的效果,即先进性C的析构,在进行B,在进行A的析构。
如果不调整,将会出现析构完C,B后就结束的情况(是这样吗???、)
而调整至C的起始地址后,保证无论基类指针是哪一个,都以相同的析构顺序进行。
因此对于a与b来说,即使他们达成相同的析构结果,但实际上:
对b的析构中首先进行了指针指向的调整,这是发生在运行期的。
这是第一种情况,即藉由指向第二个及以后的指针调用派生类的虚函数。
第二种情况是:
通过一个指向派生类的指针调用继承自第二个及以后的基类的虚函数。
C* c=new C;
c->funcb;
这是因为c一开始是指向的C的起始地址,而各个基类的vptr的偏移是固定的,如果不调整c的指向至B subobject处,则实际上将调用继承自A的vptr所指向的虚函数表格中同位置slot的函数,而鬼知道这是个什么东西。
因此此时要调整指向至B object.
第三种情况是:
B* b1=new C;
B* b2=b1->func();
说明一下func.
func函数为A,B中都有的虚函数,C中也对这个虚函数进行了改写,该函数在A中返回一个A*,在B中返回一个B*,在C中返回一个C*。
首先看b1->func,此时b1被调整至c的起始地址,然后func的C版本被调用,传回一个C*指针,在b2得到这个C*之前,必须将这个返回指针的位置调整至B subobejtct处,如果B b2=new C一般。
否则b2将指向C的起始处,而导致调用发生很多问题。
图在书上P165 这个图你TM在模糊一点
另外还有一点:
B* b=new C;
经由b只能存取B基类的成员,而不能存取派生类新增的甚至是A类的成员。因为b的静态绑定知名了b是B类的,因此他只能存取B类的成员。
但我们可以通过dynmic_cast或其他转换方式将他转换成C类,这样就A,B,C的成员都可调用了。