数据宽度
我们先来看看C语言中不同数据类型是如何存储的:
C语言代码:
char a=100; short b=100; int c=100;
我们在正向学习中知道一个char类型占据一个字节,一个short类型占据两个字节,一个int类型占据四个字节;
接下来我们在visual C++ 中观察反汇编,看看他们到底占据几个字节
我们知道在调用一个空函数的时候visual c++为我们默认分配0x40个字节。
反汇编代码:
push ebp mov ebp,esp sub esp,4Ch push ebx push esi push edi lea edi,[ebp-4Ch] mov ecx,13h mov eax,0CCCCCCCCh rep stos dword ptr [edi]
我们看到编译器为我们分配了0x4c个字节。所以不管是char,short,int类型,编译器为我们分配内存时都是分配4个字节,并不会节省空间,只是在使用时使用只使用1个字节,2个字节或者是4个字节。
在学习多级指针之前带大家看一看一级指针:
一级指针
C语言代码:
int a=10; int* p; p=&a;
我们来到反汇编观察:
反汇编代码:
mov dword ptr [ebp-4],0Ah lea eax,[ebp-4] mov dword ptr [ebp-8],eax
我们可以观察到一级指针是把一个变量(可以是C语言的各种数据类型,也可以是一个数组的首地址,甚至通过强制转换类型我们可以存储一个整数)的地址存储到一个指针变量中。在计算机中其实并不知道他存储的是什么类型,计算机内存只负责存储,只是C语言规定我们要存储地址。
1. 数据宽度
我们通过C语言sizeof函数来看看指针类型的数据宽度。
C语言代码:
int* a; printf("%d",sizeof(a));
通过程序输出我们能够知道指针类型占据4个字节空间。
2. 指针++,–,加或减一个整数运算
在这里我们通过数组可以更好地观察指针的运算。
C语言代码:
int number[6]={1,2,3,4,5,6}; int* p=number;
我们来观察反汇编代码:
反汇编代码:
mov dword ptr [ebp-18h],1 mov dword ptr [ebp-14h],2 mov dword ptr [ebp-10h],3 mov dword ptr [ebp-0Ch],4 mov dword ptr [ebp-8],5 mov dword ptr [ebp-4],6
在内存中数组的存储形式是这样的:
数据 | 1 | 2 | 3 | 4 | 5 | 6 |
地址 | ebp-18h | ebp-14h | ebp-10h | ebp-0ch | ebp-8h | ebp-4h |
这是一个int类型的数组,所以每个地址之间相差4个字节。
接下来我们来看看对指针的++操作:
C语言代码:
printf("%x\n",p); printf("%d\n",*p); printf("%d\n",*(p+1));
我们可以观察到程序输出:
19ff1c //该行输出的是该数组的首地址,这个地址在每个计算机可能不同 1 //该行输出指针p所指的地址内的数据 2 //我们对指针进行了+1的操作,p+1指向了数组的下一个地址
在这里实际上p+1看似指向了数组的下一个地址,其实这里是在数组的首地址上加了4才指向下一个数据,我们来观察反汇编:
反汇编代码:
mov ecx,dword ptr [ebp-1Ch] mov edx,dword ptr [ecx+4]
这里首先取出了在p里存储的地址,存储到寄存器ecx中,然后该地址+4,则指向下一个数据。那么在这里我们该怎么当指针加1时,寄存器内地址到底加了多少呢,按照老师讲的方法:我们去掉一个*,* p为int * 类型,去掉一个 * 就是int类型,那么地址就应该加4。我们想想实质是什么呢,在p指向的地址内存储的数据为int类型,那么该地址+4才指向下一个数据,所以应该在该地址的基础上+4指向下一个数据。
同理,如果是char类型或者是short类型的数组,那么当指向该数组的指针+1时,该指针指向的地址+1或者+2才指向下一个数据。
由此我们可以得到当指针+n(一个整数)时,该指针指向的地址应该+(n*该指针指向的地址存储的数据类型所占据的宽度)。
3. 同类型指针做加减法
同类型的指针是可以做加减运算的,我们来看看:
C语言代码:
int* p1=(int*)100; int* p2=(int*)200;//这里我们通过强制转换类型将100和200分别存入指针p1和p2中,站在编译器的角度,100和200都是地址 printf("%d",p1-p2);
在程序输出窗口我们可以看到输出:
25
那么这个25到底是怎么计算出来的呢,根据老师讲的,这里输出的实际上是100/4。
p1和p2都是int类型,那么在p1和p2所指的地址内存储的数据应该是int类型,就是占据4个字节,指针p1与p2相减,实际上是输出该段地址内一共能存储多少个同类型(指针为int,则为int类型,如果指针为char*类型,则为char类型)的数据。
在这里我们载介绍指针加(减)一个整数的另一种写法:
//C语言代码 int number[6]={1,2,3,4,5,6}; int* p=number; printf("%d\n",*p); printf("%d\n",*(p+1)); printf("%d\n",p[1]);
我们可以观察到程序输出框里输出:
1 2 2
我们可以看到*(p+1)和p[1]输出完全相同的值,我们继续查看反汇编:
反汇编代码: mov eax,dword ptr [ebp-1Ch] mov ecx,dword ptr [eax+4] push ecx p[1]的反汇编代码: mov edx,dword ptr [ebp-1Ch] mov eax,dword ptr [edx+4] push eax
我们能够清楚的看到 * (p+1)和p[1]的反汇编代码完全相同,则p[1]可以代替*(p+1)。
二级指针
为了大家更好地理解,我们二级指针使用short类型的数组。
//C语言代码 short number[5]={1,2,3,4,5}; short* p1=number; short** p2=&p1;
我们来到反汇编观察:
mov word ptr [ebp-0Ch],offset main+1Ch (0040d41c) mov word ptr [ebp-0Ah],offset main+22h (0040d422) mov word ptr [ebp-8],offset main+28h (0040d428) mov word ptr [ebp-6],offset main+2Eh (0040d42e) mov word ptr [ebp-4],offset main+34h (0040d434) lea eax,[ebp-0Ch] mov dword ptr [ebp-10h],eax lea ecx,[ebp-10h] mov dword ptr [ebp-14h],ecx
1 | 2 | 3 | 4 | 5 |
ebp-1Ch | ebp-0Ah | ebp-8 | ebp-6 | ebp-4 |
由于是short类型数组,我们可以观察到每个数据之间地址相差2。
通过反汇编我们可以观察到:
指针p1里存的是该数组的首地址,p2里存的是p1的地址,这里p2我们成为二级指针。
1. 数据宽度
通过C语言的sizeof函数我们来看看二级指针的数据宽度:
//C语言代码: short number[5]={1,2,3,4,5}; short* p1=number; short** p2=&p1; printf("%d",sizeof(p2));
在程序输出窗口我们可以看到输出:
4
不管是几级指针,它里面存储的都是地址,所以指针本身都占据4个字节。
2. 二级指针++,–,加或减一个整数运算
C语言代码:
short** p=(short**)100; printf("%d\n",p); printf("%d\n",p+1);
在这里我们通过强制类型转换把100转换成short**类型存储到二级指针p中。
在程序输出窗口我们可以看到输出:
//程序输出窗口 100 104
我们再来到反汇编看一看:
mov ecx,dword ptr [ebp-4] add ecx,4 push ecx
在这里我们看到二级指针+1是在该二级指针指向的地址基础上再加4。看到这里大家可能还是不太明白,接下来带大家到反汇编一探究竟:
3. 二级指针反汇编
1. *(*p2)反汇编:
//C语言代码 short** p2; printf("%d",*(*p2));
在使用指针时,在变量前面加一个 * 就相当于数据本身减掉一个 * ,比如这里的*(*p2):
我们定义的p2为short**类型, * p2就是short * 类型,在 *short前再加一个 *,就是 *(*p2),为short类型。我们来观察反汇编:
mov eax,dword ptr [ebp-4] mov ecx,dword ptr [eax] movsx edx,word ptr [ecx]
我们来看看在反汇编里对 *( *p2 )取值的过程:
1.取出指针变量p2里存的地址,放入eax
2.把eax里存的数据当作地址,取出该地址里存的数据放入ecx
3.把ecx里存的数据当地址,取出该地址内存放的数据放入edx
接下来我们可以在自己编译器里看到是push edx(将edx压入栈),接下来就是调用printf函数的过程了,在这里我们不再做介绍。
2. *( *(p2+1))反汇编:
C语言代码:
short** p2; printf("%d",*(*(p2+1))); • 1 • 2
我们直接从反汇编角度来分析对*(*(p2+1))取值的过程:
//反汇编代码: mov eax,dword ptr [ebp-4] mov ecx,dword ptr [eax+4] movsx edx,word ptr [ecx]
大家可以从下面贴出来的图片中加强理解,蓝色为对二级指针未作任何操作,橙色为*( *(p2+1))
1.取出指针变量p2中存放的数据放入eax
2.将取出的数据+4,将得到的值看作地址,将该地址中的数据放入ecx(二级指针本来指向第一个一级指针,因为二级指针内存放的还是地址,所以对二级指针+1实质上还是在内存上+4(4 *1)个字节,指向第二个一级指针)
3.将ecx中的数据当作地址,取出该地址中存放的数据,放入edx(二级指针内存放的数据+4(4 * 1),指向第二个一级指针,第二个一级指针指向第二个数据)