Type, Operator and Expression
变量和常量是程序处理的两种基本数据对象。声明语句说明变量的名字和类型,也可以指定变量的初值。运算符指定将要进行的操作。表达式则把变量和常量组合起来生成新的值。对象的类型决定该对象可取值的集合以及可以对该对象执行的操作。
ANSI标准对语言的基本类型与表达式做了许多小的修改与增补。所有整型都包括signed
(带符号)和unsigned
(无符号)两种形式,且可以表示无符号常量与十六进制字符常量。浮点运算可以单精度进行,还可以使用更高精度的long double
类型运算。字符串常量可以在编译时连接。ANSI C还支持枚举类型,该语言特性经过了长期的发展才形成。对象可以声明为const
(常量)类型,表明其值不能修改。该标准还对算术类型之间的自动强制转换规则进行了扩充,以适合于更多的数据类型。
1. 变量名
对变量的命名与符号常量的命名存在一些限制条件。
名字是由字母和数字组成的序列,但其第一个字符必须为字母。
下划线'_'
被看做是字母,通常用于命名较长的变量名,以提高其可读性。
由于库例程的名字通常以下划线开头,因此变量名不要以下划线开头。
大写字母与小写字母是有区别的。
在传统的C语言用法中,变量名使用小写字母,符号常量名全部使用大写字母。
对于内部名(函数内部变量的名字)而言,至少前31个字符是有效的。
函数名与外部变量名包含的字符数目可能小于31,这是因为汇编程序和加载程序可能会使用这些外部名,而语言本身是无法控制加载和汇编程序的。(别起那么长的名字)
对于外部名,ANSI标准仅保证前6个字符的唯一性,并且不区分大小写。
类似于if
、else
、int
、float
等关键字是保留给语言本身使用的,不能把它们用作变量名。
所有关键字中的字符都必须小写。
选择的变量名要能够尽量从字面上表达变量的用途,这样做不容易引起混淆。
局部变量一般使用较短的变量名(尤其是循环控制变量),外部变量使用较长的名字。
2. 数据类型及长度
基本数据类型:
char
字符型,占用一个字节,可以存放本地字符集中的一个字符int
整型,通常反映了所用机器中整数的最自然长度float
单精度浮点型double
双精度浮点型
…限定符。short
与long
用于限定整型(int
可以省略,习惯这么做)。
int
通常代表特定机器中整数的自然长度。short
类型通常为16位,long
类型通常为32位,int
类型可以为16位活32位。
各编译器可以根据硬件特性自主选择合适的类型长度,但要遵循下列限制:short
与int
类型至少为16位,而long
类型至少为32位,并且short
类型不得长于int
类型,而int
类型不得长于long
类型。
类型限定符signed
与unsigned
可用于限定char
类型或任何整型。
unsigned
类型的数总是正值或0,并遵守算术模2^n
定律。
signed
类型变量的取值范围和对二的补码的机器有关。
不带限定符的char
类型对象是否带符号则取决于具体机器,但可打印字符总是正值。
long double
类型表示高精度的浮点数。
浮点数的长度也取决于具体的实现,float
、double
与long double
类型可以表示相同的长度,也可以表示两种或三种不同的长度。
有关这些类型长度定义的符号常量以及其他与机器和编译器有关的属性可以在标准头文件<limits.h>
与<float.h>
中找到。
3. 常量
类似于1234的整数常量属于int
类型。
long
类型的常量以字面l
或L
结尾。
如果一个整数太大以至于无法用int
类型表示时,也将被当作long
类型处理。
无符号常量以字面u
或U
结尾。
后缀ul
或UL
表明是unsigned long
类型。
浮点数常量中包含一个小数点(如123.4)或一个指数(如1e-2),也可以两者都有。
没有后缀的浮点数常量为double
类型。
后缀f
或F
表示float
类型,而后缀l
或L
则表示long double
类型。
整型数除了用十进制表示外,还可以用八进制或十六进制表示。
带前缀0
的整型常量表示它为八进制形式;前缀为0x
或0X
,则表示它为十六进制形式。
八进制与十六进制的常量也可以使用后缀L
表示long
类型,使用后缀U
表示unsigned
类型。
一个字符常量是一个整数,书写时将一个字符括在单引号中。
字符在机器字符集中的数值就是字符常量的值。
如果使用字符代替这个与具体字符集有关的值,那么,程序就无需关心改字符对应的具体值,增加了程序的易读性。
字符常量一般用来与其他字符进行比较,但也可以像其他整数一样参与数值运算。
某些字符可以通过转义字符序列表示为字符和字符串常量。
转义字符序列看起来像两个字符,但只表示一个字符。
可以用'\ooo'
表示任意的字节大小的位模式。其中,ooo
代表1~3个八进制数字。
这种位模式还可以用'\xhh'
表示,其中,hh
是一个或多个十六进制数字。
\a
响铃符\b
回退符\f
换页符\n
换行符\r
回车符\t
横向制表符\v
纵向制表符\\
反斜杠\?
问号\'
单引号\"
双引号\ooo
八进制数\xhh
十六进制数
字符常量'\0'
表示值为0的字符,也就是空字符(null
)。通常用'\0'
的形式代替0,以强调某些表达式的字符属性,但其数字值为0。
常量表达式是仅仅只包含常量的表达式。这种表达式在编译时求值,而不在运行时求值。它可以出现在常量可以出现的任何位置。
字符串常量也叫字符串字面值,是用双引号括起来的0个或多个字符组成的字符序列。
双引号不是字符串的一部分,它只用于限定字符串。
字符常量中使用的转义字符序列同样也可以用在字符串中。
在字符串中使用\"
表示双引号字符。
编译时可以将多个字符串常量连接起来。字符串常量的连接为将较长的字符串分散在若干个源文件行中提供了支持。
从技术角度看,字符串常量就是字符数组。字符串的内部表示使用一个空字符\0
作为串的结尾,因此,存储字符串的物理存储单元数比括在双引号中的字符数多一个。这种表示方法也说明,C语言对字符串的长度没有限制,但程序必须扫描完整个字符串后才能确定字符串的长度。标准库函数strlen(s)
可以返回字符串参数s的长度,但长度不包括末尾的\0
。
标准头文件<string.h>
中声明了strlen
和其他字符串函数。
枚举常量是另外一种类型的常量。枚举是一个整型常量值的列表。
在没有显式说明的情况下,enum
类型中第一个枚举名的值为0,第二个为1,依次类推。如果只指定了部分枚举名的值,那么未指定值的枚举名的值将依着最后一个指定值向后递增。
不同枚举中的名字必须互不相同。同一枚举中不同的名字可以具有相同的值。
枚举为建立常量值与名字之间的关联提供了一种便利的方式。相对于#define
语句来说,它的优势在于常量值可以自动生成。尽管可以声明enum
类型的变量,但编译器不需要检查这种类型的变量中存储的值是否为该枚举的有效值。不过,枚举变量提供了这种检查的机会,因此枚举比#define
更具优势。此外,调试程序可以以符号形式打印出枚举变量的值。
4. 声明
所有变量都必须先声明后使用,尽管某些变量可以通过上下文隐式地声明。一个声明指定一种变量类型,后面所带的变量表可以包含一个或多个该类型的变量。
一个声明语句中的多个变量可以拆开在多个声明语句中声明。按照这种形式书写代码需要占用较多的空间,但便于向各声明语句中添加注释,也便于以后修改。
还可以在声明的同时对变量进行初始化。在声明中,如果变量名的后面紧跟一个等号以及一个表达式,该表达式就充当对变量进行初始化的初始化表达式。
如果变量不是自动变量,则只能进行一次初始化操作,从概念上讲,应该是在程序开始执行之前进行,并且初始化表达式必须为常量表达式。
每次进入函数或程序块时,显式初始化的自动变量都将被初始化一次,其初始化表达式可以是任何表达式。
默认情况下,外部变量与静态变量将被初始化为0.
未经显式初始化的自动变量的值为未定义值(即无效值)。
任何变量的声明都可以使用const
限定符限定。该限定符指定变量的值不能被修改。对数组而言,const
限定符指定数组所有元素的值都不能被修改。
const
限定符也可配合数组参数使用,它表明函数不能修改数组元素的值。
如果试图修改const
限定符限定的值,其结果取决于具体的实现。
5. 算术运算符
二元算术运算符包括:+
、-
、*
、/
、%
(取模运算符)。
整数除法会截断结果中的小数部分。
取模运算符%
不能应用于float
或double
类型。
在有负操作数的情况下,整数除法截取的方向以及取模运算结果的符号取决于具体机器的实现,这和处理上溢或下溢的情况是一样的。
二元运算符+
和-
具有相同的优先级,它们的优先级比运算符*
、/
和%
的优先级低,而运算符*
、/
和%
的优先级又比一元运算符+
和-
的优先级低。
算术运算符采用从左到右的结合规律。
6. 关系运算符与逻辑运算符
关系运算符包括下列几个运算符:>
>=
<
<=
。它们具有相同的优先级。优先级仅次于它们的是相等性运算符:==
!=
。
关系运算符的优先级比算术运算符低。
逻辑运算符&&
与||
有一些较为特殊的属性。由&&
与||
连接的表达式按从左到右的顺序进行求值,并且,在知道结果值为真或假后立即停止计算。绝大多数C语言程序运用了这些属性。
运算符&&
的优先级比||
的优先级高,但两者都比关系运算符和相等性运算符的优先级低。
运算符!=
的优先级高于赋值运算符的优先级(可以使用圆括号达到预期的目的)。
根据定义,在关系表达式或逻辑表达式中,如果关系为真,则表达式的结果值为数值1;如果为假,则结果值为数值0.
逻辑非运算符!
的作用是将非0操作数转换为0,将操作数0转换为1。
…读起来更直观一些…可能会难于理解。
7. 类型转换
当一个运算符的几个操作数类型不同时,就需要通过一些规则把它们转换为某种共同的类型。
一般来说,自动转换是指把“比较窄的”操作数转换为“比较宽的”操作数,并且不丢失信息的转换。
不允许使用无意义的表达式,例如,不允许把float
类型的表达式作为下标。
针对可能导致信息丢失的表达式,编译器可能会给出警告信息,比如把较长的整型值赋给较短的整型变量,把浮点型值赋值给整型变量,等等,但这些表达式并不非法。
由于char
类型就是较小的整型,因此在算术表达式中可以自由使用char
类型的变量,这就为实现某些字符转换提供了很大的灵活性。
在ASCII字符集中,大写字母与对应的小写字母作为数字值来说具有固定的间隔,并且每个字母表都是连续的——也就是说,在A~Z之间只有字母。但是,后面一点对EBCDIC字符集是不成立的。
标准头文件<ctype.h>
定义了一组与字符集无关的测试和转换函数。
将字符类型转换为整型时,我们需要注意一点。C语言没有指定char
类型的变量是无符号变量(signed
)还是带符号变量(unsigned
)。当把一个char
类型的值转换为int
类型的值时,其结果有没有可能为负整数?对于不同的机器,其结果也不同,这反映了不同机器结构之间的区别。在某些机器中,如果char
类型值的最左一位为1,则转换为负整数(进行符号扩展)。而在另一些机器中,把char
类型值转换为int
类型时,在char
类型值的左边添加0,这样导致的转换结果值总是正值。
C语言的定义保证了机器的标准打印字符集中的字符不会是负值,因此,在表达式中这些字符总是正值。但是,存储在字符变量中的位模式在某些机器中可能是负的,而在另一些机器上可能是正的。为了保证程序的可移植性,如果要在char
类型的变量中存储非字符数据,最好指定signed
或unsigned
限定符。
当关系表达式以及由&&
、||
连接的逻辑表达式的判断结果为真时,表达式的值为1;当判定结果为假时,表达式的值为0.
某些函数在结果为真时可能返回任意的非0值。在if
、while
、for
等语句的测试部分中,“真”就意味着“非0”,这二者之间没有区别。
C语言中,很多情况下会进行隐式的算是类型转换。一般来说,如果二元运算符的两个操作数具有不同的类型,那么在进行运算之前先要把“较低”的类型提升为“较高”的类型。运算的结果为较高的类型。
表达式中float
类型的操作数不会自动转换为double
类型,这一点与最初的定义有所不同。一般来说,数学函数使用双精度类型的变量。使用float
类型主要是为了在使用较大的数组时节省存储空间,有时也为了节省机器执行时间(双精度算术运算特别费时)。
赋值时也要进行类型转换。赋值运算符右边的值需要转换为左边变量的类型,左边变量的类型即赋值表达式结果的类型。
无论是否进行符号扩展,字符型变量都将被转换为整型变量。
当把较长的整数转换为较短的整数或char
类型时,超出的高位部分将被丢弃。
当把float
类型转换为int
类型时,小数部分将被截取掉;当把double
类型转换为float
类型时,是进行四舍五入还是截取取决于具体的实现。
由于函数调用的参数是表达式,所以在把参数传递给函数时也可能进行类型转换。在没有函数原型的情况下,char
与short
类型都将被转换为int
类型,float
类型将被转换为double
类型。因此,即使调用函数的参数为char
或float
类型,我们也把函数参数声明为int
或double
类型。
在任何表达式中都可以使用一个称为强制类型转换的一元运算符进行显式类型转换。
8. 自增运算符与自减运算符
C语言提供了两个用于变量递增与递减的特殊运算符。自增运算符++
使其操作数递增1,自减运算符--
使其操作数递减1.
++
与--
这两个运算符特殊的地方主要表现在:他们既可以用作前缀运算符(用在变量前面),也可以用作后缀运算符(用在变量后面)。在这两种情况下,其效果都是将变量的值加1。但是,它们之间有点不同。表达式++n
先将n的值递增1,然后再使用变量n的值,而表达式n++
则是先使用变量n的值,然后再将n的值递增1。也就是说,对于使用变量n的值的上下文来说,++n
和n++
的效果是不同的。
自增与自减运算符只能作用于变量。
在不需要使用任何具体值且仅需要递增变量的情况下,前缀方式后后缀方式的效果相同。但在某些情况下需要酌情考虑。
9. 按位运算符
C语言提供了6个位操作运算符。这些运算符只能作用于整型操作数,即只能作用于带符号或无符号的char
、short
、int
与long
类型:
&
按位与(AND)|
按位或(OR)^
按位异或(XOR)<<
左移>>
右移~
按位求反(一元操作符)
按位与运算符&
经常用于屏蔽某些二进制位。
按位或运算符|
常用于将某些二进制位置为1.
按异或运算符^
当两个操作数的对应位不相同时将该位设置为1,否则,将该位设置为0。
我们必须将位运算符&
、|
同逻辑运算符&&
、||
区分开来,后者用于从左至右求表达式的真值。
移位运算符<<
与>>
分别用于将运算的左操作数左移与右移,移动的位数则由右操作数指定(右操作数的值必须是非负值)。
…左移,右边空出的位用0填补。
在对unsigned
类型的无符号值进行右移位时,左边空出的部分将用0填补;当对signed
类型的带符号值进行右移时,某些机器将对左边空出的部分用符号位填补(即“算术移位”),而另一些机器则对左边空出的部分用0填补(即“逻辑移位”)。
一元运算符~
用于求整数的二进制反码,即分别将操作数各二进制位上的1变为0,0变为1。
…x & ~077
与机器字长无关,它比形式为x & 0177700
的表达式要好,因为后者假定x是16位的数值。这种可移植的形式并没有增加额外开销,因为~077
是常量表达式,可以在编译时求值。
10. 赋值运算符与表达式
在赋值表达式中,如果表达式左边的变量重复出现在表达式的右边,则可以将这种表达式缩写。其中的运算符+=
称为赋值运算符。
大多数二元运算符(即有左、右两个操作数的运算符)都有一个对应的赋值运算符*op=,其中,op=*可以是下面这些运算符之一:
+
-
*
/
%
<<
>>
&
^
|
如果expr1和expr2是表达式,那么expr1 op= expr2
等价于expr1 = (expr1) op (expr2)
。它们的区别在于,前一种形式expr1只计算一次。注意,在第二种形式中,expr2两边的圆括号是必不可少的。
除了简洁外,赋值运算符还有一个优点:表示方式与人们的思维习惯比较接近。另外,对于复杂的表达式,赋值运算符使程序代码更易于理解。并且,赋值运算符还有助于编译器产生高效代码。
赋值语句具有值,且可以用在表达式中。其他赋值运算符也可以用在表达式中,尽管这种用法比较少见。在所有的这类表达式中,赋值表达式的类型是它的左操作数的类型,其值是赋值操作完成后的值。
11. 条件表达式
条件表达式(使用三元运算符?:
)提供了另外一种方法…。在表达式expr1 ? expr2 : expr3
中,首先计算expr1,如果其值不等于0(为真),则计算expr2的值,并以该值作为条件表达式的值,否则计算expr3的值,并以该值作为条件表达式的值。expr2与expr3中只能有一个表达式被计算。
应该注意,条件表达式实际上就是一种表达式,它可以用在其他表达式可以使用的任何地方。如果expr2与expr3的类型不同,结果的类型将由本章前面讨论的转换规则决定。
条件表达式中第一个表达式两边的圆括号并不是必须的,这是因为条件运算符?:
的优先级非常低,仅高于赋值运算符。但我们还是建议使用圆括号,因为这可以使表达式的条件部分更易阅读。
采用条件表达式可以编写出很简洁的代码。编写这样的代码可能需要一些技巧,但比用等价的if-else
结构编写的代码要紧凑一些。
12. 运算符优先级与求值次序
C语言没有指定同一运算符中多个操作数的计算顺序。为了保证特定的计算顺序,可以把中间结果保存在临时变量中。
C语言也没有指定函数各参数的求值顺序。
函数调用、嵌套赋值语句、自增与自减运算符都有可能产生“副作用”——在对表达式求值的同时,修改了某些变量的值。在有副作用影响的表达式中,其执行结果同表达式中的变量被修改的顺序之间存在着微妙的依赖关系。对这种情况编译器的解释可能不同,并因此产生不同的结果。C语言标准对大多数这类问题有意未作具体规定。表达式何时会产生这种副作用(对变量赋值),将由编译器决定,因为最佳的求值顺序同机器结构有很大关系。(ANSI C标准明确规定了所有对参数的副作用都必须在函数调用之前生效。)
在任何一种编程语言中,如果代码的执行结果与求值顺序相关,则都是不好的程序设计风格。很自然,有必要了解哪些问题需要避免,但是,如果不知道这些问题在各种机器上是如何解决的,就最好不要尝试运用某种特殊的实现方式。