深入探索C++对象模型(五) Function语义学

 静态成员函数不可以是const的,原因是因为this指针,

[cpp]  view plain  copy
  1. class Test {  
  2.     virtual ~Test();  
  3. public:  
  4.     static void StaTest();  
  5. };  
如果StaTest()修改为:static void StaTest() const;
VS2015中编译回报错误:'StaTest': modifiers not allowed on static member functions。

根本原因是:

1. const成员函数的意思是在该函数体中,不允许修改类对象的成员变量,说白了就是成员函数的this指针在const成员函数中是const*的也就是一个指针常量,不允许通过指针修改指针所指向的内容。

2. 再来看静态成员函数的意义,他是说该成员函数是属于整个类的,而不是某一个具体的类实例,再直白一点就是不需要this指针(通过类实例调用的成员函数,会被编译器转化为this指针传入成员函数)。

到此原因就很明显了,因为这两个关键字是冲突的,在没有this指针的函数中,试图规定this指针的常量性,完全是没有意义的事情。


2. 成员函数可以分成几类:

a. 非静态成员函数:此类函数会隐含一个this指针传入函数体,举个例子,对于如下函数:

[cpp]  view plain  copy
  1. float A::non_Static_Fun(){  
  2.     return m_a * m_a;  
  3. }  

调用步骤如下:改写函数签名,添加this指针,对于const函数,会添加const A* this:
[cpp]  view plain  copy
  1. float A::non_Static_Fun(A* this){  
  2.     ...  
  3. }  
将每一个非静态数据成员的存取改为由this指针来做:
[cpp]  view plain  copy
  1. float A::non_Static_Fun(A* this){  
  2.     return this->m_a * this->m_a;  
  3. }  
将成员函数重新写成一个外部函数,对函数名字进行mangling处理,一般的处理方法是函数名称+参数数目+加参数类型,注意没有返回值,这也是为什么C++中,函数重载不允许通过返回值来区分,(添加extern C关键字可以阻止这种mangling),使其成为独一无二函数名字:
[cpp]  view plain  copy
  1. extern non_Static_Fun_7AFv(...);  
相应的对于这个函数的调用操作也会被转换:
[cpp]  view plain  copy
  1. a.non_Static_Fun();  
会转化为
[cpp]  view plain  copy
  1. non_Static_Fun_7AFv(&a);  
指针调用:
[cpp]  view plain  copy
  1. pa->non_Static_Fun();  

会转化成:

[cpp]  view plain  copy
  1. non_Static_Fun_7AFv(pa);  

b. 虚拟成员函数:严格来说,虚函数也属于非静态成员函数,二者拥有部分相同的处理,但是由于跟普通的非静态成员函数处理上又有所不同,所以单独分成一类。例如,如果对于类A的一个虚成员函数v_Fun(void),这个调用pa->v_Fun(),可能会被转成为(* pa->vptr[1])(pa);其中,

    i. vptr是编译器产生的指向虚表的指针,对于所有含有或者继承有虚函数的类,都会存在,其名字可能也会被mangling化,因为同一个类中可能存在        多个此类指针。
    ii. 1是虚表槽的索引值,说明此函数位于虚表第二个函数槽中。

    iii. 第二个pa表示this指针。

注意,使用类对象调用虚函数和调用非虚函数的处理是一样的,并不会涉及虚表,例如:a.v_Fun()会转化为:v_Fun_7AFv(&a),跟上面调用非静态成员函数是一样的。这是因为类对象的类型是确定的,所以不必通过虚函数机制来决定被调用函数的地址。

c. 静态成员函数:

静态成员函数跟非静态成员函数的区别在于,前者没有this指针,因为从所属关系上来看,前者属于整个类,不存储特定的类实例状态,而后者可能因为不同的实例而产生不同的结果。例如,对于类A的一个静态成员函数void static_Fun(void),这个调用pa->static_Fun();以及这个调用:a.static_Fun();都会转化为一般的非成员函数调用:static_Fun_7ASFv();

几个静态函数的特征:

    i. 它不能直接存取所属类的非静态数据成员。
    ii. 它不能被声明为const, volatile或者virtual。

    iii. 它不需要通过类实例对象调用,A::static_Fun()。

