C语言 数组名不是首地址指针
今天上计算机系统课的时候老师讲到了C中的聚合类型的数据结构。在解释数组名的时候说“数组名是一个指针,指向该数组的第一个元素”,附上ppt(第二行):

我觉得这是不正确的,是一个常见的由“简化”产生的错误,数组名 != 指针。数组名是一个标识符,它标识出我们之前申请的一连串内存空间,而且这个空间内的元素类型是相同的——即数组名代表的是一个内存块及这个内存块中的元素类型 。只是在大多数情况下数组名会“退化”(C标准使用的decay和converted这两个词)为指向第一个元素的指针。 而指针不是一种聚合类的数据结构,它保存着某一种类型的对象的地址(void*除外),也说它指向这个对象。我们可以通过这个地址访问这个对象。用一个图来解释,其中a代表了整个我们声明的内存块,p仅仅指向了一个char类型的对象:

C99 6.3.2.1 Lvalues, arrays, and function designators 中第三段是这样说的:
Except when it is the operand of the sizeof operator or the unary & operator, or is a
string literal used to initialize an array, an expression that has type ‘‘array of type’’ is
converted to an expression with type ‘‘pointer to type’’ that points to the initial element of
the array object and is not an lvalue. If the array object has register storage class, the
behavior is undefined.
译:除了在使用sizeof和&运算符或者使用字符串字面量初始化数组之外,一个含有数组名的表达式会转化为含有指向首元素的表达式,并且转化后不是一个左值(这也是为什么我们不能修改这个标志符,例如val++,所以有的人也会说数组名是一个const指针,从本质上说这也是错的)。如果数组的存储类型是寄存器的话,行为是未定义的。(估计也没人这么做吧。。)
下面我举5个例子,123展示了数组名不是指针的情况,45表现的是数组名“退化”为指针:
本机环境

1.sizeof运算符(另外提一点,sizeof不是函数而是运算符)

可以看到,sizeof(a)打印出了整个数组的大小而非一个指针的大小,说明它不是一个指针。
2.&运算符

如果按照”数组名就是指针”的思想来,&a应该产生一个int**类型的指针,但是编译器报了p1的警告:指针类型不兼容,而p2却没有报错,那么p1和p2的区别在哪呢?
p1是一个指向一个指向整数指针的指针,如果我们进行p1++运算,得到的将是p1+8(我是64位环境)。而p2表示的是一个指向一个元素类型为整数,元素个数为5的内存块的指针 ,如果我们进行p1++运算,得到的将是p1 + (4*5)。这也是为什么编译器会报p1的警告。
3.使用字符串字面量初始化数组
就用上面的图举例子,如果我们声明:
char a[] = "hello";
char *p = "hello";
对于第一行,其等价于char a[6] = {'h', 'e', 'l', 'l', 'o', '\0'} ,编译器会自动分配合理的空间,最终在内存中是这么个情况:

那有什么区别呢?
访存方式和地区不一样,例如,a[0]和p[0]都是'h',但是a[0]的操作是:来到a这个内存块(大小为6字节) -> 取出第一个元素(偏移量为0),而且这个元素是在栈中的。而p[0]的操作是:来到p这个内存块(大小为8字节,因为是64位环境),取出p的值,通过p获取对于对象(一个字节)的值,而且这个对象是在.data段中的! (并且是只读的)
4.算术运算与数组取下标操作符
在作为右值参与运算的时候,数组名会自动”退化“为指向首元素的指针,例如:
char a[] = "hello";
char *p = a + 1;
a会由char [5]类型退化为char *类型,所以这是可行的。
而我们常见的数组取下标操作符,c标准中对它的定义是等价于*(p + offset)运算。也是就说,你写a[3]其实等价于*(a+3),可以看到括号内是一个算术运算,于是a“退化”为一个指针,随后参与进行计算和解引用。有趣的是,由于加法的交换律,我们也可以写成*(3+a),也是就3[a]。

不过平常最好别这么写,不然别人会认为你在炫技或者脑袋有问题。。。
5.函数调用传递数组
我们学在给函数传递数组的时候,经常会听到“按值传递机制和按引用传递机制 ”这样的说法(网上也有很多),即传递数组是“按引用传递的”,这也是为什么传递数组在函数内读写数组,退出函数后数组会发生变化的原因。
其实,c语言传参只有一种,就是传递值。
那么,数组为何被改变呢?
假设数组为int a[5], 对于函数原型,我们可以有以下几种写法:
void test(int a[5])
void test(int [5])
void test(int*)
许多人认为,第一种写法是最好的,清晰(这个是对的,对于代码阅读者而言)而且可以告诉编辑器这个数组的大小。但是,这三种声明在编译器看来只有一种:void test(int*), 所以那个5不过是一个心里安慰。
所以说,test函数得到的是一个值为a“退化”后指向数组首元素(内存块首地址)的指针 ,在test内部是不知到a是一个数组的,它仅仅认为它是一个整数指针。但是我们依然可以使用数组取下标操作符进行运算,因为即使a是一个数组名,它被用作数组取下标操作符的操作数时也会“退化”为指针(参见4)。
例如:

