免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 20552 | 回复: 117

[C] 浅谈一下C语言中的指针与数组的关系 [复制链接]

论坛徽章:
11
摩羯座
日期:2013-09-16 11:10:272015亚冠之阿尔萨德
日期:2015-06-12 22:53:29午马
日期:2014-04-15 11:08:53亥猪
日期:2014-03-02 23:46:35申猴
日期:2013-12-06 22:07:00亥猪
日期:2013-11-28 12:03:13双鱼座
日期:2013-11-21 14:43:56亥猪
日期:2013-10-23 10:55:49处女座
日期:2013-10-17 18:15:43午马
日期:2013-09-27 17:40:4215-16赛季CBA联赛之青岛
日期:2016-06-22 00:45:55
发表于 2012-12-22 12:15 |显示全部楼层
本帖最后由 Ager 于 2012-12-22 13:03 编辑

浅谈一下C语言中的指针与数组的关系

_HellAngel_
在函数定义中,形式参数
char s[]

char *s
是等价的。我们通常更习惯于使用后一种形式,因为它比前者更直观地表明了该参数是一个指针。

/* 这边我的一个想法是,指针和数组,用起来是不是基本上是分不开的,而且结合起来更简洁?今后的实践中可能会明白。 */


@_HellAngel_



在C语言中,指针与数组(或许,可以更为具体地说:指针标识符与数组标识符)之间关系紧密。

为了弄清楚这个问题,我们必须在一开始,清楚地把握关于“标识符”这个范畴的直观印象。

一般的教科书,在它们阐释“标识符”的章节中,仅仅强调了标识符的组成规则,诸如:组成一个合法的标识符所能采用的字符是哪些、标识符不能以数字开头、标识符不能与语言现有的关键字重复、标识符对于组成它的字符的大小写是敏感的 …… 等等。

然而在这里,我们将要从完全不同的领域,来理解关于“标识符”的一些很重要、很关键的事情。我们依然遵循„Zu den Sachen selbst!“的思路。

首先,令一些符合标识符之组成规则的字符们,成为(to being)一个真正的标识符的那个being,在C语言中,叫做:声明器(Declarator)。

一个非常简单的声明器,体现在形如这样的一则声明(A Declaration)中:
  1. T D
复制代码
其中,“T”指明了“D”是什么“类型”的,而“D”则可以是一个标识符。“T”与“D”两下彼此阐释:D是一个T,或D是一个“T的”;T作为一个抽象观念,在D上得以实例化。

当上面的“D”是一个单纯的标识符的时候,我们就可以说:此标识符在这则声明中是一个“简单声明器”。这一类的例子,很容易理解,如下面的一行声明所呈现出来的:

  1. /* 声明行 1 */
  2. int x;
复制代码
请注意:简单声明器之得以合法地被利用,其首要前提是:程序提供了足够完整与可靠的关于前者的类型信息。在上面的“一则声明”例子中,这种信息由“T”这个部分呈现。在【声明行 1】中,由“int”这个“类型指定符(Type Specifier)”呈明。

简单声明器,以及一些其他相对简单的声明器,统称为“直接声明器(Direct-Declarator)”。

直接声明器与一对方括号,是“数组声明器(Array Declarator)”必须具备的两个组成部分。

我在这里,之所以如此强调,其实,是为了告诉大家:数组声明器中的方括号的存在,是组成前者的一个必要(但不充分)条件。

而我之所以要告诉大家这一点,是因为:

在国内一些知名的程序员社区(如CSDN)中,充斥着大量这样的见解(或说法) —— “在声明数组时,那个指明下标的方框号 …… ”

也就是说,不少人都认为数组声明器中的那对方括号具有“下标”的含义

而我要告诉你的是:数组声明器的那对方括号仅是组成前者的一个必要条件(或一个直观上的特征)。它与“下标操作符”,两者完全不是一回事!

在C语言中,用来表征一个数组声明器,以及使得一个数组元素成为左值,都采用以及可以采用“方括号”这对字符 —— 你干脆这样认为吧 —— 这只是一个巧合,而没有什么必然联系!

类似地,用来表征一个指针声明器的“指针符”,以及令程序可以透过指针去Handle一个内存对象并使得表达式成为左值的那个“间接访问操作符”,都采用“星号”这个字符,但两者的含义截然不同。

作为下标操作符的方括号,其真确含义是:

下标表达式:
  1. a[b]
复制代码
永远可以准确地对应于表达式(当然其中操作数的类型须合乎规则):
  1. *(a+b)
复制代码
这种语言特性,决定了我们可以利用下标表达式,通过指明数组名(数组标识符)与某个作为下标的整数,来使得数组中的某个元素,成为左值。

现在,我们回到数组声明器和它里面的那对方括号。

在某些情况下,C语言处理数组的时候,需要程序以某种恰当而可靠的方式,呈明当事数组的规模/长度(如数组所容纳的元素之数量)。

根据这个原则,我们不难理解以下结论:

『(i)我们应该利用数组声名器中的方括号,来呈明当事数组的规模,除非

(ii)我们可以利用其他某些恰当而可靠的方式,来呈明当事数组的规模,有如:
  1. char MyFullName[] = "Larry Ageratum Westernwall";
复制代码
即数组声明器所在的当则声明,具备一个可以可靠地计算出当事数组规模的初始化部分。

又如:
  1. extern char X[][123];
  2. ……
  3. static char X[321][123];
复制代码
即数组声明器所在的当则声明,并不具备定义的功能,该数组声明器仅作为一则“引用性声明”的一部分。

除非

(iii)数组声名器在一个根本不需要呈明当事数组的规模的场合中,有如,在函数声明器中:
  1. void foo(char UserFullName[])
复制代码
这里无需呈明作为形式参数的数组UserFullName的规模,因为:作为函数形式参数的数组声明器,实际所期盼的,是一个指向char的指针,即上面的声明行,等同于
  1. void foo(char *UserFullName)
复制代码
也就是说,C语言中的函数,天然地不关心作为参数的数组的规模。(甚至你可以认为:以参数作为函数的视阈,函数将看不到任何“数组状”的对象。)鉴于这种特性,若有必要向函数呈明事数组的规模,应该为函数设置其他的(独立的)参数作为附加,比如设置一个整数类型的参数,用以令函数可以获悉一些关于数组规模的信息(可以是当事数组的完整规模,也可以是某种“起讫(截断)位置”)。

上面,我们比较详细地,讨论了数组标识符的在“to being”上的一些原理。

至此,我们还须要总结一些事情:

(1)数组标识符是一种标识符。(2)一个单纯的标识符是简单声明器。(3)简单声明器是直接声明器的一种。

(4)数组声明器必须包含一个直接声明器与一对方括号。

(5)数组声明器中的方括号,不是数组标识符的一部分。

下面,我们讨论一下,数组标识符,在C语言的表达式中的“to be done”。

在这件事情上,C语言的法则是:

(一)数组标识符,若出现在表达式中,则该标识符所体现出来的类型,就从原本在最初的声明中由类型指定符所确定的“T的数组(T [])”,转换为“指向T的指针(T *)”,后者总是指向当事数组的首个元素。除非

(二)数组标识符,若作为操作符sizeof或取地址操作符&的操作数的时候。数组标识符若作为操作符sizeof的操作数,那么sizeof表达式返回整个数组的长度(字节数量);数组标识符若作为操作符&的操作数,那么&表达式返回一个指向数组的指针(T(*)[])。

我们分别讨论以上两条法则。

第一条法则,是我们在程序中可以利用下标操作符将数组元素成为左值的基础。在下标表达式中,一侧(不一定是左侧)的数组标识符被转换为指向数组首个元素的指针,另一侧(不一定是右侧)的整数类型的表达式,实际上成为了指针加法的一个操作数。根据指针加法可以适用的加法交换律,作为左值的
  1. MyFullName[3]
复制代码
  1. 3[MyFullName]
复制代码
是等效的。

此外,当我们定义了如下函数:
  1. void foo(char *s)
复制代码
  1. void foo(char s[])
复制代码
以及如下数组:
  1. char a[5]
复制代码
而通过
  1. foo(a)
