指针大小并不都是4

以前自己也一直认为, 指针都一样,32机器的寻址是32位,大小都是4,原来,我一直是错的,成员函数指针的大小就不是4,不过,它应该不是普通的函数指针,看来自己以前对成员函数指针的理解太过于幼稚和肤浅了。

        本文将对成员函数指针的一点点小小的总结。 

        自己只是作了下简单的测试,如下

测试如下:

[cpp]  view plain copy
  1. <span style="font-size:16px;">class A  
  2. {    
  3. };  
  4.   
  5. class B  
  6. {  
  7. };  
  8.   
  9. class C  
  10. {  
  11. };  
  12.   
  13. class D:public A  
  14. {  
  15. };  
  16.   
  17. class E:public A,B  
  18. {  
  19. };  
  20.   
  21. class F:public A,B,C  
  22. {  
  23. };  
  24.   
  25.    
  26.   
  27. int main()  
  28. {  
  29.  cout<<"void (D::*)()的大小是:"<<sizeofvoid (D::*)() )<<endl; // 4  指向成员函数的指针大小不一定等于sizeof(void*)  
  30.  cout<<"void (E::*)()的大小是:"<<sizeofvoid (E::*)() )<<endl; // 8  指向成员函数的指针大小不一定等于sizeof(void*)  
  31.  cout<<"void (F::*)()的大小是:"<<sizeofvoid (F::*)() )<<endl; // 8  指向成员函数的指针大小不一定等于sizeof(void*)  
  32.  cout<<"void* 的大小是:"<<sizeofvoid* )<<endl;  
  33.   
  34. };</span>  


 

     下文是出自网上一牛人:http://www.examda.com/ncre/three/pc/fudao/20071109/094943632.html     

      尊重作者,我只是一字不漏的拷贝过来,方便自己和大家查阅

 

一。理论篇 
在进行深入学习和分析之前,还是先看看书中是怎么介绍成员函数的。总结一下类成员函数指针的内容,应该包含以下几个知识点: 
1。成员函数指针并不是普通的函数指针。 
2。编译器提供了几个新的操作符来支持成员函数指针操作:  
1)   操作符"::*"用来声明一个类成员函数指针,例如: 
       typedef void (Base::*PVVBASEMEMFUNC)(void);        //Base is a class


2)   操作符"->*"用来通过对象指针调用类成员函数指针,例如: 
       //pBase is a Base pointer and well initialized 
      //pVIBaseMemFunc is a member function pointer and well initialized 
    (pBase->*pVIBaseMemFunc)();


3) 操作符".*"用来通过对象调用类成员函数指针,例如: 
    //baseObj is a Base object 
    //pVIBaseMemFunc is a member function pointer and well initialized 
    (baseObj.*pVIBaseMemFunc)(); 


3。成员函数指针是强类型的。     typedef void (Base::*PVVBASEMEMFUNC)(void);
    typedef void (Derived::*PVVDERIVEMEMFUNC)(void); 
PVVBASEMEMFUNC和PVVDERIVEMEMFUNC是两个不同类型的成员函数指针类型。


4。由于成员函数指针并不是真真意义上的指针,所以成员函数指针的转化就受限制。具体的转化细节依赖于不同的编译器,甚至是同一个编译器的不同版本。不过,处于同一个继承链中的不同类之间override的不同函数和虚函数还是可以转化的。     void* pVoid = reinterpret_cast<void*>(pVIBaseMemFunc);            //error
    int*  pInt  = reinterpret_cast<int*>(pVIBaseMemFunc);             //error 
  pVIDeriveMemFunc = static_cast<PVIDERIVEMEMFUNC>(pVIBaseMemFunc);   //OK


二。实践篇 
有了上面的理论知识,我们对类成员函数指针有了大概的了解,但是我们对成员函数指针还存在太多的疑惑。既然说成员函数指针不是指针,那它到底是什么东东? 编译器为什么要限制成员函数指针转化?老办法,我们还是分析汇编代码揭示其中的秘密。

首先,我写了这样两个具有继承关系的类:

[cpp]  view plain copy
  1. <span style="font-size:16px;"class Base  
  2.   
  3.  {   
  4. public:   
  5.     //ordinary member function   
  6.     void setValue(int iValue);   
  7.   
  8.     //virtual member function   
  9.     virtual void dumpMe();   
  10.     virtual void foobar();   
  11.   
  12. protected:   
  13.     int m_iValue;   
  14. };   
  15.   
  16. class Derived:public Base  
  17.   
  18. {   
  19. public:   
  20.     //ordinary member function   
  21.     void setValue(int iValue);   
  22.   
  23.     //virtual member function   
  24.     virtual void dumpMe();   
  25.     virtual void foobar();   
  26. private:   
  27.     double m_fValue;   
  28. };   
  29.   
  30. </span>  