可以看到,在main函数中,编译器认为a代表是一个数组(sizeof大小为4*5字节),而在test函数内部,a变成了一个指向整数的指针。(gcc发现了这个隐晦的可能导致错误的地方,给出了一个警告)
总之,指针就是保存地址的一个内存块,数组名就是一连串相同类型元素组成的内存块的标识符,两个不是等价的。在大多数实际使用的情况下数组名会“转化”为指向首元素的指针,也可以这么“简单”的理解,但是我们还是要记住理解他们的本质差别。
另外推荐一个工具cdecl ,它可以将很多复杂的声明用语句来解释,例如int ((foo)(const void *))[3]这个很难明白的声明:

参考
C语言 数组名不是首地址指针的更多相关文章
- C语言——数组名、取数组首地址的区别(一)
目录: 1. 开篇 2. 论数组名array.&array的区别 3. array.&array的区别表现在什么地方 4. 讨论 5. 参考 1.开篇 很多博客和贴吧都有讨论这个话题, ...
- C/C++——C语言数组名与指针
版权声明:原创文章,转载请注明出处. 1. 一维数组名与指针 对于一维数组来说,数组名就是指向该数组首地址的指针,对于: ]; array就是该数组的首地址,如果我们想定义一个指向该数组的指针,我们可 ...
- C语言 数组做函数参数退化为指针的技术推演
//数组做函数参数退化为指针的技术推演 #include<stdio.h> #include<stdlib.h> #include<string.h> //一维数组 ...
- C语言 数组名不是指针
今天上计算机系统课的时候老师讲到了C中的聚合类型的数据结构.在解释数组名的时候说"数组名是一个指针,指向该数组的第一个元素",附上ppt(第二行): 我觉得这是不正确的,是一个常见 ...
- c语言 数组名是常量指针
//数组名是常量指针 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include ...
- [skill] C语言数组名到底是个啥
1. 正常情况下,数组名是个地址常量. 2. sizeof(数组名)的时候,数组名就代表数字名,其类型为 type array[], 返回数组元素个数. 3. 除了2的情况以外,可以理解为一个指针常量 ...
- C语言数组篇(五)多级指针和二维数组指针的区别
多级指针 以二级指针为例 二级指针的由来是 指针数组 的指针形式. int *p[10] 读取的顺序是 p[] --> 10个空间的数组 * p[] --> 这10个空间的数组里面存放 ...
- C语言数组名取地址。。。
int main(){ int a[5] = { 1, 2, 3, 4, 5 }; printf("%08X ,%08X ,%08X ,%08X", a, &a, a + ...
- C语言数组的学习
什么是数组? 在程序设计中,为了处理方便,把具有相同类型的若干变量按有序的形式组织起来.这些按序排列的同类数据元素的集合称为数组. 在C语言中,数组属于构造数据类型.一个数组可以分解为多个数组元素,这 ...
随机推荐
- Linux服务之 Nginx安装
安装包下载: 链接:https://pan.baidu.com/s/1yna9nvT_9iYw4_0uVQRgFw 提取码:nurm yum -y install gcc automake autoc ...
- $.store.book[?(@.title =~ /^.*Honour.*$/i)]
{ "store": { "book": [ { "category": "reference", "auth ...
- ES6工作中常用知识点
好久不动笔了 第一个知识点:import 和 export import导入模块.export导出模块 //全部导入 import people from './example' //有一种特殊情况, ...
- 牛客假日团队赛1 I.接机
链接: https://ac.nowcoder.com/acm/contest/918/I 题意: 一场别开生面的牛吃草大会就要在Farmer John的农场举办了! 世界各地的奶牛将会到达当地的机场 ...
- 华东交通大学2017年ACM“双基”程序设计竞赛 1008
Problem Description 长度为 n 的序列,把它划分成两段非空的子序列,定义权值为:两段子序列的最大值的差的绝对值.求可能的最大的权值.数据范围:2 <= n <= 10^ ...
- python入门之os模块
import os os.getcwd() 同Linux的pwd os.chdir("/opt") 同Linux的cd os.curdir 返回当前目录 os.pardir 获取上 ...
- Spring4.x、SpringMVC和DButils整合
tomcat 8.Spring 4.X.JDK1.8 需要jar包: 1)日志组件:log4j # debug < info < warn < error log4j.rootLog ...
- Pod管理的iOS项目修改工程名
声明:本文大部分内容来自于以下网址,其余的部分是自己尝试的总结和补充. http://www.jianshu.com/p/5f088acecf64 完整修改iOS工程名1 http://www.cnb ...
- jQuery动态添加元素,并提交json格式数据到后台
参考:https://www.cnblogs.com/shj-com/p/7878727.html 下载 下载该插件的地址是:http://www.bootcdn.cn/jquery.serializ ...
- springboot 学习笔记(八)
springboot整合activemq,实现queue,topic同时支持 1.JMS中定义了两种消息模型:点对点(point to point, queue)和发布/订阅(publish/subs ...