第二章 常量、变量和表达式
1. 继续Hello World
首先注释可以跨行,也可以穿插在程序之中;
2.1.带更多注释的Hello World
第一个注释跨了四行,头尾两行是注释的界定符(Delimiter)/*和*/,中间两行开头的*号(Asterisk)并没有特殊含义,只是为了看起来整齐。
使用注释需要注意两点:
- 注释不能嵌套(Nest)使用,就是说一个注释的文字中不能再出现/*和*/了,例如/* text1 /* text2 */ text3 */是错误的,编译器只把/* text1 /* text2 */看成注释,后面的text3 */无法解析,因而会报错。
- 有的C代码中有类似// comment的注释,两个/斜线(Slash)表示从这里直到该行末尾的所有字符都属于注释,这种注释不能跨行,也不能穿插在一行代码中间。这是从C++借鉴的语法,再C99中被标准化。
像“Hello world. ”这种由双引号(Double Quote)引起来的一串字符称为字符串字面值(String Literal),或者简称字符串。
C标准规定的转义字符
如果再字符串字面值中要表示单引号和问号,既可以使用转义序列’和?,也可以直接用字符‘和?,而要表示或“则必须使用转义序列,因为字符表示转义而不表示它的字面含义,“表示字符串的界定符而不表示它的字面含义。可见转义序列有两个作用:一是把普通字符转义成特殊字符,例如把字母n转义成换行符;二是把特殊字符转义成普通字符,例如和”是特殊字符,转移后取它的字面值。Windows上的文本文件用 做行分隔符,许多应用层网络协议(如HTTP)也用 做行分割符,而Linux和各种UNIX上的文本文件只用 做行分隔符。
2. 常量
常量(Constant)是程序中最基本的元素,有字符(Character)常量、整数(Integer)常量、浮点数(Floating Point)常量和枚举常量。
下面看一个例子:
字符常量要用单引号括起来,例如上面的‘}‘,注意单引号只能括一个字符而不能像双引号那样括一串字符,字符常量也可以是一饿转义序列,例如’ ’,这时虽然单引号括了两个字符,但实际上只表示一个字符。和字符串字面值中使用转义序列有一点区别,如果再字符常量中要表示双引号“和问号?,既可以使用转义序列”和?,也可以直接用字符”和?,而要表示‘和则必须使用转义序列。
输出结果为:
printf中的第一个字符串称为格式化字符串(Format String),它规定了后面几个常量以何种格式插入到这个字符串中,在格式化字符串中%号(Percent Sign)后面加上字母c、d、f分别表示字符型、整形和浮点型的转换说明(Conversion Specification),转换说明只在格式化字符串中占个位置,并不出现在最终的打印结果中,这种用法通常叫做占位符(Placeholder)。这也是一种字面意思与真实意思不同的情况,但是转换说明和转义序列又有区别:转义序列是编译时处理的,而转换说明时在运行时调用printf函数处理的。源文件中的字符串字面值是”character: %c integer: %dfloating point: %f ”, 占两个字符,而编译之后保存在可执行文件中的字符串是character: %c换行integer: %d换行floating point: %f换行, 已经被替换成一个换行符,而%c不变,然而在运行是这个字符串被传给printf,printf再把其中的%c,%d,%f解释成转换说明。
3. 变量
变量(Variable)是编程语言中最重要的概念之一,变量是计算机存储器中的一块命名的空间,可以再里面存储的一个值(Value),存储的值是可以随时变的,比如这次存个字符‘a’下次存个字符‘b’,正因为变量的值可以随时变所以才叫变量。
应该给变量起有意义的名字,两个同样类型的变量可以定义在同一行,如int hour,minute;给变量起名有一定的限制,C语言规定必须以字母或下划线_(Underscore)开头,后面可以跟若干个字母、数字、下划线,但不能有其他字符。例如这些是合法的变量名:Abc、_abc_、_123。但这些是不合法的变量名:3abc、ab$。其实这个规则不仅适用于变量名,也适应于所有可以由程序员起名的语法元素,例如以后要讲的函数名、宏定义、结构体、成员名等,在C语言中这些统称为标识符(Identifier)。
还有一点要注意,一般来说应避免使用以下划线开头的标识符,以下划线开头的标识符只要不和C语言关键字冲突都是合法的,但是往往被编译器用作一些功能扩展,C标准库也定义了很多以下划线开头的标识符,所以除非你对编译器和C标准库特别清楚,一般应避免使用这种标识符,以免造成命名冲突。
4. 赋值
定义了变量之后,我们要把值存到它们所表示的存储空间里,可以用赋值(Assignment)语句实现。
注意变量一定要先声明后使用,编译器必须先看到变量声明,才知道firstletter、hour和minute是变量名,各自代表一块存储空间。另外,变量声明中的类型表明这个变量代表多大的一块存储空间,这样编译器才知道如何读写这块存储空间。
定义一个变量,就是分配一块存储空间并给它命名;给一个变量赋值,就是把一个值保存到这块存储空间中。变量的定义和赋值也可以一步完成,这称为变量的初始化(Initialization),例如要达到上面代码的效果也可以这样写:
在初始化语句中,等号右边的值叫做Initializer,例如上面的‘a’、11和59。注意,初始化是一种特殊的变量定义语句,而不是一种赋值语句。如果在纸上“跑“一个程序,可以用一个框表示变量的存储空间,在框的外边标上变量名,在框里记上它的值,如下图所示。
你可以用不同形状的框表示不同类型的变量,这样可以提醒你给变量赋的值必须符合它的类型。如果所赋的值和变量的类型不符会导致编译器报警或报错(这是一种语义错误),例如:
注意第三个语句,把“59“赋给minute看起来像是对的,但是类型不对,字符串不能赋给整形变量。既然可以为变量的存储空间赋值,就应该可以把值取出来用,现在我们取出这些变量的值用printf打印:
也就是说,变量名除了用在等号左边表示赋值之外,用在别的地方都表示把它的存储空间中的值取出来替换在那里。不同类型的变量所占的存储空间大小是不同的,存储表示方式也不同,最小存储单位是字节(Byte)。
5. 表达式
常量和变量都可以参与加减乘除运算,例如1+1、hour-1、hour*60+minute、minute/60等。这里的+-*/称为运算符(Operator),而参与运算的常量和变量称为操作数(Operand),上面四个由运算符和操作数组成的算是称为表达式(Expression)。
我们定义:在任意表达式后面加个;号也是一种语句,称为表达式语句。例如:
这是个合法的语句,但这个语句在程序中起不到任何作用,把hour的值和minute的值取出来加乘,得到的计算结果却没有保存,白算了一通。再比如:
这个语句就很有意义,把计算结果保存在另一个变量total_minute里。事实上等号也是一种运算符,赋值语句就是一种表达式语句,等号的优先级比+和*都低,所以先算出等号右边的结果然后才做赋值操作,整个表达式total_minute = hour*60 + minute加个;号构成一个语句。任何表达式都有值和类型两个基本属性。hour*60 + minute的值是由三个int型的操作数计算出来的,所以这个表达式的类型也是int型,它的值是多少呢?C语言规定等号运算符的计算结果就是等号左边被赋予的那个值,所以这个表达式的值和hour*60 + minute的值相同,也和total_minute的值相同。等号运算符还有一个和+-*/不同的特性,如果一个表达式中出现多个等号,不是从左到右计算而是从右到左计算,例如:
计算的顺序是先算hour*60 + minute得到一个结果,然后算右边的等号,就是把hour*60 + minute的结果赋给变量total_minute,这个结果同时也是整个表达式total_minute = hour*60 + minute的值,再算左边的等号,即把这个值再赋给变量total。同样优先级的运算符也是从左到右计算还是从右到左计算称为运算符的结合性(Associativity)。+-*/是左结合的,等号是右结合的。
比如在一条语句中完成计算、赋值和打印功能:
理解组合(Composition)规则是理解语法规则的关键所在,正因为可以根据语法规则任意组合,我们才可以用简单的常量、变量、表达式、语句搭建出任意复杂的程序。
根据语法规则组合出来的表达式在语义上并不总是正确的。例如:
等号左边的表达式要求表示一个存储位置而不是一个值,这是等号运算符和+-*/运算符的又一个显著不同。有的表达式既可以表示一个存储位置也可以表示一个值,而有的表达式只能表示值,不能表示存储位置,minute + 1这个表达式就不能表示存储位置,放在等号左边是语义错误。表达式所表示的位置称为左值(lvalue)(允许放在等号左边),而以前我们所说的表达式的值称为右值(rvalue)(只能放在等号右边)。上面的话换一种说法就是:有的表达式既有左值也有右值,而有的表达式只有右值。
如果三个变量int a, b, c;,表达式a = b = c是合法的,先求b = c的值,再把这个值赋给a,而表达式(a = b) = c是不合法的,先求(a = b)的值没问题,但(a = b)这个表达式不能在做左值了,因此放在 = c的等号左边是错的。
关于整数除法运算有一点特殊之处:
执行结果是11 and 0 hours,也就是说59/60得0,这是因为两个int型操作数相除的表达式仍为int型,只能保存计算结果的整数部分,即使小数部分是0.98也要舍去。
要得到更精确的结果可以这样:
在第二个printf中,表达式是minute / 60.0,60.0是double型的,/运算符要求左右两边的操作数类型一致,而现在并不一致。C语言规定了一套隐式类型转换规则,在这里编译器自动把左边的minute也转成double型来计算,整个表达式的值也是double型的,再格式化字符串中应该用%f转换说明与之对应。
6. 字符类型与字符编码
字符常量或字符型变量也可以当作整数参与运算,例如:
执行的结果是b.
符号在计算机内部也用数字表示,每个字符在计算机内部用一个整数表示,称为字符编码(Character Encoding),目前最常用的是ASCII码。之前我们说“整型”是指int型,而现在我们知道char型本质上就是整数,只不过范围比int型小,所以以后我们把char型和int型统称为整数类型(Inteager Type)或简称整型。
字符’a’~’z’、’A’~’Z’、‘0‘~’9’的ASCII码都是连续的,因此表达式’a’+25和‘z’的值相等,’0‘+9的值也相等。注意’0‘~’9‘的ASCII码是十六进制的30~39,和整数值0~9是不相等的。
字符也可以用ASCII码转义序列表示,这种转义序列由加上1~3个八进制数字组成,或者由x或大写x加上1~2个十六进制数字组成,可以用在字符常量或字符串字面值中。例如‘ ’表示NUL字符,‘11’或’x9’表示Tab字符,“11”或”x9”表示由Tab字符组成的字符串。