2016.07.06 – 07.20
个人英文阅读练习笔记(极低水准)。
第二章:类型、运算符以及表达式
07.11
在程序中,变量和常量是被操作的基本的数据对象。声明列出将会被使用的变量并告知这些变量的类型以及初始值(若有)。运算符指定对变量(值)执行什么样的操作。结合变量和常量的表达式将会产生一个新的值。变量的类型决定了该变量值的范围以及可对其执行的操作。以上这些内容将是本章讨论的主题。
ANSI标准已经做了许多较小的改变并增加了一些基本的类型和表达式。现在,对于整型来说,都有signed和unsigned两种格式,并为无符号常量和十六进制字符常量提供了标识。浮点运算可能以单精度形式完成;同时也有用于扩展精度的long double类型。字符串常量在编译阶段就可以被连接。枚举也成为了C语言的一部分,并形成了会长期存在的特征。对象可以被声明为const,这样就可以组织该对象被修改。算术运算间的自动转换规则也被扩展到能处理更丰富的类型了。
1 变量名
虽然在第一章中并未说明,但对于变量和常量名,是有些限制的。名由字母和数字组成;第一个字符必须是字母。下划线’_’ 当作字母;对长变量名来说下划线能够提升变量名的可读性。然而,别以下划线作为命名的开头,因为库中的命令常以下划线开头。大写和小写是被严格区分的,所以x和X是两个不同的命名。按照C的惯例,变量名小写,符号常量大写。
内部命名的前31个字符(至少)是有意义的。对于函数名或外部变量来说,它们的有效长度可能少于31个字符,因为外部变量名可能会被汇编器和加载器加前缀。对于外部命名,标准只保证每个变量名的前6个字符是否唯一。想if,else,int,float等这样的关键字是被保留的:不能使用它们作为变量名。这些关键字都是小写的。
用变量名体现变量的用途是一种聪明的做法,并不太会导致编程时的拼写错误。我们倾向是,为局部变量使用较短的命名,尤其是循环中的变量,为外部变量使用较长的命名。
2 数据类型和大小
在C中只有几种基础的数据类型:
char | 单字节,只能容纳当前字符集中的一个字符 |
int | 整数,通常反映主机自然大小的整数类型 |
float | 单精度浮点型 |
double | 双精度浮点型 |
另外,还有几个可以用来修饰基本类型的东西,short和long用来修饰整型的情形:
short int sh;
long int counter;
在以上这样的声明中,int可以省略,而且在实际中往往省略它。
short和long的意图是它们在实际中能给整型提供不同的长度;int一般是主机的自然大小。short一般是16位,long一般是32位,int可能是16位或32位。每个编译器会为其所在的主机上选择合适的大小,唯一的限制是短整型(short)和整型(int)至少得有16位,常整(long)至少得32位,且short大小不会超过int,int大小不会超过long。
signed和unsigned修饰符可以应用于char和任何整型。unsigned数总是为正或0,并且服从以 2n 为模的算术定律,n是该类型所占的位数。例,字符型变量占8位,无符号字符(unsigned char)变量对应的值的范围就是0 到 255,然而有符号字符(signed char)变量的值在-128到127之间(以补码表示数的机器上)。单用char关键字声明的变量是有符号还是无符号的基于具体的机器实现,但对于打印字符来说都是正的。
long double扩展浮点型的精度。跟整型一样,浮点型对象的大小基于具体的实现;浮点、双精度浮点(double)以及长浮点(long double)可代表一个、两个或者三种不同的大小。
基于具体机器和编译器的属性,标准头文件limits.h和float.h中包含了所有类型大小的符号常量值。这些内容将在附录B中讨论。
练习 2-1。编写一个程序,通过引用标准头文件中相应的符号常量值和直接计算两种方式打印signed和unsigned限制下的char、short、int、long类型变量表示数的范围。采用计算的方式会困难一些:体现在判断浮点类型的范围(浮点型的编码方式)。
3 常量
07.12
像1234这样的整数常量是一个int类型。长整型整数常量要以l或者L结束,如123456789L;若一个常数太大而让int类型容不下时也会被当作long类型处理。无符号常量以u或U结尾,后缀ul或UL表示unsigned long类型常量。
浮点型常量通常包含小数点(123.4)或以指数形式呈现(1e-2),或同时具有小数点和指数部分;若不用后缀说明,它们都会被当作double型。f或F是浮点型常量的后缀;l或L后缀表示常量是一个long double类型。
整数还可以八进制或十六进制的形式出现。以0(零)开头的整型常量表示八进制;以0x或0X开头的整型常量表示十六进制。如,十进制的31可以八进制037或十六进制0x1f(0X1F)的方式出现。八进制或十六进制常量也可以跟后缀L和U,分别表示该常量为long和unsigned类型:0XFUL常量表示十进制值为15的unsigned long类型常量。
在单引号中出现的字符常量(如’x’)是一个整数。字符常量的值是它在该机器中的编码值。如,在ASCII字符编码集中,字符串常量’0’的值为48(’0’跟数值0没关)。我们可以用’0’代替像48这样的常量(不同的字符编码集’0’的值可能不一样),这样有利于程序的可读性。参与运算的字符常量跟其它的整数常量一样,哪怕是两个字符常量在作比较也不例外。
特殊的字符可以转义字符或转义字符序列来表达,如\n表示换行;这些转义序列看起来像两个字符,但它们代表的却是一个字符。另外,一个字节的任意位模式也可以用来指定一个字符,如
'\ooo'
这里的ooo为八进制数(0 - 7)中的某一个,再如
'\xhh'
hh是十六进制数字(0…, a…f, A…F)中的数字。所以我们可以八进制形式来表达特殊的字符(转移字符)
#define VTAB '\013' /* ASCII中的纵向制表符 */
#define BELL '\007' /* ASCII中的响铃字符 */
或者以十六进制形式
#define VTAB 'xb' /* ASCII中的纵向制表符 */
#define BELL 'x7' /* ASCII中的响铃字符 */
完整的转义序列如下
\a | 报警(响铃)字符 |
\b | 退格键 |
\f | 换页 |
\n | 换行 |
\r | 回车 |
\t | 水平制表符 |
\v | 垂直制表符 |
\\ | 反斜线 |
\? | 问号 |
\’ | 单引号 |
\” | 双引号 |
\ooo | 八进制数 |
\xhh | 十六进制数 |
07.13
字符常量’\0’的值为0,即无效(null)字符。常用’\0’来代替0以强调字符的某些属性,但是其值确为0。
常量表达式是仅有常量构成的表达式。这样的表达式可能在编译时就会被求值,且它可被应用到常量可以出现的任何地方,如
#define MAXLINE 100
char line[MAXLINE + 1];
或
#define LEAP 1 /* 闰年 */
int days[ 31 + 28 + LEAP + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31];
由双引号引起来的0或多个字符称为字符串常量或叫字符串字面值,如
"I am a string"
或者
"" /* 空字符串 */
双引号并不是字符串的一部分,它只是用来界定字符串。转义字符序列也可以出现在字符串中;\”代表双引号字符。字符串常量可以在编译时被连接在一起:
"hello," "world"
等价于
"hello, world"
这对于分裂跨越多行的长字符串比较有用。
从技术上讲,字符串常量是字符数组。字符串的末尾有无效字符’\0’,所以字符串的物理存储空间要比双引号中的字符数再多一个字节。这代表着对字符串的长度没有限制,但程序必须完整浏览一个字符串以得到它的长度。标准库函数strlen(s)返回字符串s的长度,该长度并不包含终止字符’\0’。以下是我们编写的函数strlen()的一个版本:
/* strlen:返回字符串s的长度 */
int strlen(char s[])
{
int i;
i = 0;
while (s[i] != '\0')
++i;
return i;
}
strlen以及其它相关的字符串函数声明在标准头文件string.h中。
注意区分字符常量和字符串内只包含一个字符的情况:’x’跟”x”不同。前者是一个整型(整数),代表在机器上x在字符编码中的具体数值。后者是一个字符数组包含一个字符(x)和终止符’\0’。
还有另外一种常量,即枚举常量。枚举是一串整型常量,如
enum boolean {NO, YES};
在枚举中的第一个名称的值为0,下一个为1,如此类推,除非为其指定特定值。如果不是所有的命名都被指定值,未被指定值的命名将在上一个命名的值上增1,枚举的第二个例子如下
enum escapes { BELL = '\a', BACKSPACE = '\b', TAB = '\t',
NEWLINE = '\n', VTAB = '\v', RETURN = '\r'};
enum months { JAN = 1, FEB, MAR, APR, MAY, JUN,
JUL, AUG, SEP, OCT, NOV, DEC };
/* FEB = 2, MAR = 3, 以此类推 */
在不同的枚举中的命令必须不同。在同一个枚举中值不必不同。
07.14
枚举通过将命名和常值对应的方式使得提供常量较为方便,对于#define的一个优势是:它能够自动产生常量值。尽管可以声明enum类型的变量,但编译器不会检查存储在枚举变量中的值是否是该枚举的有效值。然而,枚举变量提供检查的机会(编译器只检查是否为枚举类型,并不检查表达式的内容是否合法),这一点比用#define定义常量要更好。另外,调试器可以以符号常量的格式打印枚举的值。
4 声明
所有变量在使用前都必须先经声明,尽管有的声明可由上下文隐式声明。声明指定类型以及该类型所包含的一个或多个变量,如
int lower, upper, step;
char c, line[1000];
变量的声明可以任何流行的方式出现;以上声明列表可以写为
int lower;
int upper;
int step;
char c;
char line[1000];
后者声明占用了更多的空间,但更便于为每个声明增加注释以及更便于后续修改。
变量也可以在其声明中被初始化。如果命名后跟着一个等号和一个表达式,表达式就充当了初始化内容,如
char esc = '\\';
int i = 0;
int limit = MAXLINE + 1;
float eps = 1.0e-5;
如果变量不是自动变量,那么初始化操作只会进行一次,从概念上讲实在程序开始执行前被初始化ICI,所以该初始化表达式必须要是一个常量表达式。初始化自动变量的表达式会在进入该初始化所在的地方(函数或块)的地方被执行一次,该初始化表达式可以是任何类型的表达式。外部变量和静态变量在默认情况下会被初始化为0。没有被初始化的局部变量的值是没有定义的(乱码)。
修饰符const可以用来修饰任何变量的声明中,用来指定该变量的值不能被修改。对于数组,const修饰符限定每个元素不能被更改。
const double e = 2.71828182845905;
const char msg[] = "warning";
const修饰符也可以用于数组参数,以表示函数不可以修改该数组:
int strlen(const char []);
若对一个const修饰的变量进行修改,其结果是基于具体实现的。
5 算术运算符
二元算术运算符有 +、-、*、 / 以及取模运算符%。整数的除法将会截断小数部分。表达式
x % y
的结果是x除以y的余数,若y除x刚好除尽的话则结果为0。例,可以被4整除但不会被100整除或者能被400整除的为闰年。因此可用以下代码来表示一年是否为闰年
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
printf("%d is a leap year\n", year);
else
printf("%d is not a leap year\n", year);
%运算符不能用于float或double类型。/ 运算符的截断(截取)方向以及%运算符得到的结果的符号(对于负数)基于具体的机器实现,具体得看是采取上溢还是下溢方式。
二元运算符+和-具有相同的优先级,它们的优先级比 *、/、以及%都要低,当然后者的优先级也要比一元运算符+和-的优先级低。算术运算符的结合性从左至右。
在本章结束时用表2-1总结了所有运算符的优先级以及结合性。
6 关系和逻辑运算符
关系运算符有
> >= < <=
它们具有相同的优先级。优先级刚好比它们低的是以下两个等同运算符:
== !=
关系运算符的优先级比算术运算符低,所以类似 i < lim – 1的表达式其实是i < (lim – 1)的含义。
更有趣的是逻辑运算符&&和||。由&&或||连接的表达式从左到右依次求值,当已经得到能够判断真或假的值是就停止继续求值。大多数C程序都利用了该属性。例如,在第一章中编写getline函数时所用的循环格式:
for (i = 0; i < lim – 1 && (c = getchar()) != '\n' && c != EOF; ++i)
s[i] = c;
在读入一个新字符之前有必要检查数组s是否还有空间来存储它,所以条件 i < lim – 1必须最先测试。同时,如果该测试为假,那么就没必要继续读任何字符了。同理,如果在调用getchar之前测试出c为EOF也是不幸的;因此调用函数和赋值必须发生在测试c之前。
逻辑运算符&&的优先级比||高,它们都比关系运算符和等同运算符低,所以像
i < lim – 1 && (c = getchar()) != '\n' && c != EOF
就不需要额外的括号。但因为 != 的优先级比赋值运算符要高,以下表达式需要括号
(c = getchar()) != '\n'
以先让c获得赋值再和’\n’作比较。
按照定义,关系或逻辑表达式的是1或0(表达式为真时值为1;反之为0)。
一元取反运算符 ! 将非0转换为0,将0转换为1。常在以下结构中用!
if (!valid)
来代替
if (valid == 0)
很难说以上两种结构哪一种更好。像 !valid 这样的结构有一个清晰的语义(如果没有效),但若用于更加复杂的情况则会让人难以理解。
练习 2-2。编写一个刚用作例子的for循环,不使用&&和||运算符。
7 类型转换
当一个运算符包含不同类型的操作数时,它们会按照为数不多的几个规则转换为一种通用的类型。通常来讲,自动转换会将一个“窄”类型操作数转换为一个“宽”类型的操作数以不至于丢失任何信息,如在表达式f + i中,整型会被转换成浮点型。在表达式中,对操作数的数据类型没那么严格,像用浮点数作为数组的下标,则是不被允许的。像将一个更长整数类型赋值给一个更短整数类型变量或将一个浮点型赋值给一个整型变量,这样的表达式可能会丢失更长数据中的信息,这样的赋值可能会带来警告,但并不是非法的。
char仅是一个小整数,所以char类型可随便用于算术表达式中。这使得在特定的几种字符转换中有了相当大的灵活性。以下代码是简单实现atoi函数的一个简单例子,该函数将数字组成的字符串转换为相应的数字。
/* atoi:将s转换为整型 */
int atoi(char s[])
{
int i, n;
n = 0;
for (i = 0; s[i] >= '0' && s[i] <= '9'; ++i)
n = 10 * n + (s[i] – '0')
return n;
}
正如我们在第一章中所讨论的那样,表达式
s[i] – '0'
得到存在s[i]中的数字字符所对应的数字值,因为’0’、’1’等都是以递增序列被编码的。
另一个char转换为int的例子是函数lower,该函数将字母转换成其对应的小写字母(针对ASCII集合)。如果字母并非大写字母,lower函数将返回字母本身。
/* lower:将c转换为小写字母;只限于ASCII */
int lower(int c)
{
if (c >= 'A' && c <= 'Z')
return c + 'a' - 'A';
else
return c;
}
该函数适用于ASCII码集合,因为其中的大写字母和小写字母之间的差值是固定的且相邻字母之间也是连续的 —— 在A和Z之间除了字符之外无其它。然而,后者这个特点对于EBCDIC字符集合来说就不符合,所以,对于EBCDIC来说,该代码转换的就不仅是字母了。
附录B中描述的标准头ctype.h,提供了许多不依赖字符编码的测试和转换的函数。例如,函数tolower(c)将返回c相应的小写形式,所以,相对于以上的lower函数来说,tolower是一个可移植的函数。相应地,测试
c >= '0' && c <= '9'
可以被isdigit(c)语句代替。我们将从现在开始使用ctype.h中的函数了。
字符转换为整型有一个微妙的地方。C并未指明char是有符号还是无符号的。当将char转换为int时,是否会产生一个负整数?答案会随着机器的不同而不同,对于不同结构的机器有不同的结果。一些机器会将最高位为1(符号扩展)的char类型转换为一个负整数。在另外一些机器上,当将char转换为int时,会在int空出的位上补0,这样转换得来的数就永远都是正数。
07.15
C定义保证了在任何机器上打印字符时都不会为负,所以这些字符在表达式中总是为负。但若已任意的位存储于字符变量之中,在某些机器上就可能会让该变量被当成负数解释,在另一些机器上也有可能被解释为正。为了可移植性,若在char变量中不是存储字符时指定该变量是为signed还是unsigned类型。
像i > j这样的关系表达式以及由&&和||连接的逻辑表达式的值要么为1(表达式为真时)要么为0(表达式为假时)。所以表达式
d = c >= '0' && c <= '9'
在c为数字字符时d的值为1,否则为0。然而,像isdigit这样的函数可能会返回非0值以作为表示真。在if、while、for等的条件测试部分,“真”的含义就是“非0”,所以这没什么分别。
隐式的算术转换如预料的那么多。通常来讲,只要像 + 或 * 这样的二元运算符含两个不同的操作数时,在执行计算前“更低”类型会被提升为“更高”类型。最终结果的类型是更高类型。附录A的第六节将精确地描述转换规则。然而,若表达式中无unsigned类型操作数,下面的转换规则就够用了:
如果其中有一个操作数为long double,那么另外一个参数将会转换为long double
否则,若其中一个操作数为double,另外一个操作数会被转换为double
否则,如果其中一个操作数为float,另外一个操作数会被转换为float
否则,就将char类型转换为int类型
然后,如果其中一个操作数为long类型,就将另一个操作数转换为long类型
注意表达式中的float类型不会被自动转换为double类型;这跟原来的定义有所不同。一般来讲,在math.h中的函数都会使用双精度。主要的原因是因为使用float类型可以节约存储(对于大数组来说),或者是为了节约运行时间(更少用),因为双精度运算会消耗更多的时间。
若包含unsigned类型操作数,则转换规则就会复杂得多。问题在于signed和unsigned值的比较是基于具体机器的,因为它们基于各种整数所占内存大小。例如,假设int为16位而long为32位。那么-1L < 1U,因为unsigned int将提升为signed long类型。但是-1L > 1UL,因为-1L将提升为unsigned long类型且它在内存中的编码会被解释为一个无符号的正数。
转换在赋值时也会发生;等号右边的值会转换为左边变量的类型,赋值结果的类型同等号左边变量的类型。
字符会转换为整型,不管是否发生符号扩展。
更长的整型转换为更短的整型时会丢到更长整型的高位,所以在以下代码中
int i;
char c;
i = c;
c = i;
c的值不会改变。不管在转换过程中是否发生了符号扩展。但如果赋值顺序颠倒,就会丢失信息了。
如果x为float类型且i为int类型,x = i;和i = x;都会发生类型转换;float转换为int时会引起小数部分的截断。当double类型转换为float类型时,值是向上舍入还是截断基于具体的实现。
因为函数调用的参数是表达式,当给函数传递参数时也会有类型转换。在没有函数原型的情况下,char和short都会转换为int,float会转换为double。这就是为什么将函数参数声明为int和double类型,尽管在调用时传递给该函数的参数为char和float类型。
最后,精确的类型转换可以强制的形式发生在任何表达式中,用一个名为cast的强制运算符。在结构
(type-name) expression
中,会将expression的值转换为type-name类型。cast精确的含义是先将expression的值转换为cast类型的值并赋值给一个cast类型的变量(内存空间),然后再使用该变量的值代替整个结构((type-name) expression)的值。例如,库函数sqrt期望一个double类型的参数,如果让它处理其他的类型它将产生无意义的结果(sqrt声明在math.h中)。所以当要传递一个整数给sqrt时,可以使用
sqrt((double)n)
在n传递给sqrt之前来将n的值转换为double类型。注意cast只是将n的值转换为期待的类型;n本身的值并没有发生改变。cast运算符跟其它一元运算符具有相同的高优先级,在本章的表中对运算符的优先级和结合性做了总结。
如果参数在函数原型中被声明,它们本也该被声明,当函数调用时会将传递来的参数自动转换为参数的类型。因此,对于sqrt的函数原型:
double sqrt(double);
调用
root2 = sqrt(2);
将会将整数2强制转换为双精度型2.0,且这个过程不需要任何的强制类型转换。
标准库包含了可移植的伪随机数产生函数和一些用来初始化产生随机数种子的函数;前者可以作为cast个一个良好演示:
unsigned long int next = 1;
/* rand:从0..32767之间返回一个伪随机数 */
int rand(void)
{
next = next * 1103515245 + 12345;
return (unsigned int)(next / 65535) % 32768;
}
/* srand:为rand函数设置种子 */
void srand(unsigned int seed)
{
next = seed;
}
练习 2-3。编写函数htoi(s),它将转换十六进制数的字符串(包括0x或0X)转换为相应的整数值。允许数字为0到9,a到f或者A到F。
8 自增和自减运算符
C提供了两个独特的运算符用作变量的自增和自减。自增运算符 ++ 让其操作数增1,自减运算符 – 让其操作数减1。我们常用到 ++ 来自增变量的值,如
if (c == '\n')
++nl;
++ 和 – 独特的一个方面是,它们既可以作为变量的前缀(如++n)也可以作为变量的后缀使用(如n++)。不管哪一种情况,n都会自增1。但表达式++n在n的值被使用之前将n增1,n++则是在n的值被使用之后再将n增1。这意味着n的值什么时候被使用时++n和n++有所不同,n的最终值都会被增1。如果n的值为5,然后
x = n++;
会使得x的值为5,而
x = ++n;
会使x的值为6。不管哪一种情况,n的最终值都是6。自增和自减运算符仅可应用于变量;像(i + j)++这样的表达式是非法的。
若上下文不需要结果值,仅是想增加变量的结果,如
if (c == '\n')
nl++;
使用前缀还是后缀都是一样的。但有一些情况是需被指定的。如,见函数squeeze(s, c),它将s中所有出现的字符c都移除。
/* squeeze:从s中删除所有的c */
void squeeze(char s[], int c)
{
int i, j;
for (i = j = 0; s[i] != '\0'; i++)
if (s[i] != c)
s[j++] = s[i];
s[j] = '\0';
}
只要不是c字符,它就会被拷贝到j所指向的当前位置且也只有在此时j增1指向下一个位置。这等价于
if (s[i] != c) {
s[j] = s[i];
j++;
}
另外一个具有类似的结构的是在第一章所编写的函数getline,我们可以将以下代码
if (c == '\n') {
s[i] = c;
++i;
}
换成更为紧凑的格式
if (c == '\n')
s[i++] = c;
作为第三个例子,见标准库函数strcat(s, t),该函数将t拷贝到字符串s的末尾。strcat假设s有足够的空间来容纳这样的连接。当我们编写该函数时,它不返回任何值;标准库函数将返回指向结果字符串的指针。
/* strcat:将t连接到s末尾;s必须足够大 */
void strcat(char s[], char t[])
{
int i, j;
i = j = 0;
while (s[i] != '0') /* 找到s的结尾处 */
i++;
while ((s[i++] = t[j++] ) != '\0') / *拷贝t */
;
}
随着t中的每个字符被拷贝到s中,后缀 ++ 被同时应用到i和j以保证它们指向下一个位置。
练习 2-4。编写另一个版本的squeeze(s1, s2)函数,将s1中所包含的出现在s2字符串中的所有字符删除。
练习 2-5。编写函数any(s1, s2),它返回某个字符第一次出现的位置,该字符包含在s2字符串中;若s1中不包含s2中的任何字符则返回-1。(标准库函数strpbrk就是该功能)
9 按位运算符
07.16
C提供了六种位操作的运算符;这些位运算符只能应用于整型操作数,即char、short、int以及long类型,不管是signed还是unsigned类型。
& | 按位与 |
| | 按位或 |
^ | 按位异或 |
<< | 左移 |
>> | 右移 |
~ | 二进制的反码(一元) |
按位与运算符 & 常用来屏蔽掉一些位;如下例
n = n & 01777;
将设置n的除低7位之外的位都为0。
按位或运算符 | 常用来打开某些位:
x = x | SET_ON;
将SET_ON中为1的位相应地设置到x中。
按位异或运算符 ^ 在参与运算的位相同时结果为0,参与运算的两个位不同时结果为1。
必须要注意区分按位运算符 & 和 | 与逻辑运算符 && 和 ||,逻辑运算符是从左至右求值。假设x 为1,y为2,那么x & y的结果为0而x && y的结果为1。
左移运算符 << 和右移运算符 >> 会根据它们右边的数字而对其左边的操作数进行左移和右移,右边的操作数不可以为负数。因此 x << 2将x左移两位,将(右边)空出来的位补0;这等效于将x乘以4。右移unsigned数时(左边)空出来的位也是补0。右移一个signed数时,在有些机器上会在(左边)空出来的位上补符号位(“算术右移”),而在有的机器上补0(“逻辑右移”)。
一元运算符 ~ 将一个整数按位取反;即将为1的位翻转为0的位,反之亦然(vice versa)。例如
x = x & ~077
会将x的最后0位设置为0。注意 x & ~077是独立于字长的,因此这种表达方式是一种很好的表达方式,例如,x & 0177700,会假设x是16位的。以上可移植的格式不会有额外的消耗,因为~077是一个常量表达式,即可以在编译阶段被求值。
作为某些位运算符的演示,见函数getbits(x, p, n),该函数将返回x的(往右数)从位置p开始的n位。我们假设位置0为最右端且n和p都是正数。如getbits(x, 4, 3)返回位置为4,3,2的三个位,往右数。
/* getbits:从位置p开始获取n个位 */
unsigned getbits(unsigned x, int p, int n)
{
return (x >> (p + 1 – n) ) & ~(~0 << n);
}
表达式x >> (p + 1 – n)将x相应的位移到最右端。~0将会得到所有位都为1;将其左移n个位就会在右端空出n个位0的位;再对其按位取反时,右边n个为0的位就为1了。
练习 2-6。编写函数setbits(x, p, n, y),将x中开始于位置的n位设置给y的最右n位并返回该n位,其余位保持不变。
练习 2-7。编写函数invert(x, p, n),返回x中的开始于p的n位的翻转值(如为1的位翻转为0,为0的位翻转为1),保留其余位不变。
练习 2-8。编写函数rightrot(x, n),将x循环右移n位(从最右端移出的位移入最左端),再返回x的值。
10 赋值运算符和表达式
07.19
像
i = i + 2;
中左边变量也参与到右边变量的表达式可以表达成以下格式
i += 2;
运算符 += 叫作赋值运算符。
大多数二元运算符(都有左和右两个操作数)都有一个相对应的赋值运算符 op = ,这里的op可以是以下运算符
+ - * / % << >> & ^ |
如果 expr1 和 expr2 都是表达式,那么
expr1 op = expr2
就等价于
expr1 = (expr1) op (expr2)
除开 expr1 只会被计算一次。注意在 expr2 上的括号:
x *= y + 1
的含义为
x = x * (y + 1)
而不是
x = x * y + 1
例如,函数bitcount统计整数参数中为1的位数。
/* bitcount:数x中为1的位 */
int bitcount(unsigned x)
{
int b;
for (b = 0; x !=0; x >>= 1)
if (x & 01)
b++;
return b;
}
将参数x声明为unsigned是为了确保当其做右移操作时,左边空出的位被补0,而不是符号位,这样就可以忽略本程序所运行的机器。
除了简洁,赋值运算符更接近人们思考的方式。我们说“加2到i”或者“将i增2”而不是“将i,加2,然后再将结果给i。”因此表达式i += 2比i = i + 2更好。另外,对于像
yyval[yypv[p3+p4] +yypv[p1+p2]] += 2
这样较为复杂的表达式,赋值表达式能够让代码更易被理解,因为阅读者不会刻意检查两个长表达式是否相同。且,赋值运算符还有助于编译器产生高效的代码。
我们已经知道赋值语句有值且能够出现在表达式中;最常见的形式为
while ((c = getchar()) != EOF)
...
其它的赋值运算符(+=,-=等)也能出现在表达式中,尽管它们出现的频率较低。
在所有这样的表达式中,赋值表达式的类型跟其左边的类型一样,最终的值是赋值过后的值。
练习 2-9。在补码机上,x &= (x – 1)删除x最右边的位。解释为什么。根据这个结论编写一个更快的bitcount。
11 条件表达式
语句
if (a > b)
z = a;
else
z = b;
将a和b中更大者赋值给z。条件表达式,用三元运算符“?:”表示,可以提供另外一种方法编写这种类似的结构。在表达式
expr1 ? expr2 : expr3
中,expr1首先会被求值。如果它的值非0(为真),表达式expr2就会被求值,该值就会被作为条件表达式的最终值。否则,expr3会被求值并作为条件表达式的最终值。expr2和expr3只有一个会被求值。所以将a和b更大者赋值给z可以这样写:
z = (a > b) ? a : b; /* z = max(a, b) */
应该注意条件表达式的确是一个表达式,它能够被用于其它表达式可使用的地方。如果expr2和expr3是不同类型的,结果的类型由本章之前讨论的转换规则决定。例如,f为float且n是一个int,那么表达式
(n > 0) ? f : n
将会得到float类型的值,不管n的值是否为正。
在条件表达式中,第一个表达式中的括号不是必要的,因为 ?: 的优先级非常低,仅比赋值的优先级高。然而,该括号是被建议使用的,因为这样能够使得条件部分更突显。
条件表达式常能够用来编写较简洁的代码。例如,下例中的循环将会打印数组的n个元素,每行10个,每列之间由一个空白符隔开,每一行(包括最后一行)由换行符结束。
for (i = 0; i < n; i++)
printf("%6d%c", a[i], (i % 10 == 9 || i == n – 1) ? '\n' : ' ');
每第10个以及第n个元素后都会打印一个换行符。所有的其它的元素后将会打印一个空白符。这看起来有些微妙,但这比if-else结构更加的紧凑。另外一个比较好的例子是
printf("You have %d item%s.\n", n, n == 1 ? "" : "s");
练习 2-10。用条件表达式代替if-else结构,重新编写lower函数,该函数将大写字母转换为小写字母。
12 优先级和结合性
07.20
表2-1. 运算符的优先级和结合性
运算符 | 结合性 |
() [] -> . | 从左到右 |
! ~ ++ – + - * & (type) sizeof | 从右到左 |
* / % | 从左到右 |
+ - | 结合性 |
<< >> | 从左到右 |
< <= > >= | 从左到右 |
== != | 从左到右 |
& | 从左到右 |
^ | 从左到右 |
| | 从左到右 |
&& | 从左到右 |
|| | 从左到右 |
?: | 从右到左 |
= += -= /= %= &= ^= |= <<= >>= | 从右到左 |
, | 从左到右 |
表2-1总结了所有运算符优先级和结合性的所有规则,包括我们还未讨论的运算符的。在同一行中的运算符拥有相同的优先级;行与行之间按照优先级递减的方式列举,所以,如、/以及%具有相同的优先级,它们的优先级比+和-高。“运算符”()涉及函数调用。运算符 -> 和 . 用来访问结构体成员;它们将在第六章连同sizeof(对象的大小)一起被讲解。第五章将会讨论 (通过指针间接访问)和 & (对象的地址),第三章将讨论逗号运算符。
注意位运算符& 、 ^ 以及 | 的优先级低于 == 和 != 。这意味着像以下的位测试表达式
if ((x & MASK) == 0)…
必须要添加括号才能得到正确的结果。
C跟大多数语言一样,并不指定运算符的操作数的求值顺序(除&&、||、?:以及’,’)。如想以下表达式
x = f() + g();
f可能比g先被求值,g也有可能比f先求值;因此,如果f或g会修改另一个函数基于的变量的值,那么x的值可能会由于求值顺序的不同而不同。可以将中间结构存储值来判断是怎样的一种求值顺序。
同理,函数参数的求值顺序也是不定的,所以语句
printf("%d %d\n", ++n, power(2, n)); /* 错误的表示 */
对于不同的编译器会产生不同的结果,基于n是否会在power被调用之前自增。解决该问题的方法为
++n;
printf("%d %d\n", n, power(2, n));
像嵌套在赋值语句中的函数调用、自增和自减运算符这些能够引起“副作用”的操作 —— 一些变量的值因为表达式的求值而被附带改变。在任何具有副作用的表达式中,对变量在表达式中参与更新的顺序有一种微妙的依赖。以下表达式就是一种不乐观的情形
a[i] = i++;
问题在于下标i的值取旧值还是新值。编译器能够以不同的方式翻译,基于不同翻译会产生不同的结果。标准故意不指定大多数这样的问题(具体该怎么实现)。当副作用(赋值给变量)发生在表达式中,裁决权是编译器的,因为最佳顺序是及依赖于具体机器结构的。(标准指定所有在参数上的副作用发生在函数调用之前,但这并不包括printf函数。)
不管在任何编程语言中,编写会基于求值顺序的代码都是一种坏的编程(习惯)。不用说,有必要知道要避免的事情,但如果你并不知道它们在不同的机器上如何被操作,你就不应该冒险去利用这一种特定的实现。
[2016.08.01 - 09:53]