数组名与指针,及数组退化

来源:互联网 发布:腾讯网络认证 编辑:程序博客网 时间:2024/05/16 00:25
  我们都知道,在C语言中,数组名和指针有多很相似之处, 例如二者均可用于
指针赋值,均可通过下标的形式来访问元素等,这常常会令到我们有一种“数组名
就是指针”的感觉。 尽管有不少C语言教程上就是这么认为的,但是果真如此吗?
也许你考虑到数组名不能被赋值后,会补充说“数组名是只读的指针”,但这仍然
与事实有一定的差距。那么,数组名与指针究竟有什么不同呢?
  1.数组名的实质
  让我们先看看下面的一小段程序:
1       #include
2
3       int x[4] = {12, 34, 56, 78};
4
5       int main(void)
6       {
7           int num;
8           int *y;
9
10          y = x;
11          num = y[1];
12
13          //x = y;   // error
14          num = x[1];
15
16          return 0;
17      }
  考虑第10行的语句“y = x”,由于 y 是一个指针(假设其地址为0x1234),
为变量 y 赋值也就是将一个地址保存到 y 所对应的内存区域中, 于是“=”号的
右边就应该是一个地址,即说明了 x 的右值是一个地址(假设其值为0x5678)。 
这个赋值过程可以图示如下:
           0x1230 |        |
        y: 0x1234 | 0x5678 <-- 0x5678 (x's r-value)
           0x1238 |        |
    这样,指针 y 就指向了一个内存地址0x5678, 该地址对应的内存区域是数组
 x 的首地址。
  再考虑第11行的语句“num = y[1]”,其操作是取出 y[1] 所对应内存单元中
 int 类型的数值并赋给 num ,具体过程如下:先获取变量 y 自身的地址0x1234,
然后到地址为0x1234的内存单元中去取数据0x5678,这只是一个起始地址,还需要
加上4字节的偏移量(1 * sizeof(int) = 4),这样就得到了地址0x567C,最后从
地址为0x567C的内存单元中取出数值34并赋给 num 。这一过程可以图示如下:
           0x1230 |        |              |        |
        y: 0x1234 | 0x5678 | ----> 0x5678 |   12   |
           0x1238 |        |       0x567C |   34   | ----> num
                                   0x5680 |   56   |
                                   0x5684 |   78   |
                                          |        |
  可以看出,为了获取 y[1] 的数值,需要进行两次内存寻址操作。
  看看程序中注释掉的第13行,在VC++ 6.0中编译该语句会出现如下错误提示:
“error C2106: '=' : left operand must be l-value”,可见数组名 x 不能作
为左值。x 有可能是一个只读的变量(类似于指针int * const x = 0x5678),也
可能是一个符号常量(类似于#define x 0x5678)。由于数组名与指针的相似性,
我们有理由怀疑 x 是前者,即是一个只读的指针,抱着这个疑问,我们继续。
  考虑第14行的语句“num = x[1]”, 如果 x 是一个指针,即使它是只读的,
为了获取 x[1] 的数值,需要进行两次内存寻址操作(与第12行的语句类似),但
实际上只需要进行一次内存寻址操作就够了!为了证明这一点,我们可以查看一下
汇编代码(VC++ 6.0, Debug版本):
10:       y = x;
00401028   mov         dword ptr [ebp-8],offset _x (00427330)
11:       num = y[1];
0040102F   mov         eax,dword ptr [ebp-8]
00401032   mov         ecx,dword ptr [eax+4]
00401035   mov         dword ptr [ebp-4],ecx
12:
13:       //x = y;   // error
14:       num = x[1];
00401038   mov         edx,dword ptr [_x+4 (00427334)]
0040103E   mov         dword ptr [ebp-4],edx
  显而易见,获取 y[1] 的数值需要进行两次内存寻址操作:第一次是获取指针
 y 中保存的地址,第二次才是从偏移地址[eax+4]处获取数值;而获取 x[1] 的数
值只需要一次内存寻址操作就够了:直接从x的偏移地址[_x+4 (00427334)]处获取
数值。 因此,数组名 x 并不是一个指针,其本身就是数组的起始地址(常量),
而并非是保存了数组起始地址的变量,而这个地址常量是在编译时就已经确定的了,
直接保存在代码中。
  结论:数组名是一个地址常量,而指针则是一个用于保存地址的变量。
  2.数组名的退化
  尽管在编译器看来,数组名是一个地址常量,但它与一般的地址常量(例如:
(int * const)0x1234)不同,它同时还指代了数组这样一种数据结构, 因此下面
程序的输出结果之所以是“16, 4”,是因为当编译器看到符号 a 的时候,除了获
得其表示的地址以外,同时也获得了上下文语义,也即是 a 表示一个拥有4个元素
的 int 类型的数组,所以sizeof(a)的结果是:4 * 4 = 16。
#include
int main(void)
{
    int a[4];
    int *b = a;
    printf("%d, %d\n", sizeof(a), sizeof(b));
    return 0;
}
  但下面的程序则不同,打印出的结果是两个4,看起来编译器似乎变“糊涂”
了。为什么会这样呢?其实仔细想一想就明白了:
  函数 arrTest2() 的形参是一个指针,实参 a 传递给形参 ptr 的时候,是将
数组名 a 表示的地址压到栈里去,该函数再从栈中获取,而对于函数 arrTest1()
也不例外,同样需要一个参数压栈的过程, 尽管形参看起来是一个int arr[]的数
组,但在函数内部要访问 arr 的时候,它已经不是一个地址常数了, 而是栈上的
一个变量(如果不压到栈中保存,就需要使用寄存器来保存),无论如何,这时候
的 arr 已经不是编译时就可以确定的地址常量,因为无论传入的实参 a 包含多少
个元素,arr 在函数 arrTest1() 内部看起来都没什么两样,只是知道传进来的是
一个数组而已,编译器已经无法再获取相关联的上下文信息了。实际上,arr 已经
退化成了一个指针。
#include
void arrTest1(int arr[])
{
    printf("%d\n", sizeof(arr));
}
void arrTest2(int *ptr)
{
    printf("%d\n", sizeof(ptr));
}
int main(void)
{
    int a[4];
    arrTest1(a);
    arrTest2(a);
    return 0;
}
  所以,函数 arrTest1() 和 arrTest2() 的形参含义是相同的,尽管写法不同,
但都是一个 int 类型的指针,这也是为什么我们会见到如下两种不同的 main()
函数写法:
int main(int argc, char *argv[]) {}
int main(int argc, char **argv) {}
  从前者看 argv 是一个指针数组,从后者看 argv 是一个双重指针,其实二者
是一致的,并没有什么区别。
函数参数中的数组一定会退化为指针。。。

0 0
原创粉丝点击