接着,我又定义了一些成员函数指针类型:    

    typedef void (Base::*PVVBASEMEMFUNC)(void); 
    typedef void (Derived::*PVVDERIVEMEMFUNC)(void); 
    typedef void (Base::*PVIBASEMEMFUNC)(int); 
    typedef void (Derived::*PVIDERIVEMEMFUNC)(int);

 


最后,在main函数写了一些测试代码:

[cpp]  view plain copy
  1. <span style="font-size:16px;">int _tmain(int argc, _TCHAR* argv[])   
  2. {   
  3.     PVIBASEMEMFUNC pVIBaseMemFunc = &Base::setValue;   
  4.     PVIDERIVEMEMFUNC pVIDeriveMemFunc = static_cast<PVIDERIVEMEMFUNC>(pVIBaseMemFunc);  
  5.   
  6.     PVVBASEMEMFUNC      pVVBaseMemFunc   = &Base::foobar;   
  7.     PVVDERIVEMEMFUNC  pVVDeriveMemFunc = static_cast<PVVDERIVEMEMFUNC>(pVVBaseMemFunc);  
  8.   
  9.     Base baseObj;   
  10.     (baseObj.*pVIBaseMemFunc)(10);   
  11.     (baseObj.*pVVBaseMemFunc)();   
  12.   
  13.     Derived deriveObj;   
  14.     (deriveObj.*pVIDeriveMemFunc)(20);   
  15.     (deriveObj.*pVVDeriveMemFunc)();   
  16.   
  17.     return 0;   
  18. }</span>  


       成功编译后生成汇编代码。

       老规矩,在分析汇编代码的过程中还是只分析对解决问题有意义的汇编代码,其他的就暂时忽略。


1。 成员函数指针不是指针。

       从代码看出,在main函数的调用栈(calling stack)中首先依次压入四个成员函数指针,如果它们是普通指针的话,它们之间的偏移量应该是4个字节,可是实际的情况却是这样的: _deriveObj$ = -88
_baseObj$ = -60 
_pVVDeriveMemFunc$ = -44 
_pVVBaseMemFunc$ = -32 
_pVIDeriveMemFunc$ = -20 
_pVIBaseMemFunc$ = -8 
_argc$ = 8 
_argv$ = 12 


       由此可以看出,他们之间的偏移量是12个字节。这12个字节中应该可以包含三个指针,其中的一个指针应该指向函数的地址,那另外两个指针又指向那里呢?

       在《C++ Common Knowledge: Essential Intermediate Programming》(中文译名:C++必知必会)这本书的第16章对这部分的内容做了说明,这个12个字节的偏移量正好印证了书中的内容:

        ”The implementation of the pointer to member function must store within itself information as to whether the member function to which it refers is virtual or nonvirtual, information about where to find the appropriate virtual function table pointer (see The Compiler Puts Stuff in Classes [11, 37]), an offset to be added to or subtracted from the function’s this pointer (see Meaning of Pointer Comparison [28, 97]), and possibly other information. A pointer to member function is commonly implemented as a small structure that contains this information, although many other implementations are also in use. Dereferencing and calling a pointer to member function usually involves examining the stored information and conditionally executing the appropriate virtual or nonvirtual function calling sequence.“ 

2。 成员函数指针的转化。本文所采用的代码是想比较普通成员函数指针和虚函数指针在转化的过程中存在那些差异 ;

       PVIBASEMEMFUNC pVIBaseMemFunc = &Base::setValue; 
       mov    DWORD PTR _pVIBaseMemFunc$[ebp], OFFSET FLAT:?setValue@Base@@QAEXH@Z ;  
       取出Base::setValue函数的地址,存放于变量pVIBaseMemFunc所占内存的前4个字节(DWORD)中。

