今天上计算机系统课的时候老师讲到了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]这个很难明白的声明:

参考

ISO/IEC 9899:TC3

Arrays and Pointers

stackoverflow1

stackoverflow2

C语言 数组名不是首地址指针的更多相关文章

  1. C语言——数组名、取数组首地址的区别(一)

    目录: 1. 开篇 2. 论数组名array.&array的区别 3. array.&array的区别表现在什么地方 4. 讨论 5. 参考 1.开篇 很多博客和贴吧都有讨论这个话题, ...

  2. C/C++——C语言数组名与指针

    版权声明:原创文章,转载请注明出处. 1. 一维数组名与指针 对于一维数组来说,数组名就是指向该数组首地址的指针,对于: ]; array就是该数组的首地址,如果我们想定义一个指向该数组的指针,我们可 ...

  3. C语言 数组做函数参数退化为指针的技术推演

    //数组做函数参数退化为指针的技术推演 #include<stdio.h> #include<stdlib.h> #include<string.h> //一维数组 ...

  4. C语言 数组名不是指针

    今天上计算机系统课的时候老师讲到了C中的聚合类型的数据结构.在解释数组名的时候说"数组名是一个指针,指向该数组的第一个元素",附上ppt(第二行): 我觉得这是不正确的,是一个常见 ...

  5. c语言 数组名是常量指针

    //数组名是常量指针 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include ...

  6. [skill] C语言数组名到底是个啥

    1. 正常情况下,数组名是个地址常量. 2. sizeof(数组名)的时候,数组名就代表数字名,其类型为 type array[], 返回数组元素个数. 3. 除了2的情况以外,可以理解为一个指针常量 ...

  7. C语言数组篇(五)多级指针和二维数组指针的区别

    多级指针   以二级指针为例 二级指针的由来是 指针数组 的指针形式. int *p[10] 读取的顺序是 p[] --> 10个空间的数组 * p[] --> 这10个空间的数组里面存放 ...

  8. C语言数组名取地址。。。

    int main(){ int a[5] = { 1, 2, 3, 4, 5 }; printf("%08X ,%08X ,%08X ,%08X", a, &a, a + ...

  9. C语言数组的学习

    什么是数组? 在程序设计中,为了处理方便,把具有相同类型的若干变量按有序的形式组织起来.这些按序排列的同类数据元素的集合称为数组. 在C语言中,数组属于构造数据类型.一个数组可以分解为多个数组元素,这 ...

随机推荐

  1. heap 堆

    实现了交换.向上维护,向下维护的原子功能,其它插入.删除.修改的功能应该不在话下. 于是有了代码:(luogu3378模板题) // luogu-judger-enable-o2 #include & ...

  2. 与pocket 对接技术文档

    同步每日新增用户接口(kwai 提供) 注释:该接口 每天0点(北京时间)之后 向kwai服务器同步前一天  新增的IMEI号 url:http://m.kwai.com/rest/o/pocket/ ...

  3. Python之PIP安装

    Python有两个著名的包管理工具easy_install.py和pip.Python2.7的安装包中自带了easy_install.py,而pip需要手动安装.而在Python3.5之后都是默认安装 ...

  4. HIVE获取表的大小和修改日期

    ### 获取表的大小 hdfs dfs -du /user/hive/warehouse/database_name.db/ > 360_du ### 获取表的修改日期 hdfs dfs -ls ...

  5. CollabNet Subversion Edge 迁移的方法

    服务器迁移或重新搭建时,数据迁移方法,安装配置在https://www.cnblogs.com/pinpin/p/9889362.html种 这里只是迁移用户和数据,做个备注而且,比较简单所以不截图了 ...

  6. Java面向对象_Object类

    Object类 是类层次结构的根类,每个类都是用Object类作为超类,所有对象(包括数组)都实现这个类的方法.所有类都是Object类的子类. 下面先说两个方法toString()和equals(O ...

  7. Unity3D游戏高性能战争迷雾系统实现

    一 效果图 先上效果图吧,这是为了吸引到你们的ヽ(。◕‿◕。)ノ゚ 战争迷雾效果演示图 战争迷雾调试界面演示图 由于是gif录制,为了压缩图片,帧率有点低,实际运行时,参数调整好是不会像这样一卡一顿的 ...

  8. pat1099. Build A Binary Search Tree (30)

    1099. Build A Binary Search Tree (30) 时间限制 100 ms 内存限制 65536 kB 代码长度限制 16000 B 判题程序 Standard 作者 CHEN ...

  9. Visual Studio 使用

    目录结构 solution_dir Debug: 存放Debug版本信息的.exe Release: Release的.exe .sln: visual studio 项目文件 project_dir ...

  10. rabbitmq的总结

    rabbitmq的术语 组件 Server(broker):接收客户端连接,实现AMQP消息队列的路由功能的进程.简单来说就是消息队列服务器实体. Vhost:虚拟主机,一个broker里可以开设多个 ...