Post

《C程序设计语言》笔记 第2章 类型、运算符与表达式

变量和常量是程序处理的两种基本数据对象。声明语句说明变量的名字及类型,也可以指定变量的初值。运算符指定要对变量进行的操作。表达式把变量与常量组合起来生成新的值。本章将详细讲述这些内容。

2.1 变量名

名字可以由字母、数字或下划线( “_” )组成,必须以字母或下划线开头,区分大小写。ifforint等关键字不能用作变量名。

习惯上,变量名使用小写字母,符号常量名全部使用大写字母。

变量名要尽量能够从字面上表达变量的用途。局部变量一般使用较短的变量名(循环控制变量习惯上使用ij等),外部变量使用较长的名字。

2.2 数据类型及长度

C语言只提供了以下几种基本数据类型:

  • char:字符
  • int:整型
  • float:单精度浮点型
  • double:双精度浮点型

此外,还可以在这些基本数据类型前面加上一些限定符。shortlong两个限定符用于限定整型,表示不同长度的整型数:

1
2
short int a;
long int b;

在上述声明中,关键字int可以省略。

限定符signedunsigned用于限定char类型或任何整型。signed表示“有符号”,即可以表示正数、0和负数,可省略;unsigned表示“无符号”,即只能表示非负数。

与类型长度有关的符号常量定义在标准头文件<limits.h>和<float.h>中。

注:计算机使用二进制表示整数,n位无符号二进制数的范围是0~11…1(n个1),即0~2n-1。例如,2位二进制数能表示0~3四个数:

1
2
3
4
5
二进制	十进制
00		0
01		1
10		2
11		3

为了表示负数,计算机使用最高位作为符号位,因此n位有符号二进制数的范围是-2n-1~2n-1-1。

例如,char类型的长度是1字节(8位),因此范围为-27~27-1,即-128~127;unsigned char类型的长度也是1字节(8位),范围是0~28-1,即0~255。

C语言基本类型的长度及范围如下:

类型含义长度范围
char字符1字节-128~127
unsigned char字符1字节0~255
short短整型2字节-32768~32767
unsigned short短整型2字节0~65535
int整型4字节-2147483648~2147483647
unsigned int整型4字节0~4294967295
long长整型4字节-2147483648~2147483647
unsigned long长整型4字节0~4294967295
float单精度浮点型4字节-3.40282×1038~3.40282×1038
double双精度浮点型8字节-1.79769×10308~1.79769×10308

练习2-1 编写一个程序以确定分别由signedunsigned限定的charshortintlong类型变量的取值范围。采用打印标准头文件中的相应值以及直接计算两种方式实现。后一种方法的实现较困难一些,因为要确定各种浮点类型的取值范围。

注:使用直接计算的方法需要了解位运算(在2.9节介绍)。

2.3 常量

整型常量

类型格式后缀示例
int十进制数字,可带正负号,不能以0开头(0除外)1234, -5678
long同上lL123456789L
unsigned十进制数字,不能以0开头(0除外)uU1234U
unsigned long同上ulUL123456789UL

八进制和十六进制整型常量

进制格式前缀示例
八进制八进制数字(0~7),可带正负号0037
十六进制十六进制数字(0~9, A~F或a~f),可带正负号0x0X0x1f

八进制与十六进制常量也可以使用后缀LUUL。例如0xFUL是一个unsigned long类型的十六进制整型常量,其值等于十进制数15。

浮点数常量

类型格式后缀示例
double由整数部分、小数点、小数部分、一个eE和一个可选的带符号整数的指数部分组成3.14159, 6.02e23, 5e-3, -1e5
float同上fF3.14159F
long double同上lL1e100L

注:由于浮点数精度问题,不能用==判断两个浮点数是否相等,应该用fabs(x - y) < 1e-6。但是对于非常大的浮点数,精度1e-6就不够了,例如:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <math.h>

