今天上计算机系统课的时候老师讲到了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. python 之 函数 基础2

    5.36 命名关键字 什么是命名关键字参数? 格式:在*后面参数都是命名关键字参数 特点: 1 必须被传值 2 约束函数的调用者必须按照key=value的形式传值 3 约束函数的调用者必须用我们指定 ...

  2. promise封装小程序的请求类(request,清爽易懂)

    话不多说直接上代码,清爽易懂: import { config } from '../config.js' const tips = { 1:'抱歉出现了一个错误', 2:'网络错误', 1005:' ...

  3. mac安装scrapy

    Mac自带python2.7,所以直接安装scrapy.默认安装了Xcode总共分以下几步:1.安装 homebrew.wget2.安装pip3.安装scrapy 安装homebrew在termina ...

  4. (反NIM)

    题目大意是和普通的NIM游戏一样,但是却是取到最后一个是输的,天真的以为就是反过来,其实并不是这样的 结论 先手必胜的条件为 ①:所有堆的石子数均=1,且有偶数堆. ②:至少有一个堆的石子数>1 ...

  5. lifecycle-mapping-metadata.xml

    <?xml version="1.0" encoding="UTF-8"?> <lifecycleMappingMetadata> &l ...

  6. Python 的execfile用法

    可以直接执行脚本 而import是将脚本导入另一个文件里,可以看 http://docs.python.org/2/library/functions.html 例如一个Python文件 a.py: ...

  7. Python3.5 调用Ansible 执行命令

    ansible.py #!/usr/bin/env python3 # -*- coding: utf-8 -*- import os import tempfile from collections ...

  8. SQL海量数据读写性能优化

    这是给某数据中心做的一个项目,项目难度之大令人发指,这个项目真正的让我感觉到了,商场如战场,而我只是其中的一个小兵,太多的战术,太多的高层之间的较量,太多的内幕了.具体这个项目的情况,我有空再写相关的 ...

  9. php 转码函数 你还在用iconv吗?-- 解决sqlserver插入中文失败问题

    文章来源 :http://www.veryhuo.com/a/view/41348.html 这次给客户同步sqlserver数据,临时搭的 PHP Query Analyzer 插入某些中文一直有些 ...

  10. OracleJDK与OpenJDK的区别和联系

    OpenJDK原是SunMicrosystems公司为Java平台构建的Java开发环境(JDK)的开源版本,完全自由,开放源码.OracleJDK里面包含的JVM是HotSpotVM,HotSpot ...