数组和指针概念的联系与区分的经典文档.docx
- 文档编号:9198320
- 上传时间:2023-05-17
- 格式:DOCX
- 页数:53
- 大小:43.28KB
数组和指针概念的联系与区分的经典文档.docx
《数组和指针概念的联系与区分的经典文档.docx》由会员分享,可在线阅读,更多相关《数组和指针概念的联系与区分的经典文档.docx(53页珍藏版)》请在冰点文库上搜索。
数组和指针概念的联系与区分的经典文档
第一章数组与指针概念剖析
数组与指针生来就是双胞胎,多数人就是从数组的学习开始指针的旅程的。
在学习的过
程中,很自然就会经常听到或见到关于数组与指针的各种各样的看法,下面我节选一些
在各种论坛和文章里经常见到的文字:
“一维数组是一级指针”
“二维数组是二级指针”
“数组名是一个常量指针”
“数组名是一个指针常量”
........................
这些文字看起来非常熟悉吧?
类似的文字还有许多。
不过非常遗憾,这些文字都是错误
的,实际上数组名永远都不是指针!
这个结论也许会让你震惊,但它的确是事实。
但是,
在论述这个问题之前,首先需要解决两个问题:
什么是指针?
什么是数组?
这是本章的
主要内容,数组名是否指针这个问题留在第二章进行讨论。
看到这里,也许有人心里就
会嘀咕了,这么简单的问题还需要说吗?
int*p,a[10];不就是指针和数组吗?
但是,
笔者在过往的讨论过程中,还真的发现有不少人对这两个概念远非清晰,这会妨碍对后
面内容的理解,所以还是有必要先讨论一下。
什么是指针?
一种普遍存在的理解是,把指针变量理解成就是指针,这种理解是片面的,
指针变量只是指针的其中一种形态,但指针并不仅仅只有指针变量。
一个指针,包含了
两方面的涵义:
实体(entity)和类型。
标准是这样描述指针类型的:
6.2.5Types
Apointertypemaybederivedfromafunctiontype,anobjecttype,oran
incompletetype,calledthereferencedtype.Apointertypedescribesanobject
whosevalueprovidesareferencetoanentityofthereferencedtype.Apointer
typederivedfromthereferencedtypeTissometimescalled‘‘pointertoT’’.
Theconstructionofapointertypefromareferencedtypeiscalled‘‘pointer
typederivation’’.
请留意第二句所说的内容:
指针类型描述了这样一个对象,其值为对某种类型实体的引
用。
标准在这里所用的措词是指针类型描述了一个对象。
再来看看标准关于取址运算符&的规定:
6.5.3.2Addressandindirectionoperators
Semantics
Theunary&operatorreturnstheaddressofitsoperand.Iftheoperandhastype
“type”,theresulthastype“pointertotype”.......Otherwise,theresultis
apointertotheobjectorfunctiondesignatedbyitsoperand.
这个条款规定,&运算符的结果是一个指针。
但问题是,&表达式的结果不是对象!
标准
自相矛盾了吗?
当然不是,这说明的是,指针的实体有对象与非对象两种形态。
我们常说的指针变量只是指针实体的对象形态,但对象与非对象两种形态合起来,才是
指针的完整涵义,就是说,无论是否对象,只要是一个具有指针类型的实体,都可以称
之为指针,换言之,指针不一定是对象,也不一定是变量。
后一种情况,指的是当需要
产生一个指针类型的临时对象时,例如函数的传值返回或者表达式计算产生的中间结
果,由于是一个无名临时对象,因此不是变量。
在C++中,由于引入了OOP,增加了一种也称为“指针”的实体:
类非静态成员指针,虽然
也叫指针,但它却不是一般意义上的指针。
C++标准是这样说的:
3.9.2Compoundtypes
.......Exceptforpointerstostaticmembers,textreferringto“pointers”does
notapplytopointerstomembers..........
接下来,该谈谈数组了。
数组是一种对象,其对象类型就叫数组类型。
但笔者发现有个
现象很奇怪,有些人根本没有数组类型的意识,不过也的确有些书并没有将数组作为一
个类型去阐述,也许原因就在于此吧。
数组类型跟指针类型都属于派生类型,标准的条
款:
6.2.5Types
Anarraytypedescribesacontiguouslyallocatednonemptysetofobjectswith
aparticularmemberobjecttype,calledtheelementtype.Arraytypesare
characterizedbytheirelementtypeandbythenumberofelementsinthearray.
Anarraytypeissaidtobederivedfromitselementtype,andifitselement
typeisT,thearraytypeissometimescalled“arrayofT”.Theconstruction
ofanarraytypefromanelementtypeiscalled“arraytypederivation”.
数组类型描述了某种对象的非空集合,不允许0个元素,我们有时候看见某个结构定义
内部定义了一个大小为0的数组成员,这是柔性数组成员的非标准形式,这个留在第八
章讲述。
数组类型的语法(注意不是数组对象的声明语法)是elementtype[interger
constant],例如对于
inta[10];
a的数组类型描述就是int[10]。
数组名作为数组对象的标识符,是一个经过“隐式特例化”处理的特殊标识符。
整数对象
的标识符、浮点数的标识符等等虽然也是标识符,但数组名与之相比却有重大的区别。
计算机语言存在的目的,是为了将人类的自然语言翻译为计算机能够理解的机器语言,
让人类更加容易地利用和管理各种计算机资源,易用是思想,抽象是方法,语言将计算
机资源抽象成各色各样的语言符号和语言规则,数组、指针、整数、浮点数等等这些东
西本质上就是对内存操作的不同抽象。
作为抽象的方法,可以归纳为两种实现,一是名
字代表一段有限空间,其内容称为值;二是名字是一段有限空间的引用,同时规定空间
的长度。
第一种方法被各种计算机语言普遍使用,在C/C++中称为从左值到右值的转换。
但数组不同于一般的整数、浮点数对象,它是一个聚集,无法将一个聚集看作一个值,
从一个聚集中取值,在C/C++的对象模型看来缺乏合理性,是没有意义的。
在表达式计
算的大多数情况中,第一种方法并不适合数组,使用第二种方法将数组名转换为某段内
存空间的引用更适合。
因此,与一般标识符相比,数组名既有一般性,也有特殊性。
一般性表现在其对象性质
与一般标识符是一样的,这种情况下的数组名,代表数组对象,同时由于符合C/C++的
左值模型,它是一个左值,只不过是不可修改的,不可修改的原因与上一段中叙述的内
容相同,通过一个名字试图修改整个聚集是没有意义的;而特殊性则反映在表达式的计
算中,也就是C/C++标准中所描述的数组与指针转换条款,在这个条款中,数组名不被
转换为对象的值,而是一个符号地址。
现在来看看标准是如何规定数组与指针的转换的:
C89/90的内容:
6.2.2.1Lvaluesandfunctiondesignators
Exceptwhenitistheoperandofthesizeofoperatorortheunary&operator,
orisacharacterstringliteralusedtoinitializeanarrayofcharactertype.
orisawidestringliteralusedtoinitializeanarraywithelementtype
compatiblewithwchar-t,anlvaluethathastype“arrayoftype”isconverted
toanexpressionthathastype“pointertotype”thatpointstotheinitial
elementofthearrayobjectandisnotanlvalue.
C99的内容:
6.3.2.1Lvalues,arrays,andfunctiondesignators
Exceptwhenitistheoperandofthesizeofoperatorortheunary&operator,
orisastringliteralusedtoinitializeanarray,anexpressionthathastype
“arrayoftype”isconvertedtoanexpressionwithtype“pointertotype”that
pointstotheinitialelementofthearrayobjectandisnotanlvalue.Ifthe
arrayobjecthasregisterstorageclass,thebehaviorisundefined.
数组类型到指针类型转换的结果,是一个指向数组首元素的类型为pointertotype的
指针,并且从一个左值转换成一个右值。
经过转换后,数组名不再代表数组对象,而是
一个代表数组首地址的符号地址,并且不是对象。
特别指出的是,数组到指针的转换规
则只适用于表达式,只在这种条件下数组名才作为转换的结果代表数组的首地址,而当
数组名作为数组对象定义的标识符、初始化器及作为sizeof、&的操作数时,它才代表
数组对象本身,并不是地址。
这种转换带来一个好处,对于数组内部的指针运算非常有利。
我们可以用a+1这种精
炼的形式表示a[1]的地址,无须用&a[1]这种丑陋的代码,实际上,&a[1]是一种代码冗
余,是对代码的浪费,因为&a[1]等价于&*(a+1),&与*由于作用相反被抵消,实际
上就是a+1,既然这样我们何不直接使用a+1呢?
撇开为了照顾人类阅读习惯而产生
的可读性而言,&a[1]就是垃圾。
但是,另一方面,这种异于一般标识符左值转换的特例化大大增加了数组与指针的复杂
性,困扰初学者无数个日日夜夜的思维风暴从此拉开了帷幕!
在两个版本的转换条款中,有一点需要留意的是,两个版本关于具有数组类型的表达式
有不同的描述。
C89/90规定:
anlvaluethathastype“arrayoftype”is......
但C99却规定:
anexpressionthathastype“arrayoftye”is.......
C99中去掉了lvalue的词藻,为什么?
我们知道,数组名是一个不可修改的左值,但实
际上,也存在右值数组。
在C中,一个左值是具有对象类型或非void不完整类型的表达
式,C的左值表达式排除了函数和函数调用,而C++因为增加了引用类型,因此返回引用
的函数调用也属于左值表达式,就是说,非引用返回的函数调用都是右值,如果函数非
引用返回中包含数组,情况会怎样?
考虑下面的代码:
#include
structTest
{
inta[10];
};
structTestfun(structTest*);
intmain(void)
{
structTestT;
int*p=fun(&T).a;
int(*q)[10]=&fun(&T).a;
printf("%d",sizeof(fun(&T).a));
return0;
}
structTestfun(structTest*T)
{
return*T;
}
/*A*/
/*B*/
/*C*/
在这个例子里,fun(&T)的返回值是一个右值,fun(&T).a就是一个右值数组,是一
个右值表达式,但a本身是一个左值表达式,要注意这个区别。
在C89/90中,由于规定
左值数组才能进行数组到指针的转换,因此A中的fun(&T).a不能在表达式中进行从数
组类型到指针类型的转换,A中的fun(&T).a是非法的,但C99在上述条款中不再限定
左值表达式,即对这个转换不再区分左值还是右值数组,因此都是合法的;C中的
fun(&T).a是sizeof运算符的操作数,这种情况下fun(&T).a并不进行数组到指针的
转换,因此C在所有C/C++标准中都是合法的;B初看上去仍然有点诡异,&运算符不是已
经作为例外排除了数组与指针的转换吗?
为什么还是非法?
其实B违反了另一条规定,&
的操作数要求是左值,而fun(&T).a是右值。
C++继承了C99的观点,也允许右值数组
的转换,其条款非常简单:
Anlvalueorrvalueoftype“arrayofNT”or“arrayofunknownboundofT”can
beconvertedtoanrvalueoftype“pointertoT.”Theresultisapointerto
thefirstelementofthearray.
数组类型到指针类型的转换与左值到右值的转换、函数类型到指针类型的转换一起是
C/C++三条非常重要的转换规则。
C++由于重载解析的需要,把这三条规则概念化了,统
称为左值转换,但C由于无此需要,只提出了规则。
符号是语言对计算机的高级抽象,
但计算机并不认识符号,它只认识数值,因此一个符号要参加表达式计算必须先对其进
行数值化,三条转换规则就是为了这个目的而存在的。
看到这里,大概有些初学者已经被上述那些左值右值、对象非对象搞得稀里糊涂了。
的
确,数组与指针的复杂性让人望而生畏,不是一朝一夕就能完全掌握的,需要一段较长
的时间慢慢消化。
因此笔者才将数组与指针称为一门艺术,是的,它就是艺术!
第二章:
数组名是一个指针常量吗?
数组名是一个指针常量这种观点来源于数组名在表达式计算中与指针的结果等效性。
例
如下面的代码:
inta[10],*p=a,*q;
q=a+1;
q=p+1;
在效果上看,a+1与p+1是相同的,这很容易给人一种a就是p的假象,但,这仅仅
是假象。
鉴于指针常量包含了指针和常量两类概念,我们可以把这个问题分开两部分进
行讨论。
一、数组名是指针吗?
在《C与指针》一书中,作者用一个著名的例子阐述了数组名与指针的不同。
在一个文
件中定义:
inta[10];然后在另一个文件中声明:
externint*a;笔者不在这里重复
其中的原理,书中的作者试图从底层操作上阐述数组名与指针的不同点,但笔者认为这
个例子存在一些不足,a在表达式中会转换为一个非对象的符号地址,而指针a却是一个
对象,用一个非对象去跟一个对象比较,有“偷跑”的嫌疑,这个例子只是说明了数组名
的非对象性质,只能证明对象与非对象实体在底层操作上的不同,事实上,如上一章所
述,指针也有非对象形态。
笔者认为,无须从底层的角度上花费那么多唇舌,仅仅从字
面上的语义就可以推翻数组名是一个指针的观点。
首先,在C/C++中,数组类型跟指针类型是两种不同的派生类型,数组名跟指针是
两种不同类型的实体,把数组类型的实体说成“是”另一个类型的实体,本身就是荒谬的;
其次,a+1在效果上之所以等同于p+1,是因为a进行了数组到指针的隐式转换,这
是一个转换的过程,是convertedto而不是isa的过程。
如果是两个相同的事物,又怎
会有转换的过程呢?
当把a放在a+1表达式中时,a已经从一个数组名转换为一个指针,
a是作为指针而不是数组名参与运算的;
第三,a+1与p+1是等效关系,不是等价关系。
等价是相同事物的不同表现形式,而
等效是不同事物的相同效果。
把数组名说成是指针实际上把等效关系误解为等价关系。
因此,数组名不是指针,永远也不是,但在一定条件下,数组名可以转换为指针。
二、数组名是一个常量吗?
看见这句话有人会觉得奇怪,数组定义之后就不能改变了,数组名不就是个常量吗?
在
表达式中,数组名的确可以转换为一个不变的符号地址,但在C中,不变的实体不一定
是常量!
而且,C/C++有常量与常量表达式之分,常量与常量表达式是两种不同的实体,
但常量表达式可以作为常量使用。
C/C++中的常量虽然有所不同,但都不包括数组或数
组名,而且数组名也不一定是常量表达式。
请在C90的编译器中编译如下代码,注意不能是C99和C++的,因为C99和C++不再规定数
组的初始化器必须是常量表达式,会看不到效果:
intmain(void)
{
staticinta[10],b[10];
intc[10],d[10];
int*e[]={a,b};/*A*/
int*f[]={c,d};/*B*/
return0;
}
B为什么不能通过编译?
是由于自动数组名并不是常量表达式。
在C中,常量表达式必须
是编译期的,只在运行期不变的实体不是常量表达式,请看标准的摘录:
6.6Constantexpressions
Aconstantexpressioncanbeevaluatedduringtranslationratherthanruntime,
andaccordinglymaybeusedinanyplacethataconstantmaybe.
c和d是自动数组,首地址在编译期是不可知的,因为这样的对象在编译期还不存在;a
和b是静态数组,静态对象从程序开始时就已存在,因此a和b的首地址在编译期是已知
的,它们都属于常量表达式中的地址常量表达式。
所以,C/C++中的数组名,都不是常量。
C中的数组名,是否常量表达式要视其存储连续
性而定,全局数组、静态数组名都是常量表达式,而自动数组名不是。
在C++中,由
于不再规定常量表达式必须是编译期的,因此C++的数组名都是常量表达式。
第三章:
数组的解剖学
C/C++的数组不同于VB等语言的数组,是有层次的,这个层次指的不是维度,而是象俄
罗斯有名的套娃一样,一维套一维,亦即数组的嵌套,数组的元素也是数组,VB等语言
的数组与之相比更像一个平面。
数组嵌套这个现象从其它语言的角度来看有点奇特,但其实原因也很简单。
C/C++的对
象模型并不视数组为某种数值的简单集合,而是对象的聚集,每个元素都是一个对象。
元素为整数对象,就是整数数组,为浮点数对象,就是浮点数数组。
然而,数组本身也
是一种对象,因此一个数组也能作为另一个数组的元素。
当某个一维数组以一维数组作
为元素时,这个一维数组每个元素都具有数组类型,这个一维数组其实是二维数组,同
理,一个以二维数组作为元素的一维数组其实是三维数组。
因此,使用C/C++数组的时
候应该用数组嵌套的观点去看待。
有人据此认为,C/C++的数组不是真正的数组,还有
的认为C/C++没有多维数组,这些观点都有失偏颇,与其它语言的数组相比,两者只是
同一事物的不同实例,是实现方法的不同,而本质是一样的,C/C++的数组嵌套可视为
对数组概念的发展。
现在来看看数组的定义:
6.5.4.2Arraydeclarators
Semantics
If,inthedeclaration“TDl.”Dlhastheform
D[constantexpressionopt]
这个定义非常简单,其中T代表元素类型,D代表标识符,constantexpression必须为
大于0的常量表达式,opt表示可选,即[]中的内容可以为空,当[]为空时叫不完整类型,
表示这个数组对象的长度未知,不完整数组类型可以在程序的某个地方补充完整。
细心
的人马上就会发现,从形式上看,怎么只有一维数组的定义?
这个形式如何定义多维数
组?
刚才说过,C/C++的数组是数组的嵌套,因此多维数组的定义也反映了这个本质。
多维数组的定义是通过嵌套的一维数组定义构造的。
对于一维数组:
TD[M]
当元素为一维数组T[N]时,元素的类型也为数组类型,用T[N]代替T,则为:
T[N]D[M]
这个语法结构不符合C/C++数组定义的语法形式,将[N]移动到[M]后,就是正式的二维
数组的定义了:
TD[M][N]
其中D[0]---D[M-1]都是一维数组,具有数组类型T[N]。
各种维度的多维数组可以用同
样的嵌套方法构造出来。
一个一维数组T[M],经过数组到指针的转换后,类型转换为T*,二维数组T[M][N]转换
为指针后,类型转换为T(*)[N],有些初学者对T(*)[N]这种形式较难理解,怎么
多了一维,形式就有这么大的差别呢,其实原理还是跟嵌套有关,二维数组为一维数组
的嵌套,元素为数组类型,因此用T[N]代替T,则二维数组转换之后的指针类型为T[N]*,
将[N]移动到*的右边,就是T*[N],由于[]的优先级比*高,因此需要加括号,就成为
T(*)[N]了,否则就不是指针类型,而成了指针数组类型了。
围绕数组名,存在一些有趣的表达式,下面的内容通过讨论这些表达式中较为重要的几
个,来加深对数组的理解。
对于二维数组:
Ta[M][N]
a:
表达式中的a的类型转换为T(*)[N],代表数组的首地址;
&a:
是一个指向二维数组对象的指针,类型为T(*)[M][N]。
在C标准出现之前,一些
早期的实现并不允许&a,因为这些编译器认为此时的a转换为一个右值,而&运算符要求
一个左值,因此非法。
C标准委员会鉴于对象的概念已经得到了扩展,而且允许&a并没
有害处,因此把&运算符作为一个例外写进了数组到指针的转换条款中。
这种情况下的a
代表数组对象,&a表示对数组对象取地址,因此&a的结果跟a是相同的,但类型不同。
笔者曾经见过某些观点认为,&a才是数组的首地址,不是a。
这个观点初看起来似乎很
有道理,一个数组对象的引用,不正是首地址吗?
但实际上这种论述是不符合标准的,
数组到指针的转换条款规定,当产生一
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 数组 指针 概念 联系 区分 经典 文档