现在的位置: 首页 > 综合 > 正文

C 语言中的一维数组与指针

2019年07月28日 ⁄ 综合 ⁄ 共 5739字 ⁄ 字号 评论关闭

数组和指针,是同样的东西吗?

数组和指针都属于数据类型,分别是这么定义的:
    
    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同步更新: 

抱歉!评论已关闭.