虚函数指针sizeof不为sizeof(void*)

虚函数指针sizeof不为sizeof(void*)

ref:http://bbs.csdn.net/topics/360249561

 

一个继承了两个虚基类又增加了自己的一个虚函数pif的类,sizeof(指向pif的指针)竟然是8(X86)。
我是从这里http://www.codeproject.com/KB/cpp/FastDelegate.aspx看到的。

试验代码(VS2010,Win32)
    
#include <iostream>
#include <cstdlib>
 
class CBase
{

public:
    int a;
    int b;
    int c;
    int d;
    int e;
 
    virtual void fa(){std::cout<<“base fa”<<std::endl;}
    virtual void fb(){std::cout<<“base fb”<<std::endl;}
};
 
class CBase2
{

public:
    int a;
    int b;
    int c;
    int d;
    int e;
 
    virtual void f2a(){std::cout<<“base2 fa”<<std::endl;}
    virtual void f2b(){std::cout<<“base2 fb”<<std::endl;}
};
 
class CInh:public CBase,public CBase2
{

public:
    virtual void fa(){std::cout<<“inh fa”<<std::endl;}
    virtual void fb(){std::cout<<“inh fb”<<std::endl;}
 
    virtual void f2a()
    {

        std::cout<<“inh f2a”<<std::endl;
        CInh *p=this;
    }
    //virtual void f2b(){std::cout<<“inh f2b”<<std::endl;}
 
    virtual void fia(){};
 
    int a;
};
 
int main()
{

    typedef void (CInh::* pf_t)(void);
    std::cout<<sizeof(pf_t)<<std::endl;//输出8
 
    pf_t pft=&CInh::fia;
    int n=5;
    int *pn=&n;
    void *pv=pn;
    pf_t *ppp=(pf_t *)pv;
 
    typedef void (CBase2::* func_t)(void);
    std::cout<<sizeof(func_t)<<std::endl;
 
    CInh *a=new CInh;
    a->f2a();
 
    func_t pf=&CBase2::f2b;
    (a->*pf)();
 
    std::cout<<&a<<std::endl;
    std::cout<<(CBase *)(&a)<<std::endl;
    std::cout<<(CBase2 *)(&a)<<std::endl;
    std::cout<<(CInh *)(CBase2 *)(&a)<<std::endl;
 
    CBase2 *p=a;
    p->f2a();
 
    std::system(“pause”);
 
    return 0;
}

问题:
标准里对指针的size是怎么规定的?
如果指针的size是编译器相关的话,那么用int来保存各种指针岂不是不可靠的了?就是说将指针转换为int可能会丢失信息?可是印象中好多代码是这样写的啊?

 

   在编程工作中常会遇到在一个“类”中通过函数指针调用成员函数的要求,如,当在一个类中使用了C++标准库中的排序函数qsort时,因qsort参数需要一个“比较函数”指针,如果这个“类”使用某个成员函数作“比较函数”,就需要将这个成员函数的指针传给qsort供其调用。本文所讨论的用指针调用 “类”的成员函数包括以下三种情况:
    (1)将 “类”的成员函数指针赋予同类型非成员函数指针,如:
C/C++ code

#include <stdio.h>
#include <stdlib.h>
 
typedef void (*Function1)(); //定义一个函数指针类型。
Function1 f1;
 
class Test1
{

public:
    // 被调用的成员函数
    void Memberfun1()
    {

        printf(“%s \n”, “Calling Test1::Memberfun1 OK”);
    }
 
    void Memberfun2()
    {

        f1 = reinterpret_cast<Function1>(Memberfun1); // 将成员函数指针赋予普通函数指针f1,编译出错
        f1();
    }
};
 
int main()
{

    Test1 t1;
    t1.Memberfun2();
    return 0;
}

    (2)在一个“类”内,有标准库函数,如qsort, 或其他全局函数,用函数指针调用类的成员函数。如:
C/C++ code

#include <stdio.h>
#include <stdlib.h>
 
class Test2
{

public:
    int __cdecl Compare(const void* elem1, const void* elem2) // 成员函数
    {

        printf(“%s \n”, “Calling Test2::Memberfun OK”);
        return *((int*)elem1) – *((int*)elem2);
    }
 
    void Memberfun()
    {

        data[0] = 2;
        data[1] = 5;
        qsort(data, 2, sizeof(int), Compare); // 标准库函数调用成员函数,编译出错
    }
 
private:
    int data[2];
};
 
int main()
{

    Test2 t2;
    t2.Memberfun(); //调用成员函数。
    return 0;
}

    (3)同一个“类”内,一个成员函数调用另一个成员函数, 如:
C/C++ code

    
#include <stdio.h>
#include “stdlib.h”
 
class Test3
{

public:
    void Memberfun1(void(*f2)())
    {

        f2(); // 成员函数1调用成员函数2
    }
    //成员函数
    void Memberfun2()
    {

        printf(“%s \n”, “Calling Test3::Memberfun2 OK”);
    }
    void Memberfun3()
    {

        Memberfun1(Memberfun2);  // 编译出错
    }
};
 
int main()
{

    Test3 t3;
    t3.Memberfun3(); //调用成员函数。
    return 0;
}

    以上三种情况的代码语法上没有显著的错误,在一些较早的编译环境中,如,VC++ 4.0,通常可以编译通过,或至多给出问题提醒(Warning)。后来的编译工具,如,VC++6.0和其他一些常用的C++编译软件,不能通过以上代码的编译,并指出错误如下(以第三种情况用VC++ 10.0编译为例):
编译错误信息

    error C3867: ‘Test3::Memberfun2’: function call missing argument list; use ‘&Test3::Memberfun2’ to create a pointer to member

即:Memberfun1参数中所调用的函数类型不对。
    按照以上提示,仅通过改变函数的类型无法消除错误,但是,如果单将这几个函数从类的定义中拿出来,不作任何改变就可以消除错误通过编译,仍以第三种情况为例,以下代码可通过编译:
C/C++ code
    
#include <stdio.h>
#include <stdlib.h>
 
typedef void (*Function1)(); //定义一个函数指针类型。
Function1 f1;
 
// 被调用的成员函数
void Memberfun1()
{

    printf(“%s \n”, “Calling Memberfun1 OK”);
}
 
class Test1
{

public:
    void Memberfun2()
    {

        f1 = reinterpret_cast<Function1>(Memberfun1); // 将成员函数指针赋予普通函数指针f1,编译出错
        f1();
    }
};
 
int main()
{

    Test1 t1;
    t1.Memberfun2();
    return 0;
}

    第1、 2种情况和第3种情况完全相同。
    由此可以的得出结论,以上三种情况编译不能通过的原因表面上并不在于函数类型调用不对,而是与 “类”有关。没通过编译的情况是用函数指针调用了 “类”的成员函数,通过编译的是用函数指针调用了非成员函数,而函数的类型完全相同。那么, “类”的成员函数指针和非成员函数指针有什么不同吗?
    在下面的程序中,用sizeof()函数可以查看各种“类”的成员函数指针和非成员函数指针的长度(size)并输出到屏幕上。
C/C++ code
    
#include <iostream>
#include <typeinfo.h>
 
class Test;
// 一个未定义的类。
 
class Test2 // 一个空类。
{

};
 
class Test3 // 一个有定义的类。
{

public:
    void (*memberfun)();
    void Memberfun1(void(*f2)())
    {

        f2(); //成员函数1调用成员函数2
    }
    void Memberfun2(); //成员函数2。
};
 
class Test4 : virtual Test3, Test2 // 一个有virtual继承的类(derivative class)
{

public:
    void Memberfun1(void(*f2)())
    {

        f2();
    }
};
 
class Test5 : Test3, Test2 // 一个继承类(derivative class)
{

public:
    void Memberfun1(void(*f2)())
    {

        f2();
    }
};
 
int main()
{

    std::cout << “一般函数指针长度= ” << sizeof(void(*)()) << std::endl;
    std::cout  << std::endl << “类的成员函数指针长度:” << std::endl << std::endl;
    std::cout << “Test3类成员函数指针长度=” << sizeof(void(Test3::*)()) << std::endl;
    std::cout << “Test5类成员函数指针长度=” << sizeof(void(Test5::*)()) << std::endl;
    std::cout << “Test4类成员函数指针长度=” << sizeof(void(Test4::*)()) << std::endl;
    std::cout << “Test类成员函数指针长度=” << sizeof(void(Test::*)()) << std::endl;
    return 0;
}

    输出结果为(VC++10.0编译,运行于Win7操作系统,其他操作系统可能有所不同):
