一个C语言指针数组和二维数组的小实验
最近在用C语言写toy compiler,写的过程中突然发现自己对指针数组和二维数组的语法有了新的认识。
1. 指针数组
指针数组其实就是一个数组,数组中每个元素都是指针。
2. 二维数组
二维数组也是一个数组,不过数组中的每一个元素都是数组。
乍看起来好像很像对不对,区别只是数组元素不同,但其实这两种东西在内存中的布局完全不一样。再看下面的代码就会发现它们俩更像了:
char *a[5]; // 指针数组
char b[5][5]; // 二维数组
char c = a[3][3];
char d = b[3][3];
这段程序编译是可以通过的,这会给我们一种假象——二维数组和指针数组是一回事。
这当然是不对的,原因其实很简单,a[3][3]
和b[3][3]
完全不是一回事,虽然长得很像。
其实如果你学过汇编或者计算机组成原理之类的课就会知道,a[3][3]
其实可以分成两步:
char *tmp = a[3];
char c = tmp[3];
或者
char c = *(tmp+3);
但是再一想,b[3][3]
也可以分成这两步啊,大写懵逼对不对:
char *tmp = b[3];
char d = tmp[3];
或者
char d = *(tmp+3);
区别就在于char *tmp = a[3]
这一句,指针数组中a[3]
的值就是a
向后移动3个sizeof(char *)
那个存储单元中的值。而b[3]
的值是是b
向后移动3*5+3
个sizeof(char)
那个位置的地址。
很清楚了,第一个tmp
的值是内存里的数据(a
是一个指针数组,所以tmp
的值就是指针),第二个tmp
的值是地址(这个地址其实是编译器在代码生成时就静态生成了的)。
以一个例子结尾,这个例子只能在i386平台或者其他32位小端模式的CPU上运行。
#include <stdio.h>
void m(char *ar[]) {
char a = ar[0][3];
printf("%c", a);
}
int main() {
char a[5][5];
char b = 'a';
char *pb = &b;
pb = pb-3;
a[0][0] = (char)((int)pb);
a[0][1] = (char)((int)pb>>8);
a[0][2] = (char)((int)pb>>16);
a[0][3] = (char)((int)pb>>24);
m((char **)a);
}
其中a
是一个二维数组,而m()
中的ar
是一个元素是char *
的数组。我的目的就是想让把a
传入m()
中,在m()
中使用ar
访问。
对ar[0][3]
的访问就像刚刚说的,访问的是ar[0]
指向的那个数组中第三个元素。
现在我们让a
的前四个元素a[0][0-3]
分别赋值为&b-3
的地址各八位。然后将a
赋值给ar
。所以ar[0][3]
访问的就是由a[0][0-3]
所构成的地址指向的数组的第三个成员b
。这里ar[0]
其实并没有指向一个数组指针,只是我们把ar[0]
变成了&b-3
罢了。
需要注意的是,在Intel的32位机器上地址才是4byte,4个连续的char
才能存的下;如果是64位机器则有可能需要8个char
才能放下,但不排除64位机器仍然使用32位地址的可能。
为此我们需要在编译时加上-m32参数
gcc test.c -m32
另外,在Intel机器上,默认采用的是小端模式存储超过一个字节的数据类型。所以我们将a[0][0]
赋值为&b-3
最低8位,a[0][3]
赋值为最高8位。
总结
C语言的指针数组和二维数组完全不一样,虽然都是通过[][]
的形式访问,但一个是运行时生成,一个是编译时生成。C语言的指针和强制类型转换可以写出强大的代码,能够修改程序内存中的任意位置,但可读性以及可移植性都大大降低,这恐怕也是Java这类语言的出现原因吧。