数组名初探

http://www.doc88.com/p-779898948040.html

http://blog.csdn.net/zhangyulin54321/article/details/7843531

为避免歧义此处给出指针定义:本文理解的指针就是地址 int b; &b就是指针常量。
    一直以来对数组名的理解都是一知半解,今天我们就来初步探讨一下数组名之本质。闲话少说,单刀直入!
  
 有数组:
    int a[10] = {0};
 在Kenneth A.Reek 的<<c和指针>>一书中是这样描述的:
    在C中,几乎所有使用数组名表达式中,数组名是一个指针常量,也就是数组第一个元素的地址。他的类型取决于数组元素的类型。且&a = a =&a[0]。
    这段文字似乎已经阐述的足够明白,但是有的时候我们仍然有一些困惑,下面给出一个例子:
    int *const p = a;
    p同样是指针常量,可是执行下面代码你会马上得到一个困惑的结果:
    &a = a;
    &p != p;
    这是为什么呢?很明显,我们的理解出现了纰漏,至于纰漏在哪,还需进一步探索。
    紧接着我又尝试了一个式子:
    p = &a; //警告类型不匹配,说明a 与 &a类型不同,会强制类型转化。
   printf("%x %x\n",p,*p);//结果为p值与&a一样, 但*p = 0;说明发生了强制类型转化,*p = a[0];
   printf("%x\n",*(&a));//输出a的值 ---地址


   通过这些小测试,我们明白了数组名不只是指针常量这么简单,而且&a 与 a也不是完全等价的。为此我们必须知道数组名在内存中的实现方式,没办法临时抱佛腿,拿出多年前打酱油学到的那点汇编语言知识来初步看一下,测试程序test.c如下:

 test.c

int mian()
{
   int a[2] = {0,1};
   int *const p = a;

    a[0] = 3;
   p[0] = 2;
   return 1;
}

 


linux 下执行:gcc test.c -S  获得汇编代码test.s

  (�p = % EBP    �x=% EAX  + 改小写   无法正常显示,无语   )

    .file "test.c"
    .text
.global main
    .type main, @function

 main:
      pushl �p
      movl %esp , �p
      subl $16, %esp
      movl $0 , -12(�p)
      movl $1 , -8(�p)
      leal -12(�p) , �x
      movl �x , -4(�p)
      movl $3 , -12(�p)
      movl -4(�p) , �x
      movl $2 , (�x)
      leave
      ret
      .size main, .-main


(此处为AT&T汇编语法:http://wenku.baidu.com/view/738e7638376baf1ffc4fad46.html) 
笔者也不是很懂这段代码,大致分析如下:(i 表示第i行)

1)�p保存函数调用起始处地址,也即函数返回时返回的地址(保存现场)。

2)%esp用于内存分配,把分配的首地址赋值给�p。

3)$16是整个程序需要内存空间+4(3*4+4),也就是16个存储单元被分配,那么内存分配寄存器%esp需要减16。

4)�p存储了分配给程序的地址首地址,-16(�p)留做他用(干嘛我还没搞清楚),把数组首元素赋值当前可用值-12(�p),放入0。

5)同理4)可得。

6)将当前内存空间首地址存入�x中(待确定)。

7)为指针p分配地址-4(�p),同时初始化p为数组首地址-12(�p)(由上一条语句存入�x。

8)给数组首元素赋值3,即a[0] = 3。

9)将指针常量p内存-4(�p)中的数组首元素取出到�x中。

10) 对地址(�x)即数组首地址赋值为2。

  由这些汇编代码我们可以得出,寄存器�p起始就相当于数组名a(但实际不是),

a = (�p),也就是说a就是数组首元素地址,他和数组初始地址是一个东西,它是数组元素地址的别名,它在任何时候都不表示a[0],只是表示&a[0]。准确地说你可以把它理解为“用宏定义的数组首地址的别名”。

