设为首页 加入收藏

TOP

C/C++中数组与指针的关系理解(一)
2017-04-14 10:23:36 】 浏览:590
Tags:C/C 指针 关系 理解

长期以来,在C/C++中,数组名和指向数组首元素的指针常量到底是以一种什么关系,一直困扰着很多人。很多地方,甚至是一些教科书中都在说,“数组名就是一个指向数组首元素的指针常量”。但事实是,这是一种错误的说法!我们可以在很多场合中把数组名看作一个指向数组首元素的指针常量,但绝不能将这两者当成同一个东西。


数组是数组,指针是指针,这是两种不同的类型


数组既可以表示一种数据类型,也可以表示这种类型的一个对象(非面向对象之对象,下同),表示对象时也可以称之为数组变量,和其他类型变量一样,数组变量有地址也有值。


数组的地址就是数组所占据内存空间的第一块存储单元的编号,而数组的值是由数组所有元素的值构成。


数组名既不是指针,也不是指针变量,而是数组变量的名字,与数组名相对应的是指针变量的变量名,都是符号


空说费劲,看代码说话:


作为数组,a拥有可以存放10个int型数据的空间,可以将一个int型的值存储到a中任意一个元素中。但作为一个指针的p,只能存储一个地址。


sizeof 操作符可以获取到一种数据类型所占据的内存大小,指针类型在x64位机器上的大小是8,而数组类型的大小是所有元素大小之和,上例中即为10个int型的大小,是40。


可以将数组名赋值给一个指针,而赋值后的指针是指向数组首元素的,这让数组名看起来确像一个指针。


直接输出数组名会得到数组首元素的地址,这让人们误以为“数组名的值就是数组首元素地址“,符合指针的定义。


数组名可以像指针一样运算,对数组的索引和指针的运算看起来也是相同的。


输出:


数组的地址和数组首元素的地址虽然值相同,但意义不同。


值相同是因为,一个变量无论在在内存中占据多大空间,它的地址总是该空间第一个内存单元的地址。而数组的元素依次连续分布在整块数组空间中,数组空间的第一个内存单元被数组首元素占据,必然也同时是数组首元素所占空间的第一块空间单元,所以数组的地址与数组首元素的地址相同。


意义不同是因为,数组地址代表了整块数组所占据的内存空间,而数组首元素的地址只代表了首元素所占据的空间。


&a 表示取数组的地址,其结果是一个指向该数组的指针,它可以赋值给另一个同类型的指针。


&a[0]表示取数组首元素的地址,其结果是指向该数组首元素的指针,可以赋值给另一个同类型的指针。


注意:指向数组的指针和指向数组首元素的指针是两种不同类型的指针。


编译后运行,输如下:


我们发现,取数组地址(&a)得到的指针pa和取数组首元素(&a[0])得到的指针pi是两种不同类型的指针,pa是一个指向有三个int型元素的数组的指针,pi是一个指向int型对象的指针。虽然pi和pa的值相同,但所指的内存空间不同,pi所指的空间处于pa所指空间的内部,而且是内部最靠前的部分。pi和pa所指内存块的大小显然是不同的,因此我们看到pa+1并不等于pi+1。


由指针运算规则可知,pa+1的值就是pa所指空间的下一个空间的地址,所以pa+1的值就是pa的地址向后偏移一段后的地址,这个偏移量就是pa所指的数组a的大小,即12个内存单元。同样,pi+1的值是pi向后偏移4个单位(int型的大小)后的地址。


ps: 看到有些地方说地址就是指针,我觉得这个说法不对。地址就是地址,它是数据对象的一个属性,表明它在内存中的位置。指针本身也有地址,总不能说“地址的地址”吧?此外,指针不光带有地址信息,它还附带有所指对象的类型信息,这就是为什么指针知道如何准确的指向下一个地址。


C11标准中,6.3.2.1 [Lvalues, arrays, and function designators] 第3段,有如下表述:


同样的转换在C++中也是一样,在 C++11 标准(ISO/IEC 14882)的 4.2 Array-to-pointer conversion 一节中有如下表述:


可见,除了作为 sizeof 、**_Alignof** 和 & 这3个操作符的操作数以及用于初始化数组的串字面量外, 表达式中的数组都会被自动转换为指向其首元素的指针 ,转换而来的指针不是一个左值(lvalue)。因为这种转换丢失了数组的大小和类型,因此有一个专用的称谓叫做 “decay”


于是,所有的疑惑都有了最权威的解释。


我们发现,一旦有了 decay ,表达式中所有让一个数组看上去像个指针的现象都合情合理了。除赋值外,可用于指针的运算都可以适用于会发生转换的数组。不可赋值是因为先要转换,转换后不是左值。


ps:lvalue这个术语翻译成左值会过分强调它可以作为赋值操作符(=)的左操作数,实际上lvalue的核心在于location,而不是left,虽然它最初被命名为lvalue确是因为left。lvalue意味着在内存中有确切位置(location),可以定位(locator)。所以,数组被转换为指针后不是lvalue的原因是没有确切地址,而不能被赋值是结果。


可能读到这里会有一个疑问,那就是转换后不是左值不可以赋值,那么 a[0] = 'x'; 却怎么解释?注意,转换后得到左值的是a,而非a[0]。什么意思呢?把这个表达式中除去a的部分([0])看成是对转换后得到的指针的继续运算,结果就是数组第一个元素,有确切地址,那么a[0]整体就是一个左值了。 于是赋值成功!


值得注意的是,取址时取到的是数组地址而非转换后指针的地址,因为取址时数组不会发生转换,实际上,转换后得到的指针没有确切地址不是左值,是无法取到地址的。这里多说这么几句是因为,有一些博客中纠结于 “数组名作为一个指针常量有没有被分配空间?分配到的地址是什么?” 这样伤神的问题中。


C规范中指出了四种不发生转换的例外情况。


前三种情况是说数组作为 sizeof 、**_Alignof** 和 & 这3个操作符的操作数时是不会被转换为指针,这个较好理解,就是这三个操作符直接作用于数组时,不会发生转换,如 sizeof(a)&a 中的a都不会被转换。而像 &a[0] 这样的表达式中,&的优先级不是最高的,所以&的直接作用对象是 a[0] 这个子表达式,此时a转换为指针后进行运算,得到数组的第一个元素,&作用于这个元素后取得其地址,得到的最终结果指向数组首元素的指针。


下面的代码也说明了这种规则的关键是直接作用:


那么用于初始化数组的串字面量说的又是什么呢?


ISO/IEC 9899:201x ,6.4.5 [String literals] 节中对串字面量的规范可知,编译期间,串字面量所声明的多字节字符序列(multibyte character sequence)会先行合并(如果有多个相邻)并在结尾追加一个零值( '\0' ),然后用此序列初始化一个静态存储周期(static storage duration)的等长度字符数组


所以,串字面量作为一个原始表达式(Primary expressions),其结果是一个字符数组!因为地址可知,它是一个 lvalue 。


需要注意的是,程序中对这种数组试图修改的行为在C11标准中是未定义的,C11标准同样也没有说明对于内容相同的这种数组是否可以可以视为一个(只存储一份)[^ISO/IEC 9899:201x §6.4.5 para7]。


看下面的代码:


第一行代码中的串字面量 "abc" 的本质是一个长度为4

首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇C语言动态内存分配之malloc与real.. 下一篇C++设计成员变量可动态调整的动态..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目