数组与指针

来源:互联网 发布:河南禁止网络外卖 编辑:程序博客网 时间:2024/06/05 18:11

对此问题的探讨始于西山居的一个笔试题,笔试没过,宣讲会得了个大抱枕~~题目如下:
void main()
{
 int aa[2][5] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
 int *ptr1 = (int*)(&aa+1);
 int *ptr2 = (int*)(*(aa+1));
 printf("%d,%d", *(ptr1-1), *(ptr2-1));
}
问输出结果?(结果见最最后面~)
并不打算直接解答这个题目,比这题乱的题目还很多。先系统的总结下这两个东西的异同,以便较深刻地认识这两个问题,以下内容有参考C专家编程。

一、本质上的不同

说的粗糙点,就是类型不同,因为类型不同会导致编译器行为的不同,所以本质上,要从编译器的角度来看这个问题。对编译器来讲,数组和指针的访问是不一样的。

char a[] = "hello";
c = a[i];
对于这样的一个把a[i]的值赋值给c的操作,编译器得出a[i]的内容分两步:
首先,取i的值,并与编译器符号表a对应的地址9999(假设)相加,得到a[i]的地址;
然后,编译器取地址(9999+i)的内容。

char *p;
c = p[i];
这样的操作,编译器会分三步来执行:
1.编译器取符号表p对应的地址4666的内容,即‘5555’;
2.取得i的值,并与5555相加;
3.取得地址i+5555的内容。

问什么a[i]和p[i]编译器的行为完全不一样呢?因为类型不一样!对于编译器而言,数组就是一个地址,而指针就是一个地址的地址。对于指针,编译器会多一次额外的提取。
所以一个常见的问题,如果
文件1:
int food[100];
文件2:
extern int *food;

想通过*food来访问文件1中的内容,会导致严重的错误,可能会修改未知的地址。
正确的写法是:
extern int food[];
因为编译器是通过food对应地址加偏移量来访问的,所以不需要数组的大小。

指针是用来保存地址的,而数组是直接保存数据的,它们两个的不同也直接体现在sizeof大小上面。定义指针的时候,编译器并不为指针指向的对象分配空间,它只是分配指针的空间,除非在定义的时候初始化。但要注意只有字符串常量初始化时候才可以分配空间,如果是
float *p = 3.14;
是无法通过编译的(可能是为了避免p[1]这种对浮点等类型未定义的访问)。

二、“相同”之处

C语言标准有以下规则:
1.表达式中的数组被编译器当作一个指向改数组第一个元素的指针
2.下标总是与指针的偏移量相同
3.在函数声明中,数组名被编译器当作指向该数组第一个元素的指针。

int a[10], *p, i=2;
所以根据1规则,可以有p = a;这个操作,对数组a[i]的引用在编译的时候都被编译器改写成*(a+i)的形式。在表达式中,指针和数组是可以互换的,因为它们在编译器里的最终形式都是指针,都可以进行取下标的操作。编译器自动把下标值的步长调整到元素的大小。
以下三种形式都可以用来访问a[i]
1. p = a; p[i];  //此处编译器把a当作指向第一个元素的指针
2. p = a; *(p+i); //*(p+i) 其实就是编译器处理的时候对p[i]的转化,
3. p = a + i; *p; //a + i的步长是根据类型编译器计算的

对与C语言标准的第三点可以理解为“数组作为函数参数的数组名时”等同于指针。这样可以提高效率,因为数组可能是很大的一块空间,编译器只像函数传递数组的地址,而不是整个数组的拷贝。在函数的内部,编译器已经把它改写成一个指向数组的第一个元素的指针(并不知道有几个元素,通过sizeof可以看出),这时候,数组参数已经退化成了一个指针。下面用一段代码说明这个问题。

#include <stdio.h>

void my(char a[]);
int main()
{
        char a[] = "hello";

        printf("-----this is in main-----\n");
        printf("a     is %x\n", &a);
        printf("&a    is %x\n", &a);
        printf("&(*a) is %x\n", &(*a));
        printf("&a[0] is %x\n", &a[0]);
        my(a);

        return 0;
}
void my(char a[])
{
        printf("-----this is in function-----\n");
        printf("a     is %x\n", &a);
        printf("&a    is %x\n", &a);
        printf("&(*a) is %x\n", &(*a));
        printf("&a[0] is %x\n", &a[0]);
}


结果是:
-----this is in main-----
a     is bfaa95ce
&a    is bfaa95ce
&(*a) is bfaa95ce
&a[0] is bfaa95ce
-----this is in function-----
a     is bfaa95b0
&a    is bfaa95b0
&(*a) is bfaa95ce
&a[0] is bfaa95ce

