今天上计算机系统课的时候老师讲到了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. 51nod1024(math+set)

    题目链接:https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1024 题意:中文题诶- 思路:要是能求出a^b的值来就好了. ...

  2. 洛谷P3380 【模板】二逼平衡树(树套树)(线段树+树状数组)

    P3380 [模板]二逼平衡树(树套树) 题目描述 您需要写一种数据结构(可参考题目标题),来维护一个有序数列,其中需要提供以下操作: 查询k在区间内的排名 查询区间内排名为k的值 修改某一位值上的数 ...

  3. 剑指offer刷题记录

    目录 二维数组中的查找 替换空格 从尾到头打印链表 反转链表 重建二叉树 用两个栈实现队列 旋转数组的最小数字 斐波拉切数列 跳台阶 变态跳台阶 矩形覆盖 二进制中1的个数 数值的整次方 链表中倒数第 ...

  4. QT的学习

    背景 最近正忙着做一个项目,由于之前对面向对象编程了解的非常少,所以导致项目的代码有很多不太清楚:看到代码的时候整个人是懵的.所以在国庆期间,结合着大神的博客看了一下面向对象编程,并学习了开发GUI应 ...

  5. kindle资源

    化繁为简!Kindle 漫画和电子书 资源汇总 & 用法 刚入Kindle那会,在网上翻看了不少文章,有的讲怎么下载电子书,有的讲怎么看漫画,有的讲怎么设置推送邮箱…… 好吧,我不想在用Kin ...

  6. netstat命令怎么查看端口是否占用

    转自:http://www.ahlinux.com/start/cmd/527.html netstat命令是一个监控TCP IP网络的非常有用的工具,它可以显示路由表.实际的网络连接以及每一个网络接 ...

  7. keil编译器

    1 数组比指针效率高. 2 v4.73不能查看局部变量的值:可先将C/C++选项下的优化级别设置成O0,查看完之后修改回去.因默认级别是2级.

  8. hdu2069(Coin Change)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=2069 Coin Change Time Limit: 1000/1000 MS (Java/Other ...

  9. Unity Gizmos可视化辅助工具

    所有gizmo绘制需要在脚本的OnDrawGizmos或OnDrawGizmosSelected里函数完成. OnDrawGizmos在每帧调用.所有在OnDrawGizmos中渲染的gizmos都是 ...

  10. Java面向对象_单例设计模式

    单例设计模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点 1.构造方法私有化 2.声明一个本类对象 3.给外部提供一个静态方法获取对象实例 两种实现方式:饿汉式和懒汉式 何种情况下使用呢?当 ...