指
针
是
C
语
言
规
范里面一
项
核心内容,指
针
具有与生
俱
来的
优势
,利用指
针
可以写出
许
多短小精悍、效率极高的代
码
,它是
C
语
言一把无可替代的利器,凭着
这
把利器,
C
语
言与其它高
级语
言相比至少在效率方面高人一
筹
。但是,由于指
针
的原理与使用方式跟人
们
通常的思
维习惯
有
较
大的差
别
,造成了指
针
比
C
语
言其它概念
难
理解得多,
这
使得
对
指
针认识
不足成
为
了一
种
在
C
程序
员
中普遍存在的
现
象,
这种
不足必然
导
致程序
员
在指
针
的使用
过
程中不断遭受挫折,挫折多了,指
针俨
然
变
成一道无法逾越的
难关
,恐惧感也就油然而生了。在恐惧感面前,某些程序
员
甚至
产
生了要避免使用指
针
的念
头
,
这
是非常不可取的。指
针
是如此犀利,正是它才使得
C
语
言威猛无比,如果就
这样
把它放弃了,那
么
C
语
言就算是白学了。我
们应
当
让
指
针
成
为
你手中那把砍掉索
伦
手指上魔戒的
举
世无双的
纳
西
尔
圣
剑
,而不是成
为
你心中永
远
的魔戒。
本文的目的,是希望通
过
跟各位朋友一起
讨论关
于指
针
的几个
关键
概念及常
见问题
,以加深
对
指
针
的理解。因此,本文并不是
讲
述形如
int *p
、
struct {int i;float j;} *p
等
这
些
东
西是什
么
的文章,
阅读
本文的朋友最好
对
指
针
已
经
具有一定的使用
经验
,正因如此,笔者才
给
文章起名叫《再再
论
指
针
》。笔者不敢奢望能
够
完全解
开
你心中的魔
结
,但如果通
过阅读
本文,能
够让
你在日后的指
针
使用
过
程中减少失
误
,那笔者就心
满
意足了。本文将
讨论
如下十个主
题
,
读
者最好按主
题
的
顺
序一个一个地
阅读
,当然,如果你只
对
其中某个或某几个主
题
感
兴
趣,只看那几个也未
尝
不可。
当你
阅读
本文后:
如果你有不同的意
见
,
欢
迎你在
评论
里留下自己的
见
解,笔者很
乐
意跟你一起
讨论
,共同
进步
。
如果你
觉
得我
说
的全都是
废话
,那
么
恭喜你,你的指
针
已
经毕业
了。
如果你有太多不明白的地方,那
么
我介
绍
你先找一些
关
于数
组
与指
针
的
读
物看看,笔者推荐你
阅读
一本叫《
C
与指
针
》的
书
,看完后再回来
继续
思考你的
问题
。
数
组
是指
针
的基
础
,多数人就是从数
组
的学
习开
始指
针
的旅程的。下面我
节选
一些在各
种论坛
和文章里
经
常
见
到的
关
于数
组
的文字:
“ 一 维 数 组 是一 级 指 针 ”
“ 二 维 数 组 是二 级 指 针 ”
“ 数 组 名可以作 为 指 针 使用 ”
“ 数 组 名就是 .......... 的常量指 针 ”
“ 数 组 名就是 .......... 的指 针 常量 ”
..................................
这 些文字看起来非常熟悉吧? 类 似的文字 还 有 许 多,或 许 你就是 经 常 说这 些 话 的人呢。不 过 非常 遗 憾, 这 些文字都是 错误 的, 实际 上数 组 名永 远 都不会是指 针 ! 这 个 结论 也 许 会 让 你震惊,但它的确是事 实 。数 组 名、指 针 、地址 这 几个概念 虽 然是基 础 中的基 础 ,但它 们 恰恰是被混淆和 滥 用得最多的概念,把数 组 名 说 成指 针 ,是一个概念性的 错误 , 实质 是混淆了指 针 与地址两个概念的本 质 。俗 话说 得好:浅水淹死人。因此,在 讨论 数 组 之前,有必要先回 过头 来澄清一下什 么 是指 针 ,什 么 是地址,什 么 是数 组 名。
“ 一 维 数 组 是一 级 指 针 ”
“ 二 维 数 组 是二 级 指 针 ”
“ 数 组 名可以作 为 指 针 使用 ”
“ 数 组 名就是 .......... 的常量指 针 ”
“ 数 组 名就是 .......... 的指 针 常量 ”
..................................
这 些文字看起来非常熟悉吧? 类 似的文字 还 有 许 多,或 许 你就是 经 常 说这 些 话 的人呢。不 过 非常 遗 憾, 这 些文字都是 错误 的, 实际 上数 组 名永 远 都不会是指 针 ! 这 个 结论 也 许 会 让 你震惊,但它的确是事 实 。数 组 名、指 针 、地址 这 几个概念 虽 然是基 础 中的基 础 ,但它 们 恰恰是被混淆和 滥 用得最多的概念,把数 组 名 说 成指 针 ,是一个概念性的 错误 , 实质 是混淆了指 针 与地址两个概念的本 质 。俗 话说 得好:浅水淹死人。因此,在 讨论 数 组 之前,有必要先回 过头 来澄清一下什 么 是指 针 ,什 么 是地址,什 么 是数 组 名。
指
针
是
C
语
言具有低
级语
言特征的最直接的
证
据。在
汇编语
言里面,指
针
的概念随
处
可
见
。比如
SP
,
SP
寄存器又叫堆
栈
指
针
,它的
值
是地址,由于
SP
保存的是地址,并且
SP
的
值
是不断
变
化的,因此可以看作一个
变
量,而且是一个地址
变
量。地址也是
C
语
言指
针
的
值
,
C
语
言的指
针
跟
SP
这样
的寄存器
虽
然不完全一
样
,但原理却是相通的。
C
语
言的指
针
也是一
种
地址
变
量,
C89
明确
规
定,指
针
是一个保存
对
象地址的
变
量。
这
里要注意的是,指
针
跟地址概念的不同,指
针
是一
种
地址
变
量,通常也叫指
针变
量,
统
称指
针
。而
地址
则
是地址
变
量的
值
。
看到
这
里,也
许
你会
觉
得,
这么简单
的
东
西
还
用你来
说吗
?的确,
对
于
p
与
&p
来
说
,
99%
的人都能在
0.1
秒内脱口而出
谁
是指
针
,
谁
是地址,但是,又有多少人在使用指
针
的
过
程中能
够
始
终
如一毫不
动摇
地遵循
这
两个概念呢?不少人使用指
针
的
时
候就会自
觉
或不自
觉
地把指
针
和地址两个概念混淆得一塌糊涂了,数
组
名的
滥
用就是一个活生生的例子。
这
一点甚至
连
一些
经
典著作也没能避免。
不
过
也不能全怪你自己,笔者
认为
某些国内教材
应该
承担最大的
责
任。
这
些教材一
开
始就没有
给读
者好好地分清指
针
与地址的区
别
,相反
还
在
讲
述的
过
程中有意无意地混用
这
两个概念。更有甚者,甚至在
书
中明言指
针
就是地址!
说这话
的家伙最
应该
在
C
语
言
这
个地
图
上抹掉,呵呵。两个月前我在
购书
中心随手翻
开
了某个作者主
编
的一本被冠以国家
“
十五
”
规
划重点研究
项
目的
书
,
书
里就是
这么
写的。当
时
笔者就感慨:不知道又要有多少人的思想被
这
家伙
“
强
奸
”
了。
实际
上,地址
这
个
东
西,本来就是一
种
基本数据
类
型,本
应该
在介
绍
整数、浮点、字符等基本
类
型的
时
候把地址
显
式地放在一起
讨论
,
这样
在后面介
绍
指
针
与数
组
的
时
候就能避免
许
多
误
解。可惜不少教材或
者根本没有
谈
及,或者就算提起
这
个
类
型也用了指
针类
型
这
个字眼。
这
就
错
了,指
针
不是
类
型,真正的
类
型是地址,指
针
只是存
储
地址
这种
数据
类
型的
变
量!打个比方,
对
于
int i=10 ;
10 是整数,而 i 是存 储 整数的 变 量,指 针 就好比 这 个 i ,地址就好比那个 10 。指 针 能 够进 行加减法,原因并不是因 为 它是指 针 ,加减法 则 不是属于指 针这种变 量的,而是地址 这种 数据 类 型的本能,正是因 为 地址具有加减的能力,所以才使指 针 作 为 存放地址的 变 量能 够进 行加减运算。 这 跟整数 变 量因 为 整数能 够进 行加减乘除因而它也能 进 行加减乘除一个道理。
int i=10 ;
10 是整数,而 i 是存 储 整数的 变 量,指 针 就好比 这 个 i ,地址就好比那个 10 。指 针 能 够进 行加减法,原因并不是因 为 它是指 针 ,加减法 则 不是属于指 针这种变 量的,而是地址 这种 数据 类 型的本能,正是因 为 地址具有加减的能力,所以才使指 针 作 为 存放地址的 变 量能 够进 行加减运算。 这 跟整数 变 量因 为 整数能 够进 行加减乘除因而它也能 进 行加减乘除一个道理。
那 么 数 组 名又 应该 如何理解呢?用来存放数 组 的区域是一 块 在 栈 中静 态 分配的内存 ( 非 static) ,而数 组 名是 这块 内存的代表,它被定 义为这块 内存的首地址。 这 就 说 明了数 组 名是一个地址,而且, 还 是一个不可修改的常量,完整地 说 ,就是一个地址常量。数 组 名跟枚 举 常量 类 似,都属于符号常量。数 组 名 这 个符号,就代表了那 块 内存的首地址。注意了!不是数 组 名 这 个符号的 值 是那 块 内存的首地址,而是数 组 名 这 个符号本身就代表了首地址 这 个地址 值 ,它就是 这 个地址, 这 就是数 组 名属于符号常量的意 义 所在。由于数 组 名是一 种 符号常量,因此它是一个右 值 ,而指 针 ,作 为变 量,却是一个左 值 ,一个右 值 永 远 都不会是左 值 ,那 么 ,数 组 名永 远 都不会是指 针 ! 不管什 么话 ,只要 说 数 组 名是一个指 针 的,都是 错误 的!就象把 刚 才 int i=10 例子中的 10 说 成是整数 变 量一 样 ,在最基本的立足点上就已 经 完 错 了。
总
之要牢牢
记
住,数
组
名是一个地址,一个符号地址常量,不是一个
变
量,更不是一个作
为变
量的指
针
!
在数
组
名并非指
针这
个
问题
上,通常会
产
生两
种
疑
问
:
1 。作 为 形参的数 组 ,不是会被 转换为 指 针吗 ?
2 。如果形参是一个指 针 ,数 组 名可以作 为实 参 传递给 那个指 针 , 难 道不是 说 明了数 组 名是一个指 针吗 ?
1 。作 为 形参的数 组 ,不是会被 转换为 指 针吗 ?
2 。如果形参是一个指 针 ,数 组 名可以作 为实 参 传递给 那个指 针 , 难 道不是 说 明了数 组 名是一个指 针吗 ?
首先,
C
语
言之所以把作
为
形参的数
组
看作指
针
,并非因
为
数
组
名可以
转换为
指
针
,而是因
为
当初
ANSI
委
员
会制定
标
准的
时
候,从
C
程序的
执
行效率出
发
,不主
张
参数
传递时复
制整个数
组
,而是
传递
数
组
的首地址,由被
调
函数根据
这
个首地址
处
理数
组
中的内容。那
么谁
能承担
这种
“
转换
”
呢?
这
个主体必
须
具有地址数据
类
型,同
时应该
是一个
变
量,
满
足
这
两个条件的,非指
针
莫属了。要注意的是,
这种
“
转换
”
只是一
种逻辑
看法上的
转换
,
实际
当中并没有
发
生
这
个
过
程,没有任何数
组实
体被
转换为
指
针实
体。另一方面,大
家不要被
“
转换
”
这
个字眼
给
蒙蔽了,
转换
并不意味着相同,
实际
上,正是因
为
不相同才会有
转换
,相同的
话还转
来干
吗
?
这
好比
现
在社会上有不少人
“
变
性
”
,一个男人可以
“
转换
”
为
一个女人,那是不是
应该认为
男人跟女人是相同的?
这
不是笑
话么
。
第二,函数参数
传递
的
过
程,本
质
上是一
种赋值过
程。
C89
对
函数
调
用是
这样规
定的:函数
调
用由一个后
缀
表达式(称
为
函数
标
志符
,function designator
)后跟由
圆
括号括起来的
赋值
表达式列表
组
成,在
调
用函数之前,函数的
每
个
实际
参数将被
复
制,所有的
实际
参数
严
格地按
值传递
。因此,形参
实际
上所期望得到的
东
西,并不是
实
参本身,而是
实
参的
值
或者
实
参所代表的
值
!
举
个例来
说
,
对
于一个函数声明:
void fun(int i);
我
们
可以用一个整数
变
量
int n
作
实
参来
调
用
fun
,就是
fun(n)
;当然,也正如大家所熟悉的那
样
,可以用一个整数常量例如
10
来做
实
参,就是
fun(10)
;那
么
,按照第二个疑
问
的看法,由于形参是一个整数
变
量,而
10
可以作
为实
参
传递给
i
,
岂
不就
说
明
10
是一个整数
变
量
吗
?
这显
然是
谬误
。
实际
上,
对
于形参
i
来
说
,用来声明
i
的
类
型
说
明符
int
,所起的作用是用来
说
明需要
传递给
i
一个整数,并非要求
实
参也是一个整数
变
量,
i
真正所期望的,只是一个整数,
仅
此而已,至于
实
参是什
么
,跟
i
没有任何
关
系,它才不管呢,只要能正确
给
i
传递
一个整数就
OK
了。当形参是指
针
的
时
候,所
发
生的事情跟
这
个是相同的。指
针
形参并没有要求
实
参也是一个指
针
,它需要的是一个地址,
谁
能
给
予它一个地址?
显
然指
针
、地址常量和符号地址常量都能
满
足
这
个要求,而数
组
名作
为
符号地址常量正是指
针
形参
所需要的地址,
这
个
过
程就跟把一个整数
赋值给
一个整数
变
量一
样简单
!
在后面的章
节
中,笔者将
严
格地使用地址
这
一概念,
该
是地址
时
就用地址,
该
是指
针时
就用指
针
,以免象其它教材那
样给读
者一个
错误
的暗示
。
看
见这
个
题
目,也
许
有些人就会
嘀
咕了:
难
道两者不是一
样
的
吗
?
C
语
言的多
维
数
组
不就是数
组
的数
组吗
?不!两者是有区
别
的,而且
还
不小呢。
首先看看两者的共同点:
1
。内存映象一
样
。
2
。数
组
引用方式一
样
,都是
“
数
组
名
[
下
标
][
下
标
]........”
。
3
。数
组
名都是数
组
的首地址,都是一个符号地址常量、一个右
值
。
由于两者的共同点主要反映在外部表 现 形式上,因此,从外部看来,数 组 的数 组 跟多 维 数 组 似乎是一 样 的, 这 造成了 C 程序 员对 两者的区 别长 期以来模糊不清。但 实际 上, c 语 言限于本身的 语 言特性, 实现 的并非真 正的多 维 数 组 ,而是数 组 的数 组 。
数
组
的数
组
与多
维
数
组
的主要区
别
,就在于数
组
的数
组
各
维
之
间
的内在
关
系是一
种鲜
明的
层级关
系。上一
维
把下一
维
看作下一
级
数
组
,也就是数
组
嵌套。数
组
引用
时
需要
层层
解析,直到最后一
维
。
举
个例,
对
于数
组
:
int a[7][8][9] ;
如果要 访问 元素 a[4][5][6] ,首先就要 计 算第一 维 元素 4 的地址,也就是 a+4 ,由于是数 组 的数 组 ,元素 4 的 值 代表了一个数 组 ,因此元素 4 的 值 就是它所代表的那个数 组 的首地址,我 们 用一个符号 address1 代表它,也就是 address1=*(a+4) ,接着 计 算第二 维 , 显 然元素 5 的地址是 address1+5 ,其 值 也是一个数 组 的首地址,用 address2 表示它,就是 address2=*(address1+5) ,最后一 维 ,由于已 经 到达了具体的元素,因此 这 个元素的地址是 address2+6 ,其 值 *(address2+6) 是一个整数,把 address1 和 address2 分 别 代入相 应 表达式,就成了:
*(*(*(a+4)+5)+6);
这 就是我 们 熟知的 [] 运算符的等价表达式。
int a[7][8][9] ;
如果要 访问 元素 a[4][5][6] ,首先就要 计 算第一 维 元素 4 的地址,也就是 a+4 ,由于是数 组 的数 组 ,元素 4 的 值 代表了一个数 组 ,因此元素 4 的 值 就是它所代表的那个数 组 的首地址,我 们 用一个符号 address1 代表它,也就是 address1=*(a+4) ,接着 计 算第二 维 , 显 然元素 5 的地址是 address1+5 ,其 值 也是一个数 组 的首地址,用 address2 表示它,就是 address2=*(address1+5) ,最后一 维 ,由于已 经 到达了具体的元素,因此 这 个元素的地址是 address2+6 ,其 值 *(address2+6) 是一个整数,把 address1 和 address2 分 别 代入相 应 表达式,就成了:
*(*(*(a+4)+5)+6);
这 就是我 们 熟知的 [] 运算符的等价表达式。
而真正的多
维
数
组
并没有
这么
多
“
束
缚
”
,相比之下
简单
得多,由于各
维
之
间
不是
这种复杂
的
层级关
系,元素
a[4][5][6]
的偏移量可以
这样
直接
获
得:
(4x8x9+5x9+6)xsizeof(int)
,再加上数
组
的首地址
a
就是元素
a[4][5][6]
的地址了。但是,
c
语
言的数
组
能
够这样
用首地址加上
(4x8x9+5x9+6)xsizeof(int)
的形式来
访问
元素
吗
?
显
然是不行的。
归
根到底就在于
C
语
言的地址数据
类
型不但有
类
型,
还
具有
级别
。就是
这种层级关
系造成了
C
语
言只能用数
组
的数
组
当作多
维
数
组
。如果
C
语
言非得要
实现
真正的多
维
数
组
,那
么
地址与指
针
的概念就得重新改写了
。
这
一章我
们
来
讨论
一下数
组
的内涵,
对
数
组
的内部构造
进
行一次解剖,看看里面究竟
隐
藏了什
么
秘密。
有了前面两章
对
数
组
名和
C
语
言数
组
本
质
的澄清,再来理解
这
一章的内容,就容易多了。
在下面的叙述中,笔者会用到一个运算符
sizeof
,由于在不同的
编译
器和
编译
模式下,
对
一个地址
进
行
sizeof
运算的
结
果有可能是不同的,
为
了方便
讨论
,我都假
设
地址
长
度
为
4
个字
节
。
多数教材在
讲
述数
组
的
时
候,都是把重点放在外部表
现
形式上,很少
涉
及数
组
的内部,只告
诉
你如何做,却忽
视
了
为
什
么
要
这样
做。在解
释
的
过
程中,
还
会列出各
种
各
样
的表达式,例如:
a
、
a+1
、
a[0]
、
a[0][0]
、
&a[0]
、
&a[0][0]
、
*(a+1)
等等,
让
人眼花
缭
乱。但
实际
上真正能
够
用来描述数
组
内部构造的表达式只有其中的几个。
上一章
讲
到,
C
语
言的数
组实现
并非真正的多
维
数
组
,而是数
组
嵌套,
访问
某个元素的
时
候,需要逐
层
向下解析。仍然以上一章的例子数
组
int a[7][8][9]
来
说
,第一
维
元素
0
的
值
a[0]
是
a[0]
所代表的那个数
组
的首地址,
这
个表达式在
C
语
言的数
组
里面具有特殊的意
义
,之所以特殊,不
仅仅
在于它所代表的
东
西与一般的地址不同,而且
类
型也并非一般的地址
类
型,它的
类
型叫做数
组类
型,数
组类
型
这
个名称在
绝
大多数教材中是从来没有出
现过
的,在
C89
标
准中,也
仅仅
出
现
在介
绍
数
组
定
义
的那一段。具有数
组类
型的地址跟一般
类
型地址的主要区
别
,在于
长
度不一
样
,
对
一个一般
类
型的地址
进
行
sizeof
运算,
结
果是
4
个字
节
,而
a[0]
由于代表了一个数
组
,
sizeof(a[0])
的
结
果是整个数
组
的
长
度
8x9xsizeof(int)
,并非
4
个字
节
。具有数
组类
型的地址跟数
组
名一
样
都是一个符号地址常量,因此它必定是一个右
值
。数
组类
型在数
组
的定
义
与引用中具有非常重要的作用,它可以用来
识别
一个
标识
符或表达式是否真正的数
组
,
一个真正数
组
的数
组
名,是一个具有数
组类
型的符号地址常量,它的
长
度,是整个数
组
的
长
度,并非一般地址的
长
度,如果一个
标识
符不具
备
数
组类
型,那它就不是一个真正的数
组
。
在后面的章
节
里,
还
会再次使用
这
个概念。
与
a[0]
类
似的数
组类
型地址
还
有
a[0][0]
,
a[0][0]
是
a[0]
的下一
层
数
组
,因此
sizeof(a[0][0])
的
结
果是
9xsizeof(int)
。
类
似地,
对
于一个三
维
数
组
:
a[i][j][k]
a
、
a[x]
、
a[x][y]
(其中
x
、
y
大于等于
0
而小于
i
、
j
)都是具有数
组类
型的地址常量,而且都是一个右
值
。
这
一点要牢牢
记
住。正是由
这
些特殊
类
型的地址构成了整个数
组
。
以上
结论对
于
n
维
数
组
同
样
适用。
接下来跟各位一起
讨论
一下跟数
组
有
关
的各
种
表达式的意
义
及其
类
型:
&a[0][0][0]
:
&a[0][0][0]
仅仅
是一个地址,它的意
义
,
仅仅
表示元素
a[0][0][0]
的地址,
sizeof(&a[0][0][0])
的
结
果是
4
。不少人把它
说
成是数
组
a
的首地址,
这
是
错误
的,
这
是
对
数
组
首地址概念的
滥
用。
真正能代表数
组
a
的数
组
首地址只有
a
本身,
a
与
&a[0][0][0]
的意
义
根本就是两回事,真正的数
组
首地址是具有数
组类
型的地址,
sizeof(a)
结
果是
ixjxkxsizeof(int)
,而不是
4
,只不
过
由于
a[0][0][0]
位置特殊,恰好是数
组
a
的第一个元素,所以它
们
的地址
值
才相同。
而
对
于
a[0]
和
a[0][0]
,它
们
是在数
组
a
内部
a[0]
和
a[0][0]
所代表的那个数
组
的首地址,它
们
的地址
值
也是由于
位置
“
特殊
”
,因此才跟
a
和
&a[0][0][0]
一
样
。
这
一点一定要区分清楚了。
a+i
:
可能有些人会
对
a+i
感到迷惑,数
组
的首地址加上一个整数是什
么
呢?它是第一
维
元素
i
的地址,
sizeof(a+i)
为
4
。
a[i]+j
:
跟上面的
类
似,
a[i]+j
是
a[i]
所代表的那个数
组
的元素
j
的地址,
sizeof(a[i]+j)
的
结
果也
为
4
。
&a
:
对
数
组
名取地址在
C
标
准里面是未定
义
的。
这
个表达式曾
经
引起
过
争
论
,焦点在于
对
一个右
值
取地址的合法性。
C89
规
定
&
运算符的操作数必
须
具有具体的内存空
间
,
换
言之就是一个左
值
,
但数
组
名却是一个右
值
,按照
&
运算符的要求,
这
是非法行
为
。因此,早期的
编译
器通常
规
定
&a
是非法的。但不知道什
么
原因,
现
在的
编译
器都把
&a
人
为
地定
义
成一个比
a
高一
级
而地址
值
跟
a
一
样
的地址,但作
为
比
a
高一
级
的地址,有一个行
为
却非常怪
诞
,
sizeof(&a)
的
结
果跟
sizeof(a)
相同,
这
也是人
为
的痕迹。笔者
倾
向于把
&a
定
义为
非法,
应该维护
&
运算符的
权
威性,而不是在
规
定
对
某个右
值
取地址
为
非法的同
时
,又允
许对
另一个右
值
取地址,
这
是互相矛盾的。
&a[i]
和
&a[i][j]
:
跟
&a
一
样
,也是未定
义
的,同
样
不符合
&
运算符的
规则
。由于
a[i]
是
a[i][j]
的上一
层
数
组
,有些人可能会想当然地以
为
:
a[i]=&a[i][j]
,
错
也,
实际
上,由于
a[i][j]=*(a[i]+j)
,因此
&a[i][j]=&*(a[i]+j)
,
结
果是
a[i]+j
。
对
于
sizeof(&a[i])
和
sizeof(&a[i][j])
,由于是未定
义
的,因此有些
编译
器
规
定其
值
跟
sizeof(a[i])
和
sizeof(a[i][j])
相同,有些
编译
器却
规
定
为
4
,就是一个地址的
长
度
。
数
组
是存在于人
们头脑
中的一个
逻辑
概念,而
编译
器其
实
并不知道有数
组这
个
东
西,它所知道的,只是
[]
运算符,当遇到
[]
运算符的
时
候,
编译
器只是
简单
地把它
转换为类
似
*(*(a+i)+j)
这样
的等价表达式,之所以是
这种
表达式,如前几章所述,是因
为
C
语
言的数
组实现
本
质
上是数
组
的嵌套。
由于
这种
等价
关
系的存在,会
产
生一些古零精怪的表达式,例如:
10[a]
这
个表达式初看上去
让
人摸不着
头脑
,它是什
么
呢?如上所述,
编译
器会把它
转换为
*(10+a)
,把
a
和
10
调换
一下,就是
*(a+10)
了,
这
个就是
a[10]
。
[]
运算符之前
还
可以是一个表达式,例如:
(10+20)[a]
。
严
格来
讲
,以上两个表达式是非法的,因
为
C89
对
于数
组
的引用(注意不是数
组
定
义
)
规
定:
带
下
标
的数
组
引用后
缀
表达式由一个后
缀
表达式后跟一个括在方括号中的表达式
组
成。方括号前的后
缀
表达式的
类
型必
须为
“
指向
T
类
型的指
针
”
,其中
T
为
某
种类
型;方括号中表达式的
类
型必
须为
整型。
这
个
规
定
说
明,
进
行数
组
引用的
时
候,
[]
运算符的左
边
并非必
须为
数
组
名,而可以是一个表达式,但
这
个表达式的
类
型必
须为
“
指向某
类
型的指
针
”
。
显
然
10
跟
(10+20)
连
地址都不是,因此
实际
上他
们
是非法的,
编译
器在
这
里并没有
严
格遵守
标
准的
规
定。但如果是:
int a[10], *p = a;
(p+1)[2]
这样
就是合法的,因
为
p+1
的
结
果仍然是一个指
针
。
要注意的是,
虽
然后
缀
表达式是一个
“
指向某
类
型的指
针
”
,但不要被
这
里所
说
的指
针
一
词
搞混了,上面的
规
定不能反
过
来使用。
还
是以上面的例子
为
例,我
们
可以
p[i]
这样
使用
p
,
这
是符合上述
规
定的,但并不能
因
为
指
针
p
能
够
以
p[i]
这种
形式使用就
认为
p
是一个数
组
,
这
就
错误
了,不能反
过
来
应
用上述
规则
。
最后
说
一下
编译
器
对
&*
的
优
化,
对
于数
组
int a[10]
,如果
对
其中一个元素取地址,例如
&a[1]
,
这
条表达式等价于
&*(a+1)
,
编译
器并不会先
计
算
*
再运算
&
,而是
对
&*
两个运算符
进
行
优
化,把它
们
同
时
去掉,因
为
两者的作用是相反的,最后得到
计
算的是
a+1
表达式。
讲
到第五章了,数
组
两个字
还
离不
开
我
们
的左右,数
组
的内容也真多,另一方面也因
为
数
组
与指
针
的
关
系的确非常密切。
通常,
对
于
int a[8][9]
这
个二
维
数
组
,我
们
可以
这样
定
义
一个指向它的指
针
:
int (*p)[9];
这
个声明的形式跟人
们
所熟悉的
int *p
的形式大相庭径,初学者通常会感到迷惑,不理解的地方大致有四个:
1
。
为
什
么
会以
这种
形式声明?
2
。
(*p)
应该
如何理解?
3
。
为
什
么
必
须
把第二
维显
式地声明?
4
。
为
什
么
忽略第一
维
?
下面我
们
就一起逐个
讨论这
四个
问题
:
1
。
这种
形式是
C
标
准的声明
语
法
规
定的,由于本章不是
对标
准的解
释
,只是
对标
准的
应
用,因此笔者尽量以
简洁
的方式解
释这
个声明,
详细
的
讨论
将在第七章
进
行。
C
标
准的声明包含了两部分:
声明:
声明
说
明符
初始化声明符表
opt (opt
的意思是可
选
)
在声明
说
明符里面有一
项类
型
说
明符,
int
就是
这种类
型
说
明符。而初始化声明符表里面的其中一
种
形式,就是:
直接声明符
[
常量表达式
opt]
(*p)[9]
就是
这种
直接声明符加
[]
的形式。
2
。
p
左
边
的
*
在
这
里不是取
值
运算符,而是一个声明符,它指出
p
是一个指
针
。而
()
括号是不能去掉的,如果去掉了,由于
[]
运算符
优
先
级
比
*
高,
p
就会先跟
[]
结
合,
这样
p
就
变
成了一个指
针
数
组
,而不是指向数
组
的指
针
。
题
外
话
:
*p
还
有一
种
用法,就是当
*
是取
值
运算符的
时
候,
*p
是一个左
值
,表示一个
变
量,
为
什
么
*p
是一个
变
量呢?也
许
有人会
说
,因
为
int i, *p=&i
嘛,其
实这
是
结
果不是原因。
严
格来
说
,
i
只是一个
变
量名,不是
变
量,在
编译
器的符号表里面,
变
量名是一个符号地址,它所代表的地址
值
是它指向的那段内存
单
元的地址,真正叫
变
量的是那段内存
单
元,懂
汇编
的朋友能很容易地区分出来,在
汇编
里面,可以
这样
定
义
一个
变
量名:
VARW DW 10,20
VARW
就是一个
变
量名,它在
汇编
里面是一个地址,代表了
10
所在的内存
单
元
这
个
变
量。由于
p
被初始化
为
&i
,
*p
指向
i
所代表的那段内存
单
元,因此
说
*p
是一个
变
量。
把
i
称
为变
量是一
种习惯
上的
统
称。
3
。定
义
一个指
针
的
时
候,首先必
须
定出指
针
的
类
型,由于
这
是一个指向数
组
的指
针
,如果数
组
的元素的
类
型定下来了,那
么这
个指
针
的
类
型也就定下来了。前面
说过
,
C
语
言的多
维
数
组实质
上是数
组
的嵌套,那
么
所指向数
组
的元素必定具有数
组类
型,也就是
说
,
这
个数
组
的元素是一个具有
6
个
int
元素的数
组
,因此,
p
定
义
的
时
候,必
须
指定第二
维
的上界,
这样
才能把
p
的
类
型定下来。
4
。有
这种
疑
问
的人已
经
犯了一个
错误
,没有分清楚什
么
是指
针
,什
么
是数
组
,以数
组
的思
维
模式来看待
这
个指
针
p
。定
义
一个数
组
(非
static
)的
时
候,需要在
栈
中静
态
分配一
块
内存
,那
么
就需要知道
这块
内存的大小,因此定
义
数
组时
需要确定各
维
的上界。而
这
里只是定
义
一个指
针
而已,
对
于一个指
针
的定
义
,需要知道的是它所指向
对
象的
类
型,并不需要知道
对
象的大小,
这
是多余的。因此,所有指向数
组
的指
针
的第一
维
被忽略。
以上介
绍
了如何声明一个指向二
维
数
组
的指
针
,
类
似地,
对
一个指向
n
维
数
组
的指
针
也可以用同
样
的方法来声明,如下:
int (*p)[x2][x3]......[xn];
同
样
可以忽略第一
维
,而其它
维
必
须
指定上界。
最后再
讨论
一
种
很常
见
的
对
多
维
数
组
的
错误
理解,有些
人常常会以
为
,二
维
数
组
就是二
级
指
针
,
这种错误
的根源,来自于可以把一个二
级
指
针
int **p
以
p[i][j]
这种
形式使用。首先把数
组
称
为
指
针
就是
错误
的,第一章笔者已
经说
明了数
组
名是地址,不能理解
为
指
针
。第二,并非能以
p[i][j]
这种
形式使用,那
么
p
就是一个二
维
数
组
了,
C
标
准
对
数
组
引用的
规
定,并没有指定数
组
引用
时
[]
运算符的左
边
必
须
是数
组
名,而可以是一个表达式。第三,
这
是一
种
“
巧合
”
,
归
根到底是由于
C
语
言的数
组实现
是数
组
的嵌套同
时
C
标
准把
[]
运算符
转换为类
似
*(*(a+i)+j)
这样
的等价表达式造成的,那两个
取
值
运算符
“
恰好
”
可以用于一个二
级
指
针
。第四,
p
与
p[i]
并不具有数
组类
型,
sizeof(p)
和
sizeof(p[i])
的
结
果只是一个指
针
的大小
4
字
节
。而
对
于一个真正的数
组
,
p
与
p[i]
都是具有数
组类
型的地址。
实际
上,
int **p
只是一个指向一
维
指
针
数
组
的指
针
,而不是指向二
维
数
组
的指
针
。同
样
地,
对
于
n
级
指
针
,都可以看作一个指向一
维
指
针
数
组
的指
针
,
这
个指
针
数
组
的元素都是
n-1
级
指
针
。
动态
数
组
与字符串常量可算是两
种
“
另
类
”
数
组
。
VLA
可
变长
数
组
并不
为
C89
所支持,
C99
才
开
始支持
VLA
。但如果想在只支持
C89
的
编译环
境中使用
VLA
的
话
,怎
么办
呢?我
们
可以用
动态
数
组
来
“
模
拟
”
,
动态
数
组
在矩
阵
的运算中很常
见
,常用来向函数
传递
一个大小可
变
的矩
阵
。
动态
数
组
的原理,是利用一
块
或多
块动态
分配的
内存存
储
各
维
的首地址,
这样
就可以
p[i][j]
的形式
访问
数
组
的数据了。但是,
动态
数
组
并非真正的数
组
,它只是
对
数
组
的一
种
模
拟
。由于具有数
组类
型的数
组
名是系
统
行
为
,在用
户这
一
级
没法做到,因此只能以指
针
的形式存放首地址,
sizeof(p)
和
sizeof(p[i])
结
果都是
4
字
节
。
虽
然
动态
数
组
是依靠
动态
分配内存来建立的,但
动态
的意
义
并非来自
这
里,而是指大小可
变
。笔者
觉
得用
“
动态
数
组
”
这
个名称来命名非常适合,既不失大小可
变
的特征,又可以跟
VLA
可
变长
数
组
区分
开
来。
下面是建立
动态
数
组
的示例:
#include <stdio.h>
#include <stdlib.h>
#include <stdlib.h>
void computedata(int *, int, int);
int main(void)
{
int iData[100], x, y;
do
{
printf("The product obtained by multiplying x and y must be less than 100!");
printf("x=");
scanf("%d", &x);
printf("y=");
scanf("%d", &y);
}
while(x*y > 100);
computedata(iData, x, y);
return 0;
}
{
int iData[100], x, y;
do
{
printf("The product obtained by multiplying x and y must be less than 100!");
printf("x=");
scanf("%d", &x);
printf("y=");
scanf("%d", &y);
}
while(x*y > 100);
computedata(iData, x, y);
return 0;
}
void computedata(int *ipSource, int iRow, int iColumn)
{
int **ipTemp, i, j;
ipTemp = (int **)malloc(iRow*sizeof(int*));
for(i=0; i<iRow; ++i) ipTemp[i] = ipSource+i*iColumn;
for(i=0; i<iRow; ++i) for(j=0; j<iColumn; ++j) ipTemp[i][j] += 1;
free(ipTemp);
return;
}
{
int **ipTemp, i, j;
ipTemp = (int **)malloc(iRow*sizeof(int*));
for(i=0; i<iRow; ++i) ipTemp[i] = ipSource+i*iColumn;
for(i=0; i<iRow; ++i) for(j=0; j<iColumn; ++j) ipTemp[i][j] += 1;
free(ipTemp);
return;
}
以上示例把
动态
数
组
ipTemp
的元素都加了
1
,由于只是示例,笔者省略了
检测
数据合法性的代
码
。
iRow
是第一
维
上界,
iColumn
是第二
维
上界,
iData
是源数据
缓
冲区,
iRow*iColumn
的
积
不能超
过
iData
缓
冲区的大小,否
则
就会越界了,但可以比它小。示例中
iData
被定
义为
一
维
数
组
,当然根据自己的需要也可以用其它
类
型的
缓
冲区代替,例如
动态
分配的一
块
内存,或者多
维
数
组
,如果是多
维
数
组
,例如三
维
数
组
int iData[10][10][10]
,
调
用
computedata
函数
时
,
实
参
iData
得做一些
转换
:
computedata((int *)iData, x, y);
或者
computedata(&iData[0][0][0], x, y);
都可以。
ipSource
指
针
用来
传递缓
冲区的首地址,
这
个指
针
由于要用来
计
算各
维
的地址,因此最好定
义为
一
级
指
针
,
这样
比
较
方便。
ipTemp
是一个二
级
指
针
,
这
是因
为
它指向的那
块
内存存放的是指
针
,
这
些指
针
指向各
维
的首地址,
对
ipTemp
的元素来
说
,
ipTemp
就是二
级
的。最后
记
得
free(ipTemp);
以上是定
义
一个二
维动态
数
组
的例子,多
维动态
数
组
的
创
建方法跟
这
个
类
似
,下面
给
出三
维动态
数
组
的代
码
:
void computedata(int *ipSource, int iHigh, int iRow, int iColumn)
{
int ***ipTemp, i, j, k;
ipTemp = (int ***)malloc(iHigh*sizeof(int**));
for(i=0; i<iHigh; ++i) ipTemp[i] = (int **)malloc(iRow*sizeof(int*));
for(i=0; i<iHigh; ++i) for(j=0; j<iRow; ++j) ipTemp[i][j] = ipSource+i*iRow*iColumn+j*iColumn;
for(i=0; i<iHigh; ++i) for(j=0; j<iRow; ++j) for(k=0; k<iColumn; ++k) ipTemp[i][j][k] += 1;
for(i=0; i<iHigh; ++i) free(ipTemp[i]);
free(ipTemp);
return;
}
{
int ***ipTemp, i, j, k;
ipTemp = (int ***)malloc(iHigh*sizeof(int**));
for(i=0; i<iHigh; ++i) ipTemp[i] = (int **)malloc(iRow*sizeof(int*));
for(i=0; i<iHigh; ++i) for(j=0; j<iRow; ++j) ipTemp[i][j] = ipSource+i*iRow*iColumn+j*iColumn;
for(i=0; i<iHigh; ++i) for(j=0; j<iRow; ++j) for(k=0; k<iColumn; ++k) ipTemp[i][j][k] += 1;
for(i=0; i<iHigh; ++i) free(ipTemp[i]);
free(ipTemp);
return;
}
下面来
讨论
字符串常量。
众所周知, C 语 言是没有字符串 变 量的,因而, C89 规 定,字符串常量就是一个字符数 组 。因此,尽管字符串常量的外部表 现 形式跟数 组 完全不同,但它的确是一个真正的数 组 , 实际 上,字符串常量本身就是 这 个数 组 的首地址,并且具有数 组类 型, 对 一个字符串常量 进 行 sizeof 运算,例如 sizeof("abcdefghi") , 结 果是 10 ,而不是 4 。字符串常量与一般数 组 的主要区 别 ,是字符串常量存放在静 态 存 储 区,而一般数 组 (非 static ) 则 是在 栈 中静 态 分配的。由于字符串常量是数 组 首地址,因此可以数 组 引用的形式使用它,例如:
众所周知, C 语 言是没有字符串 变 量的,因而, C89 规 定,字符串常量就是一个字符数 组 。因此,尽管字符串常量的外部表 现 形式跟数 组 完全不同,但它的确是一个真正的数 组 , 实际 上,字符串常量本身就是 这 个数 组 的首地址,并且具有数 组类 型, 对 一个字符串常量 进 行 sizeof 运算,例如 sizeof("abcdefghi") , 结 果是 10 ,而不是 4 。字符串常量与一般数 组 的主要区 别 ,是字符串常量存放在静 态 存 储 区,而一般数 组 (非 static ) 则 是在 栈 中静 态 分配的。由于字符串常量是数 组 首地址,因此可以数 组 引用的形式使用它,例如:
printf("%s", &"abcdefghi"[4]);
这
将打印出字符串
efghi
。
还
可以
这样
:
printf("%s", "abcdefghi"+4);
同
样
打印出字符串
efghi
。
实际
上,
&"abcdefghi"[4]
等价于
&*("abcdefghi"+4)
,去掉
&*
后,就是
"abcdefghi"+4
了。
我
们
可以利用字符串常量
这
些特性写出一些有趣的程序来,例如:
#include <stdio.h>
int iLine=1;
int main(void)
{
printf("%*s/n", 7-(iLine>4?iLine-4:4-iLine), "*******"+2*(iLine>4?iLine-4:4-iLine));
if(++iLine != 8) main();
return 0;
}
{
printf("%*s/n", 7-(iLine>4?iLine-4:4-iLine), "*******"+2*(iLine>4?iLine-4:4-iLine));
if(++iLine != 8) main();
return 0;
}
这
个程序不使用任何数
组
形式的引用,不使用循
环
,就可以打印出用
*
号
组
合出来的菱形。当然,笔者并非鼓励大家
编
写
这样
的代
码
,但通
过这样
的例子加深
对
字符串常量的
认识
,仍然是非常重要的
。
人
们
常
说
,
C
语
言的声明太
复杂
了,的确,
这
也是
C
语
言
饱
受批
评
的地方之一。不
过
,笔者
认为
,真正要受到批
评
的不是
语
言本身,而是那些
传
播者。
传
播者
们
通常都有一个共
识
:
讲
述要由浅入深。作
为
原
则
,笔者并非要反
对
它,
毕
竟笔者
对
C
语
言的学
习
,也
经历
了相同的
过
程。但是,由浅入深并不意味着一切从
简
,以偏盖全。
计
算机
语
言不同于数学理
论
(
虽
然它的确根植于数学,与数学密不可分),数学理
论
是一
种
循
序
渐进
的
过
程,后面的理
论
以前面的理
论为
基
础
。但
C
语
言
归
根
说
底,就是一堆
语
言
规则
而已,
应该让
学
习
者一
开
始就全面且
详细
地了解它,而不是象
现
在某些教材所做的那
样
,只
说
一部分,不
说
另一部分,以
为这
就是由浅入深了,
实际
上
这
是以偏盖全。
语
言如此,声明作
为
C
语
言的一部分更是如此。我
们
最常
见
到的
对
声明的描述是
这样
的:
存
储类别
类
型限定
词
类
型
标识
符
这种说
明会
给
人
们
一
种
暗示:
C
语
言的声明是静止的、死板的,什
么
声明都能
够
以
这
个
为
基
础
,往上一套就
OK
了。事
实
真的如此
吗
?
说
句心里
话
,笔者也祈祷事
实
真的
如此,
这样
世界就
简单
多了、清静多了。但
别
忘了,
这
个世界
总
是
让
人事与愿
违
的。
实际
上,
C
的声明的
组织
形式是以嵌套
为
基
础
的,是用嵌套声明
组织
起来的,并非象上面所述那
么
死板,存
储类说
明符一定得放在限定
词
前面
吗
?
类
型
说
明符一定要
紧贴标识
符
吗
?不!
C
标
准从来没有
这样说过
!下面来看一看
C89
对
声明的形式是如何
规
定的:
声明:
声明
说
明符
初始化声明符表
opt [opt
的意思是
option
,可
选
]
其中声明
说
明符由以下三
项
构成:
声明
说
明符:
存
储类说
明符
声明
说
明符
opt
类 型 说 明符 声明 说 明符 opt
类 型 限定符 声明 说 明符 opt
类 型 说 明符 声明 说 明符 opt
类 型 限定符 声明 说 明符 opt
在
这
里,一个声明
说
明符可以包含另一个声明
说
明符,
这
就是声明的嵌套,
这种
嵌套
贯
穿于整个声明之中,今天我
们
看来一个非常
简单
的声明,其
实
就是由多个声明嵌套
组
成的,例如:
static const int i=10, j=20, k=30;
变
量
i
前面就是声明
说
明符部分,有三个声明
说
明符:
static const int
,
static
是一个存
储类说
明符,它属于
这种
形式:
static
声明
说
明符
static
后面的声明
说
明符就是
const int
,
const
是一个
类
型限定符,
这
也是个
嵌套,它是由
const
声明
说
明符
组
成,最后的
int
是一个
类
型
说
明符,到
这
里已
经
没有嵌套了,
int
就是最底的一
层
。
对
于存
储类说
明符、
类
型
说
明符和
类
型限定符的排列
顺
序,
C
标
准并没有
规
定其
顺
序,
谁
嵌套
谁
都可以。
换
言之,上面的声明可以写成
:
int static const i=10, j=20, k=30;
或者
const int static i=10, j=20, k=30;
这
无所
谓
,跟原声明是一
样
的。再
举
一个有趣的例子:
const int *p;
与
int const *p;
有些人会
对
后面一
种
形式感
到困惑,因
为
他一直以来学
习
的都是那
种
死板的形式,因此他无法理解
为
什
么
那个
const
可以放在
int
的后面。
实际
上
对
于
标
准来
说
,
这
是再正常不
过
的行
为
了。
上面
举
的例子是
变
量的声明,函数的声明也同
样
道理,例如:
static const int func(void);
......
......
int main(void)
{
int static const (*p)(void);
p=func;
.........
return 0;
}
{
int static const (*p)(void);
p=func;
.........
return 0;
}
const int static func(void)
{
.......
return 0;
}
{
.......
return 0;
}
func
的函数原型声明、函数定
义
跟
main
内的函数指
针
p
的声明是一
样
的。
但是,笔者并非鼓励大家把声明
说
明符写得乱七八糟,作
为
一个良好的
风
格,
应该
按照已
经习惯约
定的方式排列
说
明符,但懂得其中的原理非常重要。
声明
static const int i=10, j=20, k=30;
的
int
后面的部分就是初始化声明符表,
这
比
较
容易理解,
这
个符表
实际
上也是嵌套的:
初始化声明符表:
初始化声明符
初始化声明符表, 初始化声明符
初始化声明符表, 初始化声明符
初始化声明符:
声明符
声明符 = 初 值
声明符 = 初 值
声明符是初始化声明符的主体, 现 在来 讨论 一下声明符是如何 规 定的:
声明符:
指
针
opt
直接声明符
这
里写的指
针
opt
指的是那个指
针
声明符
*
,要注意的是,
*
属于声明符,而不是声明
说
明符的一部分。
指
针
opt
又包含:
指
针
:
*
类
型限定符表
opt
* 类 型限定符表 opt 指 针
* 类 型限定符表 opt 指 针
在
这
里有一个常
见
的
问题
,就是
const int *p;
与
int * const p
的区
别
,第一个声明的
const
属于声明
说
明符,它跟
int
一起,是用来
说
明
*p
这
个声明符的,因此
const
修
饰
的是
p
所指向的那个
对
象,
这
个
对
象是
const
的。
而第二个声明的
const
是声明符的一部分,它修
饰
的
对
象是
p
本身,因此
p
是
const
的。
上面
规
定的第二条
值
得注意,
这
条
规
定
产
生了一
种
指
针
与
const
的
复杂
形式,例如:
const int * const *** const ** const p;
(是不是有
种
想冲向
厕
所的冲
动
?)
这
是一
种复杂
的声明嵌套,如何解
读这种
声明?其
实
只要掌握了它的
规
律,无
论
它有多少个
const
、多少个
*
都不
难
解
读
的
,
这
个内容我将在第九章
进
行解
释
。
剩下的就是直接声明符和
类
型限定
词
表的内容:
直接声明符:
标识
符
(声明符)
直接声明符 [ 常量表达式 opt]
直接声明符(形式参数 类 型表)
直接声明符( 标识 符表 opt )
(声明符)
直接声明符 [ 常量表达式 opt]
直接声明符(形式参数 类 型表)
直接声明符( 标识 符表 opt )
类 型限定符表:
类
型限定符
类 型限定符表 类 型限定符
类 型限定符表 类 型限定符
这 一章的最后一个内容,是 讨论 一下 typedef , typedef 用来声明一个 别 名, typedef 后面的 语 法,是一个声明。本来笔者以 为这 里不会 产 生什 么误 解的,但 结 果却出乎意料, 产 生 误 解的人不在少数。罪魁 祸 首又是那些害人的教 材。在 这 些教材中介 绍 typedef 的 时 候通常会写出如下形式:
typedef int PARA;
这种
形式跟
#define int PARA
几乎一
样
,如前面几章所述,
这
些教材的宗旨是由浅入深,但
实际
做出来的行
为
却是以偏盖全。的确,
这种
形式在所有形式中是最
简单
的,但却没有
对
typedef
进
一
步
解
释
,使得不少人用
#define
的思
维
来看待
typedef
,把
int
与
PARA
分
开
来看,
int
是一部分,
PARA
是另一部分,但
实际
上根本就不是
这么
一回事。
int
与
PARA
是一个整体!就象
int i:
声明一
样
是一个整体声明
,只不
过
int i
定
义
了一个
变
量,而
typedef
定
义
了一个
别
名。
这
些人由于持有
这种错误
的
观
念,就会无法理解如下一些声明:
typedef int a[10];
typedef void (*p)(void);
typedef void (*p)(void);
他
们
会以
为
a[10]
是
int
的
别
名,
(*p)(void)
是
void
的
别
名,但
这样
的
别
名看起来又似乎不是合法的名字,于是陷入困惑之中。
实际
上,上面的
语
句把
a
声明
为
具有
10
个
int
元素的数
组
的
类
型
别
名,
p
是一
种
函数指
针
的
类
型
别
名。
虽
然在功能上,
typedef
可以看作一个跟
int PARA
分离的
动
作,但
语
法上
typedef
属于存
储类
声明
说
明符,因此
严
格来
说
,
typedef int PARA
整个是一个完整的声明。
上一章
费
那
么
多唇舌
讨论
C
语
言的声明,其
实
目的都是
为
了
这
一章,期望
读
者通
过对
C
语
言声明形式的
详细
了解,
树
立声明嵌套的
观
念,因
为
C
语
言所有
复杂
的指
针
声明,都是由各
种
声明嵌套构成的。如何解
读复杂
指
针
声明呢?右左法
则
是一个既著名又常用的方法。不
过
,右左法
则
其
实
并不是
C
标
准里面的内容,它是从
C
标
准的声明
规
定中
归纳
出来的方法。
C
标
准的声明
规则
,是用来解决如何
创
建声明的,而右左法
则
是用来解
决如何
辩识
一个声明的,两者可以
说
是相反的。右左法
则
的英文原文是
这样说
的:
The right-left rule: Start reading the declaration from the innermost parentheses, go right, and then go left.
When you encounter parentheses, the direction should be reversed. Once everything in the parentheses has been parsed, jump out of it. Continue till the whole declaration has been parsed.
这 段英文的翻 译 如下:
右左法
则
:首先从最里面的
圆
括号看起,然后往右看,再往左看。
每
当遇到
圆
括号
时
,就
应该
掉
转阅读
方向。一旦解析完
圆
括号里面所有的
东
西,就跳出
圆
括号。重
复这
个
过
程直到整个声明解析完
毕
。
笔者要
对这
个法
则进
行一个小小的修正,
应该
是从未定
义
的
标识
符
开
始
阅读
,而不是从括号
读
起,之所以是未定
义
的
标识
符,是因
为
一个声明里面可能有
多个
标识
符,但未定
义
的
标识
符只会有一个。
现
在通
过
一些例子来
讨论
右左法
则
的
应
用,先从最
简单
的
开
始,逐
步
加深:
int (*func)(int *p);
首先找到那个未定
义
的
标识
符,就是
func
,它的外面有一
对圆
括号,而且左
边
是一个
*
号,
这说
明
func
是一个指
针
,然后跳出
这
个
圆
括号,先看右
边
,也是一个
圆
括号,
这说
明
(*func)
是一个函数,而
func
是一个指向
这类
函数的指
针
,就是一个函数指
针
,
这类
函数具有
int*
类
型的形参,返回
值类
型是
int
。
int (*func)(int *p, int (*f)(int*));
func
被一
对
括号包含,且左
边
有一个
*
号,
说
明
func
是一个指
针
,跳出括号,右
边
也有个括号,那
么
func
是一个指向函数的指
针
,
这类
函数具有
int *
和
int (*)(int*)
这样
的形参,返回
值为
int
类
型。再来看一看
func
的形参
int (*f)(int*)
,
类
似前面的解
释
,
f
也是一个函数指
针
,指向的函数具有
int*
类
型的形参,返回
值为
int
。
int (*func[5])(int *p);
func
右
边
是一个
[]
运算符,
说
明
func
是一个具有
5
个元素的数
组
,
func
的左
边
有一个
*
,
说
明
func
的元素是指
针
,要注意
这
里的
*
不是修
饰
func
的,而是修
饰
func[5]
的,原因是
[]
运算符
优
先
级
比
*
高,
func
先跟
[]
结
合,因此
*
修
饰
的是
func[5]
。
跳出
这
个括号,看右
边
,也是一
对圆
括号,
说
明
func
数
组
的元素是函数
类
型的指
针
,它所指向的函数具有
int*
类
型的形参,返回
值类
型
为
int
。
int (*(*func)[5])(int *p);
func
被一个
圆
括号包含,左
边
又有一个
*
,那
么
func
是一个指
针
,跳出括号,右
边
是一个
[]
运算符号,
说
明
func
是一个指向数
组
的指
针
,
现
在往左看,
左
边
有一个
*
号,
说
明
这
个数
组
的元素是指
针
,再跳出括号,右
边
又有一个括号,
说
明
这
个数
组
的元素是指向函数的指
针
。
总结
一下,就是:
func
是一个指向数
组
的指
针
,
这
个数
组
的元素是函数指
针
,
这
些指
针
指向具有
int*
形参,返回
值为
int
类
型的函数。
int (*(*func)(int *p))[5];
func
是一个函数指
针
,
这类
函数具有
int*
类
型的形参,返回
值
是指向数
组
的指
针
,所指向的数
组
的元素是具有
5
个
int
元素的数
组
。
要注意有些
复杂
指
针
声明是非法的,例如:
int func(void) [5];
func
是一个返回
值为
具有
5
个
int
元素的数
组
的函数。但
C
语
言的函数返回
值
不能
为
数
组
,
这
是因
为
如果允
许
函数返回
值为
数
组
,那
么
接收
这
个数
组
的内容的
东
西,也必
须
是一个数
组
,但
C
语
言的数
组
名是一个右
值
,它不能作
为
左
值
来接收另一个数
组
,因此函数返回
值
不能
为
数
组
。
int func[5](void);
func
是一个具有
5
个元素的数
组
,
这
个数
组
的元素都是函数。
这
也是非法的,因
为
数
组
的元素除了
类
型必
须
一
样
外,
每
个元素所占用的内存空
间
也必
须
相同,
显
然函数是无法达到
这
个要求的,即使函数的
类
型一
样
,但函数所占用的空
间
通常是不相同的。
作
为练习
,下面列几个
复杂
指
针
声明
给读
者自己来解析,答案放在第十章里。
int (*(*func)[5][6])[7][8];
int (*(*(*func)(int *))[5])(int *);
int (*(*func[7][8][9])(int*))[5];
实际
当中,需要声明一个
复杂
指
针时
,如果把整个声明写成上面所示的形式,
对
程序可
读
性是一大
损
害。
应该
用
typedef
来
对
声明逐
层
分解,增
强
可
读
性,例如
对
于声明:
int (*(*func)(int *p))[5];
可以
这样
分解:
typedef int (*PARA)[5];
typedef PARA (*func)(int *);
typedef PARA (*func)(int *);
这样
就容易看得多了
。
const
一
词
是英文
constant
的
缩
写,
设
立
这
个
关键
字的本意,是希望
让
它所修
饰
的
对
象成
为
一个常量。
记
得在国家
间
的外交中,有一个
经
常用到的
术语
:
“
从事与身份不符的活
动
”
,
这
个
const
恰恰也正从事着
这样
的活
动
,呵呵。
C
语
言可以有三
种
方法定
义
一个常量:
#define
、
const
和枚
举
,但只有枚
举
才
是真正的常量,什
么
是真正的常量?真正的常量是没有存
储
空
间
的,是一个右
值
,
这
意味着通
过
任何合法的手段也不会被修改,但被
const
修
饰
的
对
象依然是一个左
值
,尽管
这
个
对
象被
const
限定,笔者仍然至少可以找到三
种
合法的手段去修改它,而
#define
所做的只不
过
是
编译
期替
换
而已,只有枚
举
常量才能真正做到
这
一点。
const
实
在不
应该
被命名
为
const
,
这
会
让
人
们产
生
误
解,它
应该
命名
为
readonly
或
类
似的字眼,意即不能通
过
被
const
修
饰
的
对
象修改它所指向的
对
象或者它所代表的
对
象。但在
C
的世界里把
const
称
为
常量
早已是普遍的
现
象,那我
们
就只好随大流咯,也称之
为
常量吧,只要知道它
实际
上不是真正的常量就行了。
第七章曾
经讨论过
const int *p;
与
int * const p
的区
别
,
这
两个声明的中文名称常常搞得混乱不堪。第一个声明的
const
是声明
说
明符,它修
饰
p
所指向的
对
象,但
p
仍然是可
变
的,
这
意味着
p
是一个指向常量的指
针
,
简
称常量指
针
。第二个声明的
const
是声明符的一部分,它修
饰
的
对
象是
p
,
这
意味着
p
是一个常量,而且是一个指
针类
型的常量,
简
称指
针
常量。指
针
常量又常常被人称
为
“
常指
针
”
或
“
常指
针变
量
”
,常指
针变
量
这
个名称有点蹩脚,又常又
变
的,容易
让
人摸不着
头脑
,最好
还
是不要
这样
称呼。
这
里
还
得再
强调
一次指
针
常量与地址常量是不同的,不能把数
组
名称
为
指
针
常量,也不能把指
针
常量称
为
地址常量,因
为
指
针
常量依然是一个左
值
,而数
组
名是一个右
值
,
这
里肯定有人会
问
:
“
什
么
?指
针
常量是一个左
值
?我没听
错
吧?
”
你的确没有听
错
,
C89
对
于左
值
是
这样
定
义
的:
对
象是一个命名的存
储
区域,左
值
(
lvalue
)是引用某个
对
象的表达式。
换
言之,如果一个表达式引用的是一个具有具体存
储
空
间
的
对
象,它就是一个左
值
!那
么
既然指
针
常量是一
个左
值
,
为
什
么
却不能
给
它
赋值
呢?是因
为
它受限于
赋值
表达式的一条
规则
:
赋值
表达式的左
值
不能含有限定
词
!
为
了防止指
针
指向的常量被修改,
C
标
准
对
于指
针间赋值
有一个
规
定,就是左
值
必
须
包含右
值
的所有限定
词
。
这
就限定了一个指向
const
对
象的指
针
不能
赋值给
指向非
const
对
象的指
针
,但反
过
来就允
许
。
这
个
规
定初看上去非常合理,但其效用其
实
只限于一
级
指
针
,二
级
指
针间
的
赋值
即使
满
足
规
定也不再安全,下面
举
个例子:
const int i=10;
const int **p1;
int *p2;
p1 = &p2;
*p1 = &i;
*p2 = 20;
const int **p1;
int *p2;
p1 = &p2;
*p1 = &i;
*p2 = 20;
现
在你会
发现
,作
为
常量的
i
的
值
被修改了。
i
的
值
被修改的
关键
原因在
*p1=&i;
这
一句,
&i
是一个指向常量的一
级
地址,如果没有二
级
指
针
p1
,受限于上述
规
定,作
为
左
值
接受
这
个一
级
地址的指
针
就必
须
也是一个指向常量的一
级
指
针
,于是就不能
进
行下一
步赋值
20
的操作。因此,正由于指向
const
对
象的二
级
指
针
p1
的出
现
,使得
*p1
也是一个指向
const
的指
针
,于是
*p1=&i
能
够
合法地运行,常量
i
的
值
被修改也就成了一个
预
想中的
结
果了。有
鉴
于此,某些
编译
器也会限定非
const
二
级
指
针
之
间
的
赋值
,
规
定上面的
p1=&p2
也是非法的。
第七章介
绍
声明符的指
针
部分有一
种
形式:
*
类
型限定符表
opt
指
针
这种
形式
产
生了一
种
比
较复杂
的
带
const
的指
针
,例如:
const int * const *** const ** const p;
这
是一个会
让
人
头晕
目眩的表达式,声明符部分嵌套了九次,如何辨
认谁
是
const
,
谁
不是
const
呢?一旦明白了其中的原
则
,其
实
是非常
简单
的。第一和最后一个
const
大家都已
经
很熟悉的了。
对
于藏在一堆
*
号中的
const
,有一个非常
简单
的原
则
:
const
与左
边
最后一个声明
说
明符之
间
有多少个
*
号,那
么
就是多少
级
指
针
是
const
的。例如从右数起第二个
const
,它与
int
之
间
有
4
个
*
号,那
么
p
的四
级
部分就是
const
的,下面的
赋值
表达式是非法的:
**p = (int *const***)10;
但下面的 赋值 是允 许 的:
***p=(int*const**)10;
从左 边 数起第二个 const ,它与 int 之 间 有 1 个 * ,那 么 p 的一 级 部分是 const 的,也就是 *****p = (int*const***const*)10; 是非法的。
但下面的 赋值 是允 许 的:
***p=(int*const**)10;
从左 边 数起第二个 const ,它与 int 之 间 有 1 个 * ,那 么 p 的一 级 部分是 const 的,也就是 *****p = (int*const***const*)10; 是非法的。
对
于一个函数:
void func(void);
我
们
通常可以定
义
一个
这样
的函数指
针
指向它:
void (*p)(void) = func;
通
过
p
调
用
func
时
,通常有两
种
写法:
p();
或者
(*p)();
围绕这
两
种
写法,当初
C89
制定的
时
候曾
经
有
过
争
论
。
(*p)();
是一
种
旧
式的
规
定,旧式
规
定
圆
括号左
边
必
须
具有
“
函数
”
类
型,如果是指向函数的指
针
,那
么
必
须
加上
*
声明符。但
C89
不再把
圆
括号的左
边
限定
为
“
函数
”
类
型,而是一个后
缀
表达式。那
么问题
就来了,如果
p
的
值
是函数地址,那
么
*
号就是声明符,但如果
p
指向的内容是函数地址,
*
号就得被看作运算符了。同一
种
形式会有两
种
解
释
,
这
是一个矛盾。不
仅
函数
调
用如此,指向数
组
的指
针
也存在
这种
矛盾。
编译
器
为
了
处
理
这种
情况得增加代
码
,效率自然就降低了。争
论
的最后
结
果是
谁
也不能把
对
方完全
说
服,于是就干脆两
种
都支持了。笔者
认为应该
抛弃旧式的
规
定,
p();
这种
形式
简洁
明了,又符合函数的一般形式,何
乐
而不
为
?
第八章 练习 的答案,同 时给 出用 typedef 的分解方法:
int (*(*func)[5][6])[7][8];
func
是一个指向数
组
的指
针
,
这类
数
组
的元素是一个具有
5X6
个
int
元素的二
维
数
组
,而
这
个二
维
数
组
的元素又是一个二
维
数
组
。
typedef int (*PARA)[7][8];
typedef PARA (*func)[5][6];
typedef PARA (*func)[5][6];
int (*(*(*func)(int *))[5])(int *);
func
是一个函数指
针
,
这类
函数的返回
值
是一个指向数
组
的指
针
,所指向数
组
的元素也是函数指
针
,指向的函数具有
int*
形参,返回
值为
int
。
typedef int (*PARA1)(int*);
typedef PARA1 (*PARA2)[5];
typedef PARA2 (*func)(int*);
typedef PARA1 (*PARA2)[5];
typedef PARA2 (*func)(int*);
int (*(*func[7][8][9])(int*))[5];
func
是一个数
组
,
这
个数
组
的元素是函数指
针
,
这类
函数具有
int*
的形参,返回
值
是指向数
组
的指
针
,所指向的数
组
的元素是具有
5
个
int
元素的数
组
。
typedef int (*PARA1)[5];
typedef PARA1 (*PARA2)(int*);
typedef PARA2 func[7][8][9];
typedef PARA1 (*PARA2)(int*);
typedef PARA2 func[7][8][9];
在
这
篇后
记
中,笔者将
对
三个
问题进
行
补
充:
一、
关
于数
组
名取地址的
问题
。
c89
、
c99
允
许对
数
组
名取地址,是由于数
组
符合一个
对
象的定
义
,按照一
个
对
象的
语义
,
对
其取地址是合理的。但矛盾在于,数
组
名是一个符号地址,是一个右
值
,
对
其取地址不
符合
&
运算符的
语
法。
c89
、
c99
委
员
会
经过权
衡,
认为维护
一个
对
象的合理性比一个运算符更重要、更合
理,因此允
许对
数
组
名取地址。但是,
&a
的意
义
,已
经
不是
对
一个数
组
名取地址,而是
对
一个数
组对
象取
地址,因此,
&a
所代表的地址
值
才跟
a
地址
值
一
样
,同
时
sizeof(&a)
应该
等于
sizeof(a)
。
c89
、
c99
对这
个
观
点是
这样阐
述的:
Some implementations have not allowed the & operator to be applied to an array or a function.(The construct was permitted in early versions of C, then later made optional.) The C89 Committee endorsed the construct since it is unambiguous, and since data abstraction is enhanced by allowing the important & operator to apply uniformly to any addressable entity.
二、
对
于二
级
const
指
针间
的
赋值
,笔者曾
经
在第九章
举
了一个例子,以
说
明二
级
const
指
针间
的
赋值
会
导
致一个常量被修改。
这
里再
补
充一条理由。以
const int *p1
和
int *p2
为
例
类
型比
较涉
及两个方面。首先,
p1
和
p2
所指向的
对
象都是
int
,
这
是相容的;
p1
带
有
const
而
p2
没有,
这
也是相容的,因
为
它符合
“
指
针间赋
值
左
值
要包含右
值
所有的限定
词
”
这
条
规则
。但
对
于
const int ** p1
和
int **p2
来
说
,情况就不一
样
了。
虽
然
p1
仍然符合
“
左
值
包含右
值
所有限定
词
”
规则
,但
p1
所指向的
对
象是一个指向
const
对
象的指
针
,而
p2
所指
向的
对
象却是一个指向非
const
对
象的指
针
,两者的
类
型不相容,因此
p1=p2
非法。
这
条理由是一
种
已
过时
的
现
象,更多地存在于早期的
编译
器里,由于它往往
让
用
户难
以理解,因此后期的
编译
器都
倾
向于允
许这种
赋值
。
三、
关
于
p()
与
(*p)()
之
间
的争
论
,有人提出,由于
c89
存在一条
隐
含声明,会使得
p()
这种
形式存在危
险
性。
首先,
c89
、
c99
推荐使用
p()
这种
形式已
经
是盖棺定
论
的事
实
,两个
标
准是
这样说
的:
Pointers to functions may be used either as (*pf)() or as pf(). The latter construct, not sanctioned in K&R, appears in some present versions of C, is unambiguous, invalidates no old code, and can be an important shorthand. The shorthand is useful for packages that present only one external name, which designates a structure full of pointers to objects and functions: member functions can be called as graphics.open(file) instead of (*graphics.open)(file).
...........................
The C89 Committee saw no real harm in allowing these forms; outlawing forms like (*f)(), while still permitting *a for a[], simply seemed more trouble than it was worth.
...........................
The C89 Committee saw no real harm in allowing these forms; outlawing forms like (*f)(), while still permitting *a for a[], simply seemed more trouble than it was worth.
可
见
(*p)()
这种
形式是
K&R
时
代的旧式
规
定,
c89
、
c99
仍然支持它只不
过
是因
为对
旧代
码
的支持。
第二,
这
些人以
为
,未声明
p
而使用
p()
的
时
候,由于
这
条
隐
含声明的存在,
p
就是已被声明了,
这
是
对隐
含声明的
错误
理解,
实际
上
c89
、
c99
只是把
这种
情况下的
p()
当作
extern int p();
而已,在
语
法上,
p
仍
然是一个未声明的
标识
符!它
违
反了
另一条
规则
:一个
标识
符
应
当在声明之后才能使用。但它不
应该
象其
它未声明
标识
符那
样产
生一条
error
,否
则隐
含声明跟不存在没有什
么
两
样
。它
应该产
生一条
warning
,以
说
明
p
是
undefined
或者函数原型未声明。
如果一定要
说
会
产
生什
么问题
的
话
,那就是可能会有某些程序
员
忽略
这
个
warning
,
这
会
产
生一些
问题
。
但
这
是
设计
者的原因,不是
标
准的原因。出于杜
绝这种隐
患的原因,
c99
干脆
废
除了
c89
的
隐
含声明,以使
编译
器
产
生一条
error
。
c99
关
于
这
一点是
这样说
的:
The rule for implicit declaration of functions has been removed in C99.The effect is to guarantee the production of a diagnostic that will catch an additional category of programming errors.
第四点写于
11
月
8
日
第四。第十章笔者解
释
了函数的返回
值
不能
为
数
组
的原因,文中笔者是
这样
写的:
int func(void) [5];
func
是一个返回
值为
具有
5
个
int
元素的数
组
的函数。但
C
语
言的函数返回
值
不能
为
数
组
,
这
是因
为
如果允
许
函数返回
值为
数
组
,那
么
接收
这
个数
组
的内容的
东
西,也必
须
是一个数
组
,但
C
语
言的数
组
名是一个右
值
,它不能作
为
左
值
来接收另一个数
组
,因此函数返回
值
不能
为
数
组
。
以上
这
个解
释
有偏差,不
应
当用数
组
名来解
释这
个
问题
,因
为
数
组
名不是数
组对
象的引用,在
C
中,没有
对
数
组对
象的引用,因此,函数的返回
值
不能
为
数
组
。