本章导读

学会表示数据之后,下一步是处理数据。本章讲解 C 的赋值运算符(assignment operator)、算术运算符(arithmetic operators)、运算符优先级(precedence)、表达式与语句的区别、自动/强制类型转换,并用 while 循环初探流程控制,最后学会编写带参数的函数。

本章关键词与运算符:whiletypedef%++--(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 再存回。

/图:赋值语句 i = 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)
()从左往右
-(一元) +(一元)从右往左
* /从左往右
+(二元) -(二元)从左往右
=从右往左

/图:表达式树(expression tree)展示求值顺序

优先级不等于求值顺序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-64+21a*(b + c/d)/20q = 5*2q > 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、圆括号中的测试条件、条件为真时执行的那条语句。

/图:简单 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 循环

风格提示:缩进对编译器毫无意义,是给读者看的。花括号另起一行或跟在 while 同一行两种风格等价,选一种用到底。

5.5 类型转换

混用类型时 C 按自动转换规则行事:

  1. 表达式中,charshort(含无符号)自动提升为 int(必要时 unsigned int)——这类升级叫提升(promotion)
  2. 两种类型参与运算时,都转成其中较高的类型;
  3. 类型排行从高到低:long doubledoublefloatunsigned long longlong longunsigned longlongunsigned intint(long 与 int 等长时 unsigned int 例外高于 long);
  4. 赋值时结果转成被赋值变量的类型——可能提升,也可能降级(demotion)
  5. 函数传参时 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 显式表达意图是好习惯。
  • 带参数的函数三步走:声明原型调用(实参复制给形参) → 定义