取静态成员函数的地址: &A::static_Fun(),得到的是其在内存中的位置,也就是一个普通的函数指针:void (*)(),而不是指向类成员函数的指针void (A::*)()(关于指向类成员函数的指针,跟指向类成员变量的指针有点类似。


1. 单继承情况下的虚函数调用: 

对于多态虚函数的调用(通过基类指针或者引用),例如ptr->z();,需要知道两个信息:

    a. ptr所指对象的真实类型,这可以使我们选择正确的z()实体;

    b. z()实体位置,以便可以调用它。

结合以上的所需信息,需要为每一个多态的类对象身上增加两个成员:

    a. 一个字符串或数字,表示class的类型;

    b. 一个指针,指向某个表格,表格中带有程序的虚函数的执行期地址。

为了找到这些函数地址,每一个虚函数会被指派一个表格索引值。也就是说这个虚函数表中含有该类对象所有激活的虚函数,包括:

    a. 该类定义的函数,可能会改写基类的虚函数,也可能时该类特有的虚函数;

    b. 继承自基类的函数实体,他们没有被子类改写;

    c. 一个pure_virtual_called()函数实体,它既可以扮演纯虚函数的空间保卫者角色,也可以当作执行期异常处理函数(偶尔)。

例如,以下三个依次继承的类:


对应的虚函数表如下:


再回过头来看一看之前的例子:ptr->z();对于这个调用,我们知道:

    a. 经由ptr可以访问到该对象对应的虚表;

    b. 虽然不知道哪一个z()函数会被调用,但是我知道每一个z()函数都在虚表的第五个slot中也就是slot4。

所以改调用会转化为:(*ptr->vptr[4])(ptr);第二个ptr代表传入的this指针,这个在上一篇中有过记录。到目前为止,唯一需要在运行期才能决定的东西就是ptr所指内容的类型,也就是slot4是上述三个虚表的哪一个。

2. 多继承情况下的虚函数调用: 

多继承的复杂主要围绕在第二个以及后续的基类身上,因为涉及需要在执行期调整this指针。例如,有如下三个类:


此时的虚表情况:


对于Base2来说,Derived支持虚函数要复杂很多,依次看三种情况:

a. 通过后继基类调用子类的虚析构函数,对于如下代码:

[cpp]  view plain  copy
  1. Base2* pbase2 = new Derived;  
  2. delete pbase2;  

Derived对象的地址必须调整以指向Base2子对象,编译时期可能产生如下代码:

[cpp]  view plain  copy
  1. Derived* temp = new Derived;  
  2. Base2* pbase2 = temp ? temp + sizeof(Base1) : 0;//调整使其指向Derived中Base2处  
如果没有这样的调整,指针的任何非多态行为都会失败,例如pbase2->data_Base2,因为未作调整的pbase2指向Derived起始处,无法访问Base2的数据成员data_Base2。此时如果需要删除pbase2所指对象,指针必须在进行一次调整,以指向Derived对象起始处,一种调整方法是,扩展虚函数表,表中的每一条记录,不再仅仅时虚函数地址,而是地址加上可能的偏移量,该偏移量用以代表this指针的调整情况,于是以下虚函数的调用操作:

[cpp]  view plain  copy
  1. (*pbase2->vptr[1])(pbase2);  

会变为:

[cpp]  view plain  copy
  1. (*pbase2->vptr[1].faddr)(pbase2 + pbase2->vptr[1].offset);//faddr代表虚函数地址,offset代表指针需要的偏移量,例如上例中offset可能为4  

但是改做法的缺点是所有的虚函数调用操作都会受影响,即便不需要调整,空间和时间效率都会有所降低,一种比较由效率的方法时使用thunk技术,所谓thunk,是一小段汇编代码用来做两件事:以适当的offset调整this指针,然后跳转到对应的虚函数,可能像这样:this += sizeof(Base1); Derived::!Derived(this);,这样,虚函数表中的内容可以直接指向虚函数,也可以指向一个相关的thunk(如果需要调整this指针的话),不需要所有虚函数都承担不必要的额外负担。注:关于Thunk技术,会单独整理一篇文章

b. 第二个情况是通过子类指针调用第二个基类中一个继承而来的虚函数,此种情况下,子类指针必须调整,以指向第二个子对象,例如:

[cpp]  view plain  copy
  1. Derived* pder = new Derived;  
  2. //调用Base2::mumble()  
  3. //pder必须向前调整sizeof(Base1)个bytes  
  4. pder->mumble();  

c. 第三种情况是对于clone()函数,这是一个C++本身的扩充性质:允许一个虚函数的返回值类型有所变化,可能是base,也可能是publicly derived type,此时,当我们通过指向Base2的指针调用clone()时,又会牵扯this指针的offset问题:

[cpp]  view plain  copy
  1. Base2* pbase2_1 = new Derived;  
  2. //调用Derived* Derived::clone()  
  3. //返回值必须被调整,以指向Base2子对象  
  4. Base2* pbase2_2 = pbase2_1->clone();  
执行语句pbase2_1->clone();时,pbase2_1会被调整指向Derived对象的起始地址,于是clone()的Derived版本会被调用,它会传回一个指针,指向一个新的Derived对象,该对象在被指定给pbase2_2之前,必须经过调整,以指向Base2子对象(subobject)。

猜你喜欢

转载自blog.csdn.net/coolwriter/article/details/80555072