int main() {
    float x, y, z;
    scanf("%f %f %f", &x, &y, &z);
    printf("%g %g\n", x, fabs(x - 3.1415926));
    printf("%g %g\n", y, fabs(y - 6.02e23));
    printf("%g %g\n", z, fabs(z - 1.60e-19));
    return 0;
}

输入

1
3.1415926 6.02e23 1.60e-19

输出

1
2
3
3.14159 9.7406e-08
6.02e+23 1.72719e+16
1.6e-19 5.07752e-27

从输出中可以看出,即使看上去相等的两个浮点数,实际差值可能非常大。一种可行的解决方法是使用相对误差:fabs(1 - x / y) < 1e-6。例如,上面的程序改用相对误差后输出:

1
2
3
3.14159 3.10053e-08
6.02e+23 2.86909e-08
1.6e-19 3.17345e-08

字符常量

字符常量是一个整数,表示字符集中的一个字符,其值等于字符的ASCII码。

类型格式示例
普通字符单引号括起来的一个字符'a', '0', '@'
转义字符序列单引号括起来的反斜杠加一个字符'\n', '\t'
八进制数'\ooo',其中ooo表示三个八进制数'\012', '\173'
十六进制数'\xhh',其中hh表示两个十六进制数'\xa', '\x7b'

注:后两种形式的含义是直接用八进制数或十六进制数表示字符的ASCII码,该字符常量的值也等于这个整数。例如,在数值上'\n'等于'\012'等于'\xa'等于10。

C语言中的全部转义字符序列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
\a	响铃符
\b	回退符
\f	换页符
\n	换行符
\r	回车符
\t	横向制表符
\v	纵向制表符
\\	反斜杠
\?	问号
\'	单引号
\"	双引号
\0	空字符
\ooo	八进制数
\xhh	十六进制数

常量表达式是只包含常量的表达式。这种表达式在编译时求值,而不是在运行时求值。它可以出现在常量可以出现的任何位置。例如:

1
2
#define MAXLINE 1000
char line[MAXLINE + 1];

字符串常量

字符串常量也叫字符串字面值(string literal),是用双引号括起来的0个或多个字符组成的字符序列,例如"hello, world"""。字符常量中使用的转义字符序列也可以用在字符串中。编译器会自动将多个字符串常量连接起来,例如"hello," " world"等价于"hello, world"

从技术角度看,字符串常量就是字符数组,编译器会自动在结尾添加一个'\0',因此数组长度比字符串长度大1。例如,字符串"hello"的长度是5,但类型是char[6],声明char s[] = "hello";等价于char s[] = {'h', 'e', 'l', 'l', 'o', '\0'};

C语言对字符串的长度没有限制,但程序必须扫描完整个字符串后才能确定字符串的长度。标准库函数strlen(s)返回字符串s的长度,不包括末尾的'\0'。下面是strlen函数的一个版本:

strlen函数

标准头文件<string.h>声明了strlen和其他字符串函数。

字符常量与仅包含一个字符的字符串之间的区别:'x'"x"是不同的。前者是一个整数,其值是字符x的ASCII码(120);后者是一个包含x和\0两个字符的字符数组。

枚举常量

枚举(enumeration)是一个常量整型值的列表,例如:

1
enum boolean { NO, YES };

在没有显式说明的情况下,enum中第一个枚举名的值为0,第二个为1,以此类推。如果只指定了部分枚举名的值,未指定值的枚举名将从上一个指定的值向后递增。例如:

1
2
3
4
5
6
7
8
9
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,以此类推 */

不同枚举中的名字必须互不相同。同一枚举中不同的名字可以具有相同的值。

枚举为建立常量值与名字之间的关联提供了一种便利的方式。相对于#define语句来说,枚举的优势在于常量值可以自动生成。

尽管可以声明enum类型的变量,但编译器不检查这种类型的变量中存储的值是否为该枚举的有效值。例如:

1
2
enum months m = DEC;  /* m == 12 */
++m;  /* m == 13 */

注意:

  • 声明枚举变量时要加上enum关键字,但枚举常量名是全局的(这也是不同枚举中的名字必须互不相同的原因)
  • 枚举值本质上就是一个整数,因此可以进行整型运算、使用%d说明符打印
  • 枚举最常见的用法是检查枚举变量的值是否等于该枚举类型声明的某个常量值,因此虽然枚举变量的取值不限于该枚举声明的值,但这样就失去了使用枚举的意义

2.4 声明

所有变量都必须先声明后使用,尽管全局变量可以通过上下文隐式声明。声明指定一种类型,以及一个或多个这种类型的变量。例如:

1
2
int lower, upper, step;
char c, line[1000];

一个声明语句中的多个变量可以拆开在多个声明语句中声明。上面的两个声明语句也可以等价地写成下列形式:

1
2
3
4
5
int lower;  /* 温度表下限 */
int upper;  /* 温度表上限 */
int step;   /* 步长 */
char c;
char line[1000];

这种形式占用更多空间,但便于向各声明语句后添加注释,也便于以后修改。

可以在声明的同时对变量进行初始化。如果变量名的后面紧跟一个等号以及一个表达式,该表达式就充当变量的初始值。例如:

1
2
3
4
char esc = '\\';
int i = 0;
int limit = MAXLINE + 1;
float eps = 1.0e-5;

每次进入函数或程序块时,显式初始化的自动变量(局部变量)都将被初始化一次,其初始化表达式可以是任何表达式,未显式初始化的自动变量的值是未定义值(无效值)

外部变量(全局变量)和静态变量只进行一次初始化(在程序开始执行之前),初始化表达式必须为常量表达式,默认初始化为0

任何变量的声明都可以使用const限定符指定该变量的值不能被修改。对数组而言,const限定符指定数组所有元素的值都不能被修改。例如:

1
2
const double e = 2.71828182845905;
const char msg[] = "warning: ";

const限定符也可配合数组参数使用,表示函数不能修改数组元素的值:

1
int strlen(const char s[]);

注:

  • const的含义是“常量(constant)”,这里是指变量的值不能被修改,而2.3节所说的“常量”是指“字面值(literal)”
  • 声明const常量时必须初始化

2.5 算术运算符

二元算术运算符包括:+-*/%(取模运算符)。整数除法会截断结果中的小数部分。

表达式x % y的结果是x除以y的余数,当x能被y整除时其值为0。例如,如果某一年的年份能被4整除但不能被100整除,或者能被400整除,那么这一年就是闰年。因此,可用下列语句判断闰年:

1
2
3
4
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);

取模运算符%不能用于floatdouble类型。如果有负操作数,则余数的符号与除数相同(见 除法舍入问题)。

运算符+-的优先级比*/%的优先级低,相同优先级的算术运算符采用从左到右的结合规则。例如,a + b - c等价于(a + b) - ca + b * c等价于a + (b * c)

完整的运算符优先级和结合律见2.12节。

2.6 关系运算符与逻辑运算符

关系运算符包括>>=<<===!=。关系运算符的优先级低于算术运算符,因此i < lim - 1等价于i < (lim - 1)

逻辑运算符&&||分别是逻辑与和逻辑或,非0表示“真”,0表示“假”。这两个运算符采用短路逻辑:由&&||连接的表达式按从左到右的顺序进行求值,并且在知道结果为真或假后立即停止计算。运算符&&的优先级高于||,但两者都比关系运算符的优先级低。

在关系表达式和逻辑表达式中,如果结果为真,则表达式的值为1;如果为假,则值为0

逻辑运算符!的作用是将非0操作数转换为0,将操作数0转换为1。

注:在ifwhilefor等语句的测试部分中,“真”等价于“非0”。因此if (expr != 0)等价于if (expr)if (expr == 0)等价于if (!expr)(此时将!读作 “not” 会更直观)。

练习2-2 在不使用运算符&&||的条件下编写一个与1.9节getline函数中的for循环等价的循环语句。

1
2
for (i = 0; (i < maxline - 1) * ((c = getchar()) != EOF) * (c != '\n'); ++i)
    line[i] = c;

2.7 类型转换

隐式类型转换

如果二元运算符的两个操作数具有不同的类型,那么在进行运算之前要先把“较低”的类型提升为“较高”的类型,运算的结果为较高的类型。

简单来说,按照以下规则转换(不考虑无符号类型):

隐式类型转换

  • 如果一个操作数是long double,则将另一个转换为long double
  • 否则,如果一个操作数是double,则将另一个转换为double
  • 否则,如果一个操作数是float,则将另一个转换为float
  • 否则,将charshort转换为int
  • 之后,如果一个操作数是long,则将另一个转换为long

由于char类型就是较小的整型,因此在算术表达式中可以自由使用char类型的变量,这就为实现某些字符转换提供了很大的灵活性。例如,下面的函数atoi将字符串转换为相应的整数:

atoi函数

注:atoi函数在遇到第一个非数字字符时就返回,因此该函数只考虑参数字符串的数字前缀,例如,atoi("123abc")返回123,atoi("abc")返回0

函数lower是将char类型转换为int类型的另一个例子,它将字符转换为小写形式,如果待转换的字符不是大写字母则返回字符本身:

lower函数

注:lower函数依赖于在ASCII字符集中大写字母和小写字母都是连续的,并且具有固定的间隔,A~Z的ASCII码分别是65~90,a~z的ASCII码分别是97~122,每个小写字母与对应的大写字母之间的间隔都是'a' - 'A' = 97 - 65 = 32,因此大写字母加上这个值就是对应的小写字母,例如'B' + 32 = 66 + 32 = 98 = 'b'

标准头文件<ctype.h>定义了一组与字符集无关的字符测试和转换函数。

赋值时也要进行类型转换,赋值运算符右边的值需要转换为左边变量的类型。针对可能导致信息丢失的转换,编译器可能给出警告,但这些转换并不非法。当把较长的整数转换为较短的整数时,超出的高位部分将被丢弃。例如:

1
2
3
int i = 12345678;
char c = i;
i = c;  /* i == 78 */

这是因为int值12345678有4个字节,其二进制是00000000 10111100 01100001 01001110,将其赋值给char类型的变量将被截断,只保留最低1个字节01001110,转换为十进制即78。

在把参数传递给函数时也可能进行类型转换。

强制类型转换

强制类型转换(cast)将表达式转换为指定的类型:

1
(类型名) 表达式

可以这样理解:表达式首先被赋值给指定类型的某个变量,然后用该变量替换整条语句。强制类型转换与其他一元运算符具有相同的优先级,高于二元运算符。

例如:

1
2
int a = 5, b = 2;
double c = (double) a / b;

下面的例子实现了一个伪随机数生成器rand以及初始化随机数种子的函数srand,其中rand函数使用了强制类型转换:

伪随机数生成器

注:从代码中可以看出,只要初始状态确定,rand函数生成的随机数序列就是固定的,这也是叫做“伪随机数”的原因

练习2-3 编写函数htoi(s),把由十六进制数字组成的字符串(包含可选的前缀0x或0X)转换为与之等价的整型值。字符串中允许包含的数字包括:0-9、a~f以及A~F。

2.8 自增运算符与自减运算符

自增运算符++使变量递增1,自减运算符--使变量递减1。它们既可以用作前缀运算符(如++n),也可以用作后缀运算符(如n++),区别是:前缀运算符先将变量的值加1,然后再使用变量的值;而后缀运算符先使用变量的值,然后再将变量的值加1。 例如,假设n的值为5,那么x = n++;将x的值置为5,x = ++n;将x的值置为6。这两条语句执行完后,变量n的值都是6。

自增与自减运算符只能用于变量1++(i + j)++是非法的。

在仅需要递增/递减变量而不需要使用具体值的情况下,前缀和后缀形式的效果相同。但在某些情况下需要专门使用前缀/后缀形式。例如,下面的函数squeeze(s, c)删除字符串s中出现的所有字符c:

squeeze函数

该函数使用下标i遍历每一个字符,只有不等于c时才拷贝到下标j的位置,之后将j的值加1(最早的双指针方法)。其中s[j++] = s[i];等价于s[j] = s[i]; j++;

在第1章中编写的getline函数中的line[i] = c; ++i替换为line[i++] = c;

第三个例子考虑函数strcat(s, t),将字符串t连接到字符串s的尾部(标准库<string.h>声明了该函数,但返回了指向新字符串的指针)。函数strcat假设字符串s尾部有足够的空间保存字符串t(否则就会下标越界):

strcat函数

例如,以下代码将打印”hello,world”:

1
2
3
4
5
6
7
int main() {
    char s[15] = "hello";
    char t[] = ",world";
    strcat(s, t);
    printf("%s\n", s);
    return 0;
}

下面详细分析一下strcat函数的处理过程。第1步:找到字符串s的结尾(即'\0'的位置),对应第1个while语句:

strcat函数第1步

注意:main函数中将字符数组s的大小声明为15,从而确保能够容纳字符串t(实际上长度至少需要12),多余的元素自动初始化为'\0'

第2步:从上述位置开始,将t中的字符逐个拷贝到s,包括结尾的'\0',对应第2个while语句:

strcat函数第2步

上图是循环结束后的状态

  • 由于while语句的条件是先赋值后判断是否等于'\0',因此t结尾的'\0'也会被拷贝
  • 由于i和j使用后缀自增运算符,因此最终指向'\0'的下一个位置
  • 红色部分是字符串s真正的内容
  • 如果数组s不够大,则拷贝过程中i会超过数组s的最后一个元素,从而覆盖不属于数组s的内存单元的值

练习2-4 重新编写函数squeeze(s1, s2),将字符串s1中任何与字符串s2中字符匹配的字符都删除。

练习2-5 编写函数any(s1, s2),将字符串s2中的任一字符在字符串s1中第一次出现的位置作为结果返回。如果s1中不包含s2中的字符,则返回-1。(标准库函数strpbrk具有同样的功能,但它返回的是指向该位置的指针)

2.9 按位运算符

C语言提供了6个位操作运算符,这些运算符只能用于整型操作数:

1
2
3
4
5
6
&    按位与(AND)
|    按位或(OR)
^    按位异或(XOR)
<<   左移
>>   右移
~    按位取反(NOT)

注:这里的“位”(bit)是指二进制位,因此要先了解整数的二进制表示才能理解这一节。一个二进制位只能表示0和1两个数,运算规则如下:

aba & ba | ba ^ b~a
000001
010111
100110
111100

如果1和0分别表示“真”和“假”,则一个二进制位&|~分别等价于逻辑“与”、“或”、“非”。“异或”在两个操作数不相同时结果为1,否则为0。

位运算具有以下性质:

1
2
3
x & 0 = 0, x & 1 = x
x | 0 = x, x | 1 = 1
x ^ 0 = x, x ^ 1 = ~x

推广到一般的整数,按位运算符就是对两个操作数的对应二进制位分别进行运算。例如,101 & 44 == 36101 | 44 == 109~101 == -102

1
2
3
4
  01100101		  01100101
& 00101100		| 00101100		~ 01100101
----------		----------		----------
  00100100		  01101101		  10011010

(这里仅展示了最低8位,实际上int类型有32位,但是在这个例子中高24都是0)

注意:按位运算符(&|~)与逻辑运算符(&&||!)的区别,前者对整数的每一个二进制位分别进行运算;后者只考虑操作数是否为0,计算结果只有整数0或1。例如,101 & 44 == 36101 && 44 == 1

移位运算符<<>>分别将左操作数左移(left shift)和右移(right shift),移动的位数由右操作数指定(必须是非负值)。所谓“左移”,就是整数的每个二进制位都向左移动一位,左边移出的位丢失,右边空出的位补0;“右移”同理。无符号类型的值右移时,左边空出的位用0填补;有符号类型的值右移时,左边空出的位用符号位(最高位)填补(正数的符号位为0,负数的符号位为1)。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main() {
    int a = 123, b = -123;
    int i;
    
    for (i = 0; i < 32; ++i)
        printf("a << %d = %d, b << %d = %d\n", i, a << i, i, b << i);

    for (i = 0; i < 32; ++i)
        printf("a >> %d = %d, b >> %d = %d\n", i, a >> i, i, b >> i);
    return 0;
}
ia « ib « i
000000000 00000000 00000000 01111011 (123)11111111 11111111 11111111 10000101 (-123)
100000000 00000000 00000000 11110110 (246)11111111 11111111 11111111 00001010 (-246)
200000000 00000000 00000001 11101100 (492)11111111 11111111 11111110 00010100 (-492)
300000000 00000000 00000011 11011000 (984)11111111 11111111 11111100 00101000 (-984)
2401111011 00000000 00000000 00000000 (2063597568)10000101 00000000 00000000 00000000 (-2063597568)
2511110110 00000000 00000000 00000000 (-167772160)00001010 00000000 00000000 00000000 (167772160)
2611101100 00000000 00000000 00000000 (-335544320)00010100 00000000 00000000 00000000 (335544320)
3011000000 00000000 00000000 00000000 (-1073741824)01000000 00000000 00000000 00000000 (1073741824)
3110000000 00000000 00000000 00000000 (-2147483648)10000000 00000000 00000000 00000000 (-2147483648)
ia » ib » i
000000000 00000000 00000000 01111011 (123)11111111 11111111 11111111 10000101 (-123)
100000000 00000000 00000000 00111101 (61)11111111 11111111 11111111 11000010 (-62)
200000000 00000000 00000000 00011110 (30)11111111 11111111 11111111 111000101 (-31)
300000000 00000000 00000000 00001111 (15)11111111 11111111 11111111 111100101 (-16)
600000000 00000000 00000000 00000001 (1)11111111 11111111 11111111 11111110 (-2)
700000000 00000000 00000000 00000000 (0)11111111 11111111 11111111 11111111 (-1)
3100000000 00000000 00000000 00000000 (0)11111111 11111111 11111111 11111111 (-1)

可以看出:左移n位等价于乘以2n(从最高位丢失开始不再成立),右移n位等价于除以2n(下取整)。

注意:移位运算符的右操作数必须在0~31之间,否则行为是未定义的(并且在右操作数是常量和变量时结果也不同)!例如:

1
2
3
4
5
6
int n;

n = -1;
printf("%u %u\n", 1 << -1, 1 << n);
n = 32;
printf("%u %u\n", 1 << 32, 1 << n);

这段代码将输出

1
2
0 2147483648
0 1

结合位运算的性质和移位操作,可以对一个整数的二进制位进行任意的操作。 根据位运算的性质,置0用&,置1用|。利用整型常量和位运算可以构造出任意需要的掩码(mask)。

例如,假设x是int类型(4字节=32位),x = x | (1 << 3)(等价于x = x | 8)将x的第3位置1(最低位是第0位):

1
2
3
4
  xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx
| 00000000 00000000 00000000 00001000
-------------------------------------
  xxxxxxxx xxxxxxxx xxxxxxxx xxxx1xxx

x = x & ~(1 << 3)将x的第3位置0:

1
2
3
4
  xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx
& 11111111 11111111 11111111 11110111
-------------------------------------
  xxxxxxxx xxxxxxxx xxxxxxxx xxxx0xxx

x = x | 7将x的最低3位置1:

1
2
3
4
  xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx
| 00000000 00000000 00000000 00000111
-------------------------------------
  xxxxxxxx xxxxxxxx xxxxxxxx xxxxx111

x = x & ~7将x的最低3位置0:

1
2
3
4
  xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx
& 11111111 11111111 11111111 11111000
-------------------------------------
  xxxxxxxx xxxxxxxx xxxxxxxx xxxxx000

x = x & 7将x除最低3位外全部置0:

1
2
3
4
  xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx
& 00000000 00000000 00000000 00000111
-------------------------------------
  00000000 00000000 00000000 00000xxx

x = x & 8将x除第3位外全部置0:

1
2
3
4
  xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx
& 00000000 00000000 00000000 00001000
-------------------------------------
  00000000 00000000 00000000 0000x000

注:这种方法可用于“取出”整数的某个二进制位,如果结果为0则表示该位为0,否则表示该位为1,例如if ((x & 8) == 0) ...。利用这一思想可以将一个整数当作一组“开关”使用,每个二进制位表示一个“开关”,0表示“关”、1表示“开”,使用位运算可以测试、打开或关闭某个“开关”。

下面看一个实例,函数getbits(x, p, n)返回x中从右数第p位开始的n位。这里假定最右边是第0位,n与p都是合理的正值(0<n≤p+1≤32, n<=31)。例如,getbits(58, 4, 3)返回6。

getbits函数

注:这里的掩码~(~0 << n)最低n位为1,其他位都为0,也可以用(1 << n) - 1代替

解释:

getbits函数解释

练习2-6 编写一个函数setbits(x, p, n, y),该函数返回对x执行下列操作后的结果:将x中从第p位开始的n位设置为y中最右边n位的值,其余各位保持不变。

练习2-7 编写一个函数invert(x, p, n),该函数返回对x执行下列操作后的结果:将x中从第p位开始的n位求反(即1变成0,0变成1),其余各位保持不变。

练习2-8 编写一个函数rightrot(x, n),该函数返回将x循环右移(即从最右端移出的位将从最左端移入)n位后所得到的值。

2.10 赋值运算符与表达式

如果expr1expr2是表达式,op是二元运算符,那么expr1 = (expr1) op (expr2)等价于expr1 op= expr2,其中op=称为赋值运算符(assignment operator)。它们的区别在于,第二种形式expr1只计算一次

例如,i = i + 2等价于i += 2x = x * (y + 1)等价于x *= y + 1(由于赋值运算符的优先级较低,因此第二种形式可以省略圆括号)。

大多数二元运算符op都有一个相应的赋值运算符op=,其中op可以是以下运算符之一:

1
+  -  *  /  %  &  |  ^  <<  >>

下面是一个示例,函数bitcount(x)统计x中值为1的二进制位的个数:

bitcount函数

赋值语句具有值,且可以用在表达式中。下面是最常见的一个例子:

1
2
while ((c = getchar()) != EOF)
    ...

其他赋值运算符(如+=-=等)也可以用在表达式中,尽管这种用法比较少见。

赋值表达式的类型是它的左操作数的类型,其值是赋值操作完成后的值。

练习2-9 在求对2的补码时,表达式x &= (x - 1)可以删除x中最右边值为1的一个二进制位。请解释这样做的道理。用这一方法重写bitcount函数,以加快其执行速度。

任何整数的二进制都可以表示为xx…x10…0的形式,减1后等于xx…x01…1,二者按位与得到xx…x00…0,即相当于将最右边一个1置为0。

2.11 条件表达式

条件表达式(conditional expression)使用三元运算符?:

1
expr1 ? expr2 : expr3

首先计算expr1的值,如果其值不等于0(为真),则计算expr2的值并作为条件表达式的值,否则计算expr3的值并作为条件表达式的值。expr2expr3只有一个被计算。

例如,以下语句求a和b的最大值,并赋值给z:

1
2
3
4
if (a > b)
    z = a;
else
    z = b;

使用条件表达式可改写为:

1
z = (a > b) ? a : b;  /* z = max(a, b) */

条件表达式实际上就是一种表达式,它可以用在其他表达式可以使用的任何地方。

如果expr2expr3的类型不同,结果的类型将由2.7节讨论的转换规则决定。例如,如果f为float类型,n为int类型,那么表达式(n > 0) ? f : nfloat类型,与n是否为正值无关。

条件表达式中第一个表达式两边的圆括号不是必需的,这是因为条件运算符?:的优先级低于关系运算符。但还是建议使用圆括号,因为这可以使表达式的条件部分更易于阅读。

使用条件表达式可以编写出很简洁的代码。例如,下面的这个循环语句打印一个数组的前n个元素,每行打印10个元素,每列之间用一个空格隔开,每行用一个换行符结束(包括最后一行):

1
2
for (i = 0; i < n; ++i)
    printf("%6d%c", a[i], (i%10==9 || i==n-1) ? '\n' : ' ');

下面是另一个比较好的例子:

1
printf("You have %d item%s.\n", n, n == 1 ? "" : "s");

练习2-10 重新编写将大写字母转换为小写字母的函数lower,并用条件表达式替代其中的if-else结构。

2.12 运算符优先级与求值次序

下表总结了所有运算符的优先级和结合性。从上到下优先级逐渐降低,同一行中的运算符具有相同的优先级。

运算符结合性
() [] -> .从左到右
! ~ ++ -- + - * & (type) sizeof从右到左
* / %从左到右
+ -从左到右
<< >>从左到右
< <= > >=从左到右
== !=从左到右
&从左到右
^从左到右
|从左到右
&&从左到右
||从左到右
?:从右到左
= += -= *= /= %= &= ^= |= <<= >>=从右到左
,从左到右

注:第2行的一元运算符+-*&比相应的二元运算符的优先级高。

运算符()表示函数调用。运算符->.用于访问结构体成员,第6章将讨论这两个运算符以及sizeof(对象长度)运算符。第5章将讨论运算符*(通过指针间接访问)和&(对象地址)。第3章将讨论逗号运算符。

注:从左到右结合是指:a + b + c等价于(a + b) + c,从右到左结合是指:!!a等价于!(!a)

注意,位运算符&^|的优先级比==!=低,因此位测试表达式必须用圆括号括起来才能得到正确结果,例如:

1
if ((x & MASK) == 0) ...

C语言没有指定一个运算符中多个操作数的计算顺序&&||?:,运算符除外)。例如,在语句

1
x = f() + g();

中,f()可以在g()之前计算,也可以在g()之后计算。因此,如果函数fg改变了另一个函数所使用的变量,那么x的结果可能会依赖于这两个函数的计算顺序。为了保证特定的计算顺序,可以把中间结果保存在临时变量中。

C语言也没有指定函数各参数的求值顺序。 因此,下列语句

1
printf("%d %d\n", ++n, power(2, n));  /* 错 */

在不同的编译器中可能会产生不同的结果,这取决于n的自增运算在power调用之前还是之后执行。解决的办法是把该语句改写成下列形式:

1
2
++n;
printf("%d %d\n", n, power(2, n));

函数调用、嵌套赋值语句、自增与自减运算符都有可能产生“副作用” ——在对表达式求值的同时,修改了某些变量的值。例如,a[i] = i++;数组下标i是引用旧值还是新值?(++i) + (++i)结果是多少?对这种情况不同编译器的解释可能不同,并因此产生不同的结果。

在任何一种编程语言中,如果代码的执行结果与求值顺序相关,都不是好的程序设计风格。

This post is licensed under CC BY 4.0 by the author.