可以看出在main函数中a,&a,&(*a) //这种表达竟然也可以通过编译//,&a[0]的结果是一样的(但是不是完全一样的),而在函数中,a和&a是一样的是一个编译器生成的中间的指针,而这个指针指向的内容(或者说这个指针的值),是a的第一个元素(a的第一个元素的地址)。

三、多维数组

C语言中,定义和引用多维数组的唯一方法是通过数组的数组。
而且编译器在编译的时候对a[i][j]的访问都是通过*(*(a+i)+j)的形式的。注意,i和j的步长是不一样的。a的类型是数组指针,它指向一个数组(注意这个数组的类型和元素个数也是a的类型的一个方面),就像一个指向整数的指针,a++的时候的步长是数组的元素个数*元素类型,如果类型是int那么,a++步长是j*sizeof(int)。也就是移动了整个的“一行”。而*(a+i)的类型是一个指向一个元素的指针,(对一个数组指针进行解引用后是一个指向数组元素类型的指针),所以j的步长应该是sizeof(int)。

开始下面内容前,区分几个概念
int *a[10];
int (*a)[10];
int **a;

第一个为指针数组sizeof(a)为40,为整个数组的大小;
第二个为数组指针sizeof(a)为4;
第三个为一个二级指针sizeof(a)为4;
都可以通过a[i][j]的方式来访问内容。

一个三维数组int a[2][3][5];
int (*p)[3][5] = a; //编译成功,这也是a的类型,一个数组指针,这个指针指向一个3*5为的整型数组,注意那个括号,很重要
int ***p = a; //编译失败“不能将 ‘int (*)[3][5]’ 转换为 ‘int***’”
int ***q = (int ***)a; //这样可以编译

int (*r)[5] = a[i]; //同样,r为一个数组指针,指向一个有5个元素的整型的数组,这里r的地址为a[i][0]的地址
int *t = a[i][j]; //t为一个整型的指针,指向一个整型的元素

数组指针的步长大小等于所指向的“元素”,也就是指向数组的大小,p++的步长为3*5*sizeof(int),r的步长为5*sizeof(int),t的步长为sizeof(int)
还有一个问题&a,是什么类型?这也与西山居的笔试题有些类似。看看编译器是怎么说,还是int (*)[3][5]么?
int (*p)[3][5] = &a; //编译错误,编译器说“不能将 ‘int (*)[2][3][5]’ 转换为 ‘int (*)[3][5]’”
是的,&a还是一个数组指针,只不过它指向的一个int (*)[2][3][5]的数组,可以想象成,a[2][3][5]只是一个四维数组的一个元素而已,虽然a和&a值都是一样的,但是它们的类型是完全不同的,所以它们的步长也是不相同的。其实a已经代表一个最终的内存地址,&a与a也是相同的,为什么要这样做呢?(其实a,a[0],a[0][0],它们的值也是相同的,不同的是类型)。我想大概就是当想让一个内存中的数组作为另外一个高维的数组的元素时候,通过这样类型转换来实现赋值,满足这样的需求。

四、指针与多维数组

1.初始化与内存布局
数组初始化的时候,如果个数不到维数,那么其它剩余的会自动初始化为0。
关于内存布局,上面提到对于一个二维空间,编译器都会转化成*(*(a+i)+j)的形式,那么从编译器解析的方式也可以推断出内存的布局,是一行接着一行的,而不是假象中的那种行与行平行的结构。

2.存储的选择
当需要保存多个字符串的时候,可以定一个固定大小的二维字符数组,char a[50][256],但是并不是每个字符串都是256大小的,这个时候可以用锯齿壮的字符串数组来存储,先创建一个char *a[50]的字符指针数组,在把每一个字符数组的地址赋值给a[i]指针。而且可以拷贝指针尽量不要去拷贝整个数组,指针的拷贝会比拷贝整个字符串拷贝快的多。但是如果用拷贝指针的方法,可能会导致不同的字符串在不同的页面上,访问的时候会有页面交换,从而降低效率。

3.作为函数参数的情况

多维数组的情况,注意“数组名被改写成一个指针”并不是递归定义的,也就是说一个二维数组char a[i][j],到函数行参时并不是char **a,具体的情况有四种:
1.实参为char a[i][j] 那么形参为 char(*)[j] 为一个数组指针
2.实参为char *a[i]   那么形参为 char **a为一个指针的指针
3.实参为char (*)c[i] 那么形参为 char (*)c[i] 不改变
4.实参为char char **a 那么形参为 char **a 不改变
之所以main函数中第二个参数有时候为 char **a,有时候为char *a[],是因为在函数内部,它们的形式都是一样的都为char **a。

