2.5.2 各类型指针的工作方式

在C++中,任何数据类型都有对应的指针类型。从前面的学习中了解到,指针中保存的都是地址,为什么还需要类型作为修饰呢?因为需要用类型去解释这个地址中的数据。每种数据类型在内存中所占的内存空间不同,指针中只保存了存放数据的首地址,而没有指明该在哪里结束。这时就需要根据对应的类型来寻找解释数据的结束地址。例如,同一地址,使用不同类型指针进行访问,取出的内容就会不一样,如代码清单2-6所示。

代码清单2-6 各类型指针访问同一地址


//C++源码对比,定义int类型变量,初始化为0x12345678

int nVar=0x12345678;

;为地址赋值4字节数据12345678h

0040EB1D mov dword ptr[ebp-10h],12345678h

//C++源码对比,定义int类型指针变量,初始化为变量nVar地址

int*pnVar=&nVar;

0040EB24 lea ecx,[ebp-10h]

0040EB27 mov dword ptr[ebp-14h],ecx

//C++源码对比,定义char类型指针变量,初始化为变量nVar地址

char*pcVar=(char*)&nVar;

0040EB2A lea edx,[ebp-10h]

0040EB2D mov dword ptr[ebp-18h],edx

//C++源码对比,定义short类型的指针变量,初始化为变量nVar地址

short*psnVar=(short*)&nVar;

0040EB30 lea eax,[ebp-10h]

0040EB33 mov dword ptr[ebp-1Ch],eax

//C++源码对比,取出指针pnVar指向的地址内容并显示

printf("%08x\r\n",*pnVar);

;取出pnVar中保存的地址值并放入ecx中

0040EB36 mov ecx, dword ptr[ebp-14h]

;从ecx保存的地址中,以4字节方式读取数据,存入edx中

0040EB39 mov edx, dword ptr[ecx]

;printf函数调用部分略

//C++源码对比,取出指针pnVar指向的地址内容并显示

printf("%08x\r\n",*pcVar);

;取出pcVar中保存的地址值并放入eax中

0040EB49 mov eax, dword ptr[ebp-18h]

;从eax保存的地址中,以1字节方式读取数据,存入ecx中

0040EB4C movsx ecx, byte ptr[eax]

;printf函数调用部分略

//C++源码对比,取出指针pnVar指向的地址内容并显示

printf("%08x\r\n",*psnVar);

;取出psnVar中保存的地址值并放入edx中

0040EB5D mov edx, dword ptr[ebp-1Ch]

;从edx保存的地址中,以2字节方式读取数据,存入eax中

0040EB60 movsx eax, word ptr[edx]

;printf函数调用部分略


代码清单2-6中使用了三种方式对变量nVar的地址进行解释。变量nVar在内存中的数据为“78 56 34 12”,首地址从“78”开始。指针pnVar为int类型指针,以int类型在内存中占用的空间大小和排列方式对地址进行解释,然后取出数据。int类型占4字节内存空间,以小尾方式排列,取出内容为“12345678”,是一个十六进制的数字。同理,pcVar、psnVar将会按照它们的指针类型对地址数据进行解释。指针的取内容操作分为两个步骤:先取出指针中保存的地址信息,然后针对这个地址进行取内容,也就是一个间接寻址的过程,这也是识别指针的重要依据。该示例运行结果如图2-10所示。

图 2-10 各类型指针解释地址的结果

通过代码清单2-6中的指针取内容的过程可得出结论,所有类型的指针对地址的解释都取自于自身指针类型。

指针都支持哪些运算符号呢?在C++中,所有指针类型只支持加法和减法。指针是用于保存数据地址、解释地址而存在的。因此,只有加法与减法才有意义,其他运算对于指针而言没有任何意义。

指针加法用于地址偏移,但指针的加法并不像数学中的加法那样简单。指针加1后,指针内保存的地址值并不一定会加1,具体的值取决于指针类型,如指针类型为int,地址值将会加4。这个4是根据类型大小所得到的值。C++为什么要用这种烦琐的地址偏移方法呢?当指针中保存的地址为数组首地址时,为了能够利用指针加1后访问到数组内下一成员,所以加的是类型长度,而非数字1,如代码清单2-7所示。