输出结果

    一般函数指针长度= 4

    类的成员函数指针长度:

    Test3类成员函数指针长度=4
    Test5类成员函数指针长度=8
    Test4类成员函数指针长度=12
    Test类成员函数指针长度=16

    以上结果表明,在32位win7操作系统中,一般函数指针的长度为4个字节(32位),而类的成员函数指针的长度随类的定义与否、类的继承种类和关系而变,从无继承关系类(Test3)的4字节(32位)到有虚继承关系类(Virtual Inheritance)(Test4)的12字节(96位),仅有说明(declaration)没有定义的类(Test)因为与其有关的一些信息不明确成员函数指针最长为16字节(128位)。显然, 与一般函数指针不同,指向“类”的成员函数的指针不仅包含成员函数地址的信息,而且包含与类的属性有关的信息,因此,一般函数指针和类的成员函数指针是根本不同的两种类型,当然,也就不能用一般函数指针直接调用类的成员函数,这就是为什么本文开始提到的三种情况编译出错的原因。尽管使用较早版本的编译软件编译仍然可以通过,但这会给程序留下严重的隐患。
  至于为什么同样是指向类的成员函数的指针,其长度竟然不同,从32位到128位,差别很大,由于没有看到微软官方的资料只能推测VC++10.0在编译时对类的成员函数指针进行了优化,以尽量缩短指针长度,毕竟使用128位或96位指针在32位操作系统上对程序性能会有影响。但是,无论如何优化,类的成员函数指针包含一定量的对象(Objects)信息是确定的。其他的操作系统和编译软件是否进行了类似的处理,读者可以用以上程序自己验证。
大致原理:
对于Mircosoft来说,成员函数指针实际上分两种:
一种需要调节this指针,一种不需要调节this指针。
先分清楚那些情况下成员函数指针需要调整this指针,那些情况下不需要。
可以总结如下:
如果一个类对象obj含有一些子对象subobj,这些子对象的首地址&subobj和对象自己的首地址&obj不等的话,就有可能需要调整this指针。因为我们有可能把subobj的函数当成obj自己的函数来使用。
根据这个原则,可以知道下列情况不需要调整this指针:
1.继承树最顶层的类。
2.单继承,若所有类都不含有虚拟函数。
3.单继承,若最顶层的类含有虚函数。
下列情况可能进行this指针调整:
1.多继承的类。
2.单继承,最顶的类不含有虚函数,但继承类含虚函数。
Microsoft把这两种情况分得很清楚。所以成员函数的内部表示大致分下面两种:
struct pmf_type1{

void* vcall_addr; // 成员函数的地址
};
struct pmf_type2{

void* vcall_addr; // 编译器生成的函数的地址
int delta; // 调整this指针用
};
这两种表示导致成员函数指针的大小可能不一样,pmf_type1大小为4,pmf_type2大小为8。
上面两个结构中出现的vcall_addr是一个指针,这个指针隐藏了它所指的函数是虚拟成员函数还是普通成员函数。
若它所指的是一个普通成员函数,那么包含的地址也就是这个成员函数的函数地址。
若它所指的是一个虚拟成员函数,那么包含的地址就是指向一小段编译器生的代码,这段代码会根据this指针和虚函数表索引号寻找出真正的虚拟成员函数地址,然后跳转(注意是跳转jmp,而不是函数调用call)到真实的函数地址处执行。
Microsoft的这种实现需要对一个类的每个用到了的虚函数,都分别产生这样的一段代码。
这一小段编译器生的代码就像一个template函数:
template <int index>
void vcall(void* this){

    jmp this->vptr[index]; // 此处为伪代码
}
虚拟函数表的每个不同的索引号都要产生一个实例。
Microsoft就是采用这样的方式实现了虚成员函数指针的调用。
但GCC对于成员函数指针的实现和Microsoft的方式有很大的不同。
GCC对于成员函数指针统一使用类似下面的结构进行表示:
struct{