; PVVBASEMEMFUNC      pVVBaseMemFunc   = &Base::foobar; 
mov    DWORD PTR _pVVBaseMemFunc$[ebp], OFFSET FLAT:??_9@$B3AE ; `vcall’ 
取出符号”??_9@$B3AE“的值,存放于变量pVVBaseMemFunc所占内存的前4个字节(DWORD)中。  
对于符号”??_9@$B3AE“,我又找到了这样的汇编代码:     _TEXT    SEGMENT 
    ??_9@$B3AE PROC NEAR                    ; `vcall’, COMDAT 
    mov    eax, DWORD PTR [ecx] 
    jmp    DWORD PTR [eax+4] 
    ??_9@$B3AE ENDP                        ; `vcall’ 
    _TEXT    ENDS

符号”??_9@$B3AE“代表的应该是一个存根函数,这个函数首先根据this指针获得虚函数表的指针,然后将指令再跳转到相应的虚函数的地址。由此可以看出,对于虚函数,即使是用过成员函数指针间接调用,仍然具有和直接调用一样的特性。
   ; PVIDERIVEMEMFUNC pVIDeriveMemFunc = static_cast<PVIDERIVEMEMFUNC>(pVIBaseMemFunc);
    mov    eax, DWORD PTR _pVIBaseMemFunc$[ebp] 
    mov    DWORD PTR _pVIDeriveMemFunc$[ebp], eax 
直接将变量pVIBaseMemFunc所占内存的前4个字节(DWORD)的值付给了变量_pVIDeriveMemFunc所占内存的前4个字节中。 
    ; PVVDERIVEMEMFUNC    pVVDeriveMemFunc = static_cast<PVVDERIVEMEMFUNC>(pVVBaseMemFunc);
    mov    eax, DWORD PTR _pVVBaseMemFunc$[ebp] 
    mov    DWORD PTR _pVVDeriveMemFunc$[ebp], eax 
直接将变量pVVBaseMemFunc所占内存的前4个字节(DWORD)的值付给了变量pVVDeriveMemFunc所占内存的前4个字节中。由此可以看出,基类的成员函数指针转化到相应的派生类的成员函数指针,值保持不变。当然这里的例子继承关系相对来说比较简单,如果存在多继承和虚继承的情况下,结果可能会复杂的多。

3。函数调用 
下面的函数调用都大同小异,这里是列出其中的一个:     ; (baseObj.*pVIBaseMemFunc)(10); 
    mov    esi, esp 
    push    10                    ; 0000000aH 
    lea    ecx, DWORD PTR _baseObj$[ebp] 
    call    DWORD PTR _pVIBaseMemFunc$[ebp] 
    cmp    esi, esp 
    call    __RTC_CheckEsp    
这里的汇编代码并没有给我们太多新鲜的内容:将对象的首地址(this指针)存放于寄存器ECX中,接着就将指令转到变量_pVIBaseMemFunc所占内存的前4个字节所表示的地址。

到了这里,我们应该对成员函数指针有了进一步的了解

### 指针大小及其与系统架构的关系 #### 什么是指针指针是一个变量,用于存储另一个变量的内存地址。由于指针本质上保存的是内存地址,因此它的大小取决于系统的寻址能力。 --- #### 不同系统架构下指针大小 在计算机体系结构中,32位和64位系统的主要区别在于它们能够处理或存储的最大数据量的不同。这直接影响到指针大小: - **32位系统** 在32位系统中,CPU寄存器和内存总线的设计决定了它最多可以访问 \(2^{32}\) 字节的物理内存空间[^2]。这意味着一个指针只需要占用 4 字节的空间来表示整个可寻址范围内的任意位置。因此,在大多数情况下,32位操作系统的指针大小固定为 **4 字节 (32 bits)**。 - **64位系统** 对于64位系统而言,理论上它可以支持高达 \(2^{64}\) 的地址空间。然而实际应用中,并不需要如此庞大的地址范围,现代硬件通常只实现了部分宽度(如 48 位),但这已经远远超过当前可用 RAM 容量的需求。即便如此,为了兼容性和未来扩展考虑,64位平台上每个指针仍然占据完整的 **8 字节 (64 bits)** 长度[^4]。 --- #### 跨平台的一致性问题 尽管上述规律适用于绝大多数主流环境,但在某些特定场景下可能会存在例外情况: 1. 如果程序运行在一个特殊的嵌入式环境中,则即使目标设备具备较高位宽也可能采用压缩形式减少资源消耗; 2. 使用 LLP64 数据模型(Windows 平台常用)时 int 型仍保持原有尺寸不变而 long 和 pointer 扩展至双倍长度;而在 LP64 下仅 long 及 pointers 加大到了八字节规模——Linux/MacOS 默认遵循后者定义方式[^1]. 以下是 C/C++ 中获取指针大小的一个简单示例代码片段: ```c #include <stdio.h> int main() { printf("Size of a pointer on this system is %zu bytes.\n", sizeof(void*)); return 0; } ``` 通过执行该脚本即可得知所在机器上的确切数值表现如何. --- ### 总结 综上所述, 指针的实际大小会随着所处的操作系统类型(比如 Windows 或 Unix-like 系统), 编译选项以及具体 CPU 架构等因素有所变化;不过一般来讲, 我们可以总结如下规则: - 在标准配置下的 32-bit OS 上, `sizeof(pointer)` 应当等于 4 Bytes. - 当切换到对应的 64-bit 版本之后, 则相应增长成为两倍即 8 Bytes 大小.
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值