代码清单2-7 各类型指针的寻址方式


//C++源码对比,定义字符型数组,占5字节内存空间

//由于内存对齐问题,这里的cVar实际占用8字节

char cVar[5]={0x01,0x23,0x45,0x67,0x89};

;为数组各成员赋值,由于数组为char类型,每次赋值使用byte ptr

0040EB1D mov byte ptr[ebp-14h],1

0040EB21 mov byte ptr[ebp-13h],23h

0040EB25 mov byte ptr[ebp-12h],45h

0040EB29 mov byte ptr[ebp-11h],67h

0040EB2D mov byte ptr[ebp-10h],89h

//C++源码对比,定义int类型的指针变量保存数组的首地址

int*pnVar=(int*)cVar;

;取出数组的首地址并保存在ebp-18h中

0040EB31 lea ecx,[ebp-14h]

0040EB34 mov dword ptr[ebp-18h],ecx

//C++源码对比,定义char类型的指针变量保存数组的首地址

char*pcVar=(char*)cVar;

;取出数组的首地址并保存在ebp-1Ch中

0040EB37 lea edx,[ebp-14h]

0040EB3A mov dword ptr[ebp-1Ch],edx

//C++源码对比,定义short类型的指针变量保存数组的首地址

short*psnVar=(short*)cVar;

;取出数组首地址保存在ebp-20h

0040EB3D lea eax,[ebp-14h]

0040EB40 mov dword ptr[ebp-20h],eax

//C++源码对比,对int指针加1

pnVar+=1;

;取出指针内保存的地址并放入ecx中

0040EB43 mov ecx, dword ptr[ebp-18h]

;根据指针类型获取偏移长度。int类型的指针,追加偏移值4

0040EB46 add ecx,4

;存回指针中

0040EB49 mov dword ptr[ebp-18h],ecx

//C++源码对比,对char指针加1

pcVar+=1;

0040EB4C mov edx, dword ptr[ebp-1Ch]

;根据指针类型获取偏移长度,char类型指针,追加偏移值1

0040EB4F add edx,1

0040EB52 mov dword ptr[ebp-1Ch],edx

//C++源码对比,对short指针加1

psnVar+=1;

0040EB55 mov eax, dword ptr[ebp-20h]

;根据指针类型获取偏移长度,short类型指针,追加偏移值2

0040EB58 add eax,2

0040EB5B mov dword ptr[ebp-20h],eax


代码清单2-7演示了对不同类型指针进行加1偏移。它们偏移后的地址都是由指针类型决定的,以指针保存的地址作为寻址[首地址],加上[偏移量],最终得到[目标地址]。偏移量的计算方式为指针类型长度乘以移动次数,因此得出指针寻址公式如下:


type*p;//这里用type泛指某类型的指针

//省略指针赋值代码

p+n的目标地址=首地址+sizeof(指针类型type)*n


在偏移量为负数的情况下,也可以运用此公式。套用公式,得到的地址值会小于首地址,这时指针是在向后进行寻址。所以指针可以做减法操作,但乘法与除法对于指针寻址而言是没有意义的。两指针做减法操作是在计算的两个地址之间的元素个数,结果为有符号整数,进行减法操作的两指针必须是同类指针相减。可用于两指针中的地址比较,也可用于其他场合,比如求数组元素个数,其计算公式如下:


type*p,*q;//这里用type泛指某类型的指针

//省略指针赋值代码

p-q=((int)p-(int)q)/sizeof(指针类型type)


另外,两指针相加也是没有意义的。

将指针访问公式与指针寻址公式相结合后,可针对所有类型的指针操作。在实际运用中要灵活使用。同时也要谨慎操作,以免将指针指向意料之外的地址,错误地修改地址中的数据,造成程序的崩溃。

在代码清单2-7中,指针pnVar加1后取出的内容就是数组cVar以外的数据。cVar数组只有5字节数据长度,而pnVar将访问到数组的第4项,对其取内容后得到的数据是以cVar数组的第4项为起始地址的4字节数据。分析出的结果如图2-11所示。

图 2-11 字符数组cVar的内存信息