复制代码
去调用函数foo的时候,a在这里,即是指向数组a的首个元素的指针。所以这个调用,与
  1. foo(&a[0])
复制代码
是等效的。

请注意:a在这里,并不是操作符&的操作数,a[0]才是。下标操作符的优先级属于最高一级,而操作符&的则属于次最高一级。

有些初学者,或许受其他编程语言中关于数组用法的污染,有一种倾向,即用“a[]”或“a[5]”这样的写法,在表达式中呈现一个关于“数组a之整体”的概念 —— 这是错误的!

如果使用
  1. foo(a[])
复制代码
来调用函数foo,那么将产生下标操作符缺失操作数的错误。

如果使用
  1. foo(a[5])
复制代码
来调用函数foo,那么,这里被传入函数的参数将是数组a中下标为5的元素的值。显然这个元素处于某种“越界”的位置,但比越界更加糟糕的是,这个值的类型是char,而不是函数foo由其定义所期盼的char *。

现在我们回到最初的例子:

函数foo的参数声明部分,即
  1. char s[]
复制代码
  1. char *s
复制代码
为什么能让我们感到我们可以由一个“数组状”的形式参数s直接地俘获到了一个作为实际参数的数组a呢?

也就是说,我们感到,我们可以将作为实际参数的数组a之整体“耦合般地投放”到作为形式参数的数组s之整体上,这是为什么?

其实,这种“耦合般地投放”是一种错觉。造成这种错觉的根本原因,并非在于函数的参数声明部分,而是在于我们调用函数的部分。

也就是说,这种错觉,仅发生在我们这样调用函数foo的时候:
  1. foo(a)
复制代码
这里,调用函数时所设置的参数,是一个作为表达式存在的数组标识符a,根据上面的法则(一),这里的参数实际上是一个指向数组a首个元素的指针。那么,我们在函数foo这端,在某种意义上,标识符s自然也具有与数组标识符a相当的地位 —— 即一个本质上的指针标识符被当作一个影子般的数组标识符来使用。不过,当我们在函数foo内部使用
  1. sizeof(s)
复制代码
的时候,只能获得某一个指针的长度,而无法达到
  1. sizeof(a)
复制代码
的效果了。(这映证了这麽一个事实:C语言的实现机构中,必定有一块地方,记录了数组a的存在规模,即a作为一个数组之整体的存在,在实现机构中有一席之地。)

打破上述错觉的另外一招是,我们改变调用函数foo所采用的参数。比如,我们这样来调用函数foo:
  1. foo(a+2)
复制代码
  1. foo(&a[2])
复制代码
即,我们利用“指针加法”或“操作符&与操作符[]配搭使用”来构造一个新的指针,将其作为实际参数传给函数foo,此时,在函数foo内部,就再也没有“数组a的影子般的数组标识符”了。

现在,我们再回到函数声明中关于参数声明的这一层。

我们已经明白:在函数的参数声明中,形如
  1. s[]
复制代码
这样的“数组状”的声明器,实际上终将被“转化”为一个指针声明器,即:一个表面上的数组标识符,实际上是作为一个指针标识符而存在。(同时,一切关于数组存在规模的信息,被蔽弃。)

C语言为什么要有这样的“转化法则”?

在以前的帖子里,我们已经知道:C语言的函数策略,使得函数治下的变量的数据,可以是一份由形式参数机构所俘获的实际参数的数据的副本。从实现的角度看,这些副本性的数据,存在于内存空间中属于当事函数的帧(Frame)上。

显然,副本性的数据的数量/规模越大,实现在函数帧的开销上,就越大。

而数组的存在,天然地就带有“规模性”的目的因素。

也就是说,如果令函数内部为数组之整体承受其私有副本的话,函数帧将占用相应的规模性资源。

C语言没有采用上面这种策略,而是采用了另外一种:

当程序企图将数组作为参数传入参数(准确地说,是期望利用含有一个单纯的数组声明器的参数声明来俘获目标数组)的时候,被调用函数仅利用其形式参数机制,承建并维护一个该数组的基性地址(指针)的私有副本。这样的副本性数据,给函数帧带来的开销,是非常小的。

