从汇编看c++对静态成员的存取
c++中静态成员变量不存在于对象之中,而存在于全局数据段,只是其可见性受到限制,仅能被所属类访问,而非静态成员变量存在于对象中,
因而,在访问两种不同数据成员时,会有些许差别。
对于静态数据成员的访问,是直接操作其所在内存;对于非静态数据成员,则是由对象首地址 + 成员变量相对于对象首地址的偏移量来访问(对涉及到虚拟继承可能更复杂),有一定的间接性。
下面看c++源码:
class X { public: static int x1; int x2; int x3; }; int X::x1 = 1; int main() { X x; X* xp = &x; x.x1 = 1; xp->x1 = 2; X::x1 = 3; x.x3 = 1; xp->x3 = 2; }
其中静态成员变量分了三种方式存取。
下面是main函数对应的汇编码:
; 10 : int main() { push ebp mov ebp, esp sub esp, 12 ; 为变量预留12byte空间,8byte给对象x 4byte给指针xp ; 11 : X x; ; 12 : X* xp = &x; lea eax, DWORD PTR _x$[ebp];获取对象首地址,存入寄存器eax mov DWORD PTR _xp$[ebp], eax;将对象首地址给指针xp ; 13 : x.x1 = 1; mov DWORD PTR ?x1@X@@2HA, 1 ; 将1赋给?x1@X@@2HA(即静态成员变量x1所在内存)所处的内存 ; 14 : xp->x1 = 2; mov DWORD PTR ?x1@X@@2HA, 2 ; 将2赋给?x1@X@@2HA(即静态成员变量x1所在内存)所处的内存 ; 15 : X::x1 = 3; mov DWORD PTR ?x1@X@@2HA, 3 ; 将3赋给?x1@X@@2HA(即静态成员变量x1所在内存)所处的内存 ; 16 : ; 17 : x.x3 = 1; mov DWORD PTR _x$[ebp+4], 1;将1赋给偏移变量首地址4byte处内存,即将1赋给成员变量x3 ; 18 : xp->x3 = 2; mov ecx, DWORD PTR _xp$[ebp];将指针xp存的值(对象首地址)给寄存器ecx mov DWORD PTR [ecx+4], 2;将2赋给偏移变量首地址4byte处内存,即将2赋给成员变量x3 ;对于非静态成员变量,访问的方式为对象首地址 + 成员变量相对于对象首地址的偏移量来完成 ;而不是想静态成员变量一样,直接操作其内存 ; 19 : } xor eax, eax mov esp, ebp pop ebp ret 0 _main ENDP
可以看到,静态成员变量三种存取方式一样,直接操作其所在内存,而非静态数据成员要通过对象首地址和偏移量来访问。正式因为静态成员不存在于对象之中,因此,
对于所有的对象,都只有一份实体,不管该静态变量是继承而来(甚至是虚拟继承),存取都是直接操作其内存
下面是非虚拟的继承情形:
下面是c++源码:
class X { public: static int x; int i; }; class Y : public X {}; int Y::x = 1; int main() { Y y; Y* yp = &y; y.x = 1; yp->x = 2; Y::x = 3; }
下面是main函数的汇编码:
; 10 : int main() { push ebp mov ebp, esp push ebp mov ebp, esp sub esp, 8;为变量预留8byte 4byte给对象y 4字节给指针变量yp ; 11 : Y y; ; 12 : Y* yp = &y; lea eax, DWORD PTR _y$[ebp];将对象y的首地址给寄存器eax mov DWORD PTR _yp$[ebp], eax;将对象首地址给指针yp ; 13 : y.x = 1; mov DWORD PTR ?x@X@@2HA, 1 ; 将1赋给?x@X@@2HA(即静态成员变量x所在内存)所代表的内存 ; 14 : yp->x = 2; mov DWORD PTR ?x@X@@2HA, 2 ; 将2赋给?x@X@@2HA(即静态成员变量x所在内存)所代表的内存 ; 15 : Y::x = 3; mov DWORD PTR ?x@X@@2HA, 3 ; 将3赋给?x@X@@2HA(即静态成员变量x所在内存)所代表的内存 ;可以看到,虽然静态成员变量对于对象y来说是继承而来,但是 ;对于其操作认识直接访问内存地址 ; 16 : } xor eax, eax mov esp, ebp pop ebp ret 0 _main ENDP
可以看到,即使静态成员变量x是对象y继承而来,存取方式也没有变化。
下面来看虚拟继承的情形:
下面是c++源代码:
class X { public: static int x; int i; }; class Y : virtual public X {}; int Y::x = 1; int main() { Y y; Y* yp = &y; y.x = 1; yp->x = 2; Y::x = 3; }
下面主要是main函数的汇编码:
; 12 : int main() { push ebp mov ebp, esp sub esp, 12 ;为变量预留12byte空间 8byte给对象y(由于虚拟继承的原因,对象y除了包含继承自虚基类X的成员变量i ;外,还有一个额外增加的指针变量) 4byte给指针变量yp ; 13 : Y y; push 1;压入标志,在虚拟继承中有用,目的是为了防止重复构造虚基类子对象(比如菱形继承) lea ecx, DWORD PTR _y$[ebp];将对象y的首地址给寄存器ecx,作为隐含参数传递给对象y的构造函数 call ??0Y@@QAE@XZ;调用对象y的构造函数 ; 14 : Y* yp = &y; lea eax, DWORD PTR _y$[ebp];将对象y的首地址给寄存器eax mov DWORD PTR _yp$[ebp], eax;将对象首地址给指针变量yp ; 15 : y.x = 1; mov DWORD PTR ?x@X@@2HA, 1 ; 将1赋给?x@X@@2HA(即静态成员变量x所在内存)处内存 ; 16 : yp->x = 2; mov DWORD PTR ?x@X@@2HA, 2 ; 将2赋给?x@X@@2HA(即静态成员变量x所在内存)处内存 ; 17 : Y::x = 3; mov DWORD PTR ?x@X@@2HA, 3 ; 将3赋给?x@X@@2HA(即静态成员变量x所在内存)处内存 ; 18 : } xor eax, eax mov esp, ebp pop ebp ret 0 _main ENDP
可以看到,由于虚拟继承的特殊性,编译器插入了一些特殊操作,并且为对象y提供了非无用的默认构造函数,但是,对于静态成员变量的存取方式, 仍然未变。
通过上面的分析,可以知道,取一个静态成员变量的地址和取一个非静态成员变量的地址是不一样的,前者取到的是一个真正的指针,后者则是一个 指向对象成员的指针,其值并不是一个地址,而是偏移量。
下面通过c++程序来验证:
#include <iostream> #include <cstdio> #include <typeinfo> using namespace std; class X { public: static int i; int j; int k; }; int X::i = 1; int main() { cout << typeid(&X::i).name() << endl;//取静态成员变量地址 cout << typeid(&X::k).name() << endl;//取非静态成员变量地址 printf("&X::i = %d\n", &X::i);//输出静态成员变量地址 printf("&X::k = %d\n", &X::k);//输出非静态成员变量地址 不能用cout,否则得不到正确结果 }
运行结果: