C程序设计语言-附录A参考手册
附录A 参考手册
A.1 引言
本手册描述的C语言是1988年10月31日提交给ANSI的草案,批准号为“美国国家信息系统标准————C程序设计语言,X3.159-1989”。尽管我们已尽最大努力,力求准确地将该手册作为C语言的指南介绍给读者,但它毕竟不是标准本身,而仅仅只是对标准的一个解释而已。
该手册的组织与标准基本类似,与本书的第1版也类似,但是对细节的组织有些不同。本手册给出的语法与标准是相同的,但是,其中少量元素的命名可能有些不同,词法记号和预处理器的定义也没有形式化。
本手册中,说明部分的文字指出了ANSI标准C语言与本书第1版定义的C语言或其他编译器支持的语言之间的差别。
A.2 词法规则
程序由存储在文件中的一个或多个翻译单元(translation unit)组成。程序的翻译分几个阶段完成,这部分内容将在A.12节中介绍。翻译的第一阶段完成低级的词法转换,执行以字符#
开头的行中的指令,并进行宏定义和宏扩展。在预处理(将在A.12节中介绍)完成后,程序被归约成一个记号序列。
A.2.1 记号
C语言中共有6类记号:标识符、关键字、常量、字符串字面值、运算符和其他分隔符。空格、横向制表符和纵向制表符、换行符、换页符和注释(统称空白符)在程序中仅用来分割记号,因此将被忽略。相邻的标识符、关键字和常量之间需要用空白符来分割。
A.2.2 注释
注释以字符/*
开始,以*/
结束。注释不能够嵌套,也不能够出现在字符串字面值或字符字面值中。
A.2.3 标识符
标识符是由字母和数字构成的序列。第一个字符必须是字母,下划线“_”
也被看成是字母。大写字母和小写字母是不同的。标识符可以为任意长度。对于内部标识符来说,至少前31个字母是有效的,在某些实现中,有效的字符数可能更多。内部标识符包括预处理器的宏名和其他所有没有外部连接(参见A.11.2节)的名字。带有外部连接的标识符的限制更严格一些,实现可能只认为这些标识符的前6个字符是有效的,而且有可能忽略大小写的不同。
A.2.4 关键字
下列标识符被保留作为关键字,且不能用于其他用途:
auto double int struct
break else long switch
case enum register typedef
char extern return union
const float short unsigned
continue for signed void
default goto sizeof volatile
do if static while
某些实现还把fortran和asm保留为关键字。
说明:关键字const、signed和volatile是ANSI标准中新增加的;enum和void是第1版后新增加的,现已被广泛应用;entry曾经被保留为关键字但从未被使用过,现在已经不是了。
A.2.5 常量
常量有多种类型。每种类型的常量都有一个数据类型。基本数据类型将在A.4.2节讨论。
常量:
整型常量
字符常量
浮点常量
枚举常量
- 整型常量
整型常量由一串数字组成。如果它以数字0开头,则为八进制数,否则为十进制数。八进制常量不包含数字8和9。以0x和0X开头的数字序列表示十六进制数,十六进制数包含从a(或A)到f(或F)的字母,它们分别表示数值10到15。
整型常量若以字母u或U为后缀,则表示它是一个无符号数;若以字母l或L为后缀,则表示它是一个长整型数;若有字母UL为后缀,则表示它是一个无符号长整型数。
整型常量的类型同它的形式、值和后缀有关(有关类型的讨论,参见A.4节)。如果它没有后缀且是十进制表示,则其类型很可能是int、long int或unsigned long int。如果它没有后缀且是八进制或十六进制表示,则其类型很可能是int、unsigned int、long int或unsigned long int。如果它的后缀为u或U,则其类型很可能是unsigned int或unsigned long int。如果它的后缀为l或L,则其类型很可能是long int或unsigned long int。
说明:ANSI标准中,整型常量的类型比第1版要复杂得多。在第1版中,大的整型常量仅被看作是long类型。U后缀是新增加的。
- 字符常量
字符常量是用单引号引起来的一个或多个字符构成的序列,如'x'
。单字符常量的值是执行时机器字符集中此字符对应的数值,多字符常量的值由具体实现定义。
字符常量不包括字符'
和换行符。可以使用以下转义字符序列表示这些字符以及其他一些字符:
换行符 NL(LF) \n 反斜杠 \ \\
横向制表符 HT \t 问号 ? \?
纵向制表符 VT \v 单引号 ' \'
回退符 BS \b 双引号 " \"
回车符 CR \r 八进制数 ooo \ooo
换页符 FF \f 十六进制数 hh \xhh
响铃符 BEL \a
转义序列\ooo
由反斜杠后跟1个、2个或3个八进制数字组成,这些八进制数字用来指定所期望的字符的值。\0
便是一个常见的例子,它表示字符NUL。转义序列\xhh
中,反斜杠后面紧跟x以及十六进制数字,这些十六进制数用来指定所期望的字符的值。数字的个数没有限制,但如果字符值超过最大的字符值,该行为是未定义的。对于八进制或十六进制转义字符,如果实现中将类型char看作是带符号的,则将对字符值进行符号扩展,就好像它被强制转换为char类型一样。如果\
后面紧跟的字符不在以上指定的字符中,则其行为是未定义的。
在C语言的某些实现中,还有一个扩展的字符集,它不能用char类型表示。扩展集中的常量要以一个前导符L开头(例如L'x'
),称为宽字符常量。这种常量的类型为wchar_t
。这是一种整型类型,定义在标准头文件<stddef.h>
中。与通常的字符常量一样,宽字符常量可以使用八进制或十六进制转义字符序列;但是,如果值超过wchar_t
可以表示的范围,则结果是未定义的。
说明:某些转义序列是新增加的,特别是十六进制字符的表示。扩展字符也是新增加的。通常情况下,美国和西欧所用的字符集可以用char类型进行编码,增加
wchar_t
的主要目的是为了表示亚洲的语言。
- 浮点常量
浮点常量由整数部分、小数点、小数部分、一个e或E、一个可选的带符号整型类型的指数和一个可选的表示类型的后缀(即f、F、l或L之一)组成。整数和小数部分均由数字序列组成。可以没有整数部分或小数部分(但不能两者都没有),还可以没有小数点或者e和指数部分(但不能两者都没有)。浮点常量的类型由后缀确定,F或f后缀表示它是float类型;l或L后缀表示它是long double类型;没有后缀则表明是double类型。
说明:浮点类型的后缀是新增加的。
- 枚举常量
声明为枚举符的标识符是int类型的常量(参见A.8.4节)。
A.2.6 字符串字面量
字符串字面值(string literal)也称为字符串常量,是用双引号引起来的一个字符序列,如“…”。字符串的类型为“字符数组”,存储类为static(参见A.4节),它使用给定的字符进行初始化。对相同的字符串字面值是否进行区分取决于具体的实现。如果程序试图修改字符串字面值,则行为是未定义的。
我们可以把相邻的字符串字面值连接为一个单一的字符串。执行任何连接操作后,都将在字符串的后面增加一个空字节\0,这样,扫描字符串的程序便可以找到字符串的结束位置。字符串字面值不包括换行符和双引号字符,但可以用与字符常量相同的转义字符序列表示它们。
与字符常量一样,扩展字符集中的字符串字面值也以前导符L表示,如L"…"。宽字符字符串字面值的类型为“wchar_t类型的数组
”。将普通字符串字面值和宽字符字符串字面值进行连接的行为是未定义的。
说明:下列规定都是ANSI标准中新增加的:字符串字面值不必进行区分、禁止修改字符串字面值以及允许相邻字符串字面值进行连接。宽字符字符串字面值也是ANSI标准中新增加的。
|
|
A.3 语法符号
在本手册用到的语法符号中,语法类型后跟有一个冒号。多个候选类别通常列在不同的行中,但在一些情况下,一组字符长度较短的候选项可以放在一行中,并以短语“one of”标识。可选的终结符或非终结符带有下标“opt”。例如:
{ 表达式_opt }
表示一个括在花括号中的表达式,该表达式是可选的。A.13节对语法进行了总结。
说明:与本书第1版给出的语法所不同的是,此处给出的语法将表达式运算符的优先级和结合性显式表达出来了。
A.4 标识符的含义
标识符也称为名字,可以指代多种实体:函数、结构标记、联合标记和枚举标记;结构成员或联合成员;枚举常量;类型定义名;标号以及对象等。
名字还具有一个作用域和一个连接。作用域即程序中可以访问此名字的区域,连接决定另一作用域中的同一个名字是否指向同一个对象或函数。作用域和连接将在A.11节中讨论。
对象有时也称为变量,它是一个存储位置。对它的解释依赖于两个主要属性:存储类和类型。存储类决定了与该标识对象相关联的存储区域的生存期,类型决定了标识对象中值的含义。
A.4.1 存储类
存储类分为两类:自动存储类(automatic)和静态存储类(static)。声明对象时使用的一些关键字和声明的上下文共同决定了对象的存储类。自动存储类对象对于一个程序块(参见A.9.3节)来说是局部的,在退出程序块时该对象将消失。如果没有使用存储类说明符,或者如果使用了auto限定符,则程序块中的声明生成的都是自动存储类对象。声明为register的对象也是自动存储类对象,并且将被存储在机器的快速寄存器中(如果可能的话)。
静态对象可以是某个程序块的局部对象,也可以是所有程序块的外部对象。无论是哪一种情况,在退出和再进入函数或程序块时其值将保持不变。在一个程序块(包括提供函数代码的程序块)内,静态对象用关键字static声明。在所有程序块外部声明且与函数定义在同一级的对象总是静态的。可以通过static关键字将对象声明为某个特定翻译单元的局部对象,这种类型的对象将具有内部连接
。当省略显式的存储类(见A.8.1)或通过关键字extern进行声明时,对象对整个程序来说是全局可访问的,并且具有外部连接
。
A.4.2 基本类型
基本类型包括多种。附录B中描述的标准头文件<limits.h>
中定义了本地实现中每种类型的最大值和最小值。附录B给出的数值表示最小的可接受限度。
声明为字符(char)的对象要大到足以存储执行字符集中的任何字符。如果字符集中的某个字符存储在一个char类型的对象中,则该对象的值等于字符的整型编码值,并且是非负值。其他类型的对象也可以存储在char类型的变量中,但其取值范围,特别是其值是否带符号,同具体的实现有关。
以unsigned char声明的无符号字符与普通字符占用同样大小的空间,但其值总是非负的。以signed char显式声明的带符号字符与普通字符也占用同样大小的空间。
说明:本书的第1版中没有unsigned char类型,但这种用法很常见。signed char是新增加的。
除char类型外,还有3种不同大小的整型类型:short int、 int和long int。普通int对象的长度与由宿主机器的体系结构决定的自然长度相同。其他类型的整型可以满足各种特殊的用途。较长的整数至少要占用与较短整数一样的存储空间;但是具体的实现可以使得一般整型(int)与短整型(short int)或长整型(long int)具有同样的大小。除非特别说明,int类型都表示带符号数。
以关键字unsigned声明的无符号整数遵守算术模2^n
的规则,其中,n是表示相应整数的二进制位数,这样,对无符号数的算术运算永远不会溢出。可以存储在带符号对象中的非负值的集合是可以存储在相应的无符号对象中的值的子集,并且,这两个集合的重叠部分的表示是相同的。
单精度浮点数(float)、双精度浮点数(double)和多精度浮点数(long double)中的任何类型都可能是同义的,但精度从前到后是递增的。
说明:long double是新增加的类型。在第1版中,long float与double类型等价,但现在是不相同的。
枚举是一个具有整型值的特殊的类型。与每个枚举相关联的是一个命名常量的集合(参见A.8.4节)。枚举类型类似于整型。但是,如果某个特定枚举类型的对象的赋值不是其常量中的一个,或者赋值不是一个同类型的表达式,则编译器通常会产生警告信息。
因为以上这些类型的对象都可以被解释为数字,所以,可以将它们统称为算术类型。char类型、各种大小的int类型(无论是否带符号)以及枚举类型都统称为整型类型(integral type)。类型float、double和long double统称为浮点类型(floating type)。
void类型说明一个值的空集合,它常被用来说明不返回任何值的函数的类型。
A.4.3 派生类型
除基本类型外,我们还可以通过以下几种方法构造派生类型,从概念来讲,这些派生类型可以有无限多个:
- 给定类型对象的数组
- 返回给定类型对象的函数
- 指向给定类型对象的指针
- 包含一系列不同类型对象的结构
- 可以包含多个不同类型对象中任意一个对象的联合
一般情况下,这些构造对象的方法可以递归使用。
A.4.4 类型限定符
对象的类型可以通过附加的限定符进行限定。声明为const的对象表明此对象的值不可以修改;声明为volatile的对象表示它具有与优化相关的特殊属性。限定符既不影响对象取值的范围,也不影响其算术属性。限定符将在A.8.2节中讨论。
A.5 对象和左值
对象是一个命名的存储区域,左值(lvalue)是引用某个对象的表达式。具有合适类型与存储类的标识符便是左值表达式的一个明显的例子。某些运算符可以产生左值。例如,如果E是一个指针类型的表达式,*E
则是一个左值表达式,它引用由E指向的的对象。名字“左值”来源于赋值表达式E1=E2,其中,左操作数E1必须是一个左值表达式。对每个运算符的讨论需要说明此运算符是否需要一个左值操作数以及它是否产生一个左值。
A.6 转换
根据操作数的不同,某些运算符会引起操作数的值从某种类型转换为另一种类型。本节将说明这种转换产生的结果。A.6.5节将讨论大多数普通运算符所要求的转换,我们在讲解每个运算符时将做一些补充。
A.6.1 整型提升
在一个表达式中,凡是可以使用整型的地方都可以使用带符号或无符号的字符、短整型或整型位字段,还可以使用枚举类型的对象。如果原始类型的所有值都可用int类型表示,则其值将被转换为int类型;否则将被转换为unsigned int类型。这一过程称为整型提升(integral promotion)。
A.6.2 整型转换
将任何整数转换为某种指定的无符号类型数的方法是:以该无符号类型能够表示的最大值加1为模,找出与此整数同余的最小的非负值。在对二的补码表示中,如果该无符号类型的位模式较窄,这就相当于左截取;如果该无符号类型的位模式较宽,这就相当于对带符号值进行符号扩展和对无符号值进行0填充。
将任何整数转换为带符号类型时,如果它可以在新类型中表示出来,则其值保持不变,否则它的值同具体的实现有关。
A.6.3 整数和浮点数
当把浮点类型的值转换为整型时,小数部分将被丢弃。如果结果值不能用整型表示,则其行为是未定义的。特别是,将负的浮点数转换为无符号整型的结果是没有定义的。
当把整型值转换为浮点类型时,如果该值在该浮点类型可表示的范围内但不能精确表示,则结果可能是下一个较高或较低的可表示值。如果该值超出可表示的范围,则其行为是未定义的。
A.6.4 浮点类型
将一个精度较低的浮点值转换为相同或更高精度的浮点类型时,它的值保持不变。将一个较高精度的浮点类型值转换为较低精度的浮点类型时,如果它的值在可表示范围内,则结果可能是下一个较高或较低的可表示值。如果结果在可表示范围之外,则其行为是未定义的。
A.6.5 算术类型转换
许多运算符都会以类似的方式在运算过程中引起转换,并产生结果类型。其效果是将所有操作数转换为同一公共类型,并以此作为结果的类型。这种方式的转换称为普通算术类型转换。
首先,如果任何一个操作数为long double类型,则将另一个操作数转换为long double类型。
否则,如果任何一个操作数为double类型,则将另一个操作数转换为double类型。
否则,如果任何一个操作数为float类型,则将另一个操作数转换为float类型。
否则,同时对两个操作数进行整型提升;然后,如果任何一个操作数为unsigned long int类型,则将另一个操作数转换为unsigned long int类型。
否则,如果一个操作数为long int类型且另一个操作数为unsigned int类型,则结果依赖于long int类型是否可以表示所有的unsigned int类型的值。如果可以,则将unsigned int类型的操作数转换为long int;如果不可以,则将两个操作数都转换为unsigned long int类型。
否则,如果一个操作数为long int类型,则将另一个操作数转换为long int类型。
否则,如果任何一个操作数为unsigned int类型,则将另一个操作数转换为unsigned int类型。
否则,将两个操作数都转换为int类型。
说明:这里有两个变化。第一,对float类型操作数的算术运算可以只用单精度而不是双精度;而在第1版中规定,所有的浮点运算都是双精度。第二,当较短的无符号类型与较长的带符号类型一起运算时,不将无符号类型的属性传递给结果类型;而在第1版中,无符号类型总是处于支配低位。新规则稍微复杂一些,但减少了无符号数与带符号数混合使用情况下的麻烦。当一个无符号表达式与一个具有同样长度的带符号表达式相比较时,结果仍然是无法预料的。
A.6.6 指针和整数
指针可以加上或减去一个整型表达式。在这种情况下,整型表达式的转换按照加法运算符的方式进行(参加A.7.7节)。
两个指向同一数组中同一类型的对象的指针可以进行减法运算,其结果将被转换为整型;转换方法按照减法运算符的方式进行(参见A.7.7节)。
值为0的整型常量表达式或强制转换为void *
类型的表达式可通过强制转换、赋值或比较操作转换为任意类型的指针。其结果将产生一个空指针,此空指针等于指向同一类型的另一空指针,但不等于任何指向函数或对象的指针。
还允许进行指针相关的其他类型转换,但其结果依赖于具体的实现。这些转换必须由一个显式的类型转换运算符或强制类型转换来指定(参见A.7.5节和A.8.8节)。
指针可以转换为整型,但此整型必须足够大;所要求的大小依赖于具体的实现。映射函数也依赖于具体的实现。
整型对象可以显式地转换为指针。这种映射总是将一个足够宽的从指针转换来的整数转换为同一个指针,其他情况依赖于具体的实现。
指向某一类型的指针可以转换为指向另一类型的指针,但是,如果该指针指向的对象不满足一定的存储对齐要求,则结果指针可能会导致地址异常。指向某对象的指针可以转换为一个指向具有更小或相同存储对齐限制的对象的指针,并可以保证原封不动地再转换回来。“对齐”的概念依赖于具体的实现,但char类型的对象具有最小的对齐限制。我们将在A.6.8节的讨论中看到,指针也可以转换为void *
类型,并可原封不动地转换回来。
一个指针可以转换为同类型的另一个指针,但增加或删除了指针所指的对象类型的限定符(参见A.4.4节和A.8.2节)的情况除外。如果增加了限定符,则新指针与原指针等价,不同的是增加了限定符带来的限制。如果删除了限定符,则对底层对象的运算仍受实际声明中的限定符的限制。
最后,指向一个函数的指针可以转换为指向另一个函数的指针。调用转换后指针所指的函数的结果依赖于具体的实现。但是,如果转换后的指针被重新转换为原来的类型,则结果与原来的指针一致。
A.6.7 void
void对象的(不存在的)值不能够以任何方式使用,也不能被显式或隐式地转换为任一非空类型。因为空(void)表达式表示一个不存在的值,这样的表达式只可以用在不需要值的地方,例如作为一个表达式语句(参见A.9.2节)或作为逗号运算符的左操作数(参见A.7.18节)。
可以通过强制类型转换将表达式转换为void类型。例如,在表达式语句中,一个空的强制类型转换将丢掉函数调用的返回值。
说明:void没有在本书的第1版中使用,但是在本书第1版出版后,它一直被广泛使用着。
A.6.8 指向void的指针
指向任何对象的指针都可以转换为void *
类型,且不会丢失信息。如果将结果再转换为初始指针类型,则可以恢复初始指针。我们在A.6.6节中讨论过,执行指针到指针的转换时,一般需要显式的强制转换,这里所不同的是,指针可以被赋值为void *
类型的指针,也可以赋值给void *
类型的指针,并可与void *
类型的指针进行比较。
说明:对
void *
指针的解释是新增加的。以前,char *
指针扮演着通用指针的角色。ANSI标准特别允许void *
类型的指针与其他对象指针在赋值表达式和关系表达式中混用,而对其他类型指针的混用则要求进行显式强制类型转换。
A.7 表达式
本节中各主要小节的顺序就代表了表达式运算符的优先级,我们将依次按照从高到低的优先级介绍。举个例子,按照这种关系,A.7.1至A.7.6节中定义的表达式可以用作加法运算符+
(参见A.7.7节)的操作数。在每一小节中,各个运算符的优先级相同。每个小节中还将讨论该节涉及到的运算符的左、右结合性。A.13节中给出的语法综合了运算符的优先级(从低到高)和结合性。
运算符的优先级和结合性有明确的规定,但是,少数例外情况,表达式的求值次序没有定义,甚至某些有副作用的子表达式也没有定义。也就是说,除非运算符的定义保证了其操作数按某一特定顺序求值,否则,具体的实现可以自由选择任一求值次序,甚至可以交换求值次序。但是,每个运算符将其操作数生成的值结合起来的方式与表达式的语法分析方式是兼容的。
说明:该规则废除了原先的一个规则,即:当表达式中的运算符在数学上满足交换律和结合律时,可以对表达式重新排序,但是,在计算时可能会不满足结合律。这个改变仅影响浮点数在接近其精度限制时的计算以及可能发生溢出的情况。
C语言没有定义表达式求值过程中的溢出、除法检查和其他异常的处理。大多数现有C语言的实现在进行带符号整型表达式的求值以及赋值时忽略溢出异常,但并不是所有的实现都这么做。对除数为0和所有浮点异常的处理,不同的实现采用不同的方式,有时候可以用非标准库函数进行调整。
A.7.1 指针生成
对于某类型T,如果其表达式或子表达式的类型为“T类型的数组”,则此表达式的值是指向数组中第一个对象的指针,并且此表达式的类型将被转换为“指向T类型的指针”。如果此表达式是一元运算符&
或sizeof
,则不会进行转换。类似地,除非表达式被用作&
运算符的操作数,否则,类型为“返回T类型值的函数”的表达式将被转换为“指向返回T类型值的函数的指针”类型。
A.7.2 初等表达式
初等表达式包括标识符、常量、字符串或带括号的表达式。
初等表达式:
标识符
常量
字符串
(表达式)
如果按照下面的方式对标识符进行适当的声明,该标识符就是初等表达式。其类型由其声明指定。如果标识符引用一个对象(参见A.5节),并且其类型是算术类型、结构、联合或指针,那么它就是一个左值。
常量是初等表达式,其类型同其形式有关。更详细的信息,参见A.2.5节中的讨论。
字符串字面值是初等表达式。它的初始类型为“char类型的数组”类型(对于宽字符字符串,则为“wchar_t
类型的数组”类型),但遵循A.7.1节中的规则。它通常被修改为“指向char类型(或wchar_t
类型)的指针”类型,其结果是指向字符串中第一个字符的指针。某些初始化程序中不进行这样的转换,详细信息,参见A.8.7节。
用括号括起来的表达式是初等表达式,它的类型和值与无符号的表达式相同。此表达式是否是左值不受括号的影响。
A.7.3 后缀表达式
后缀表达式中的运算符遵循从左到右的结合规则。
后缀表达式:
初等表达式
后缀表达式[表达式]
后缀表达式(参数表达式表_opt)
后缀表达式.标识符
后缀表达式->标识符
后缀表达式++
后缀表达式--
参数表达式表:
赋值表达式
参数表达式表,赋值表达式
- 数组引用
带下标的数组引用后缀表达式由一个后缀表达式后跟一个括在方括号中的表达式组成。方括号前的后缀表达式的类型必须为“指向T类型的指针”,其中T为某种类型;方括号中表达式的类型必须为整型。结果得到下标表达式的类型为T。表达式E1[E2]在定义上等同于*((E1)+(E2))
。有关数组引用的更多讨论,参见A.8.6节。
- 函数调用
函数调用由一个后缀表达式(称为函数标志符,function designator)后跟由圆括号括起来的赋值表达式列表组成,其中的赋值表达式列表可能为空,并由逗号进行分隔,这些表达式就是函数的参数。如果后缀表达式包含一个当前作用域中不存在的标识符,则此标识符将被隐式地声明,等同于在执行此函数调用的最内层程序块中包含下列声明:
extern int 标识符();
该后缀表达式(在可能的隐式声明和指针生成之后,参见A.7.1节)的类型必须为“指向返回T类型的函数的指针”,其中T为某种类型,且函数调用的值的类型为T。
说明: 在第1版中,该类型被限制为“函数”类型,并且,通过指向函数的指针调用函数时必须有一个显式的
*
运算符。ANSI标准允许现有的一些编译器用同样的语法进行函数调用和通过指向函数的指针进行函数调用。旧的语法仍然有效。
通常用术语“实际参数”表示传递给函数调用的表达式,而术语“形式参数”则用来表示函数定义或函数声明中的输入对象(或标识符)。
在调用函数之前,函数的每个实际参数将被复制,所有的实际参数严格地按值传递。函数可能会修改形式参数对象的值(即实际参数表达式的副本),但这个修改不会影响实际参数的值。但是,可以将指针作为实际参数传递,这样,函数便可以修改指针指向的对象的值。
可以通过两种方式声明函数。在新的声明方式中,形式参数的类型是显式声明的,并且是函数类型的一部分,这种声明称为函数原型。在旧的方式中,不指定形式参数类型。有关函数声明的讨论 ,参见A.8.6节和A.10.1节。
在函数调用的作用域中,如果函数是以旧式方式声明的,则按以下方式对每个实际参数进行默认参数提升:对每个整型参数进行整型提升(参见A.6.1节);将每个float类型的参数转换为double类型。如果调用时实际参数的数目与函数定义中形式参数的数目不等,或者某个实际参数的类型提升后与相应的形式参数类型不一致,则函数调用的结果是未定义的。类型一致性依赖于函数是以新式方式定义的还是以旧式方式定义的。如果是旧式的定义,则比较经提升后函数调用中的实际参数类型和提升后的形式参数类型;如果是新式的定义,则提升后的实际参数类型必须与没有提升的形式参数自身的类型保持一致。
在函数调用的作用域中,如果函数是以新式方式声明的,则实际参数将被转换为函数原型中的相应形式参数类型,这个过程类似于赋值。实际参数数目必须与显式声明的形式参数数目相同,除非函数声明的形式参数表以省略号(,…)结尾。在这种情况下,实际参数的数目必须等于或超过形式参数的数目;对于尾部没有显式指定类型的形式参数,相应的实际参数要进行默认的参数提升,提升方法同前面所述。如果函数是以旧式方式定义的,那么,函数原型中每个形式参数的类型必须与函数定义中相应的形式参数类型一致(函数定义中的形式参数类型经过参数提升后)。
说明:这些规则非常复杂,因为必须要考虑新旧式函数的混合使用。应尽可能避免新旧式函数声明混合使用。
实际参数的求值次序没有指定。不同编译器的实现方式各不相同。但是,在进入函数前,实际参数和函数标志符是完全求值的,包括所有的副作用。对任何函数都允许进行递归调用。
- 结构引用
后缀表达式后跟一个圆点和一个标识符仍是后缀表达式。第一个操作数表达式的类型必须是结构或联合,标识符必须是结构或联合的成员的名字。结果值是结构或联合中命名的成员,其类型是对应成员的类型。如果第一个表达式是一个左值,并且第二个表达式的类型不是数组类型,则整个表达式是一个左值。
后缀表达式后跟一个箭头(由-和>组成)和一个标识符仍是后缀表达式。第一个操作数表达式必须是一个指向结构或联合的指针,标识符必须是结构或联合的成员的名字。结果指向指针表达式指向的结构或联合中命名的成员,结果类型是对应成员的类型。如果该类型不是数组类型,则结果是一个左值。
因此,表达式E1->MOS
与(*E1).MOS
等价。结构和联合将在A.8.3节中讨论。
说明:在本书的第1版中,规定了这种表达式中成员的名字必须属于后缀表达式指定的结构或联合,但是,该规则并没有强制执行。最新的编译器和ANSI标准强制执行了这一规则。
- 后缀自增运算符与后缀自减运算符
后缀表达式后跟一个++
或--
运算符仍是一个后缀表达式。表达式的值就是操作数的值。执行完该表达式后,操作数的值将加1(++
)或减1(--
)。操作数必须是一个左值。有关操作数的限制和运算细节的详细信息,参见加法类运算符(A.7.7节)和赋值类运算符(A.7.17节)中的讨论。其结果不是左值。
A.7.4 一元运算符
带一元运算符的表达式遵循从右至左的结合性。
一元表达式:
后缀表达式
++一元表达式
--一元表达式
一元运算符 强制类型转换表达式
sizeof 对象
sizeof(类型名)
一元运算符: one of
& * + - ~ !
- 前缀自增运算符与前缀自减运算符
在一元表达式的前面添加运算符++
或--
后得到的表达式是一个一元表达式。操作数将被加1(++
)或减1(--
),表达式的值是经过加1、减1以后的值。操作数必须是一个左值。有关操作数的限制和运算细节的详细信息,参见加法类运算符(参见A.7.7节)和赋值类运算符(参见A.7.17节)。其结果不是左值。
- 地址运算符
一元运算符&
用于取操作数的地址。该操作数必须是一个左值(不指向位字段、不指向声明为register类型的对象),或者为函数类型。结果值是一个指针,指向左值指向的对象或函数。如果操作数的类型为T,则结果的类型为指向T类型的指针。
- 间接寻址运算符
一元运算符*
表示间接寻址,它返回其操作数指向的对象或函数。如果它的操作数是一个指针且指向的对象是算术、结构、联合或指针类型,则它是一个左值。如果表达式的类型为“指向T类型的指针”,则结果类型为T。
- 一元加运算符
一元运算符+
的操作数必须是算术类型,其结果是操作数的值。如果操作数是整型,则将进行整型提升,结果类型是经过提升后的操作数的类型。
说明: 一元运算符
+
是ANSI标准新增加的,增加该运算符是为了与一元运算符-
对应。
- 一元减运算符
一元运算符-
的操作数必须是算术类型,结果为操作数的负值。如果操作数是整型,则将进行整型提升。带符号数的负值的计数方法为:将提升后得到的类型能够表示的最大值减去提升后的操作数的值,然后加1;但0的负值仍为0。结果类型为提升后的操作数的类型。
- 二进制反码运算符
一元运算符~
的操作数必须是整型,结果为操作数的二进制反码。在运算过程中需要对操作数进行整型提升。如果操作数为无符号类型,则结果为提升后的类型能够表示的最大值减去操作数的值而得到的结果值。如果操作数为带符号类型,则结果的计算方式为:将提升后的操作数转换为相应的无符号类型,使用运算符~
计算反码,再将结果转换为带符号类型。结果的类型为提升后的操作数的类型。
- 逻辑非运算符
运算符!
的操作数必须是算术类型或者指针。如果操作数等于0,则结果为1,否则结果为0。结果类型为int。
- sizeof运算符
sizeof运算符计算存储与其操作数同类型的对象所需的字节数。操作数可以为一个未求值的表达式,也可以为一个用括号括起来的类型名。将sizeof应用于char类型时,其结果值为1;将它应用于数组时,其值为数组中字节的总数。应用于结构或联合时,结果为对象的字节数,包括对象中包含的数组所需要的任何填充空间:有n个元素的数组的长度是一个数组元素长度的n倍。此运算符不能用于函数类型和不完整类型的操作数,也不能用于位字段。结果是一个无符号整型常量,具体的类型由实现定义。在标准头文件<stddef.h>
(参见附录B)中,这一类型被定义为size_t
类型。
A.7.5 强制类型转换
以括号括起来的类型名开头的一元表达式将导致表达式的值被转换为指定的类型。
强制类型转换表达式:
一元表达式
(类型名)强制类型转换表达式
这种结构称为强制类型转换。类型名将在A.8.8节描述。转换的结果已在A.6节讨论过。包含强制类型转换的表达式不是左值。
A.7.6 乘法类运算符
乘法类运算符*
、/
和%
遵循从左到右的结合性。
乘法类表达式:
强制类型转换表达式
乘法类表达式*强制类型转换表达式
乘法类表达式/强制类型转换表达式
乘法类表达式%强制类型转换表达式
运算符*
和/
的操作数必须为算术类型,运算符%
的操作数必须为整型。这些操作数需要进行普通的算术类型转换,结果类型由执行的转换决定。
二元运算符*
表示乘法。
二元运算符/
用于计算第一个操作数同第二个操作数相除所得的商,而运算符%
用于计算两个操作数相除后所得的余数。如果第二个操作数为0,则结果没有定义。并且,(a/b)*b+a%b
等于a永远成立。如果两个操作数均为非负,则余数为非负值且小于除数,否则,仅保证余数的绝对值小于除数的绝对值。
A.7.7 加法类运算符
加法类运算符+
和-
遵循从左至右的结合性。如果操作数中有算术类型的操作数,则需要进行普通的算术类型转换。每个运算符可能为多种类型。
加法类表达式:
乘法类表达式
加法类表达式+乘法类表达式
加法类表达式-乘法类表达式
运算符+
用于计算两个操作数的和。指向数组中某个对象的指针可以和一个任何整型的值相加,后者将通过乘以所指对象的长度被转换为地址偏移量。相加得到的和是一个指针,它与初始指针具有相同的类型,并指向同一数组中的另一个对象,此对象与初始对象之间具有一定的偏移量。因此,如果P是一个指向数组中某个对象的指针,则表达式P+1是指向数组中下一个对象的指针。如果相加所得的和相应的指针不在数组的范围内,且不是数组末尾元素后的第一个位置,则结果没有定义。
说明:允许指针指向数组末尾元素的下一个元素是ANSI中新增加的特征,它使得我们可以按照通常的习惯循环地访问数组元素。
运算符-
用于计算两个操作数的差值。可以从某个指针上减去一个任何整型的值,减法运算的转换规则和条件与加法的相同。
如果指向同一类型的两个指针相减,则结果是一个带符号整型数,表示它们指向的对象之间的偏移量。相邻对象间的偏移量为1。结果的类型同具体的实现有关,但在标准头文件<stddef.h>
中定义为ptrdiff_t
。只有当指针指向的对象属于同一数组时,差值才有意义。但是,如果P指向数组的最后一个元素,则(P+1)-P
的值为1。
A.7.8 移位运算符
移位运算符<<
和>>
遵循从左到右的结合性。每个运算符的各操作数都必须为整型,并且遵循整型提升原则。结果的类型是提升后的左操作数的类型。如果右操作数为负值,或者大于或等于左操作数类型的位数,则结果没有定义。
移位表达式:
加法类表达式
移位表达式<<加法类表达式
移位表达式>>加法类表达式
E1<<E2
的值为E1(按位模式解释)向左移E2位得到的结果。如果不发生溢出,这个结果值等价于E1乘以2^(E2)
。E1>>E2
的值为E1向右移E2位得到的结果。如果E1为无符号数或为非负值,则右移等同于E1除以2^(E2)。其他情况下的执行结果由具体实现定义。
A.7.9 关系运算符
关系运算符遵循从左到右的结合性,但这个规则没有什么作用。a<b<c
在语法分析时将被解释为(a<b)<c
,并且a<b
的结果值只能为0或1。
关系表达式:
移位表达式
关系表达式<移位表达式
关系表达式>移位表达式
关系表达式<=移位表达式
关系表达式>=移位表达式
当关系表达式的结果为假时,运算符<(小于)、>(大于)、<=(小于等于)和>=(大于等于)的结果值都为0;当关系表达式的结果为真时,它们的结果值都为1。结果的类型为int类型。如果操作数为算术类型,则要进行普通的算术类型转换。可以对指向同一类型的对象的指针进行比较(忽略任何限定符),其结果依赖于所指对象在地址空间中的相对位置。指针比较只对相同对象才有意义:如果两个指针指向同一个简单对象,则相等;如果指针指向同一个结构的不同成员,则指向结构中后声明的成员的指针较大;如果指针指向同一个联合的不同成员,则相等;如果指针指向一个数组的不同成员,则它们之间的比较等价于对应下标之间的比较。如果指针P指向数组的最后一个成员,尽管P+1已指向数组的界外,但P+1仍比P大。其他情况下指针的比较没有定义。
说明:这些规则允许指向同一个结构或联合的不同成员的指针之间进行比较,与第1版比较起来放宽了一些限制。这些规则还使得与超出数组末尾的第一个指针进行比较合法化。
A.7.10 相等类运算符
相等类表达式:
关系表达式
相等类表达式==关系表达式
相等类表达式!=关系表达式
运算符==(等于)和!=(不等于)与关系运算符相似,但它们的优先级更低。(只要a<b
与c<d
具有相同的真值,则a<b==c<d
的值总为1。)
相等类运算符与关系运算符具有相同的规则,但这类运算符还允许执行下列比较:指针可以与值为0的常量整型表达式或指向void的指针进行比较。参见A.6.6节。
A.7.11 按位与运算符
按位与表达式:
相等类表达式
按位与表达式&相等类表达式
按行按位与运算时要进行普通的算术类型转换。结果为操作数经按位与运算后得到的值。该运算符仅适用于整型操作数。
A.7.12 按位异或运算符
按位异或表达式:
按位与表达式
按位异或表达式^按位与表达式
执行按位异或运算时要进行普通的算术类型转换,结果为操作数经按位异或运算后得到的值。该运算符仅适用于整型操作数。
A.7.13 按位或运算符
按位或表达式:
按位异或表达式
按位或表达式|按位异或表达式
执行按位或表达式时要进行常规的算术类型转换,结果为操作数经按位或运算后得到的值。该运算符仅适用于整型操作数。
A.7.14 逻辑与运算符
逻辑与表达式:
按位或表达式
逻辑与表达式&&按位或表达式
运算符&&
遵循从左至右的结合性。如果两个操作数都不等于0,则结果值为1,否则结果值0。与运算符&
不同的是,&&
确保从左至右的求值次序:首先计算第一个操作数,包括所有可能的副作用;如果为0,则整个表达式的值为0;否则,计算右操作数,如果为0,则整个表达式的值为0,否则为1。
两个操作数不需要为同一类型,但是,每个操作数必须为算术类型或者指针。其结果为int类型。
A.7.15 逻辑或运算符
逻辑或表达式:
逻辑与表达式
逻辑或表达式||逻辑与表达式
运算符||
遵循从左到右的结合性。如果该运算符的某个操作数不为0,则结果值为1;否则结果值为0。与运算符|
不同的是,||
确保从左至右的求值次序:首先计算第一个操作数,包括所有可能的副作用;如果不为0,则整个表达式的值为1;否则,计算右操作数,如果不为0,则整个表达式的值为1;否则结果为0。
两个操作数不需要为同一类型,但是每个操作数必须为算术类型或者指针。其结果为int类型。
A.7.16 条件运算符
条件表达式:
逻辑或表达式
逻辑或表达式?表达式:条件表达式
首先计算第一个表达式(包括所有可能的副作用),如果该表达式的值不等于0,则结果为第二个表达式的值,否则结果为第三个表达式的值。第二个和第三个操作数中仅有一个操作数会被计算。如果第二个和第三个操作数为算术类型,则要进行普通的算术类型转换,以使它们的类型相同,该类型也是结果的类型。如果它们都是void类型,或者是同一类型的结构或联合,或者是指向同一类型的对象的指针,则结果的类型与这两个操作数的类型相同。如果其中一个操作数是指针,而另一个是常量0,则0将被转换为指针类型,且结果为指针类型。如果一个操作数为指向void的指针,而另一个操作数为指向其他类型的指针,则指向其他类型的指针将被转换为指向void的指针,这也是结果的类型。
在比较指针的类型时,指针所指对象的类型的任何类型限定符(参见A.8.2节)都将被忽略,但结果类型会继承条件的各分支的限定符。
A.7.17 赋值表达式
赋值表达式有多个,它们都是从左至右结合。
赋值表达式:
条件表达式
一元表达式 赋值运算符 赋值表达式
赋值运算符: one of
= *= /= %= += -= <<= >>= &= ^= |=
所有这些运算符都要求左操作数为左值,且该左值是可以修改的:它不可以是数组、不完整类型或函数。同时其类型不能包括const限定符;如果它是结构或联合,则它的任意一个成员或递归子成员不能包括const限定符。赋值表达式的类型是其左操作数的类型,值是赋值操作执行后存储在左操作数中的值。
在使用运算符=
的简单赋值中,表达式的值将替换左值所指向的对象的值。下面几个条件中必须有一个条件成立:两个操作数均为算术类型,在此情况下由操作数的类型通过赋值转换为左操作数的类型;两个操作数为同一类型的结构或联合;一个操作数是指针,另一个操作数是指向void的指针;左操作数是指针,右操作数是值为0的常量表达式;两个操作数都是指向同一类型的函数或对象的指针,但右操作数可以没有const或volatile限定符。
形式为E1 op= E2
的表达式等价于E1=E1 op (E2)
,唯一的区别是前者对E1仅求值一次。
A.7.18 逗号运算符
表达式:
赋值表达式
表达式, 赋值表达式
由逗号分割的两个表达式的求值次序为从左至右,并且左表达式的值被丢弃。右操作数的类型和值就是结果的类型和值。在开始计算右操作数以前,将完成左操作数涉及到的副作用的计算。在逗号有特殊含义的上下文中,如在函数参数表(参见A.7.3节)和初值列表(A.8.7节)中,需要使用赋值表达式作为语法单元,这样,逗号运算符仅出现在圆括号中。例如,下列函数调用:
f(a, (t=3, t+2), c)
包含3个参数,其中第二个参数的值为5。
A.7.19 常量表达式
从语法上看,常量表达式是限定于运算符的某一个子集的表达式:
常量表达式:
条件表达式
某些上下文要求表达式的值为常量,例如,switch语句中case后面的数值、数组边界和位字段的长度、枚举常量的值、初值以及某些预处理器表达式。
除了作为sizeof的操作数之外,常量表达式中可以不包含赋值、自增或自减运算符、函数调用或逗号运算符。如果要求常量表达式为整型,则它的操作数必须由整型、枚举、字符和浮点常量组成;强制类型转换必须指定为整型,任何浮点常量都将被强制转换为整型。此规则对数组、间接访问、取地址运算符和结构成员操作不适用。(但是,sizeof可以带任何类型的操作数。)
初值中的常量表达式允许更大的范围:操作数可以是任意类型的常量,一元运算符&
可以作用于外部、静态对象以及以常量表达式为下标的外部或静态数组。对于无下标的数组或函数的情况,一元运算符&
将被隐式地应用。初值计算的结果必须为下列二者之一:一个常量;前面声明的外部或静态对象的地址加上或减去一个常量。
允许出现在#if
后面的整型常量表达式的范围较小,它不允许为sizeof表达式、枚举常量和强制类型转换。详细信息参见A.12.5节。
A.8 声明
声明(declaration)用于说明每个标识符的含义,而并不需要为每个标识符预留存储空间。预留存储空间的声明称为定义(definition)。声明的形式如下:
声明:
声明说明符 初始化声明符表_opt;
初始化声明符表中的声明符包含被声明的标识符;声明说明符由一系列的类型和存储类型说明符组成。
声明说明符:
存储类说明符 声明说明符_opt
类型说明符 声明说明符_opt
类型限定符 声明说明符_opt
初始化声明符表:
初始化声明符
初始化声明符表,初始化声明符
初始化声明符:
声明符
声明符=初值
声明符将在稍后部分讨论(参见A.8.5节)。声明符包含了被声明的名字。一个声明中必须至少包含一个声明符,或者其类型说明符必须声明一个结构标记、一个联合标记或枚举的成员。不允许空声明。
A.8.1 存储类说明符
存储类说明符如下所示:
存储类说明符:
auto
register
static
extern
typedef
有关存储类的意义,我们已在A.4节中讨论过。
说明符auto和register将声明的对象说明为自动存储类对象,这些对象仅可用在函数中。这种声明也具有定义的作用,并将预留存储空间。带有register声明符的声明等价于带有auto说明符的声明,所不同的是,前者暗示了声明的对象将被频繁地访问。只有很少的对象被真正存放在寄存器中,并且只有特定类型才可以。该限制同具体的实现有关。但是,如果一个对象被声明为register,则将不能对它应用一元运算符&
(显式应用或隐式应用都不允许)。
说明:对声明为register但实际按照auto类型处理的对象的地址进行计算是非法的。这是一个新增加的规则。
说明符static将声明的对象说明为静态存储类。这种对象可以用在函数内部或函数外部。在函数内部,该说明符将引起存储空间的分配,具有定义的作用。有关该说明符在函数外部的作用参见A.11.2节。
函数内部的extern声明表明,被声明的对象的存储空间定义在其他地方。有关该说明符在函数外部的作用参见A.11.2节。
typedef说明符并不会为对象预留存储空间。之所以将它称为存储类说明符,是为了语法描述上的方便。我们将在A.8.9节中讨论它。
一个声明中最多只能有一个存储类说明符。如果没有指定存储类说明符,则将按照下列规则进行:在函数内部声明的对象被认为是auto类型;在函数内部声明的函数被认为是extern类型;在函数外部声明的对象与函数将被认为是static类型,且具有外部连接。详细信息参见A.10节和A.11节。
A.8.2 类型说明符
类型说明符的定义如下:
类型说明符:
void
char
short
int
long
float
double
signed
unsigned
结构或联合说明符
枚举说明符
类型定义名
其中,long和short这两个类型说明符中最多有一个可同时与int一起使用,并且,在这种情况下省略关键字int的含义也是一样的。long可与double一起使用。signed和unsigned这两个类型说明符中最多有一个可同时与int、int的short或long形式、char一起使用。signed和unsigned可以单独使用,这种情况下默认为int类型。signed说明符对于强制char对象带符号位是非常有用的;其他整型也允许带signed声明,但这是多余的。
除了上面这些情况之外,在一个声明中最多只能使用一个类型说明符。如果声明中没有类型说明符,则默认为int类型。
类型也可以用限定符限定,以指定被声明对象的特殊属性。
类型限定符:
const
volatile
类型限定符可与任何类型说明符一起使用。可以对const对象进行初始化,但在初始化以后不能进行赋值。volatile对象没有与实现无关的语义。
说明:const和volatile属性是ANSI标准新增加的特性。const用于声明可以存放在只读存储器中的对象,并可能提高优化的可能性。volatile用于强制某个实现屏蔽可能的优化。例如,对于具有内存映像输入/输出的机器,指向设备寄存器的指针可以声明为指向volatile的指针,目的是防止编译器通过指针删除明显多余的引用。除了诊断显式尝试修改const对象的情况外,编译器可能会忽略这些限定符。
A.8.3 结构和联合声明
结构是由不同类型的命名成员序列组成的对象。联合也是对象,在不同时刻,它包含多个不同类型成员中的任意一个成员。结构和联合说明符具有相同的形式。
结构或联合说明符:
结构或联合 标识符_opt{结构声明表}
结构或联合 标识符
结构或联合:
struct
union
结构声明表是对结构或联合的成员进行声明的声明序列:
结构声明表:
结构声明
结构声明表 结构声明
结构声明:
说明符限定符表 结构声明符表;
说明符限定符表:
类型说明符 说明符限定符表_opt
类型限定符 说明符限定符表_opt
结构声明符表:
结构声明符
结构声明符表,结构声明符
通常,结构声明符就是结构或联合成员的声明符。结构成员也可能由指定数目的比特位组成,这种成员称为位字段,或仅称为字段,其长度由跟在声明符冒号之后的常量表达式指定。
结构声明符:
声明符
声明符_opt: 常量表达式
下列形式的类型说明符将其中的标识符声明为结构声明表指定的结构或联合的标记:
结构或联合 标识符{结构声明表}
在同一作用域或内层作用域中的后续声明中,可以在说明符中使用标记(而不使用结构声明表)来引用同一类型,如下所示:
结构或联合 标识符
如果说明符中只有标记而无结构声明表,并且标记没有声明,则认为其为不完整类型。具有不完整结构或联合类型的对象可在不需要对象大小的上下文中引用,比如,在声明中(不是定义中),它可用于说明一个指针或创建一个typedef类型,其余情况则不允许。在引用之后,如果具有该标记的说明符再次出现并包含一个声明表,则该类型成为完整类型。即使是在包含结构声明表的说明符中,在该结构声明表内声明的结构或联合类型也是不完整的,一直到花括号“}”
终止该说明符时,声明的类型才成为完整类型。
结构中不能包含不完整类型的成员。因此,不能声明包含自身实例的结构或联合。但是,除了可以命名结构或联合类型外,标记还可以用来定义自引用结构。由于可以声明指向不完整类型的指针,所以,结构或联合可包含指向自身实例的指针。
下列形式的声明适用于一个非常特殊的规则:
结构或联合 标识符;
这种形式的声明声明了一个结构或联合,但它没有声明表和声明符。即使该标识符是外层作用域中已声明过的结构标记或联合的标记(参见A.11.1节),该声明仍将使该标识符成为当前作用域内一个新的不完整类型的结构标记或联合的标记。
说明:这是ANSI中一个新的比较难理解的规则。它旨在处理内层作用域中声明的相互递归调用的结构,但这些结构的标记可能已在外层作用域中声明。
具有结构声明表而无标记的结构说明符或联合说明符用于创建一个唯一的类型,它只能被它所在的声明直接引用。
成员和标记的名字不会相互冲突,也不会与普通变量冲突。一个成员名字不能在同一结构或联合中出现两次,但相同的成员名字可用在不同的结构或联合中。
说明:在本书的第1版中,结构或联合的成员名与其父辈无关联。但是,在ANSI标准制定前,这种关联在编译器中普遍存在。
除字段类型的成员外,结构成员或联合成员可以为任意对象类型。字段成员(它不需要声明符,因此可以不命名)的类型为int、unsigned int或signed int,并被解释为指定长度(用二进制位表示)的整型对象。int类型的字段是否看作为有符号数同具体的实现有关。结构的相邻字段成员以某种方式(同具体的实现有关)存放在某些存储单元中(同具体的实现有关)。如果某一字段之后的另一字段无法全部存入已被前面的字段部分占用的存储单元中,则它可能会被分割存放在多个存储单元中,或者是,存储单元中的剩余部分也可能被填充。我们可以用宽度为0的无名字段来强制进行这种填充,从而使得下一字段从下一分配单元的边界开始存储。
说明:在字段处理方面,ANSI标准比第1版更依赖于具体的实现。如果要按照与实现相关的方式存储字段,建议阅读一下该语言规则。作为一种可移植的方法,带字段的结构可用来节省存储空间(代价是增加了指令空间或访问字段的时间),同时,它还可以用来在位层次上描述存储布局,但该方法不可移植,在这种情况下,必须了解本地实现的一些规则。
结构成员的地址值按它们声明的顺序递增。非字段类型的结构成员根据其类型在地址边界上对齐,因此,在结构中可能存在无名空穴。若指向某一结构的指针被强制转换为指向该结构第一个成员的指针类型,则结果将指向该结构的第一个成员。
联合可以被看作为结构,其所有成员起始偏移量都为0,并且其大小足以容纳任何成员。任一时刻它最多只能存储其中的一个成员。如果指向某一联合的指针被强制转换为指向一个成员的指针类型,则结果将指向该成员。
如下所示是结构声明的一个简单例子:
该结构包含一个具有20个字符的数组、一个整数以及两个指向类似结构的指针。在给出这样的声明后,下列声明:
struct tnode s, *sp;
将把s声明为给定类型的结构,把sp声明为指向给定类型的结构的指针。在这些声明的基础上,表达式
sp->count
引用sp指向的结构的count字段,而
s.left
则引用结构s的左子树指针,表达式
s.right->tword[0]
引用s右子树中tword成员的第一个字符。
通常情况下,我们无法检查联合的某一成员,除非已用该成员给联合赋值。但是,有一个特殊的情况可以简化联合的使用:如果一个联合包含共享一个公共初始序列的多个结构,并且该联合当前包含这些结构中的某一个,则允许引用这些结构中任一结构的公共初始部分。例如,下面这段程序是合法的:
A.8.4 枚举
枚举类型是一种特殊的类型,它的值包含在一个命名的常数集合中。这些常量称为枚举符。枚举说明符的形式借鉴了结构说明符和联合说明符的形式:
枚举说明符:
enum 标识符_opt{枚举符表}
enum 标识符
枚举符表:
枚举符
枚举符表, 枚举符
枚举符:
标识符
标识符 == 常量表达式
枚举符表中的标识符声明为int类型的常量,它们可以用在常量可以出现的任何地方。如果其中不包含带有=
的枚举符,则相应常量值从0开始,且枚举常量值从左至右依次递增1。如果其中包含带有=
的枚举符,则该枚举符的值由该表达式指定,其后的标识符的值从该值开始依次递增。
同一作用域中的各枚举符的名字必须互不相同,也不能与普通变量名相同,但其值可以相同。
枚举说明符中标识符的作用与结构说明符中结构标记的作用类似,它命名了一个特定的枚举类型。除了不存在不完整枚举类型之外,枚举说明符在有无标记、有无枚举符表的情况下的规则与结构或联合中相应的规则相同。无枚举符表的枚举说明符的标记必须指向作用域中另一个具有枚举符表的说明符。
说明:相对于本书第1版,枚举类型是一个新概念,但它作为C语言的一部分已有好多年了。
A.8.5 声明符
声明符的语法如下所示:
声明符:
指针_opt 直接声明符
直接声明符:
标识符
(声明符)
直接声明符[常量表达式_opt]
直接声明符(形式参数类型表)
直接声明符(标识表_opt)
指针:
* 类型限定符表_opt
* 类型限定符表_opt 指针
类型限定符表:
类型限定符
类型限定符表 类型限定符
声明符的结构与间接指针、函数及数组表达式的结构类似,结合性也相同。
A.8.6 声明符的含义
声明符表出现在类型说明符和存储类说明符序列之后。每个声明符声明一个唯一的主标识符,该标识符是直接声明符产生式的第一个候选式。存储类说明符可直接作用于该标识符,但其类型由声明符的形式决定。当声明符的标识符出现在与该声明符形式相同的表达式中时,该声明符将被作为一个断言,其结构将产生一个指定类型的对象。
如果只考虑声明说明符(参见A.8.2节)的类型部分及特定的声明符,则声明可以表示为“T D”的形式,其中T代表类型,D代表声明符。在不同形式的声明中,标识符的类型可用这种形式来表述。
在声明T D中,如果D是一个不加任何限定的标识符,则该标识符的类型为T。
在声明T D中,如果D的形式为:
(D1)
则D1中的标识符的类型与D的类型相同。圆括号不改变类型,但可改变复杂声明符之间的结合。
- 指针声明符
在声明T D中,如果D具有下列形式:
* 类型限定符表_opt D1
且声明T D1中的标识符的类型为“类型修饰符T
”,则D中标识符的类型为“类型修饰符
类型限定符表指向T的指针
”。星号*
后的限定符作用于指针本身,而不是作用于指针指向的对象。
例如,考虑下列声明:
int *ap[];
其中,ap[]的作用等价于D1,声明“int ap[]”将把ap的类型声明为“int类型的数组”,类型限定符表为空,且类型修饰符为“…的数组”。因此,该声明实际上将把ap声明为“指向int类型的指针数组”类型。
我们来看另外一个例子。下列声明:
声明了一个整型i和一个指向整型的指针pi。不能修改常量指针cpi的值,该指针总是指向同一位置,但它所指之处的值可以改变。整型ci是常量,也不能修改(可以进行初始化,如本例中所示)。pci的类型是“指向const int的指针”,pci本身可以被修改以指向另一个地方,但它所指之处的值不能通过pci赋值来改变。
- 数组声明符
在声明T D中,如果D具有下列形式:
D1[常量表达式_opt]
且声明T D1中标识符的类型是“类型修饰符
T”,则D的标识符类型为“类型修饰符
T类型的数组”。如果存在常量表达式,则该常量表达式必须为整型且值大于0。若缺少指定数组上界的常量表达式,则该数组类型是不完整类型。
数组可以由算术类型、指针类型、结构类型或联合类型构造而成,也可以由另一个数组构造而成(生成多维数组)。构造数组的类型必须是完整类型,绝对不能是不完整类型的数组或结构。也就是说,对于多维数组来说,只有第一维可以缺省。对于不完整数组类型的对象来说,其类型可以通过对该对象进行另一个完整声明(参见A.10.2节)或初始化(参见A.8.7节)来使其完整。例如:
float fa[17], *afp[17];
声明了一个浮点数数组和一个指向浮点数的指针数组,而
static int x3d[3][5][7];
则声明了一个静态的三维整型数组,其大小为3x5x7。具体来说,x3d是一个由3个项组成的数组,每个项都是由5个数组组成的一个数组,5个数组中的每个数组又都是由7个整型数组成的数组。x3d
、x3d[i]
、x3d[i][j]
与x3d[i][j][k]
都可以合法地出现在一个表达式中。前三者是数组类型,最后一个是int类型。更准确地说,x3d[i][j]
是一个有7个整型元素的数组;x3d[i]
则是有5个元素的数组,而其中的每个元素又是一个具有7个整型元素的数组。
根据数组下标运算的定义,E1[E2]
等价于*(E1+E2)
。因此,尽管表达式的形式看上去不对称,但下标运算是可交换的运算。根据适用于运算符+
和数组的转换规则(参见A.6.6节、A.7.1节和A.7.7节),若E1是数组且E2是整数,则E1[E2]代表E1的第E2个成员。
在本例中,x3d[i][j][k]
等价于*(x3d[i][j]+k)
。第一个子表达式x3d[i][j]
将按照A.7.1节中的规则转换为“指向整型数组的指针”类型,而根据A.7.7节中的规则,这里的加法运算需要乘以整型类型的长度。它遵循下列规则:数组按行存储(最后一维下标变动最快),且声明中的第一维下标决定数组所需的存储区大小,但第一维下标在下标计算时无其他作用。
- 函数声明符
在新式的函数声明T D中,如果D具有下列形式:
D1(形式参数类型表)
并且, 声明T D1中标识符的类型为“类型修饰符
T”,则D的标识符类型是“返回T类型值且具有'形式参数类型表'
中的参数的'类型修饰符'
类型的函数”。
形式参数的语法定义为:
形式参数类型表:
形式参数表
形式参数表,...
形式参数表:
形式参数声明
形式参数表, 形式参数声明
形式参数声明:
声明说明符 声明符
声明说明符 抽象声明符_opt
在这种新式的声明中,形式参数表指定了形式参数的类型。这里有一个特例,按照新式方式声明的无形式参数函数的声明符也有一个形式参数表,该表仅包含关键字void。如果形式参数表以省略号“,…”结尾,则该函数可接受的实际参数个数比显式说明的形式参数个数要多。详细信息参见A.7.3节。
如果形式参数类型是数组或函数,按照参数转换规则(参见A.10.1节),它们将被转换为指针。形式参数的声明中唯一允许的存储类说明符是register,并且,除非函数定义的开头包括函数声明符,否则该存储类说明符将被忽略。类似地,如果形式参数声明中的声明符包含标识符,且函数定义的开头没有函数声明符,则该标识符超出了作用域。不涉及标识符的抽象声明符将在A.8.8节中讨论。
在旧式的函数声明T D中,如果D具有下列形式:
D1(标识符表_opt)
并且声明T D1中的标识符的类型是“类型修饰符T”
,则D的标识符类型为“返回T类型值且未指定参数的'类型修饰符'
类型的函数”。形式参数(如果有的话)的形式如下:
标识符表:
标识符
标识符表, 标识符
在旧式的声明符中,除非在函数定义的前面使用了声明符,否则,标识符表必须空缺(参见A.10.1节)。声明不提供有关形式参数类型的信息。
例如,下列声明:
int f(), *fpi(), (*pfi)();
声明了一个返回整型值的函数f、一个返回指向整型的指针的函数fpi以及一个指向返回整型的函数的指针pfi。它们都没有说明形式参数类型,因此都属于旧式的声明。
在下列新式的声明中:
int strcpy(char *dest, const char *source), rand(void);
strcpy是一个返回int类型的函数,它有两个实际参数,第一个实际参数是一个字符指针,第二个实际参数是一个指向常量字符的指针。其中的形式参数名字可以起到注释说明的作用。第二个函数rand不带参数,且返回类型为int。
说明:到目前为止,带形式参数原型的函数声明符是ANSI标准中引入的最重要的一个语言变化。它们优于第1版中的“旧式”声明符,因为它们提供了函数调用时的错误检查和参数强制转换,但引入的同时也带来了很多混乱和麻烦,而且还必须兼容这两种形式。为了保持兼容,就不得不在语法上进行一些处理,即采用void作为新式的无形式参数函数的显式标记。
采用省略号“, …”表示函数变长参数表的做法也是ANSI标准中新引入的,并且,结合标准头文件
<stdarg.h>
中的一些宏,共同将这个机制正式化了。该机制在第1版中是官方上禁止的,但可非正式地使用。
这些表示法起源于C++。
A.8.7 初始化
声明对象时,对象的初始化声明符可为其指定一个初始值。初值紧跟在运算符=
之后,它可以是一个表达式,也可以是嵌套在花括号中的初值序列。初值序列可以以逗号结束,这样可以使格式简洁美观。
初值:
赋值表达式
{初值表}
{初值表,}
初值表:
初值
初值表,初值
对静态对象或数组而言,初值中的所有表达式必须是A.7.19节中描述的常量表达式。如果初值是用花括号括起来的初值表,则对auto或register类型的对象或数组来说,初值中的表达式也同样必须是常量表达式。但是,如果自动对象的初值是一个单个的表达式,则它不必是常量表达式,但必须符合对象赋值的类型要求。
说明:第1版不支持自动结构、联合或数组的初始化。而ANSI标准是允许的,但只能通过常量结构进行初始化,除非初值可以通过简单表达式表示出来。
未显式初始化的静态对象将被隐式初始化,其效果等同于它(或它的成员)被赋以常量0。未显式初始化的自动对象的初始值没有定义。
指针或算术类型对象的初值是一个单个的表达式,也可能括在花括号中。该表达式将赋值给对象。
结构的初值可以是类型相同的表达式,也可以是括在花括号中的按其成员次序排序的初值表。无名的位字段成员将被忽略,因此不被初始化。如果表中初值的数目比结构的成员数少,则后面余下的结构成员将被初始化为0。初值的数目不能比成员数多。
数组的初值是一个括在花括号中、由数组成员的初值构成的表。如果数组大小未知,则初值的数目将决定数组的大小,从而使数组类型成为完整类型。若数组大小固定,则初值的数目不能超过数组成员的数目。如果初值的数目比数组成员的数目少,则尾部余下的数组成员将被初始化为0。
这里有一个特例:字符数组可用字符串字面量初始化。字符串中的各个字符依次初始化数组中的相应成员。类似地,宽字符字面量(参见A.2.6节)可以初始化wchar_t
类型的数组。若数组大小未知,则数组大小将由字符串中字符的数目(包括尾部的空字符)决定 。若数组大小固定,则字符串中的字符数(不计尾部的空字符)不能超过数组的大小。
联合的初值可以是类型相同的单个表达式,也可以是括在花括号中的联合的第一个成员的初值。
说明:第1版不允许对联合进行初始化。“第一个成员”规则并不很完美,但在没有新语法的情况下很难对它进行一般化。除了至少允许以一种简单方式对联合进行显式初始化外,ANSI规则还给出了非显式初始化的静态联合的精确语义。
聚集是一个结构或数组。如果一个聚集包含聚集类型的成员,则初始化时将递归使用初始化规则。在下列情况的初始化中可以省略括号:如果聚集的成员也是一个聚集,且该成员的初始化符以左花开括号开头,则后续部分中逗号隔开的初值表将初始化子聚集的成员。初值的数目不允许超过成员的数目。但是,如果子聚集的初值不以左花括号开头,则只从初值表中取出足够数目的元素作为子聚集的成员,其他剩余的成员将用来初始化该子聚集所在的聚集的下一个成员。
例如:
int x[] = { 1, 3, 5};
将x声明并初始化为一个具有3个成员的一维数组,这是因为,数组未指定大小且有3个初值。下面的例子:
是一个完全用花括号分隔的初始化:1、3和5这3个数初始化数组y[0]
的第一行,即y[0][0]
、y[0][1]
和y[0][2]
。类似地,另两行将初始化y[1]
和y[2]
。因为初值的数目不够,所以y[3]
中的元素将被初始化为0。完全相同的效果还可以通过下列声明获得:
y的初值以左花括号开始,但y[0]的初值则没有以左花括号开始,因此y[0]的初始化将使用表中的3个元素。同理,y[1]将使用后续的3个元素进行初始化,y[2]依次类推。另外,下列声明:
将初始化y的第一列(将y看成为一个二维数组),其余的元素将默认初始化为0。
最后
char msg[] = "Syntax error on line %s\n";
声明了一个字符数组,并用一个字符串字面值初始化该字符数组的元素。该数组的大小包括尾部的空字符。
A.8.8 类型名
在某些上下文中(例如,需要显式进行强制类型转换、需要在函数声明符中声明形式参数类型、作为sizeof的实际参数等),我们需要提供数据类型的名字。使用类型名
可以解决这个问题,从语法上讲,也就是对某种类型的对象进行声明,只是省略了对象的名字而已。
如果该结构是声明中的一个声明符,就有可能唯一确定标识符在抽象声明符中的位置。命名的类型将与假设标识符的类型相同。例如:
其中的6个声明分别命名了下列类型:“整型”、“指向整型的指针”、“包含3个指向整型的指针的数组”、“指向未指定元素个数的整型数组的指针”、“未指定参数、返回指向整型的指针的函数”、“一个数组,其长度未指定,数组的元素为指向函数的指针,该函数没有参数且返回一个整型值”。
A.8.9 typedef
存储类说明符为typedef的声明不用于声明对象。而是定义为类型命名的标识符。这些标识符称为类型定义名。
typedef声明按照普通的声明方式将一个类型指派给其声明符中的每个名字(参见A.8.6节)。此后,类型定义名在语义上就等价于相关类型的类型说明符关键字。
例如,在定义
之后,下述形式:
都是合法的声明。b的类型为long,bp的类型为“指向long类型的指针”。z的类型为指定的结构类型,zp的类型为指向该结构的指针。
typedef类型定义并没有引入新的类型,它只是定义了数据类型的同义词,这样,就可以通过另一种方式进行类型声明。在本例中,b与其他任何long类型对象的类型相同。
类型定义名可在内层作用域中重新声明,但必须给出一个非空的类型说明符集合。例如,下列声明:
extern Blockno;
并没有重新声明Blockno,但下列声明:
extern int Blockno;
则重新声明了Blockno。
A.8.10 类型等价
如果两个类型说明符表包含相同的类型说明符集合(需要考虑类型说明符之间的蕴涵关系,例如,单独的long蕴涵了long int),则这两个类型说明符表是等价的。具有不同标记的结构、不同标记的联合和不同标记的枚举是不等价的,无标记的联合、无标记的结构或无标记的枚举指定的类型也是不等价的。
在展开其中的任何typedef类型并删除所有函数形式参数标识符后,如果两个类型的抽象声明符(参见A.8.8节)相同,且它们的类型说明符表等价,则这两个类型是相同的。数组长度和函数形式参数类型是其中很重要的因素。
A.9 语句
如果不特别指明,语句都是顺序执行的。语句执行都有一定的结果,但没有值。语句可分为几种类型:
语句:
带标号语句
表达式语句
复合语句
选择语句
循环语句
跳转语句
A.9.1 带标号语句
语句可带有标号前缀。
带标号语句:
标识符: 语句
case 常量表达式: 语句
default: 语句
由标识符构成的标号声明了该标识符。标识符标号的唯一用途就是作为goto语句的跳转目标。标识符的作用域是当前函数。因为标号有自己的名字空间,因此不会与其他标识符混淆,并且不能被重新声明。详细信息参见A.11.1节。
case标号和default标号用在switch语句中(参见A.9.4节)。case标号中的常量表达式必须为整型。
标号本身不会改变程序的控制流。
A.9.2 表达式语句
大部分语句为表达式语句,其形式如下所示:
表达式语句:
表达式_opt;
大多数表达式语句为赋值语句或函数调用语句。表达式引起的所有副作用在下一条语句执行前结束。没有表达式的语句称为空语句。空语句常常用来为循环语句提供一个空的循环体或设置标号。
A.9.3 复合语句
当需要把若干条语句作为一条语句使用时,可以使用复合语句(也称为“程序块”)。函数定义中的函数体就是一个复合语句。
复合语句:
{ 声明表_opt 语句表_opt }
声明表:
声明
声明表 声明
语句表:
语句
语句表 语句
如果声明表中的标识符位于程序块外的作用域中,则外部声明在程序块内将被挂起(参见A.11.1节),在程序块之后再恢复其作用。在同一程序块中,一个标识符只能声明一次。此规则也适用于同一名字空间的标识符(参见A.11节),不同名字空间的标识符被认为是不同的。
自动对象的初始化在每次进入程序块的顶端时执行,执行的顺序按照声明的顺序进行。如果执行跳转语句进入程序块,则不进行初始化。static类型的对象仅在程序开始执行前初始化一次。
A.9.4 选择语句
选择语句包括下列几种控制流形式:
选择语句:
if(表达式) 语句
if(表达式) 语句 else 语句
switch(表达式) 语句
在两种形式的if语句中,表达式(必须为算术类型或指针类型)首先被求值(包括所有的副作用),如果不等于0,则执行第一个子语句。在第二种形式中,如果表达式为0,则执行第二个子语句。通过将else与同一嵌套层中碰到的最近的未匹配else的if相连接,可以解决else的歧义性问题。
switch语句根据表达式的不同取值将控制转向相应的分支。关键字switch之后用圆括号括起来的表达式必须为整型。此语句控制的子语句一般是复合语句。子语句中的任何语句可带一个或多个case标号(参见A.9.1节)。控制表达式需要进行整型提升(参见A.6.1节),case常量将被转换为整型提升后的类型。同一switch语句中的任何两个case常量在转换后不能有相同的值。一个switch语句最多可以有一个default标号。switch语句可以嵌套,case或default标号与包含它的最近的switch相关联。
switch语句执行时,首先计算表达式的值及其副作用,并将其值与每个case常量比较,如果某个case常量与表达式的值相同,则将控制转向与该case标号匹配的语句。如果没有case常量与表达式匹配,并且有default标号,则将控制转向default标号的语句。如果没有case常量匹配,且没有default标号,则switch语句的所有子语句都不执行。
说明:在本书第1版中,switch语句的控制表达式与case常量都必须为int类型。
A.9.5 循环语句
循环语句用于指定程序段的循环执行。
循环语句:
while(表达式) 语句
do 语句 while(表达式);
for(表达式_opt; 表达式_opt; 表达式_opt) 语句
在while语句和do语句中,只要表达式的值不为0,其中的子语句将一直重复执行。表达式必须为算术类型或指针类型。while语句在语句执行前测试表达式,并计算其副作用,而do语句在每次循环后测试表达式。
在for语句中,第一个表达式只计算一次,用于对循环初始化。该表达式的类型没有限制。第二个表达式必须为算术类型或指针类型,在每次开始循环前计算其值。如果该表达式的值等于0,则for语句终止执行。第三个表达式在每次循环后计算,以重新对循环进行初始化,其类型没有限制。所有表达式的副作用在计算其值后立即结束。如果子语句中没有continue语句,则语句
for(表达式1; 表达式2; 表达式3) 语句
等价于
for语句中的3个表达式中都可以省略。第二个表达式省略时等价于测试一个非0常量。
A.9.6 跳转语句
跳转语句用于无条件地转移控制。
跳转语句:
goto 标识符;
continue;
break;
return 表达式_opt;
在goto语句中,标识符必须是位于当前函数中的标号(参见A.9.1节)。控制将转移到标号指定的语句。
continue语句只能出现在循环语句内,它将控制转向包含此语句的最内层循环部分。更准确地说,在下列任一语句中:
如果continue语句不包含在更小的循环语句中,则其作用与goto contin语句等价。
break语句只能用在循环语句或switch语句中,它将终止包含该语句的最内层循环语句的执行,并将控制转向被终止语句的下一条语句。
return语句用于将控制从函数返回给调用者。当return语句后跟一个表达式时,表达式的值将返回给函数调用者。像通过赋值操作转换类型那样,该表达式将被转换为它所在的函数的返回值类型。
控制到达函数的结尾等价于一个不带表达式的return语句。在这两种情况下,返回值都是没有定义的。
A.10 外部声明
提供给C编译器处理的输入单元称为翻译单元。它由一个外部声明序列组成,这些外部声明可以是声明,也可以是函数定义。
翻译单元:
外部声明
翻译单元 外部声明
外部声明:
函数定义
声明
与程序块中声明的作用域持续到整个程序块的末尾类似,外部声明的作用域一直持续到其所在的翻译单元的末尾。外部声明除了只能在这一级上给出函数的代码外,其语法规则与其他所有声明相同。
A.10.1 函数定义
函数定义具有下列形式:
函数定义:
声明说明符_opt 声明符 声明表_opt 复合语句
声明说明符中只能使用存储类说明符extern或static。有关这两个存储类说明符之间的区别,参见A.11.2节。
函数可返回算术类型、结构、联合、指针或void类型的值,但不能返回函数或数组类型。函数声明中的声明符必须显式指定所声明的标识符具有函数类型,也就是说,必须包含下列两种形式之一(参见A.8.6节):
直接声明符(形式参数类型表)
直接声明符(标识符表_opt)
其中,直接声明符可以为标识符或用圆括号括起来的标识符。特别是,不能通过typedef定义函数类型。
第一种形式是一种新式的函数定义,其形式参数及类型都在形式参数类型表中声明,函数声明符后的声明表必须空缺。除了形式参数类型表中只包含void类型(表明该函数没有形式参数)的情况外,形式参数类型表中的每个声明符都必须包含一个标识符。如果形式参数类型表以“, …”结束,则调用该函数时所用的实际参数数目就可以多于形式参数数目。va_arg
宏机制在标准头文件<stdarg.h>
中定义,必须使用它来引用额外的参数,我们将在附录B中介绍。带有可变形式参数的函数必须至少有一个命名的形式参数。
第二种形式是一种旧式的函数定义:标识符表列出了形式参数的名字,这些形式参数的类型由声明表指定。对于未声明的形式参数,其类型默认为int类型。声明表必须只声明标识符表中指定的形式参数,不允许进行初始化,并且仅可使用存储类说明符register。
在这两种方式的函数定义中,可以这样理解形式参数:在构成函数体的复合语句的开始处进行声明,并且在该复合语句中不能重复声明相同的标识符(但可以像其他标识符一样在该复合语句的内层程序块中重新声明)。如果某一形式参数声明的类型为“type类型的数组”,则该声明将会被自动调整为“指向type类型的指针”。类似地,如果某一形式参数声明为“返回type类型值的函数”,则该声明将会被调整为“指向返回type类型值的函数的指针”。调用函数时,必要时要对实际参数进行类型转换,然后赋值给形式参数,详细信息参见A.7.3节。
说明: 新式函数定义是ANSI标准新引入的一个特征。有关提升的一些细节也有细微的变化。第1版指定,float类型的形式参数声明将被调整为double类型。当在函数内部生成一个指向形式参数的指针时,它们之间的区别就显而易见了。
下面是一个新式函数定义的完整例子:
其中,int是声明说明符;max(int a, int b, int c)
是函数的声明符;{...}
是函数代码的程序块。相应的旧定义如下所示:
其中,int max(a,b,c)
是声明符,int a,b,c;
是形式参数的声明表。
A.10.2 外部声明
外部声明用于指定对象、函数及其他标识符的特征。术语“外部”表明它们位于函数外部,并且不直接与关键字extern连接。外部声明的对象可以不指定存储类,也可指定为extern或static。
同一个标识符的多个外部声明可以共存于同一个翻译单元中,但它们的类型和连接必须保持一致,并且标识符最多只能有一个定义。
如果一个对象或函数的两个声明遵循A.8.10节中所述的规则,则认为它们的类型是一致的。并且,如果两个声明之间的区别仅仅在于:其中一个的类型为不完整结构、联合或枚举类型(参见A.8.3节),而另一个是对应的带同一标记的完整类型,则认为这两个类型是一致的。此外,如果一个类型为不完整数组类型(参见A.8.6节),而另一个类型为完整数组类型,其他属性都相同,则认为这两个类型是一致的。最后,如果一个类型指定了一个旧式函数,而另一个类型指定了带形式参数声明的新式函数,二者之间其他方面都相同,则认为它们的类型也是一致的。
如果一个对象或函数的第一个外部声明包含static说明符,则该标识符具有内部连接,否则具有外部连接。有关连接的详细信息,参见A.11.2节中的讨论。
如果一个对象的外部声明带有初值,则该声明就是一个定义。如果一个外部对象声明不带有初值,并且不包含extern说明符,则它是一个临时定义。如果对象的定义出现在翻译单元中,则所有临时定义都将仅仅被认为是多余的声明;如果该翻译单元中不存在该对象的定义,则该临时定义将转换为一个初值为0的定义。
每个对象都必须有且仅有一个定义。对于具有内部连接的对象,该规则分别适用于每个翻译单元,这是因为,内部连接的对象对每个翻译单元是唯一的。对于具有外部连接的对象,该规则适用于整个程序。
说明:虽然单一定义规则(one-definition rule)在表述上与本书第1版有所不同,但在效果上是等价的。某些实现通过将临时定义的概念一般化而放宽了这个限制。在另一种形式中,一个程序中所有翻译单元的外部连接对象的所有临时定义将集中进行考虑,而不是在各翻译单元中分别考虑,UNIX系统通常就采用这种方法,并且被认为是该标准的一般扩展。如果定义在程序中的某个地方出现,则临时定义仅被认为是声明,但如果没有定义出现,则所有临时定义将被转变为初值为0的定义。
A.11 作用域与连接
一个程序的所有单元不必同时进行翻译。源文件文本可保存在若干个文件中,每个文件中可以包含多个翻译单元,预先编译过的例程可以从库中进行加载。程序中函数间的通信可以通过调用和操作外部数据来实现。
因此,我们需要考虑两种类型的作用域:第一种是标识符的词法作用域,它是体现标识符特性的程序文本区域;第二种是与具有外部连接的对象和函数相关的作用域,它决定各个单独编译的翻译单元中标识符之间的连接。
A.11.1 词法作用域
标识符可以在若干个名字空间中使用而互不影响。如果位于不同的名字空间中,即使是在同一作用域内,相同的标识符也可用于不同的目的。名字空间的类型包括:对象、函数、类型定义名和枚举常量;标号;结构标记、联合标记和枚举标记;各结构或联合自身的成员。
说明:这些规则与本手册第1版中所述的内容有几点不同。以前标号没有自己的名字空间;结构标记和联合标记分别有各自的名字空间,在某些实现中枚举标记也有自己的名字空间;把不同种类的标记放在同一名字空间中是新增加的限制。与第1版之间最大的不同在于:每个结构和联合都为其成员建立不同的名字空间,因此同一名字可出现在多个不同的结构中。这一规则在最近几年使用得很多。
在外部声明中,对象或函数标识符的词法作用域从其声明结束的位置开始,到所在翻译单元结束为止。函数定义中形式参数的作用域从定义函数的程序块开始处开始,并贯穿整个函数;函数声明中形式参数的作用域到声明符的末尾处结束。程序块头部中声明的标识符的作用域是其所在的整个程序块。标号的作用域是其所在的函数。结构标记、联合标记、枚举标记或枚举常量的作用域从其出现在类型说明符中开始,到翻译单元结束为止(对外部声明而言)或到程序块结束为止(对函数内部声明而言)。
如果某一标识符显式地在程序块(包括构成函数的程序块)头部中声明,则该程序块外部中此标识符的任何声明都将被挂起,直到程序块结束再恢复其作用。
A.11.2 连接
在翻译单元中,具有内部连接的同一对象或函数标识符的所有声明都引用同一实体,并且,该对象或函数对这个翻译单元来说是唯一的。具有外部连接的同一对象或函数标识符的所有声明也引用同一实体,并且该对象或函数是被整个程序中共享的。
如A.10.2节所述,如果使用了static说明符,则标识符的第一个外部声明将使得该标识符具有内部连接,否则,该标识符将具有外部连接。如果程序块中对一个标识符的声明不包含extern说明符,则该标识符没有连接,并且在函数中是唯一的。如果这种声明中包含extern说明符,并且,在包含该程序块的作用域中有一个该标识符的外部声明,则该标识符与该外部声明具有相同的连接,并引用同一对象或函数。但是,如果没有可见的外部声明,则该连接是外部的。
A.12 预处理
预处理器执行宏替换、条件编译以及包含指定的文件。以#
开头的命令行(“#”
前可以有空格)就是预处理器处理的对象。这些命令行的语法独立于语言的其他部分,它们可以出现在任何地方,其作用可延续到所有翻译单元的末尾(与作用域无关)。行边界是有实际意义的;每一行都将单独进行分析(有关如何将行连接起来的详细信息参见A.12.4节)。对预处理器而言,记号可以是任何语言记号,也可以是类似于#include
指令(参见A.12.4节)中表示文件名的字符序列。此外,所有未进行其他定义的字符都将被认为是记号。但是,在预处理器指令行中,除空格、横向制表符外的其他空白符的作用是没有定义的。
预处理过程在逻辑上可以划分为几个连续的阶段(在某些特殊的实现中可以缩减)。
-
首先,将A.12.1节所述的三字符序列替换为等价字符。如果操作系统环境需要,还要在源文件的各行之间插入换行符。
-
将指令行中位于换行符前的反斜杠符
\
删除掉,以把各指令行连接起来(参见A.12.2节)。 -
将程序分成用空白符分隔的记号。注释将被替换为一个空白符。接着执行预处理器指令,并进行宏扩展(参见A.12.3节~A.12.10节)。
-
将字符常量和字符串字面值中的转义字符序列(参见A.2.5节与A.2.6节)替换为等价字符,然后把相邻的字符串字面值连接起来。
-
收集必要的程序和数据,并将外部函数和对象的引用与其定义相连接,翻译经过以上处理得到的结果,然后与其他程序和库连接起来。
A.12.1 三字符序列
C语言源程序的字符集是7位ASCII码的子集,但它是ISO 646-1983不变代码集的超集。为了将程序通过这种缩减的字符集表示出来,下列所示的所有三字符序列都要用相应的单个字符替换,这种替换在进行所有其他处理之前进行。
??= # ??( [ ??< {
??/ \ ??) ] ??> }
??' ^ ??! | ??- -
除此之外不进行其他替换。
说明:三字符序列是ANSI标准新引入的特征。
A.12.2 行连接
通过将以反斜杠\
结束的指令行末尾的反斜杠和其后的换行符删除掉,可以将若干指令行合并成一行。这种处理要在分隔记号之前进行。
A.12.3 宏定义和扩展
类似于下列形式的控制指令:
#define 标识符 记号序列
将使得预处理器把该标识符后续出现的各个实例用给定的记号序列替换。记号序列前后的空白符都将被丢弃掉。第二次用#define
指令定义同一标识符是错误的,除非第二次定义中的标记序列与第一次相同(所有的空白分隔符被认为是相同的)。
类似于下列形式的指令行:
#define 标识符(标识符表_opt) 记号序列
是一个带有形式参数(由标记符表指定)的宏定义,其中第一个标识符与圆括号(
之间没有空格。同第一种形式一样,记号序列前后的空白符都将被丢弃掉。如果要对宏进行重定义,则必须保证其形式参数个数、拼写及记号序列都必须与前面的定义相同。
类似于下列形式的控制指令:
#undef 标识符
用于取消标识符的预处理器定义。将#undef
应用于未知标识符(即未用#define
指令定义的标识符)并不会导致错误。
按照第二种形式定义宏时,宏标识符(后面可以跟一个空白符,空白符是可选的)及其后用一对圆括号括起来的、由逗号分割的记号序列就构成了一个宏调用。宏调用的实际参数是用逗号分割的记号序列,用引号或嵌套的括号括起来的逗号不能用于分割实际参数。在处理过程中,实际参数不进行宏扩展。宏调用时,实际参数的数目必须与定义中形式参数的数目匹配。实际参数被分离后,前导和尾部的空白符将被删除。随后,由各实际参数产生的记号序列将替换未用记号引起来的相应形式参数的标识符(位于宏的替换记号序列中)。除非替换序列中的形式参数的前面有一个#
符号,或者其前面或后面有一个##
符号,否则,在插入前要对宏调用的实际参数记号进行检查,并在必要时进行扩展。
两个特殊的运算符会影响替换过程。首先,如果替换记号序列中的某个形式参数前面直接是一个#
符号(它们之间没有空白符),相应形式参数的两边将被加上双引号("
),随后,#
和形式参数标识符将被用引号引起来的实际参数替换。实际参数中的字符串字面值、字符常量两边或内部的每个双引号("
)或反斜杠(\
)前面都要插入一个反斜杠(\
)。
其次,无论哪种宏的定义记号序列中包含一个##
运算符,在形式参数替换后都要把##
及其前后的空白符都删除掉,以便将相邻记号连接起来形成一个新记号。如果这样产生的记号无效,或者结果依赖于##
运算符的处理顺序,则结果没有定义。同时,##
也可以不出现在替换记号序列的开头或结尾。
对这两种类型的宏,都要重复扫描替换记号序列以查找更多的已定义标识符。但是,当某个标识符在某个扩展中被替换后,再次扫描并再次遇到此标识符时不再对其执行替换,而是保持不变。
即使执行宏扩展后得到的最终结果以#
打头,也不认为它是预处理器指令。
说明:有关宏扩展处理的细节信息,ANSI标准比第1版描述得更详细。最重要的变化是加入了
#
和##
运算符,这就使得引用和连接成为可能。某些新规则(特别是与连接有关的规则)比较独特(参见下面的例子)。
例如,这种功能可用来定义“表示常量”,如下例所示:
定义
#define ABSDIFF(a, b) ((a)>(b) ? (a)-(b) : (b)-(a))
定义了一个宏,它返回两个参数之差的绝对值。与执行同样功能的函数所不同的是,参数与返回值可以是任意算术类型,甚至可以是指针。同时,参数可能有副作用,而且需要计算两次,一次进行测试,另一次则生成值。
假定有下列定义:
#define tempfile(dir) #dir "/%s"
宏调用tempfile(/ussr/tmp)
将生成
"/usr/tmp" "/%s"
随后,该结果将被连接为一个单个的字符串。给定下列定义:
#define cat(x, y) x ## y
那么,宏调用cat(var, 123)
将生成var 123。但是,宏调用cat(cat(1,2),3)
没有定义: ##
阻止了外层调用的参数的扩展。因此,它将生成下列记号串:
|
|
并且,)3
不是一个合法的记号,它由第一个参数的最后一个记号与第二个参数的第一个记号连接而成。如果再引入第二层的宏定义,如下所示:
#define xcat(x,y) cat(x,y)
我们就可以得到正确的结果。xcat(xcat(1,2),3)
将生成123,因为xcat自身的扩展不包含##
运算符。
类似地,ABSDIFF(ABSDIFF(a,b),c)
将生成所期望的经完全扩展后的结果。
A.12.4 文件包含
下列形式的控制指令:
#include <文件名>
将把该行替换为文件名指定的文件的内容。文件名不能包含>
或换行符。如果文件名中包含字符"
、'
、\
或/*
,则其行为没有定义。预处理器将在某些特定的位置查找指定的文件,查找的位置与具体的实现相关。
类似地,下列形式的控制指令:
#include "文件名"
首先从源文件的位置开始搜索指定文件(搜索过程与具体的实现相关),如果没有找到指定的文件,则按照第一种定义的方式处理。如果文件名中包含字符'
、\
或/*
,其结果仍然是没有定义的,但可以使用字符>
。
最后,下列形式的指令行:
#include 记号序列
同上述两种情况都不同,它将按照扩展普通文本的方式扩展记号序列进行解释。记号序列必须被解释为<...>
或"..."
两种形式之一,然后再按照上述方式进行相应的处理。
#include文件
可以嵌套。
A.12.5 条件编译
对一个程序的某些部分可以进行条件编译。条件编译的语法形式如下:
预处理器条件:
if行 文本 elif部分_opt else部分_opt #endif
if行:
#if 常量表达式
#ifdef 标识符
#ifndef 标识符
elif部分:
elif行 文本 elif部分_opt
elif行:
#elif 常量表达式
else部分:
else行 文本
else行:
#else
其中,每个条件编译指令(if行
、elif行
、else行
以及#endif
)在程序中均单独占一行。预处理器依次对#if
以及后续的#elif
行中的常量表达式进行计算,直到发现某个指令的常量表达式为非0值为止,这时将放弃值为0的指令行后面的文本。常量表达式不为0的#if
和#elif
指令之后的文本
将按照其他普通程序代码一样进行编译。在这里,“文本”
是指任何不属于条件编译指令结构的程序代码,它可以包含预处理指令,也可以为空。一旦预处理器发现某个#if
或#elif
条件编译指令中的常量表达式的值不为0,并选择其后的文本供以后的编译阶段使用时,后续的#elif
和#else
条件编译指令及相应的文本将被放弃。如果所有常量表达式的值都为0,并且该条件编译指令链中包含一条#else
指令,则将选择#else
指令之后的文本。除了对条件编译指令的嵌套进行检查之外,条件编译指令的无效分支(即条件值为假的分支)控制的文本都将被忽略。
#if
和#elif
中的常量表达式将执行通常的宏替换。并且,任何下列形式的表达式:
defined 标识符
或
defined(标识符)
都将在执行宏扫描之前进行替换,如果该标识符在预处理器中已经定义,则用1替换它,否则,用0替换。预处理器进行宏扩展之后仍然存在的任何标识符都将用0来替换。最后,每个整型常量都被预处理器认为其后面跟有后缀L,以便把所有的算术运算都当作是在长整型或无符号长整型的操作数之间进行的运算。
进行上述处理之后的常量表达式(参见A.7.19节)满足下列限制条件:它必须是整型,并且其中不包含sizeof、强制类型转换运算符或枚举常量。
下列控制指令:
分别等价于:
说明:
#elif
是ANSI中新引入的条件编译指令,但此前它已经在某些预处理器中实现了。defined预处理器运算符也是ANSI中新引入的特征。
A.12.6 行控制
为了便于其他预处理器生成C语言程序,下列形式的指令行:
将使编译器认为(处于错误诊断的目的):下一行源代码的行号是以十进制整型常量的形式给出的,并且,当前的输入文件是由该标识符命名的。如果缺少带双引号的文件名部分,则将不改变当前编译的源文件的名字。行中的宏将先进行扩展,然后再进行解释。
A.12.7 错误信息生成
下列形式的预处理器控制指令:
#error 记号序列_opt
将使预处理器打印包含该记号序列的诊断信息。
A.12.8 pragma
下列形式的控制指令:
#pragma 记号序列_opt
将使预处理器执行一个与具体实现相关的操作。无法识别的pragma(编译指示)将被忽略掉。
A.12.9 空指令
下列形式的预处理器行不执行任何操作:
#
A.12.10 预定义名字
某些标识符是预定义的,扩展后将生成特定的信息。它们同预处理器表达式运算符defined一样,不能取消定义或重新进行定义。
__LINE__
包含当前源文件行数的十进制常量。
__FILE__
包含正在被编译的源文件名字的字符串字面值。
__DATE__
包含编译日期的字符串字面值,其形式为“Mmm dd yyyy”。
__TIME__
包含编译时间的字符串字面值,其形式为“hh:mm:ss”。
__STDC__
整型常量1。只有在遵循标准的实现中该标识符才被定义为1。
说明:
#error
与#pragma
是ANSI标准中新引入的特征。这些预定义的预处理器宏也是新引入的,其中的一些宏先前已经在某些编译器中实现。
A.13 语法
这一部分的内容将简要概述本附录前面部分中讲述的语法。它们的内容完全相同,但顺序有一些调整。
本语法没有定义下列终结符:整型常量
、字符常量
、浮点常量
、标识符
、字符串
和枚举常量
。以打字字体形式表示的单词和符号是终结符。本语法可以直接转换为自动语法分析程序生成器可以接受的输入。除了增加语法记号说明产生式中的候选项外,还需要扩展其中的“one of”结构,并(根据语法分析程序生成器的规则)复制每个带有opt符号的产生式:一个带有opt符号,一个没有opt符号。这里还有一个变化,即删除了产生式“类型定义名:标识符”
,这样就使得其中的类型定义名
成为一个终结符。该语法可被YACC语法分析程序生成器接受,但由于if-else的歧义性问题,还存在一处冲突。
;A.10
;;翻译单元由多个外部声明组成
翻译单元:
外部声明
翻译单元 外部声明
外部声明:
函数定义
声明
;A.10.1
;;声明符:函数名;声明表_opt:参数列表;复合语句:函数体
函数定义:
声明说明符_opt 声明符 声明表_opt 复合语句
;A.8
声明:
声明说明符 初始化声明符表_opt;
;;声明表由多个声明组成
声明表:
声明
声明表 声明
声明说明符:
存储类说明符 声明说明符_opt
类型说明符 声明说明符_opt
类型限定符 声明说明符_opt
;;初始化声明符表由多个初始化声明符组成
初始化声明符表:
初始化声明符
初始化声明符表,初始化声明符
初始化声明符:
声明符
声明符=初始化符
;A.8.1
存储类说明符: one of
auto register static extern typedef
;A.8.2
类型说明符: one of
void char short int long float double signed
unsigned 结构或联合说明符 枚举说明符 类型定义名
类型限定符: one of
const volatile
;A.8.3
结构或联合说明符:
结构或联合 标识符_opt { 结构声明表 }
结构或联合 标识符
结构或联合: one of
struct union
结构声明表:
结构声明
结构声明表 结构声明
结构声明:
说明符限定符表 结构声明符表;
说明符限定符表:
类型说明符 说明符限定符表_opt
类型限定符 说明符限定符表_opt
结构声明符表:
结构声明符
结构声明符表,结构声明符
结构声明符:
声明符
声明符_opt: 常量表达式
;A.8.4
枚举说明符:
enum 标识符_opt { 枚举符表 }
enum 标识符
枚举符表:
枚举符
枚举符表,枚举符
枚举符:
标识符
标识符=常量表达式
;A.8.5
声明符:
指针_opt 直接声明符
直接声明符:
标识符
(声明符)
直接声明符[常量表达式_opt]
直接声明符(形式参数类型表)
直接声明符{标识符表_opt}
指针:
* 类型限定符表_opt
* 类型限定符表_opt 指针
类型限定符表:
类型限定符
类型限定符表 类型限定符
;A.8.6
形式参数类型表:
形式参数表
形式参数表,...
形式参数表:
形式参数声明
形式参数表,形式参数声明
形式参数声明:
声明说明符 声明符
声明说明符 抽象声明符_opt
标识符表:
标识符
标识符表, 标识符
;A.8.7
初值:
赋值表达式
{初值表}
初值表:
初值
初值表,初值
;A.8.8
类型名:
说明符限定符表 抽象声明符_opt
抽象声明符:
指针
指针_opt 直接抽象声明符
直接抽象声明符:
(抽象声明符)
直接抽象声明符_opt[常量表达式_opt]
直接抽象声明符_opt(形式参数类型表_opt)
;A.8.9
类型定义名:
标识符
;A.9
语句:
带标号语句
表达式语句
复合语句
选择语句
循环语句
跳转语句
;A.9.1
带标号语句:
标识符: 语句
case 常量表达式: 语句
default: 语句
;A.9.2
表达式语句:
表达式_opt;
;A.9.3
复合语句:
{声明表_opt 语句表_opt}
语句表:
语句
语句表 语句
;A.9.4
选择语句:
if(表达式) 语句
if(表达式) 语句 else 语句
switch(表达式) 语句
;A.9.5
循环语句:
while(表达式) 语句
do 语句 while(表达式);
for(表达式_opt; 表达式_opt; 表达式_opt) 语句
;A.9.6
跳转语句:
goto 标识符;
continue;
break;
return 表达式_opt;
;A.7.18
表达式:
赋值表达式
表达式,赋值表达式
;A.7.17
赋值表达式:
条件表达式
一元表达式 赋值运算符 赋值表达式
赋值运算符: one of
= *= /= %= += -= <<= >>= &= ^= |=
;A.7.16
条件表达式:
逻辑或表达式
逻辑或表达式?表达式:条件表达式
;A.7.19
常量表达式:
条件表达式
;A.7.15
逻辑或表达式:
逻辑与表达式
逻辑或表达式||逻辑与表达式
;A.7.14
逻辑与表达式:
按位或表达式
逻辑与表达式&&按位或表达式
;A.7.13
按位或表达式:
按位异或表达式
按位或表达式|按位异或表达式
;A.7.12
按位异或表达式:
按位与表达式
按位异或表达式^按位与表达式
;A.7.11
按位与表达式
相等类表达式
按位与表达式&相等类表达式
;A.7.10
相等类表达式:
关系表达式
相等类表达式==关系表达式
相等类表达式!=关系表达式
;A.7.9
关系表达式:
移位表达式
关系表达式<移位表达式
关系表达式>移位表达式
关系表达式<=移位表达式
关系表达式>=移位表达式
;A.7.8
移位表达式:
加法类表达式
移位表达式<<加法类表达式
移位表达式>>加法类表达式
;A.7.7
加法类表达式:
乘法类表达式
加法类表达式+乘法类表达式
加法类表达式-乘法类表达式
;A.7.6
乘法类表达式:
强制类型转换表达式
乘法类表达式*强制类型转换表达式
乘法类表达式/强制类型转换表达式
乘法类表达式%强制类型转换表达式
;A.7.5
强制类型转换表达式:
一元表达式
(类型名)强制类型转换表达式
;A.7.4
一元表达表:
后缀表达式
++一元表达式
--一元表达式
一元运算符 强制类型转换表达式
sizeof 对象
sizeof(类型名)
一元运算符: one of
& * + - ~ !
;A.7.3
后缀表达式:
初等表达式
后缀表达式[表达式]
后缀表达式(参数表达式表_opt)
后缀表达式.标识符
后缀表达式->标识符
后缀表达式++
后缀表达式--
;A.7.2
初等表达式
标识符
常量
字符串
(表达式)
;A.7.3
参数表达式表:
赋值表达式
参数表达式表,赋值表达式
;A.2.5
常量:
整型常量
字符常量
浮点常量
枚举常量
下列预处理器语法总结了控制指令的结构,但不适合于机械化的语法分析。其中包含符号“文本”(即通常的程序文本)、非条件预处理器控制指令或完整的预处理器条件结构。
控制指令:
#define 标识符 记号序列
#define 标识符(标识符表_opt) 记号序列
#undef 标识符
#include<文件名>
#include"文件名"
#include 记号序列
#line 常量 "文件名"
#line 常量
#error 记号序列_opt
#pragma 记号序列_opt
#
预处理器条件指令
预处理器条件指令:
if行 文本 elif部分_opt else部分_opt #endif
if行:
#if 常量表达式
#ifdef 标识符
#ifndef 标识符
elif部分:
elif行 文本 elif部分_opt
elif行:
#elif 常量表达式
else部分:
else行 文本
else行:
#else