如果函数定义为下面几种
my_fun1( int a[2][3][5]);
my_fun2( int a[][3][5]);
my_fun3( int (*a)[3][5]);
my_fun4( int **a);
如果是
my_fun5( int a[][][5]);或者my_fun5( int a[][][]) //函数声明错误“多维数组 ‘a’ 的声明必须至少指定除第一维以外所有维的大小”。
为什么呢?为什么这样的声明就不可以呢?
引用一段原话“C语言中,没有办法向函数传递一个普通的多维数组,这时因为我们需要知道每一维的长度,以便为地址运算提供正确的单位长度。在C语言中,我们没有办法在实参和形参之间交流这种数据(它在每次调用的时候会改变)。因此,你必须提供除了左边一维以外的所有维的长度。这样就可以把实参限制为除最左边一维以外所有维都必须与形参匹配的数组”
有点不明白“我们没有办法在实参和形参之间交流这种数据”这种说法,怎么就没办呢?如果不发生转换,a的类型不就是一个三维数组么?(sizeof(a)大小是120),后面说了句“它在每次调用的时候会改变”,可能是说a的第一维大小会改变,这样函数兼容性就好些,a为3*3*5维的数组的时候也可以传进去。最后又说传递后面两维大小是为了限制实参。。前面是为了适应后面是为了限制。如果为了最大的兼容性,直接就退化成int **a就好了,后面的也不用管了。
综上,对后两维指定大小的原因。我的理解是:在提高效率的前提下,对实参进行必要的限制。首先,是必须退化的,提高效率是要考虑的,不能直接把一个数组拷贝进去,在提高效率的前提下,限制下参数也是必要的(否则,a++的时候,函数内部期望的与传进来的实参步长不匹配,会出错的),那么这个时候,使用了一种数组指针的类型,本质上还是一种指针,不过这个时候这个指针是有限制的(指向数组的维数必须匹配),而且指针空间只有4个byte(32),就可以达到了提高效率,限制参数的作用了。

那么当参数为int a[2][3][5]时候,函数1,2,3是都可以编译通过的,而在函数4的时候编译器说“不能从 ‘int (*)[3][5]’ 转换到 ‘int**’”;这时因为1,2,3在函数传参的时候都被转化成char (*a)[3][5]的形式,而不能强制转化成char **a的形式,下面是调用的形式:
my_fun1(a); //OK
my_fun2(a); //OK
my_fun3(a); //OK
my_fun4(a); //编译错误“不能从 ‘int (*)[3][5]’ 转换到 ‘int**’”
如果实参定义为int (*a)[3][5],那么这四个函数调用和定义成int a[2][3][5]的时候是一样的。如果实参被定义成int (*p)[2][3][5] = &a;
my_fun1(*p);
my_fun2(*p);
my_fun3(*p);
my_fun4(*p);
结果还是一样的,不过注意,这个时候p为指向三维数组的指针(类型为 int *[2][3][5],p++步长是2*3*5的),对这个指针解引用一次*p,这个时候*p还是一个数组指针,类型为int *[3][5]。**p为 int *[5],***p类型为 int *(这里可以说是有点变化,从指向一个数组,到指向一个整数,但是对于指针来说,变化的只不过是指向元素的类型的变化,对指针来说没什么本质的变化)。好了 ****p,就是个整数了,你可以写 int b = ****p,这估计有点难读。真的是有点乱,注意一点特别的,*和&的操作结果的返回“值”可能相同(之所以加引号是因为这里的值是数值,没有类型),这两个东西可以只改变类型,像a已经是一个数组的地址了,&a还有什么意义呢?&a的意义在于改变了它返回值的类型,它们返回值的大小是一样的。

五、题目解析
已经是最最后面了,再把题目写一遍
void main()
{
 int aa[2][5] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
 int *ptr1 = (int*)(&aa+1);
 int *ptr2 = (int*)(*(aa+1));
 printf("%d,%d", *(ptr1-1), *(ptr2-1));
}
结果为10,5
如果已经明白了上面的内容,这个题目就不难了,&aa是一个int (*)[2][5]的数组指针,&aa+1这个时候步长为2*5*4指向10后面的一个整型空间,然后强制转化成了int *类型,这个时候ptr-1由于ptr已经是int *类型了,所以步长为4了,即ptr-1指向10了。解引用下,结果为10。aa类型为int (*)[5],步长为5*4,aa+1指向6所在的空间,强制转化后赋值给ptr2,ptr2-1步长为4,所以指向5的空间,解引用为5。
顺便说下,(int *)(*(aa+1))的第二个*对结果没有影响,即使是int *ptr2 = (int*)(aa+1);结果也是一样,这个*的作用是把aa+1这个int (*)[5]的类型转化成了int *的类型,所以即使是int *ptr2 = *(aa + 1);结果也是一样的。同理

int *ptr1 = (int*)(*(&aa+1));

int *ptr1 = (int*)(**(&aa+1));

int *ptr1 = **(&aa+1);

结果都还是一样的。

 

原创粉丝点击