此文描述了数组指针与二维数组的寻址的相关内容,具体内容请看下面
引例:已知如下程序
1 #include <stdio.h>
2 main()
3 {
4 int x[3][4] = {1,3,5,7,9,11,2,4,6,8,10,12} ;
5 int (*p)[4] = x, k = 1, m, n = 0;
6 for(m=0; m < 2; m++)
7 n += *(*(p+m)+k);
8 printf("%d",n);
9 }
试写出程序的输出值。(虽然我很讨厌做这种笔头功夫的题,我也坚信编程语言是在实践中练出来的,但是这个题还是比较经典,所以还是拿来当一个例子来说明一下数组指针到底是个什么玩意)
最初在学习C语言时,就一直为这两个名词所困扰。其实也怪汉语的博大精深,两个词交换一下位置,所表示的含义就不一样了。如果直接从英文来说,指针数组叫做Array of pointers,明显重点是array,至于是什么样的array呢,就是存放pointers的array。而数组指针叫做pointer of an array,重点是pointer,那么这个pointer 指向的是什么呢,是一个array。当然这个指向的array到底是什么样的,还需要方括号的维度说明,以及前面的类型说明。
接着回到刚才的引例,x为定义的一个二维数组,p是一个数组指针,指向一个长度为4的数组,一开始指向x的第一行(x的行是一个长度为4的int型数组)接下来一个for循环,依次对p+1取值,加上k(实际就是1)后再取值,并将其累加到变量n上。循环一共执行了2次,分别取第1行和第2行(对应第一个下角标0和1)的第一个元素(也就是x[0][1],x[1][1]),因此最后的输出结果是3+11=14.
光从纸面上分析显然是不够的。GCC编译器对上述程序产生如下的代码
1 0x401340 push %ebp
2 0x401341 mov %esp,%ebp
3 0x401343 and $0xfffffff0,%esp
4 0x401346 sub $0x50,%esp
5 0x401349 call 0x4019d0 <__main>
6 0x40134e movl $0x1,0x10(%esp)
7 0x401356 movl $0x3,0x14(%esp)
8 0x40135e movl $0x5,0x18(%esp)
9 0x401366 movl $0x7,0x1c(%esp)
10 0x40136e movl $0x9,0x20(%esp)
11 0x401376 movl $0xb,0x24(%esp)
12 0x40137e movl $0x2,0x28(%esp)
13 0x401386 movl $0x4,0x2c(%esp)
14 0x40138e movl $0x6,0x30(%esp)
15 0x401396 movl $0x8,0x34(%esp)
16 0x40139e movl $0xa,0x38(%esp)
17 0x4013a6 movl $0xc,0x3c(%esp)
18 0x4013ae lea 0x10(%esp),%eax
19 0x4013b2 mov %eax,0x44(%esp)
20 0x4013b6 movl $0x1,0x40(%esp)
21 0x4013be movl $0x0,0x48(%esp)
22 0x4013c6 movl $0x0,0x4c(%esp)
23 0x4013ce jmp 0x4013f9 <main+185>
24 0x4013d0 mov 0x4c(%esp),%eax
25 0x4013d4 lea 0x0(,%eax,4),%edx
26 0x4013db mov 0x40(%esp),%eax
27 0x4013df add %edx,%eax
28 0x4013e1 lea 0x0(,%eax,4),%edx
29 0x4013e8 mov 0x44(%esp),%eax
30 0x4013ec add %edx,%eax
31 0x4013ee mov (%eax),%eax
32 0x4013f0 add %eax,0x48(%esp)
33 0x4013f4 addl $0x1,0x4c(%esp)
34 0x4013f9 cmpl $0x1,0x4c(%esp)
35 0x4013fe jle 0x4013d0 <main+144>
36 0x401400 mov 0x48(%esp),%eax
37 0x401404 mov %eax,0x4(%esp)
38 0x401408 movl $0x403024,(%esp)
39 0x40140f call 0x401c40 <printf>
40 0x401414 leave
41 0x401415 ret
其中第4行编译器为局部变量(auto)在栈上分配内存空间0x50字节,6~17行,编译器为二维数组x初始化,其中,x[0][0]的地址为%esp+10。19~22行分别为p,k,m,n初始化。(从中可以看出,p初始化使用了leal指令取第一个元素的地址,且p只占用了4个字节,也就是说,从数据大小来看,数组指针本质上还是一个指针)
现在想要研究编译器如何对数组指针进行操作,通过jle指令可以定位到循环为24~35行。在原始的C语言代码中,for循环的body-statement只有一句复合语句,最后的操作显然对应累加,也就是32行的add指令(33行的addl显然是计数器累加,因为34行用到了cmpl指令判断大小)。32行的add指令中,%esp+48对应变量n,31行用%eax的值作为地址进行寻址,将地址为%eax的值放进%eax中,显然对应C语言语句中最外层的一个*号。30行的add指令后的%eax的值显然便是表达式:*(p+m)+k的值。
重点在于理解编译器如何解析这个表达式了。24行取%esp+0x4c(m的值),25行用leal指令将m*4并放入%edx寄存器中,26行取%esp+0x40(k的值)放入寄存器%eax中,27行将%eax和%edx的值相加,得到整个的偏移地址4m+k,28行将整个偏移地址乘以4得到实际的字节偏移地址,29行再将其与数组第一个元素的地址相加,得到表达式*(p+m)+k的值了。因此,25行leal指令得到的系数4,恰好对应定义的数组指针的长度4。如果在原题中将(*p)[4]改为(*p)[3],于是编译器得到如下代码(仅截取循环内):
1 0x4013d0 mov 0x4c(%esp),%edx
2 0x4013d4 mov %edx,%eax
3 0x4013d6 add %eax,%eax
4 0x4013d8 add %eax,%edx
5 0x4013da mov 0x40(%esp),%eax
6 0x4013de add %edx,%eax
7 0x4013e0 lea 0x0(,%eax,4),%edx
8 0x4013e7 mov 0x44(%esp),%eax
9 0x4013eb add %edx,%eax
10 0x4013ed mov (%eax),%eax
11 0x4013ef add %eax,0x48(%esp)
12 0x4013f3 addl $0x1,0x4c(%esp)
13 0x4013f8 cmpl $0x1,0x4c(%esp)
14 0x4013fd jle 0x4013d0 <main+144>
这里编译器使用两条add指令计算数组长度3代替了原先的leal指令计算的数组长度4(编译器往往会选择合适的指令来减小开销,比如用移位和加法指令代替常数乘法,但是会使得汇编码和C代码的对应不是很明显),而后的代码与原先如出一辙。
可以看出,数组指针指向的是一个数组,数组指针进行自增,会将实际的地址指向下一个依靠的数组。由于二维数组在内存中实际也是按照“行优先”的规则映射到一维的线性的数组中来存储的,编译器在解释数组指针的过程中,会首先计算数组指针所指向的数组的长度(定义数组指针时确定),然后根据所指向的数组的长度计算偏移地址,将其与初始化的基地址(将其与一个二级指针关联时得到的基地址)相加,得到所指向的数组的第一个元素的地址。因此,数组指针的长度和与它相关联的实际的二维数组的行列长度并不需要严格一致,只是为了使用方便,往往会将数组指针所指向的数组的长度与实际需要操作的二维数组的行长度相对应。
事实上,访问二维数组D(定义为ElementType D[R][C])中的i行j列的元素时,通用的寻址方法是
&D[i][j]=xD+L(C·i+j),其中xD为二维数组的首地址,L为数组的元素数据类型的大小,C为定义的行长度。
数组指针的寻址本质上是一致的。在开头的例题里,公式中xD=p,i=m,j=k。
数组指针与二维数组的寻址
发表于:2017-08-08
作者:网络转载
来源:
- 周排行
- 月排行
-   智能可穿戴设备的主要测试步骤
-   缓存技术:加速应用,提高用户体验
-   一文搞懂微服务架构演进
-   工程师团队常用的六款AI工具
-   放弃后端,冲测开去了!
-   23种软件设计模式综述
-   分布式系统:常见陷阱和应对复杂性的...
-   智能可穿戴设备的主要测试步骤
-   写代码之前应该做的几件事
-   2024年,五个Java开发者应该关注的编程趋势
-   C++性能优化指南:让你的程序飞起来!
-   一个微服务业务系统的中台构建之路
-   微服务架构:构建高灵活性的分布式系统
-   软件架构五大原则,确保你的项目100%成功