(即#define a &a[0]

这里可以来点小插曲,讲述c语言中一个重要概念:在c中对于大多数变量来说,当其作为左值时,一般表示其地址;当作为右值时,一般表示其数据。或者你可以更加通俗的理解,全都表示地址,做左值表示往地址存数,做右值表示从地址取数。

当然数组名a不可以做左值,因为它是一个常量(准确地讲是一个字面值)。事实上他不是一个变量,也不是通常我们理解之常量。它的地位就像5这样的数值,非常直白,表示的就是自己本身,没有什么双重意义,即使你硬要把它当作传统意义的常量看待,那么其作为右值时也不表示其表示地址存储的值。

同时我们可以看出,指针p(本质是一个标量),本身有一个内存地址-4(�p),
然后-4(�p)中存储的是数组首元素的地址a。我们若要对数组进行操作,必须先把-4(�p)的内容先取入寄存器�x,然后再进行操作。这样看来指针与普通变量没多大差别。

由此可知指针(此处指常量和变量,非字面值),指针之所以为指针,它本身没什么特变,它本质上也是一个普通标量,唯独一样,它的存储空间中必须存储地址字面值。且编译器理解它的类型。或你可以理解它是“普通标量的子类”而已。内部实现无非就是间接寻址。

a p的区别可以这样形容:你有一样东西寄存在一个地方,你想要下回想去取来时记得该地址,第一种选择你可以直接记住它(你脑子里的就是a),下回你可以直接用;第二种选择,你可以请别人帮你记住(也就是a其实存在了别人那),自己压根不记住,假设此人叫p,那么你下回想要去东西,必须先找到p在哪,然后从他那获得地址a

 

至于&a这个表达式,严格来讲a是一个字面值,就相当于510之类的数,只不过它是一个表示地址的数,如:0x4be0563f之类。你想&a相当于&5,这显然是错的,但是数组毕竟有些特殊,为特殊需要&a只是一种编译器定义行为,他必不能取到a的地址,因为a根不不存在于内存,没有属于a的内存空间。所以编译器只好让他等于数组首元素地址,但它表示的却是数组的首地址。它其实很有用,可用于计算数组长度。

下面让我们进一步讨论数组名的含义:(我们需要工具sizeof

仍然使用前面的例子:int a[10]; int *const p = a;

printf(“%d\n”,sizeof(&a));  //4  

printf(“%d\n”,sizeof(&p));  //4

printf(“%d\n”,sizeof(a));   //8

printf(“%d\n”,sizeof(p));   //4

printf(“%d\n”,sizeof(*p));  //4

printf(“%d\n”,sizeof(*&a)); //8

printf(“%d\n”,sizeof(&a[0]));//4

printf(“%d\n”,sizeof(&p[0]));//4

从前面的阐述,似乎a&a[0]是等价的,但是实际上我们可以通过sizeof()可知事实并非如此。这里借涉及到数组名退化说

数组名原本是表示整个数组的(很难理解):

    但也不是完全不能理解,你可以把它和普通标量等同如int b =10;那么标量b就标识一段内存空间。其长度为4字节。地址为&bb这个标签也不需要另外存储,它只是供编译器分辨不同的变量,在内存中它就是以一段内存空间的形式存在。你找不到b这样的字样,事实上编译成汇编后b这个标签的使命也就结束了。

    按照以上的概念来思考的话,int a[10];完全可以复制上面的说法,a用于标识一段用于存储10个整数的内存片断。其仍然是一个存储空间的标签。这种意义上来讲数组名a完全不同于510这样的数字。你对其取地址&a,取得就是这块内存的首地址,a的类型长度就是4*10;这样成功的解释了&asizeof(a)的结果。也解释了&a+1跳跃整个数组。

    可是事情并没有这么简单,如果像上面那样理解,我们理所当然可以这样操作:

int c[10] ={0};

a = c;

也许你会争辩这样不合法,应为a是常量,“喂喂,哥们我们现在基于假设呢!”(事实上我真的希望我可以这么做,但现时这么残酷……

我们知道a表示的空间并不是不可变得,我们可以通过a[i] = 6;类似这样的语句来改变数组空间存储的值。所以说数组不是常量。

 

对这个问题的解释是,a标示整个数组,属于数组类型的情况只有以下三种:

Char a[] = “Hello”;

1) Sizeof(a);

2) &a;

3) 定义的同时初始化时(如用”Hello"来初始化);

其他任何时候数组名a退化为数组首元素的地址,它成为一个地址值的标签,它由一个“类标量”,退化为一个字面值(带类型的字面值)。(不知道为什么要这么做?)

 

此处我们要引入几个概念:

1)变量 (如 int a; struct point p; int *p;

2)常量  ( const int a; const int *p;) 