  void* __pfn; // 成员函数地址,或者是虚拟函数表的索引号
      long __delta; // 用来进行this指针调整
};
先来看看GCC是如何区分普通成员函数和虚拟成员函数的。
不管是普通成员函数,还是虚拟成员函数,信息都记录在__pfn里面。
这里有个小小的技巧,我们知道一般来说因为对齐的关系,函数地址都至少是4字节对齐的。这就意味这一个函数的地址,最低位两个bit总是0。(就算没有这个对齐限制,编译器也可以这样实现。) GCC充分利用了这两个bit。如果是普通的函数,__pfn记录该函数的真实地址,最低位两个bit就是全0,如果是虚拟成员函数,最后两个bit不是0,剩下的30bit就是虚拟成员函数在函数表中的索引号。
使用的时候,GCC先取出最低位两个bit看看是不是0,若是0就拿这个地址直接进行函数调用。若不是0,就取出前面30位包含的虚拟函数索引,通过计算得到真正的函数地址,再进行函数调用。
GCC和Microsoft对这个问题最大的不同就是GCC总是动态计算出函数地址,而且每次调用都要判断是否为虚拟函数,开销自然要比Microsoft的实现要大一些。这也差不多可以算成一种时间换空间的做法。
在this指针调整方面,GCC和Mircrosoft的做法是一样的。不过GCC在任何情况下都会带上__delta这个变量,如果不需要调整,__delta=0。
这样GCC的实现比起Microsoft来说要稍简单一些。在所有场合其实现方式都是一样的。而且这样的实现也带来多一些灵活性。而且这样的实现也带来多一些灵活性。这一点下面“语言限制与陷阱”中详细说明。

 

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/109683.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • latex中的参考文献引用为什么显示问号_参考文献中z代表什么

    latex中的参考文献引用为什么显示问号_参考文献中z代表什么1.直接写在文档尾部2.使用文献管理软件Jabref3.说明参考文献的生成过程有两种方法,一种是直接写在这个文件后面,另一种是单独写到一个文件中,下面作详细介绍.1.直接写在文档尾部这是最简单的文献写入方式.本文中生成参考文献的代码如下:\begin{thebibliography}{1}\bibitem{liu}刘海洋.\LaTeX…

  • 领悟@Page指令中的AutoEventWireup

    领悟@Page指令中的AutoEventWireupAsp.NET中可以修改AutoEventWireup=”true”,使页面与某些特殊的事件方法绑定,自动识别这些具有特定名称的事件,而不需要进行委托。这些特定名称包括:Page_Init,Page_Load,Page_DataBind,Page_PreRender和Page_Unload等。.aspx设置AutoEventWireup=false情况下,Pag…

  • 网页设计(新手入门)[通俗易懂]

    网页设计(新手入门)[通俗易懂]1.HTML的基本结构<!DOCTYPEHTML><html><head><title>百度一下,你就知道</title></head><body>……</body></html>HTML:是用于制作网页的超文本标记语…

  • Azure虚拟机中使用Tracert/Traceroute/MTR(My Traceroute)的原理讨论

    Azure虚拟机中使用Tracert/Traceroute/MTR(My Traceroute)的原理讨论命令:mtrXXX.XXX.XXX.XXX效果: 如果加上-n参数可以显示IP而不是反向解析成域名:例如mtr-nXXX.XXX.XXX.XXX mtr的工作原理:利用IP报文头部的TTL值来进行探测 我们以目标IP为106.120.78.190为例:抓包见附件,我们看到如下的交互过程:前4个报文展开来看:第一个:第二个:

  • yum linux centos安装mysql详细教程[通俗易懂]

    yum linux centos安装mysql详细教程[通俗易懂]1、前往如下地址下载https://dev.mysql.com/downloads/repo/yum/(访问较慢,文末也会附上下载地址)我的系统是centos7,选择linux7即可2、点击download,进入下载页面这里点鼠标右键–复制链接地址即可(https://dev.mysql.com/get/mysql80-community-release-el7-3.noar…

  • 非线性方程(一)

    非线性方程(一)此为全书第一章,主matlab入门——通过学习各种插值法:反线性插值、牛顿法之类。1、diff>>diff('x^2')ans=-26-44>>dif

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号