C语言数组名与数组指针,你一定不知道的东西!

数组是指针的基础,多数人就是从数组的学习开始指针的旅程的。下面我节选一些在各种论坛和文章和书籍里经常见到的关于数组的文字:

  1. “一维数组是一级指针”
  2. “二维数组是二级指针”
  3. “数组名可以作为指针使用”
  4. “数组名就是..........的常量指针”
  5. “数组名就是..........的指针常量”

我该不该说这些是误人子弟呢?可能是他们自己都没有搞清楚,又或者是他们为了便于初学者的理解。

实际上数组名永远都不会是指针!这个结论也许会让你震惊,但它的确是事实。数组名、指针、地址这几个概念虽然是基础中的基础,但它们恰恰是被混淆和滥用得最多的概念,把数组名说成指针,是一个概念性的错误,实质是混淆了指针与地址两个概念的本质。

指针是C语言具有低级语言特征的最直接的证据。在汇编语言里面,指针的概念随处可见。比如SP,SP寄存器又叫堆栈指针,它的值是地址,由于SP保存的是地址,并且SP的值是不断变化的,因此可以看作一个变量,而且是一个地址变量。地址也是C语言指针的值,C语言的指针跟SP这样的寄存器虽然不完全一样,但原理却是相通的。C语言的指针也是一种地址变量,C89明确规定,指针是一个保存对象地址的变量。这里要注意的是,指针跟地址概念的不同,指针是一种地址变量,通常也叫指针变量,统称指针。而地址则是地址变量中保存的值

画个图吧!

大概就是这个样子,方框内表示当前变量内保存的值,方款外表示该值存放在地址的哪块区域。

 

数组名又应该如何理解呢?用来存放数组的区域是一块在栈中静态分配的内存(非static),而数组名是这块内存的代表,它被定义为这块内存的首地址。这就说明了数组名是一个地址,而且,还是一个不可修改的常量,完整地说,就是一个地址常量。数组名跟枚举常量类似,都属于符号常量。数组名这个符号,就代表了那块内存的首地址。注意了!不是数组名这个符号的值是那块内存的首地址,而是数组名这个符号本身就代表了首地址这个地址值,它就是这个地址,这就是数组名属于符号常量的意义所在。由于数组名是一种符号常量,因此它是一个右值,而指针,作为变量,却是一个左值,一个右值永远都不会是左值,那么,数组名永远都不会是指针!不管什么话,只要说数组名是一个指针的,都是错误的!

总之要牢牢记住,数组名是一个地址,一个符号地址常量,不是一个变量,更不是一个作为变量的指针!

 

那么数组名跟指针之间到底有什么区别呢?

第一

在声明上,除了作为函数参数的数组名总是编译器转化成指针,其他情况下,数组名就是数组名,指针就是指针,二者不能混淆,你不能在一个文件中定义一个数组,而在另一个文件中把它声明成一个指针。

char a[]; //定义一个数组a
extern char *a; //在另一个文件中将a声明成一个指针

在编译器中,符号表用来存放C语言中有关标识符的属性信息,这些信息集中反应了标识符的特征属性。等到词法分析到代码声称的各个阶段的时候,编译器需要根据源代码提出的要求,从表中获取不同标识符的不同属性。值得注意的是,数组标识符的属性和指针是完全不同的。因此,在一个文件中定义一个数组的时候,编译器会把它记录在符号表中,而在编译器分析另一个文件中的声明时,通过符号的语义检查发现不一致。也许这样的声明可以编译通过(在gcc下通过了),但是把一个数组名当成一个指针来使用,别指望它能运行起来。

第二

指针是一个变量,而数组名不是。数组名是数组的首地址,即它本身就是一个地址,对应到汇编语言级别就是一个常量,一个固定的数(地址)。因此数组名不能进行++,–等运算。

在大多数编译器中,对数组的引用a[i]总是被编译器改写成*(a+i)的格式。也就是说,编译器每遇到a[i],都会把它当作*(a+i)来处理。我们都知道,*addr表示内存中(addr)的位置存储的值,比如*0×8048000就表示地址为0×8048000的内存中所存储的值。所以a[i]就表示a的值加上i所得到的数作为一个内存地址里面所存储的值。

// 对数组名的取值就类似于下面这个样子
int a[10];
// 我们假设a这个符号常量值为0x143232

int b = *a;
// 编译器编译之后的值就语义就变化成了
int b = *((int*)143232)

验证一下看看是不是

int main()
{
    int a[5];
    for(int i=0;i<5;i++){
        a[i] = i;
    }
    printf("%d",*a);
    printf("%d\n",*(a+1));
    printf("%d\n",a);
    printf("%d\n",&a);
    // 这里的6422244是我上面输出a的时候得到的值,不同机器运行不同,要修改成对应的值
    printf("%d\n",*((int*)6422244));
    int* c = &a;
    printf("%d\n",c+1);
}

输出结果,正确访问到了数组的值

01
6422244
6422244
0
6422248

至于为什么&a的值和a的值一样,这也是编译器进行了处理,获取出来的是同样的值,只不过值的类型变成了一个指针,且指针步长就等于数组的长度。上面我们代码中是用一个int*的变量c来接收&a的值,他的指针步长还是4字节。如果用int (*c)[5]来接收,那么其指针步长就是20了,指针步长不知道的可以看我的另外一个博客

那么a的值是什么呢?编译器在做词法分析和语法分析的时候,遇到一个数组的定义,就会把数组的有关信息汇集在一个叫做“内情向量”或“信息向量”的表各种,其中的信息包括数组的类型,维数,各维的上、下边界,以及数组的首地址,然后将这个“内情向量”相关信息存储在符号表中。数组定义后位置就是固定的,因此其首地址可以在编译阶段得到。当编译器到达代码生成的各阶段时,每次遇到数组名这个标识符,编译器都会从符号表中取出这个数组的首地址,然后用这个地址来替代数组名,例如,假设数组a起始地址是0×8048000,则a[1]就会被编译器转化成*(0×8048000+1),因此在生成的汇编代码中,数组名已经完全被转化成一个常量,一个固定的数(地址)。

但是,对于指针p,它是一个变量,其值存储在地址&p中,这个值在编译时是不能得到的。因为是变量,所以指针可以作为表达式中的左值操作,如++或--,而被认为是常量的数组名却不可以,正如你可以骑走一辆自行车,但是不能骑走一棵树。另外,C语言把数组下标改写成指针偏移量的根本原因是指针和偏移量是底层硬件所使用的基本模型。

第三

对数组的引用,如a[i],或*(a+1),需要访存一次;而对指针的引用,如*(p+1),需要访存两次。

如果理解了第二条的解释,这个应该就不难理解。因为a被认为是常数,所以取*(a+1)的值只需将a所表示的常数加1,然后从得到的地址里访存取一次即可。而对于指针,需要先从&p这个地址里把p的值取出来,然后加1,再从得到的地址里访存取一次,一共需要两次访存。

第四

假设a是一个数组名,而p是一个指针,当你使用 a 和 &a 时,得到值是一样的,都是数组的起始地址。而使用 p 和 &p 时,得到的值是不一样的, p 表示指针 p 所指向的地址,而 &p 表示 p 这个变量的地址。再假设

p = a;

则 p 就表示数组a的起始地址,而&p是存储数组a的起始地址的那个地址。这是因为编译器把a当成数组首地址,而&a当作数组第一个元素的地址,因此得到的值是一样的。

另外,sizeof(a)得到的是a所表示的数组的大小,而sizeof(p)得到的是指针的大小。

 

总的来说,这一切的一切都是编译器在其中起作用。

 

  • 6
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值