今天上计算机系统课的时候老师讲到了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)打印出了整个数组的大小而非一个指针的大小,说明它不是一个指针。

  1. &运算符

如果按照”数组名就是指针”的思想来,&a应该产生一个int**类型的指针,但是编译器报了p1的警告:指针类型不兼容,而p2却没有报错,那么p1和p2的区别在哪呢?

p1是一个指向一个指向整数指针的指针,如果我们进行p1++运算,得到的将是p1+8(我是64位环境)。而p2表示的是一个指向一个元素类型为整数,元素个数为5的内存块的指针 ,如果我们进行p1++运算,得到的将是p1 + (4*5)。这也是为什么编译器会报p1的警告。

  1. 使用字符串字面量初始化数组

就用上面的图举例子,如果我们声明:

c 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段中的! (并且是只读的)

  1. 算术运算与数组取下标操作符

在作为右值参与运算的时候,数组名会自动”退化“为指向首元素的指针,例如:

c char a[] = "hello"; char *p = a + 1;

a会由char [5]类型退化为char *类型,所以这是可行的。

而我们常见的数组取下标操作符,c标准中对它的定义是等价于*(p + offset)运算。**也是就说,你写a[3]其实等价于*(a+3),可以看到括号内是一个算术运算,于是a“退化”为一个指针,随后参与进行计算和解引用。有趣的是,由于加法的交换律,我们也可以写成*(3+a),也是就3[a]。**

不过平常最好别这么写,不然别人会认为你在炫技或者脑袋有问题。。。

  1. 函数调用传递数组

我们学在给函数传递数组的时候,经常会听到“按值传递机制和按引用传递机制 ”这样的说法(网上也有很多),即传递数组是“按引用传递的”,这也是为什么传递数组在函数内读写数组,退出函数后数组会发生变化的原因。

其实,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发现了这个隐晦的可能导致错误的地方,给出了一个警告)


总之,指针就是保存地址的一个内存块,数组名就是一连串相同类型元素组成的内存块的标识符,两个不是等价的。在大多数实际使用的情况下数组名会“转化”为指向首元素的指针,也可以这么“简单”的理解,但是我们还是要记住理解他们的本质差别。

参考

ISO/IEC 9899:TC3

Arrays and Pointers

stackoverflow1

stackoverflow2

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

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

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

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

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

  3. C语言 数组名不是首地址指针

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

  4. sizeof数组名和字符指针是有区别的

    sizeof数组名和字符指针是有区别的. #include <stdio.h> #include <stdlib.h> void change(char url[]); int ...

  5. C/C++二维数组名和二级指针

    转载 :https://blog.csdn.net/wu_nan_nan/article/details/51741030  作者:吴一奇 1. 指针1.1 一个指针包含两方面:a) 地址值:b) 所 ...

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

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

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

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

  8. sizeof(数组名)和sizeof(指针)

    在做这道题时: 32位环境下,int *p=new int[10];请问sizeof(p)的值为()A.4              B.10              C.40           ...

  9. 别混淆了sizeof(数组名)和sizeof(指针)

    我们在挨个儿输出一个数组中的元素时,最常用的就是用一个for循环来实现,简单了事.比如类似下面的代码片段: for(i = 0; i< length; i++) { printf("数 ...

随机推荐

  1. 201521123018 《Java程序设计》第10周学习总结

    1. 本章学习总结 你对于本章知识的学习总结 2. 书面作业 一.inally 题目4-2 1.1 截图你的提交结果(出现学号) 1.2 4-2中finally中捕获异常需要注意什么? 答: 4-2中 ...

  2. Eclipse rap 富客户端开发总结(6) : 如何发布rap到tomcat

    1.先下载以来的打包插件 war products  输入下面的地址,选择相应的插件 新建一个 war product Configutation向导 下面的war  product Configut ...

  3. JSP-页面跳转大全

    转自:http://blog.sina.com.cn/s/blog_8c38b8b701013zzz.html (1). forward()方法 使用到javax.servlet.RequestDis ...

  4. testTenuringThreshold()方法的分析与问题处理

    代码如下: public class TestTenuringThreshold { private static final int _1MB = 1024 * 1024; /** * vm-arg ...

  5. C3P0 WARN: Establishing SSL connection without server's identity verification is not recommended

    c3p0的出现,是为了大大提高应用程序和数据库之间访问效率的. 它的特性: 编码的简单易用 连接的复用 连接的管理 今天在配置C3p0的时候出现了这个warn   原因是因为要验证SSL Wed Se ...

  6. JS(二)

    上周给大家介绍了一下JS基础中一点东西,今天给大家介绍一下JS基础中一个重要部分,循环和函数. 04-JS中的循环结构 一.[循环结构的步骤] 1.首先要先声明循环变量. 2.判断循环条件 3.执行循 ...

  7. 中国移动飞信WAP登陆分析及脚本

    中国移动飞信WAP网页版 http://f.10086.cn/im5/ 用WAP飞信登录并向好友发送信息,同时用wireshark抓包. 1.过滤POST表单提交数据包(wireshark规则: ht ...

  8. 常用git指令

    git checkout -b newBranchName //与当前分支内容相同! git checkout -b 本地分支 origin xxx//远程分支 在本地新建一个分支,并把远程分支的代码 ...

  9. Hive简记

    在大数据工作中难免遇到数据仓库(OLAP)架构,以及通过Hive SQL简化分布式计算的场景.所以想通过这篇博客对Hive使用有一个大致总结,希望道友多多指教! 摘要: 1.Hive安装 2.Hive ...

  10. Gate One——用web展示Terminal(安装)

    Gate One可以用web来展示Terminal,虽然存在一些小缺陷,基本功能都还可以的,有兴趣的可以折腾一下. 安装环境: 系统:RHEL 6.1 ,系统自带python 2.6.6 下载需要安装 ...