3)字面值常量 ( 5 a’ “Hello” )

 

下面我们讨论一下”Hello world!”的返回值。

我们先看一段代码:

int main()

{

     char *p = "Hello world";

}

.file     "Hello.c"

     .section  .rodata

.LC0:

     .string   "Hello world"

     .text

.globl main

     .type     main, @function

main:

     pushl     �p

     movl %esp, �p

     subl $16, %esp

     movl $.LC0, -4(�p)

     leave

     ret

     .size     main, .-main

     .ident    "GCC: (GNU) 4.4.4 20100503 (Red Hat 4.4.4-2)"

     .section  .note.GNU-stack,"",@progbits

    经分析不难得出以共分配了16字节空间”Hello World”(12)+p(4)=16,

-16~-812字节用于存储”Hello World”,(具体如何不太清楚权且这么理

)$.LC0表示”Hello World”返回的常量指针字面值。我们了”Hello

World”返回了一个字面值指针常量,它其实应该和数组名差不多。

    下面我们再看一个例子:

int main()

{

     char *p = "Hello world";

     char s[] = "Hello world";

     int data = 100;

    

     int *i=&"Hello World";

     int *j=&"Hello World"[0];

     char a = "Hello"[0];

    

     if ("Hello" == &"Hello" && "Hello" == &"Hello"[0])

     printf("True!\n");

    

     if (s == &s && s == &s[0])

     printf("Array True!\n");

 

     printf("%x\n","Hello");

     printf("%x\n","Hello"+1);

     printf("%x\n",&"Hello");

     printf("%x\n",&"Hello"+1);

     printf("%x\n",&"Hello"[0]);

     printf("%x\n",&"Hello"[0]+1);

    

     printf("%c\n",*"Hello");

     printf("%s\n","Hello");

     printf("%s\n","Hello"+1);

     printf("%s\n",&"Hello");

     printf("%s\n",&"Hello"+1);

    

     printf("%d\n",sizeof(*"Hello"));

     printf("%d\n",sizeof("Hello"));

     printf("%d\n",sizeof(&"Hello"));

     printf("%d\n",sizeof(&"Hello"[0]));

    

}

输出:

True

Array True

 

804865e

804865f

804865e

8048664

804865e

804865f

H

Hello

ello

Hello

%x  -------未知字符串,越界

 

1

6

4

4

从输出可以看出完全符合数组名之特性。

也就是说字符串(如”Hello”)返回的就是数组名同种东西。

话说向数组名这么极品的东西很少啊!

 

总之变量和常量都是标识一段内存空间,它包含两层信息:内存空间地址+内存中存储的字面值常量

数组名严格来讲是一个普通变量(本质上是标量),但是由于编译器的特殊实现(当然是c作者的意愿)它在大多数时候都会退化成地址字面值(或称之为指针常量字面值----此时他就是一个地址标签,并不表示存储空间本身)。

如:

int a[10];

int *const p =a;

a的地位就是p容器(内存空间可以看成一个容器,容器具有编号(地址)+值)存储的值(别的容器的编号即地址)。

*a *p 结果虽然一样,但是工作机制不一样。

a 在汇编代码中表示的是一个立即数,相当于aó0x80484eff(两者是同一个东西)所以可以直接解引用操作。而p是一个容器,我们需要操作的是容器中的数而不是标签本身,因为变量在表达式中属于右值范畴,*p不表示对&p解引用,如果是这样那就也无需多余操作,因为在汇编中p其实就代表&p,汇编中只能记住地址通过地址取数。但是这里c中含义却是对p的内容解引用。所以步骤大致是:

load a from p

*a

这多一步操作,任何操作本质上必须直接作用于字面值操作数,所以如果是字面值的话很简单搞定,如果是变量必须先读取字面值。应用组成原理的微指令分析会更加清晰。

好了,对c数组名初探到此为止,下一篇我们来讨论下数组名a,&a,&a[0]的类型,以及对二位数组的扩展,并初步探讨一下二维指针。

&必须作用于标量,不可作用于字面值,不信你试一下啊&(&a)

   

 

 

 


    


    
    
   

你可能感兴趣的:(数组名初探)