在这种情况下,由于被调用函数治下拥有一个关于作为参数的数组的指针,这意味着,函数内部至少可以有一个“完全有把握”去Handle目标数组元素的左值,也就是说,该函数可以间接地实施(等效于直接地)读取或刷写目标数组元素的数据,而不是它(们)的副本。—— 这里,蕴涵了一个至关重要的前提性事实:在C语言中,数组元素在内存空间上的分布形势,与指针运算值域的分布形势,两者是严格地一致的。

显然,这种策略满足了某种现实上的便利性和经济性要求。而为了符合这种策略,就有了前述的“转化法则”。

而第二条法则,有助于我们获取某些关于目标数组之整体的信息(如数组之规模)。

以上,仅供参考,呵呵 ——:)

同样,本帖子须经 @pmerofc 大虾等诸位高人审订一下。尔后,你才可放心阅读无虞:)


论坛徽章:
0
发表于 2012-12-22 12:52 |显示全部楼层
讲得不错。
补充几点细节供进阶参考。
1.语法成分标识符(identifier)有两重含义,一种是预处理记号(preprocessing-token),另一种是记号(token)关键字(keyword)就字面上属于前者而不是后者:记号包括关键字,预处理记号不包括;预处理中把一部分作为预处理记号的标识符转换为作为记号的标识符,另一部分是关键字。
2.declarator(可能习惯翻译为声明符)有时候是语法元素declaratorabstract-declarator的统称。前者是出现在声明(declaration)中,后者出现于类型名(type-name)参数声明(parameter-declaration)。两种语法构造有明确的界限,直观区别是后者不包含被声明的标识符。但这里的语法构造规则是类似的,有很多一致性,例如,其中的[]或*都不是操作符。
3.习惯上称为“退化”(decay)的转换为指针的语义规则,除了这里讨论的数组标识符外,也类似地适用于函数指示符(function designator)。和另一个转换lvalue conversion一道,这里一个重要的共同点是,非值(数组标识符作为左值,函数指示符不是左值也不是值)转换为值(C语言中值=右值)。
4.这里的要点大体也适用于C++,包括语法和语义(这里没讲到C99的一些不在C++支持的声明符语法),但细节和表述上有所不同。数组退化称为array-to-pointer conversion,对应于函数指示符的函数名(function name)退化称为function-to-pointer conversion,两者都是标准转换(standard conversion),转换结果都是指针右值。C++中的function name是左值,于是这里再和另一种标准转换lvalue-to-rvalue conversion(对应于C的lvalue conversion)一起统称为左值变换(Lvalue Transformation)——这在重载决议中作为转换类别的重要依据。

论坛徽章:
323
射手座
日期:2013-08-23 12:04:38射手座
日期:2013-08-23 16:18:12未羊
日期:2013-08-30 14:33:15水瓶座
日期:2013-09-02 16:44:31摩羯座
日期:2013-09-25 09:33:52双子座
日期:2013-09-26 12:21:10金牛座
日期:2013-10-14 09:08:49申猴
日期:2013-10-16 13:09:43子鼠
日期:2013-10-17 23:23:19射手座
日期:2013-10-18 13:00:27金牛座
日期:2013-10-18 15:47:57午马
日期:2013-10-18 21:43:38
发表于 2012-12-22 12:54 |显示全部楼层
本帖最后由 hellioncu 于 2012-12-22 12:54 编辑

真能扯啊
C语言似乎没有规定多维数组的排列方式

论坛徽章:
2
程序设计版块每日发帖之星
日期:2015-06-17 22:20:00每日论坛发贴之星
日期:2015-06-17 22:20:00
发表于 2012-12-22 13:35 |显示全部楼层
提示: 作者被禁止或删除 内容自动屏蔽

论坛徽章:
2
程序设计版块每日发帖之星
日期:2015-06-17 22:20:00每日论坛发贴之星
日期:2015-06-17 22:20:00
发表于 2012-12-22 13:42 |显示全部楼层
提示: 作者被禁止或删除 内容自动屏蔽

