数组和指针,是同样的东西吗?
数组和指针都属于数据类型,分别是这么定义的:
int array[3];
int * point;
array 代表一段连续的内存,里面有3个元素,每个元素是int型的
point是一个指针,在32位平台上,大小为4字节,里面存储着一个内存地址。
看起来这俩哥们没啥联系吗,我们来sizeof一下,这个大家都知道,sizeof(point)的值为4(32位系统), sizeof(array) 的数值为12 (3 * sizeof(int)), 这样更加证明了,这俩肯定不是一个东西~
array是什么东西 ?
那好,我们想想,array是什么东西?
我们写两行代码看看:
int array[3] = {1, 2, 3};
int x1 = (int)array;
int x2 = (int)&array;
int x3 = (int)&array[0];
int x4 = *array;
相信稍微有经验的开发者都能很快的说出x1,x2,x3,x4所代表的意思。为了深入理解,
咱们今天看看汇编代码:
tips
估计不少朋友一听见汇编语言就头痛,其实本文设计的汇编指令很少,大概讲一下:
- mov 。。。这个还是不说了,地球人都知道。
- [] :可以理解为 * 运算,也就是取值运算。 [eax + 4] 就是将eax寄存器中的数值和4相加,算出一个结果,然后去读取内存中寻找这个地址处的数值(回忆一下,如果a表示一个指针,那么*a就表示这个指针的所指向内存中存储的数值)。
- dword ptr [ebp-14h]:word表示一个机器字,2字节,dword表示一个双字,也就是4字节。如我们上面所说,[]表示取值运算,也就是让cpu去内存某处取值,但cpu怎么知道取几个字节? dword ptr的意思就是取4字节。
- lea: lea指令相当于&运算,也就是取地址操作。 lea eax,[ebp-14h] 表示将 [ebp – 14h] 的内存地址放入eax中。
- [ebp – 4c]: 关于函数调用时的帧栈由于不是这次的重点,所以就不多说了, 大家只要记住 [ebp – x] 代表函数内的临时变量,如:
int main(int argc, char* argv[]) {
int x; // [ebp – 4h] 表示x
char y; // [ebp – 8h] 表示y
return 0;
}
- 为了方便对照,本文中C语句在上用蓝色表示,对应的汇编代码在下,用红色表示.
int array[3] = {1, 2, 3};
mov dword ptr [ebp-14h],1
mov dword ptr [ebp-10h],2
mov dword ptr [ebp-0Ch],3
[ebp-0ch] 代表的是 array[2].
[ebp – 10h] 代表的是array[1].
[ebp – 14h] 代表的是array[0].
对数组的每个元素初始化。
int x1 = (int)array;
lea eax,[ebp-14h]
mov dword ptr [ebp-20h],eax
第一句表示对 [ebp – 14h] 也就是array[0]取地址,eax中存储
着array[0]的地址,接着将这个地址放在 [ebp – 20h], 也就是
X1中。
int x2 = (int)&array;
lea eax,[ebp-14h]
mov dword ptr [ebp-2Ch],eax
这句的汇编代码几乎和上面的一样,看来对 array取不取地址
结果一样呀。
int x3 = (int)&array[0];
lea eax,[ebp-14h]
mov dword ptr [ebp-38h],eax
接着呢。。。还是一样, 这句倒在情理之中,正如我们上面分析的一样。
int x4 = *array;
mov eax,dword ptr [ebp-14h]
mov dword ptr [ebp-44h],eax
mov eax,dword ptr [ebp-14h] 表示将 [ebp – 14h](array[0]) 的值放入到
eax中,然后将eax的内容放入到x4中,所以x4的内容就是array[0]的值。
好了,清楚了吧, array, &array 以及 &array[0] 完全就是同样的东西,表示数组
的起始地址,也表示array[0]的地址。*array和 array[0]是同样的东西。
总结A:编译器认为数组名的值,对数组名取地址都表示数组第一个元素的地址。
换句话说,如果标示符p为数组名的时候,p和&p都可以用&p[0]替换。假如你就是
编译器,可以将上述代码转化为:
int x1 = (int)&array[0];
int x2 = (int)&array[0];
int x3 = (int)&array[0];
int x4 = *(&array[0]);
是不是很清楚了?
OK,我们继续下个问题。
int* point = array;
发生了神马事情?
int* point = array;
lea eax,[ebp-14h]
mov dword ptr [ebp-50h],eax
很明显,ponit中存储着 array[0]的地址。
接下来大家都知道, point[2]和array[2]都表示数组中的第三个元素,是同样的东西。
当然,表面看起来是相同,但真的相同吗?
继续让编译器说话,反正我说的话,木有几个人信呀~~~ T_T
再来看看这两句话:
int x5 = point[2];
int x6 = array[2];
mov eax,dword ptr [ebp-50h]
mov ecx,dword ptr [eax+8]
mov dword ptr [ebp-5Ch],ecx
首先,将point中的数值,放到eax中。Point中的数值是啥?对喽,是array[0]的地址~
接着, eax + 8 的意思就是array[0]的地址偏移8, (2个int),eax + 8的地址自然
是array[2]的地址~ 然后将array[2]的内容放到ecx中,现在ecx就是arra[2]了。
最后,将ecx的数值放入到x5中,over~~~
总结一下, 先读出point中存储的地址,然后加上一个偏移,再读出偏移处的数值就是point[2]
所做的操作~
int x6 = array[2];
mov eax,dword ptr [ebp-0Ch]
mov dword ptr [ebp-68h],eax
好的,我们再看看x6,咦, 怎么不一样了? 我可爱的 +8 操作怎么没了? ebp – 0ch?
我们回过头来看看第一句:
int array[3] = {1, 2, 3};
mov dword ptr [ebp-14h],1
mov dword ptr [ebp-10h],2
mov dword ptr [ebp-0Ch],3
编译器好聪明,直接已经计算好了 array[i]的地址。
那么我们是不是可以这么理解,array是一个符号常量(ebp - 14),编译时期已经定下来了。
有点不一样吧~ array[i]的地址编译器已经帮我们算好了,和point[i]不同。
你可以试着对array做 ++操作, array++ 可是非法的~
总结B:如果标示符p为指针, p[i]的计算方法为: *(p + sizeof(T) * i)
如果p为数组名,p[i]的计算方法为 *(&p[0] + sizeof(T) * i)
实际为数组声明为数组
好了,扯了这么多了,该说到重点了~
加入我在a.c 中定义了 int test_array[3] = {1, 2, 3}
然后我想在b.c中使用这个数组,我该怎么办呢?
需要声明呀~~
extern int test_array[];
这个没问题,表示test_array是别处定义的一个数组。
那么
extern int test_array[2] 或者extern int test_array[1100]
对吗?
都对,编译器只要知道这东西是个数组就行了,里面的数字它不关心。(多维数组可不是这样的)
test_array 声明为extern int test_array[]:
我们这么假设,test_array数组存储在417030h处, 那么test_array
第一个元素的地址就为417030h.
int x7 = (int)test_array;
mov dword ptr [ebp-74h], offset test_array (417000h)
int x8 = (int)&test_array;
mov dword ptr [ebp-80h], offset test_array (417000h)
int x9 = test_array[1];
mov eax,dword ptr [test_array+4 (417004h)]
mov dword ptr [ebp-8Ch],eax
根据总结A, test_array 为数组名的时候, test_array和&test_array
都会被编译器替换成&test_array[0],也就是417004h, 所以x7和x8的值相等。
根据总结B,test_array[1]的计算方法为: *(&test_array[0] + sizeof(int) * 1),
也就是dword ptr [test_array+4 (417004h)] 。
实际为数组却声明为指针
那么,如果我声明为
extern int* test_array;
呢? 编译下,没有编译错误,貌似也可以吧。
但当我写
int x7 = (int)test_array;
int x8 = (int)&test_array;
int x9 = test_array[1];
VC还是能编译过的,运行第三行却出错了~~
int x7 = (int)test_array;
mov eax,dword ptr [test_array (417030h)]
mov dword ptr [ebp-74h],eax
int x8 = (int)&test_array;
mov dword ptr [ebp-80h],offset test_array (417030h)
看到了吧? 此时的test_array 和 &test_array已经不一样了。
我们依然假设test_array存储在417030h处,
那么&test_array 的值就为417030h。思考下test_array
的值为多少呢?test_array表示地址为417030h处的int数(test_array[0])。
x7代表test_array[0]的值,而&test_array代表&test_array[0]
int x9 = test_array[1];
mov eax,dword ptr [test_array (417030h)]
mov ecx,dword ptr [eax+4]
mov dword ptr [ebp-8Ch],ecx
这一句自然是有问题的。根据结论B,当test_array为指针时,
test_array[i]为: *(test_array + sizeof(T) * i), 由上面可知,test_array 等于 test _array[0],
所以 test_array[1] = *(test_array[0] + 4).
eax的数值表示test_array[0]的值,eax + 4变成了 test_array[0] + 4, 再去读这个地址的数值,
变成了神马。。。。。程序不崩溃才怪。
数组作为函数的参数
于是上面花了很大的篇幅证明,数组和指针是不同的东西,然后我兴高采烈的写下下面的code:
void f(int a[]) {
}
void f (int* p) {
}
函数重载,既然数组和指针不一样,那么,这么写肯定没问题吧?
可惜编译器毫不留情的给了我一个耳光~ “Y的函数f存在重复定义~”
啊,原来这俩函数一样呀,数组作为函数的参数,和指针相同?
是的,请看汇编代码~
int a[2] = {1, 2};
mov dword ptr [ebp-18h],1
mov dword ptr [ebp-14h],2
f(a);
lea eax,[ebp-18h]
push eax
call @ILT+0(f) (00401005)
add esp,4
原来如此呀,函数参数传递的时候,传递的指向数组的指针呀~~
在C语言中,参数传递都是值传递,拷贝一份实参,即使在函数内部对参数做修改,也是不会影响实参
原值的,函数没有任何副作用。而在传递数组的时候,选择了传递指针,函数内部对数组元素的修改是
会影响外部的,有副作用。
为什么这样呢?
废话,这还用想,当然是出于效率考虑喽,传指针只需要拷贝4字节,如果要传值,岂不是每调用函数一次,都需要拷贝一次数组? 一个数组几千个元素,会浪费多少时间呀?
所以说,大型结构体和数组,还是传指针比较好。
总结:数组作为函数的参数,便退化为指针,没法再和指针区分了。在函数里,sizeof(a)的大小应该为4.
字符串数组与指针
先看看下面的代码:
char* pstr = "hello world";
char str[] = {"hello world"};
int x1 = sizeof(pstr);
int x2 = sizeof(str);
int x3 = strlen(pstr);
int x4 = strlen(str);
很简单,相信大家都很清楚运行的结果。pstr是个指针,str是个数组。sizeof操作符
只关心定义时的类型,指针就返回4(32位系统),数组就返回数组元素的个数 * 单个
元素的大小。所以x1为4, x2为12.
而strlen就更简单了,参数为指针,数组传入后,会退化为指针,所以根本就不区分,
它从指针偏移处为0开始计数,直到遇到\0为止。所以x3和x4都为11.
补充一点,pstr中只存储了一个指向"hello world"的地址,编译器在扫描到"hello world"这个常量时,
将它放入到存储在只读段内,也就是说pstr指向的是一个只读的区域,pstr[1] = ‘x’ 这种操作
是不允许的,会引起程序崩溃。
PS: 小白第一次写技术文章,压力很大。
sin blog同步更新: