关于sizeof(虚函数指针)的问题

时间:2023-01-29 18:50:12
一个继承了两个虚基类又增加了自己的一个虚函数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可能会丢失信息?可是印象中好多代码是这样写的啊?

12 个解决方案

#1


据我所知,sizeof()才是与编译器相关的吧,指针的size是跟电脑的地址体系相关的,不论你给指针指定了什么类型,它都是电脑的地址总线的宽度。
你说的将指针转换为int,其实就是cast了指针指向的结构类型,如果cast后的结构类型比原来小才会丢失信息。

那个输出8是8个字节大小的意思吗,莫非函数指针是8个字节?记得指针好像都是32位的额。。

#2


32位环境下一般的指针大小都是4字节

但是成员函数指针可能是8(被你发现了) 12 16字节,所以对一般非成员函数指针放心用(32位“基本”你认定它是4字节不会错),但是成员函数指针不一定

你可以想下为什么,一个成员函数指针原本也是4字节来存一个函数地址就够,但是由于多重继承,虚继承等情况它还需要而外地计算而外的东西

所以一个成员函数指针起码要记录1.函数的地址(这点和普通指针一样4字节就够)2。额外的是 需要一个this的偏移量(多重继承中让this调整到正确位置)...还需要一些和虚继承虚函数表有关的信息,如此下来成员函数指针的大小.........

#3


成员函数还需要它的上层this地址,内部其他函数的地址,变量的偏移等等。。非常感谢让我知道了这些。^-^

#4


本帖最后由 Loaden 于 2011-05-28 06:31:03 编辑
    在编程工作中常会遇到在一个“类”中通过函数指针调用成员函数的要求,如,当在一个类中使用了C++标准库中的排序函数qsort时,因qsort参数需要一个“比较函数”指针,如果这个“类”使用某个成员函数作“比较函数”,就需要将这个成员函数的指针传给qsort供其调用。本文所讨论的用指针调用 “类”的成员函数包括以下三种情况:
    (1)将 “类”的成员函数指针赋予同类型非成员函数指针,如:
#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, 或其他全局函数,用函数指针调用类的成员函数。如:
#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)同一个“类”内,一个成员函数调用另一个成员函数, 如:
#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参数中所调用的函数类型不对。
    按照以上提示,仅通过改变函数的类型无法消除错误,但是,如果单将这几个函数从类的定义中拿出来,不作任何改变就可以消除错误通过编译,仍以第三种情况为例,以下代码可通过编译:
#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)并输出到屏幕上。
#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来说要稍简单一些。在所有场合其实现方式都是一样的。而且这样的实现也带来多一些灵活性。而且这样的实现也带来多一些灵活性。这一点下面“语言限制与陷阱”中详细说明。

#5


收藏了

#6


顶贴标记,先睡个午觉,回来学习!

#7


原来还有着区别。学习了

#8


看C++标准E文看得真蛋疼

conclusion:
1.不要在整型与指针类型之间乱转
2.很基本的,涉及size的时候不要用4、8之类的,要用sizeof

#9


sizeof 指针的话,都是地址的字节数!

#10


可是,在各种指针之间转换有什么规则,有人懂没?

特别是,试验得出:带虚函数的类的成员函数的指针类型和void *类型不能相互转换。

#11


看看结贴后还能不能回复。
能。

#12


下面是我想要的答案(自己刚找到):
in C++ Draft N3242,
P76:
“Except for pointers to static members, text referring to “pointers” does not apply to pointers to
members.”
结论:pointer与pointer-to-member是不同的东西。
P105:
(static cast)“A value of type pointer to object converted to “pointer to cv void” and back, possibly with different cv-qualification, shall have its original value.”
结论:用void保存任意类型指针再转换回去是安全的。
P105:
(reinterpret cast)“A pointer converted to an integer of sufficient size (if any such exists on the implementation) and back to the same pointer type will have its original value;”
结论:用够大的整型保存指针再转换回去是安全的。

#1


据我所知,sizeof()才是与编译器相关的吧,指针的size是跟电脑的地址体系相关的,不论你给指针指定了什么类型,它都是电脑的地址总线的宽度。
你说的将指针转换为int,其实就是cast了指针指向的结构类型,如果cast后的结构类型比原来小才会丢失信息。

那个输出8是8个字节大小的意思吗,莫非函数指针是8个字节?记得指针好像都是32位的额。。

#2


32位环境下一般的指针大小都是4字节

但是成员函数指针可能是8(被你发现了) 12 16字节,所以对一般非成员函数指针放心用(32位“基本”你认定它是4字节不会错),但是成员函数指针不一定

你可以想下为什么,一个成员函数指针原本也是4字节来存一个函数地址就够,但是由于多重继承,虚继承等情况它还需要而外地计算而外的东西

所以一个成员函数指针起码要记录1.函数的地址(这点和普通指针一样4字节就够)2。额外的是 需要一个this的偏移量(多重继承中让this调整到正确位置)...还需要一些和虚继承虚函数表有关的信息,如此下来成员函数指针的大小.........

#3


成员函数还需要它的上层this地址,内部其他函数的地址,变量的偏移等等。。非常感谢让我知道了这些。^-^

#4


本帖最后由 Loaden 于 2011-05-28 06:31:03 编辑
    在编程工作中常会遇到在一个“类”中通过函数指针调用成员函数的要求,如,当在一个类中使用了C++标准库中的排序函数qsort时,因qsort参数需要一个“比较函数”指针,如果这个“类”使用某个成员函数作“比较函数”,就需要将这个成员函数的指针传给qsort供其调用。本文所讨论的用指针调用 “类”的成员函数包括以下三种情况:
    (1)将 “类”的成员函数指针赋予同类型非成员函数指针,如:
#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, 或其他全局函数,用函数指针调用类的成员函数。如:
#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)同一个“类”内,一个成员函数调用另一个成员函数, 如:
#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参数中所调用的函数类型不对。
    按照以上提示,仅通过改变函数的类型无法消除错误,但是,如果单将这几个函数从类的定义中拿出来,不作任何改变就可以消除错误通过编译,仍以第三种情况为例,以下代码可通过编译:
#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)并输出到屏幕上。
#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来说要稍简单一些。在所有场合其实现方式都是一样的。而且这样的实现也带来多一些灵活性。而且这样的实现也带来多一些灵活性。这一点下面“语言限制与陷阱”中详细说明。

#5


收藏了

#6


顶贴标记,先睡个午觉,回来学习!

#7


原来还有着区别。学习了

#8


看C++标准E文看得真蛋疼

conclusion:
1.不要在整型与指针类型之间乱转
2.很基本的,涉及size的时候不要用4、8之类的,要用sizeof

#9


sizeof 指针的话,都是地址的字节数!

#10


可是,在各种指针之间转换有什么规则,有人懂没?

特别是,试验得出:带虚函数的类的成员函数的指针类型和void *类型不能相互转换。

#11


看看结贴后还能不能回复。
能。

#12


下面是我想要的答案(自己刚找到):
in C++ Draft N3242,
P76:
“Except for pointers to static members, text referring to “pointers” does not apply to pointers to
members.”
结论:pointer与pointer-to-member是不同的东西。
P105:
(static cast)“A value of type pointer to object converted to “pointer to cv void” and back, possibly with different cv-qualification, shall have its original value.”
结论:用void保存任意类型指针再转换回去是安全的。
P105:
(reinterpret cast)“A pointer converted to an integer of sufficient size (if any such exists on the implementation) and back to the same pointer type will have its original value;”
结论:用够大的整型保存指针再转换回去是安全的。