论坛徽章:
1
白羊座
日期:2014-03-22 18:23:03
发表于 2012-12-22 14:55 |显示全部楼层
碉堡了。。。。大神。。。小弟膜拜你= =。。。当时就对书中写的。。。“在计算数组元素a的值时,C语言实际上先将其转换为*(a+i)的形式,然后再进行求值。”不是很理解。。。现在明了多了= =。不过好多专业术语。。还得消化好一阵子。。= =

论坛徽章:
11
摩羯座
日期:2013-09-16 11:10:272015亚冠之阿尔萨德
日期:2015-06-12 22:53:29午马
日期:2014-04-15 11:08:53亥猪
日期:2014-03-02 23:46:35申猴
日期:2013-12-06 22:07:00亥猪
日期:2013-11-28 12:03:13双鱼座
日期:2013-11-21 14:43:56亥猪
日期:2013-10-23 10:55:49处女座
日期:2013-10-17 18:15:43午马
日期:2013-09-27 17:40:4215-16赛季CBA联赛之青岛
日期:2016-06-22 00:45:55
发表于 2012-12-22 15:14 |显示全部楼层
幻の上帝 发表于 2012-12-22 12:52
讲得不错。
补充几点细节供进阶参考。
1.语法成分标识符(identifier)有两重含义,一种是预处理记号(prepr ...



pmerofc 发表于 2012-12-22 13:35
回复 1# Ager

    这还叫“浅谈”啊


感谢幻大虾、pm大虾支持 {:3_193:}

论坛徽章:
89
水瓶座
日期:2014-04-01 08:53:31天蝎座
日期:2014-04-01 08:53:53天秤座
日期:2014-04-01 08:54:02射手座
日期:2014-04-01 08:54:15子鼠
日期:2014-04-01 08:55:35辰龙
日期:2014-04-01 08:56:36未羊
日期:2014-04-01 08:56:27戌狗
日期:2014-04-01 08:56:13亥猪
日期:2014-04-01 08:56:02亥猪
日期:2014-04-08 08:38:58程序设计版块每日发帖之星
日期:2016-01-05 06:20:00程序设计版块每日发帖之星
日期:2016-01-07 06:20:00
发表于 2012-12-22 15:16 |显示全部楼层
我写不了lz这么清楚,但是我保证自己写程序时能写对,哈哈。

论坛徽章:
11
摩羯座
日期:2013-09-16 11:10:272015亚冠之阿尔萨德
日期:2015-06-12 22:53:29午马
日期:2014-04-15 11:08:53亥猪
日期:2014-03-02 23:46:35申猴
日期:2013-12-06 22:07:00亥猪
日期:2013-11-28 12:03:13双鱼座
日期:2013-11-21 14:43:56亥猪
日期:2013-10-23 10:55:49处女座
日期:2013-10-17 18:15:43午马
日期:2013-09-27 17:40:4215-16赛季CBA联赛之青岛
日期:2016-06-22 00:45:55
发表于 2012-12-22 15:32 |显示全部楼层
pmerofc 发表于 2012-12-22 13:42
回复 1# Ager

把Declarator翻译成“声明器”,我最早是在《C语言参考手册》(徐波翻译的那个版本)看到的。我对这个翻译不满意。我个人的倾向是应该翻译成“(被)声明物”


我也将“Declarator”翻译成“声明器”,是因为:我将“Declarator”理解为“declarate”(动词,其实没有这个词,不过或许可以当作名词Declaration的动词形式)的主语。按照我这样的理解,该动词的受动者(宾语)就应该叫“Declaratee”。

我这样的理解法,其实还是有一些风险,因为与“Declarator”一词最接近的词是“declaratory”,这是一个形容词。

…… 的确有点困惑 ……

此外,日语学界,统一地将“Declarator”翻译成“宣言子”。“子”这个汉字,有“行为的实施者”的意思,如日语里“演算子”,就是“Operator”,我们这里翻译为“运算符”。而“Operand”在日语学界,被翻译为“被演算子”。

以上,仅供参考,呵呵 ——:)

论坛徽章:
2
程序设计版块每日发帖之星
日期:2015-06-17 22:20:00每日论坛发贴之星
日期:2015-06-17 22:20:00
发表于 2012-12-22 16:30 |显示全部楼层
提示: 作者被禁止或删除 内容自动屏蔽
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP