本章导读
学会表示数据之后,下一步是处理数据。本章讲解 C 的赋值运算符(assignment operator)、算术运算符(arithmetic operators)、运算符优先级(precedence)、表达式与语句的区别、自动/强制类型转换,并用 while 循环初探流程控制,最后学会编写带参数的函数。
本章关键词与运算符:while、typedef;%、++、--、(type)。
5.1 循环简介
程序清单 5.1(shoes1.c)把 9 码男鞋换算成脚长:foot = SCALE * shoe + ADJUST;,只能算一个鞋码,用手算反而更快。要发挥计算机重复计算的本事,需要循环(loop)。改进版用 while 循环打印 3.0~18.5 之间所有整码的对照表。
程序清单 5.2 shoes2.c
/* shoes2.c -- 计算多个鞋码对应的脚长 */
#include <stdio.h>
#define ADJUST 7.64
#define SCALE 0.325
int main(void)
{
double shoe, foot;
printf("Shoe size (men's) foot length\n");
shoe = 3.0;
while (shoe < 18.5) /* while 循环开始 */
{ /* 块的开始 */
foot = SCALE*shoe + ADJUST;
printf("%10.1f %15.2f inches\n", shoe, foot);
shoe = shoe + 1.0;
} /* 块的结束 */
printf("If the shoe fits, wear it.\n");
return 0;
}输出(节选):3.0 8.62 inches 一路打到 18.0 13.49 inches,最后一行 If the shoe fits, wear it.。
while 的工作原理:① 执行到 while (shoe < 18.5) 时先检查条件(< 表示”小于”);② 为真就执行花括号 {} 包住的整块代码——这段代码(含花括号)称为块(block);③ 执行到 } 后回到 while 再查条件,如此往复;④ 当 shoe 达到 19.0、条件变假时,跳到循环后的第一条语句。改两个常量即可换用途:SCALE=1.8, ADJUST=32.0 变摄氏转华氏,SCALE=0.6214, ADJUST=0 变公里转英里。
5.2 基本运算符
C 用运算符表示算术运算,基本的有 =、+、-、*、/。C 没有幂运算符,数学库的 pow() 可代劳,如 pow(3.5, 2.2)。
赋值运算符:=
在 C 里 = 不是”等于”,而是赋值运算符,动作从右往左:bmw = 2002; 把 2002 存入变量 bmw。所以 i = i + 1; 在数学上荒谬,在 C 里完全合理——取出 i 加 1 再存回。

2002 = bmw; 是非法的:常量不能被赋值。= 左边必须是可修改的左值(modifiable lvalue)。几个术语:
- 数据对象(data object):存储值的一段数据存储区域;
- 左值(lvalue):标识特定数据对象的名称或表达式,来自 left;
- 右值(rvalue):可赋给可修改左值的量——常量、变量或任何能求值的表达式;
- 操作数(operand):运算符作用的对象。
C 还支持连续赋值:cheeta = tarzan = jane = 68; 从右往左依次执行,三人得分都是 68(程序清单 5.3 golf.c 验证,输出三人同为 68)。
加法、减法与符号运算符
+:两侧相加,如income = salary + bribes;-(二元):左减右,如takehome = 224.00 - 24.00;-(一元):变号,如smokey = -rocky;(rocky 为 -12 时得 12)。
需要两个操作数的叫二元运算符(binary operator),只取一个的叫一元运算符(unary operator)。C90 补了一元 +,不改变操作数,只是让 dozen = +12; 合法。

乘法运算符:*
cm = 2.54 * inch;。C 没有平方函数,用乘法即可(清单 5.4 squares.c 用 while 打印前 20 个整数的平方表)。
指数增长:清单 5.5 wheat.c 演示国际象棋麦粒故事——第 1 格放 1 粒麦子,之后每格翻倍,程序累计总数并与美国小麦年产量(约 1E15 粒)对比。前 10 格风平浪静(约 1000 粒),到第 50 格累计已达 1.13e+15——超过美国一年小麦总产量!这就是指数增长(exponential growth),人口增长、能源消耗同理。
除法运算符:/
整数除法与浮点除法规则不同:整数除法结果仍是整数,小数部分被丢弃,称为截断(truncation)。清单 5.6 divide.c 验证:5/4 是 1、6/3 是 2、7/4 是 1;而 7./4. 和 7./4 都是 1.75。整数除法不是四舍五入;混合类型运算时整数先转浮点再除。C99 规定整数除法向零取整,-3.8 截断为 -3。
整数除法陷阱
5 / 4的结果是1,不是 1.25!写percent = a/b*100;时若 a、b 都是整数,结果永远被截断。想保留小数,至少让一个操作数是浮点数,如a * 100.0 / b。
运算符优先级
butter = 25.0 + 60.0 * n / SCALE;(n=6.0、SCALE=2.0)的求值顺序:先 60.0*n 得 360,再 /SCALE 得 180,最后 +25.0 得 205。想先加法就加括号:flour = (25.0 + 60.0 * n) / SCALE;——括号内最先计算。
表 5.1 运算符优先级(从高到低)
| 运算符 | 结合律(associativity) |
|---|---|
() | 从左往右 |
-(一元) +(一元) | 从右往左 |
* / | 从左往右 |
+(二元) -(二元) | 从左往右 |
= | 从右往左 |

优先级不等于求值顺序:y = 6 * 12 + 5 * 20; 里两个乘法谁先算由编译器自定(结果都是 172)。结合律只在两个运算符共享同一操作数时起作用:12 / 3 * 2 中 / 与 * 共享 3,从左到右得 8(从右往左会得 2,这时顺序就有影响)。
程序清单 5.7 rules.c 综合演练:
/* rules.c -- 优先级测试 */
#include <stdio.h>
int main(void)
{
int top, score;
top = score = -(2 + 5) * 6 + (4 + 3 * (2 + 3));
printf("top = %d \n", top);
return 0;
}推演:(2+5)→7,一元负号→-7;(2+3)→5,3*5→15,4+15→19;-7*6→-42;-42+19→-23;先 score = -23,再 top = -23(= 从右往左)。输出 top = -23。
5.3 其他运算符
sizeof 运算符与 size_t 类型
sizeof 返回操作数占的字节数:操作数是类型名时必须加括号(sizeof(int)),是具体变量时可省(sizeof n)。返回值类型是 size_t——用 typedef 机制(给已有类型起别名,如 typedef double real;)定义的无符号整数别名,有的系统是 unsigned int,有的是 unsigned long。C99 提供 %zd 转换说明打印它(不支持时可试 %u 或 %lu)。
求模运算符:%
%(取模)只用于整数,给出整除后的余数:13 % 5 得 3。它非常实用:“每逢第 3 个月加收费”只需看 month % 3 是否为 0;判断奇偶也靠它。清单 5.9 用 秒/60 与 秒%60 拆分时间。
程序清单 5.9 min_sec.c
// min_sec.c -- 把秒数换算成分钟和秒
#include <stdio.h>
#define SEC_PER_MIN 60 // 每分钟 60 秒
int main(void)
{
int sec, min, left;
printf("Convert seconds to minutes and seconds!\n");
printf("Enter the number of seconds (<=0 to quit):\n");
scanf("%d", &sec); // 读取秒数
while (sec > 0)
{
min = sec / SEC_PER_MIN; // 截断后的分钟数
left = sec % SEC_PER_MIN; // 剩余的秒数
printf("%d seconds is %d minutes, %d seconds.\n", sec,
min, left);
printf("Enter next value (<=0 to quit):\n");
scanf("%d", &sec);
}
printf("Done!\n");
return 0;
}示例:输入 154,输出 154 seconds is 2 minutes, 34 seconds.;输入 0 退出。设计要点:每轮循环都要更新被测试的变量(scanf() 重新读 sec),否则循环永不结束。C99 的负数取模规则:第一个操作数为负则结果为负,可用 a % b == a - (a/b)*b 验证:-11 % 5 = -11 - (-2)×5 = -1。
递增与递减运算符:++ 和 —
++ 把操作数加 1,分前缀模式(prefix)(++ultra)和后缀模式(postfix)(super++)。清单 5.10 add_one.c 中两者单独使用,效果都等同 n = n + 1,程序同时把两个变量数到 5。
/* add_one.c(清单 5.10)核心循环 */
while (super < 5)
{
super++; /* 后缀 */
++ultra; /* 前缀 */
printf("super = %d, ultra = %d \n", super, ultra);
}价值在于把递增与比较合并成一个表达式:
shoe = 2.0;
while (++shoe < 18.5)
{
foot = SCALE*shoe + ADJUST;
printf("%10.1f %20.2f inches\n", shoe, foot);
}这把”测试”与”更新”聚在一处,不易漏更新而陷入死循环(infinite loop),通常还生成更高效的机器码;缺点是挤在一起可读性略差。
前缀与后缀差在哪? 看清单 5.11。
程序清单 5.11 post_pre.c
/* post_pre.c -- 后缀与前缀的区别 */
#include <stdio.h>
int main(void)
{
int a = 1, b = 1;
int aplus, plusb;
aplus = a++; /* 后缀 */
plusb = ++b; /* 前缀 */
printf("a aplus b plusb \n");
printf("%1d %5d %5d %5d\n", a, aplus, b, plusb);
return 0;
}输出:
a aplus b plusb
2 1 2 2
aplus = a++;(后缀):先用 a 的原值 1,再把 a 加到 2;plusb = ++b;(前缀):先把 b 加到 2,再用新值 2。

因此 while (++shoe < 18.5) 打印到 18 码为止;换成 shoe++ 则到 19 码——递增发生在比较之后。-- 同理把值减 1(清单 5.12 bottles.c 用 while (--count > 0) 倒数唱了一百遍”墙上挂着矿泉水”)。
i++ 与 ++i 陷阱
单独使用没区别,在表达式里区别巨大。更危险的是”聪明”代码:
y = n++ + n++;——n 最终加 2,但 y 的值是**未定义(undefined)**的;printf("%d %d\n", num, num*num++)的参数求值顺序也不由你控制。守则:同一变量在一个表达式里最多出现一次 ++/—;不要对函数多个参数里的同一变量用 ++/—。
优先级:++、-- 极高,仅低于圆括号,x*y++ 即 (x)*(y++)。但优先级不等于”何时修改”:nextnum = (y + n++) * 6; 中 n 先参与加法(结果 30)之后才变 4。口诀:n++ 是”用 n,然后加 1”;++n 是”先加 1,再用 n”。
拆开写更安全
与其纠结
b = ++i;的前缀后缀,不如拆成++i; b = i;——结果完全一样,永远不会选错。宁可稍啰嗦,也要让意图一目了然。
5.4 表达式和语句
表达式
**表达式(expression)**由运算符和操作数组成,4、-6、4+21、a*(b + c/d)/20、q = 5*2、q > 3 都是表达式。每个 C 表达式都有值:q = 5*2 的值就是赋给 q 的 10;关系表达式真为 1 假为 0。所以 6 + (c = 3 + 8) 的值是 17(合法但不推荐)。
语句
**语句(statement)**是发给计算机的完整指令,以分号结尾:legs = 4 是表达式,legs = 4; 才是语句。目前学过四类:声明语句(int toes;,建立名字与类型、分配内存,它不是表达式)、赋值语句、函数调用语句、结构化语句(如 while)。清单 5.13 addemup.c 四类全用上:声明 count、sum,赋初值 0,while 里 sum = sum + count; 累加,最后 printf() 输出 sum = 210(1+2+…+20)。
while 语句三部分(见下图):关键字 while、圆括号中的测试条件、条件为真时执行的那条语句。

副作用与序列点
- 副作用(side effect):对数据对象或文件的修改。C 的视角里,
states = 50;的”主业”是求表达式states = 50的值(50),改变量只是副作用——赋值、递增、递减都靠副作用工作。 - 序列点(sequence point):程序执行中的一点,在此之前求值的所有副作用必须在此完成。语句的分号是一个序列点;完整表达式(full expression,如 while 的测试条件)的末尾也是。
所以 while (guests++ < 10) printf("%d\n", guests); 中,guests 的递增保证发生在进入 printf() 之前(测试条件是完整表达式),但发生在与 10 比较之后(后缀特性)。而 y = (4 + x++) + (6 + x++); 里两个 x++ 只是子表达式,C 不保证各自递增的时机——这正是要避免这种写法的原因。
复合语句(块)
**复合语句(compound statement)**是花括号包起来的两条及以上语句,也叫块。对比:
/* 片段 1:没有花括号 */
index = 0;
while (index++ < 10)
sam = 10 * index + 2;
printf("sam = %d\n", sam); /* 只在循环结束后执行 1 次 */
/* 片段 2:用花括号 */
index = 0;
while (index++ < 10)
{
sam = 10 * index + 2;
printf("sam = %d\n", sam); /* 每轮循环都执行 */
}没有花括号时,while 只管到测试条件后的第一条语句(第一个分号)为止。

风格提示:缩进对编译器毫无意义,是给读者看的。花括号另起一行或跟在 while 同一行两种风格等价,选一种用到底。
5.5 类型转换
混用类型时 C 按自动转换规则行事:
- 表达式中,
char、short(含无符号)自动提升为int(必要时 unsigned int)——这类升级叫提升(promotion); - 两种类型参与运算时,都转成其中较高的类型;
- 类型排行从高到低:
long double、double、float、unsigned long long、long long、unsigned long、long、unsigned int、int(long 与 int 等长时 unsigned int 例外高于 long); - 赋值时结果转成被赋值变量的类型——可能提升,也可能降级(demotion);
- 函数传参时 char/short 升为 int,float 升为 double(函数原型可覆盖)。
降级才危险:低级别类型可能装不下整个数(8 位 char 装不下 22334);浮点转整数是向零截断——23.12 和 23.99 都变 23,-23.5 变 -23。清单 5.14 convert.c 用 char ch; int i; float fl; 演示全流程:fl = i = ch = 'C'; 后三者分别是 'C'、67、67.00(‘C’ 的 ASCII 码是 67);ch = ch + 1; 得 ‘D’(68 截断回 1 字节);ch = 5212205.17; 是降级——截断后 ch 里恰好剩 ’-’ 的 ASCII 码,典型的降级出乱子。
强制类型转换运算符
需要显式转换时用强制类型转换(cast)运算符 (type):
mice = 1.6 + 1.7; /* 自动转换:3.3 截断为 3 */
mice = (int) 1.6 + (int) 1.7; /* 先各自截断:1 + 1 = 2 */两种写法没有对错,取决于问题语义。C 不设路障,但必须自己负责别滥用自由。
显式优于隐式
与其依赖自动转换(尤其降级),不如用
(int)、(double)把意图写明白:读者一看便知是刻意转换,老编译器也不易出错。
5.6 带参数的函数
函数声明里的变量叫形式参数(formal parameter,形参);调用时传给它的值叫实际参数(actual argument,实参)。调用 pound(10) 时,10 被复制给形参 n。
程序清单 5.15 pound.c
/* pound.c -- 定义一个带参数的函数 */
#include <stdio.h>
void pound(int n); /* ANSI 风格函数原型 */
int main(void)
{
int times = 5;
char ch = '!'; /* ASCII 码是 33 */
float f = 6.0;
pound(times); /* int 参数 */
pound(ch); /* char 自动转换为 int */
pound((int) f); /* 强制转换把 f 变成 int */
return 0;
}
void pound(int n) /* ANSI 风格函数头 */
{
while (n-- > 0)
printf("#");
printf("\n");
}输出:第一行 5 个 #,第二行 33 个,第三行 6 个。要点:
- 函数名前的
void表示无返回值;参数列表的int n声明形参; pound(ch)能工作靠 char → int 的自动提升;pound((int) f)用 cast 显式转换;- 顶部函数原型(prototype)
void pound(int n);告诉编译器:无返回值、接受一个 int。有了原型,连pound(3.859)都会自动转成pound(3); - 形参名是函数私有的,与 main() 里的同名变量互不相干。
5.7 示例程序
清单 5.16 running.c 把本章知识串起来:输入公里数和跑步用时(分、秒),换算每英里配速与平均时速。核心计算只有 6 行:
time = S_PER_M * min + sec; // 时间换算成纯秒
distm = M_PER_K * distk; // 公里换算成英里
rate = distm / time * S_PER_H; // 英里每秒 × 秒每小时 = mph
mtime = (double) time / distm; // 时间/距离 = 每英里耗时
mmin = (int) mtime / S_PER_M; // 整分钟数
msec = (int) mtime % S_PER_M; // 余下秒数输入 10.0 公里、36 分 23 秒,输出:跑了 10.00 公里(6.21 英里),每英里配速 5 分 51 秒,平均时速 10.25 mph。注意两处 cast:(double) time / distm 避免整数除法截断配速;(int) mtime 配合 / 60 与 % 60 拆分秒数——和 min_sec.c 套路一致。程序开头用 const int S_PER_M = 60; 等声明符号常量,注释足够自解释。
5.8 关键概念与本章小结
- 运算符由三个维度刻画:操作数个数(一元/二元)、优先级、结合律;后两者决定共享操作数的运算符谁先执行。
- 每个 C 表达式都有值——包括赋值表达式和关系表达式(真 1 假 0)。
- 语句是完整指令,以分号结尾;花括号包起来的多条语句构成复合语句(块)。
- 类型混用触发自动转换:提升通常无害,降级可能丢数据;用 cast 显式表达意图是好习惯。
- 带参数的函数三步走:声明原型 → 调用(实参复制给形参) → 定义。
自测题(5 题,附答案解析)
1.
int x; x = (2 + 3) * 6;x 是多少?x = (12 + 6) / 2 * 3;呢?2.
7 / 4和7. / 4分别等于多少?为什么?3.
aplus = a++;与plusb = ++b;(a、b 初始都是 1)之后 aplus、plusb 各是多少?4.
13 % 5的值是多少?-11 % 5(C99)呢?%能用于浮点数吗?5. 下面片段想每轮都打印 sam 的值,实际会怎样?
index = 0; while (index++ < 10) sam = 10 * index + 2; printf("sam = %d\n", sam);答案解析:
1. 第一式:括号优先 2+3=5,56=30,x 为 30。第二式:除法乘法同级、从左到右——18/2=9,93=27,x 为 27。
2.
7 / 4是整数除法,截断得 1;7. / 4中 7.0 是浮点数,触发混合运算转浮点,得 1.75。3. aplus = 1(后缀:先用 a 的原值 1,a 再变 2);plusb = 2(前缀:b 先加到 2 再赋值)。
4.
13 % 5= 3(商 2 余 3);C99 规定第一个操作数为负则结果为负,-11 % 5= -1。%只能用于整数。5. 缩进骗了人!没有花括号,循环体只有
sam = 10 * index + 2;。printf() 在循环结束后只执行一次;此时 index 为 11,打印的是sam = 112一行。想每轮都打印,必须用花括号把两条语句括起来。