跳转至

第二章 · 基本数据类型及其运算

本页最后更新于 ,内容可能过时

《C语言程序设计(第3版)》第 2 章课后习题,共 16 题。

1 · 十进制整数转八进制与十六进制

将下列十进制整数用八进制整数和十六进制数表示。

(1)9;(2)64;(3)85;(4)-53;(5)-111;(6)2 499;(7)-28 654;(8)21 019。

答案
序号 答案
(1) 八进制 11,十六进制 0x9
(2) 八进制 100,十六进制 0x40
(3) 八进制 125,十六进制 0x55
(4) 八进制 -65,十六进制 -0x35
(5) 八进制 -157,十六进制 -0x6F
(6) 八进制 4703,十六进制 0x9C3
(7) 八进制 -67756,十六进制 -0x6FEE
(8) 八进制 51033,十六进制 0x521B
解析

八进制一位对应 3 个二进制位、十六进制一位对应 4 个,换算都按"除基取余、逆序排列"做。

负数的写法是先对绝对值做转换、再加负号。

逐项算:

  • (1)9 = 1×8 + 1 → 11;9 还是一位十六进制数字 → 0x9。
  • (2)64 = 1×64 + 0×8 + 0 → 100;64 = 4×16 + 0 → 0x40。
  • (3)85 = 1×64 + 2×8 + 5 → 125;85 = 5×16 + 5 → 0x55。
  • (4)53 = 6×8 + 5 → 65,加负号得 -65;53 = 3×16 + 5 → -0x35。
  • (5)111 = 1×64 + 5×8 + 7 → 157,加负号得 -157;111 = 6×16 + 15 → -0x6F。
  • (6)2 499 = 4×512 + 7×64 + 0×8 + 3 → 4703;2 499 = 9×256 + 12×16 + 3 → 0x9C3。
  • (7)28 654 = 6×4096 + 7×512 + 7×64 + 5×8 + 6 → 67756,加负号得 -67756;28 654 = 6×4096 + 15×256 + 14×16 + 14 → -0x6FEE。
  • (8)21 019 = 5×4096 + 1×512 + 0×64 + 3×8 + 3 → 51033;21 019 = 5×4096 + 2×256 + 1×16 + 11 → 0x521B。

真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):把 8 个量值逐个用 printf("%o %x") 打出来,正数 5 项与上表逐位吻合;负数按"绝对值转换 + 负号"的形式打,得到 53 → 65/35、111 → 157/6f、28 654 → 67756/6fee、21 019 → 51033/521b(字母大小写只是书写习惯)。

机器里存的是补码,不是负号加绝对值

C 的整型按补码存储。int k = -53; 时 printf("%o", k) 打出来是 32 位补码的位模式 37777777713,十六进制写作 ffffffcb,并不是 -65。

本题问的是"用八进制整数和十六进制数表示这个数",属于数学上的表示法,所以答案按"绝对值转换 + 负号"给;机器里的位模式是另一回事,两种口径别混。

易错:

  1. 拆 2 499 时漏掉 8 位上的 0,把 4703 写成 473
  2. 负数去查补码,把 -53 写成 37777777713
  3. 十六进制里 10~15 忘了写成 A~F

2 · 常量的词法分类

指出下列内容哪些是 C 语言的整型常量,哪些是实型常量,哪些两者都不是。

(1)E-4;(2)A423;(3)-1E-31;(4)0xABCL;(5).32E31;(6)087;(7)0xL;(8)003;

(9)0x12.5;(10)077;(11)11E;(12)056L;(13)0.;(14).0。

答案
答案
整型常量 (4) 0xABCL,long;(8) 003,int;(10) 077,int;(12) 056L,long
实型常量 (3) -1E-31,double;(5) .32E31,double;(13) 0.,double;(14) .0,double
两者都不是 (1) E-4、(2) A423:标识符,不是常量;(6) 087、(7) 0xL、(9) 0x12.5、(11) 11E:写法非法,编译不过
解析

判据只有两步:先看词法上能不能构成一个常量(能不能通过编译),能构成的再按有没有小数点或指数、带不带 L 后缀,分到整型或实型。

逐项说明:

  • (1)E-4 错:E 是标识符,整个式子被当成 E - 4 这个减法表达式,不是常量。编译探测时报 'E' undeclared。
  • (2)A423 错:标识符(变量名),不是常量。编译探测时报 'A423' undeclared。
  • (3)-1E-31 对 ✅:带指数的浮点写法 → 实型常量,类型 double。前面那个 - 是单目负号运算符,不属于常量本身,题面把它一起写出来了而已。
  • (4)0xABCL 对 ✅:0x 是十六进制前缀,A、B、C 都是合法的十六进制数字,末尾 L 是长整型后缀 → 整型常量,类型 long,值 2748。
  • (5).32E31 对 ✅:小数点前的数字可以省略,指数部分 E31 是完整的 → 实型常量,类型 double。
  • (6)087 错:以 0 开头就是八进制,而八进制数字只有 0~7,8 非法。gcc 报 invalid digit '8' in octal constant。
  • (7)0xL 错:0x 后面一个十六进制数字都没有。gcc 报 invalid suffix 'xL' on integer constant。
  • (8)003 对 ✅:八进制写法,值 3 → 整型常量,类型 int。
  • (9)0x12.5 错:十六进制浮点常量必须带二进制指数 p(要写成 0x12.5p0 这样),只跟一个小数点不行。gcc 报 hexadecimal floating constants require an exponent。
  • (10)077 对 ✅:八进制 77,值 63 → 整型常量,类型 int。
  • (11)11E 错:E 后面没有指数数字。gcc 报 exponent has no digits。
  • (12)056L 对 ✅:八进制 56 加 L 后缀,值 46 → 整型常量,类型 long。
  • (13)0. 对 ✅:小数点两侧只要有一侧有数字就行 → 实型常量,类型 double,值 0.0。
  • (14).0 对 ✅:同上 → 实型常量,类型 double。

编译探测结果:

真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):14 项逐个写一个 _Generic 探针打类型。能编译的 8 项是 -1E-31 → double、0xABCL → long、.32E31 → double、003 → int、077 → int、056L → long、0. → double、.0 → double。

另外 6 项编译不过,报错原文见上面逐项说明。

按 C23 的词法判据分类,结论与教材时代一致

教材是 C89/C90 时代的书,本题的分类在 C23 下没有变化。

C23 新增的写法在这 14 项里没有出现:二进制前缀 0b1010 的类型是 int,数位分隔符 1'000'000 的类型是 int(都实测过)。

反过来,0xABCL、056L、003、077 这四项在 -std=c89 下同样合法,类型同样是 long、long、int、int。全章一律按 gcc 15.2.0 的 -std=gnu23 给答案。

易错:

  1. 把 0xABCL 当成"写错的常量"划到两者都不是(它完全合法)
  2. 把 -1E-31 里那个负号算进常量本身,进而以为"常量不能带负号"
  3. 把 087 按十进制读成 87(这个写法根本编译不过)

3 · printf 排版格式设计

若有整型变量 i=1,j=2,k=3,u=47215;实型变量 x=2.2,y=3.4,z=-5.6;字符型变量 c1='a',c2='b'。试按以下排版格式用函数 printf() 组织输出。

i=1    j=2    k=3
x=2.20000,y=3.40000,z=-5.60000
x+y=5.60  y+z=-2.20  z+x=-3.40
u=47215
c1='a' or 97(ASCII)  c2='b' or 98(ASCII)
答案
#include <stdio.h>

int main(void)
{
    int i = 1, j = 2, k = 3, u = 47215;
    float x = 2.2f, y = 3.4f, z = -5.6f;
    char c1 = 'a', c2 = 'b';

    printf("i=%-5dj=%-5dk=%d\n", i, j, k);
    printf("x=%.5f,y=%.5f,z=%.5f\n", x, y, z);
    printf("x+y=%.2f  y+z=%.2f  z+x=%.2f\n", x + y, y + z, z + x);
    printf("u=%d\n", u);
    printf("c1='%c' or %d(ASCII)  c2='%c' or %d(ASCII)\n", c1, c1, c2, c2);
    return 0;
}
解析

排版格式是逐行反推格式串的,一行一句话:

  • 第 1 行 i=1 j=2 k=3:1 后面跟 4 个空格,说明数字左对齐、字段宽 5 → %-5d。k 在行尾,宽度无所谓,直接 %d 就行。
  • 第 2 行 x=2.20000,...:小数点后是 5 位 → %.5f。默认的 %f 是 6 位,会打成 2.200000。
  • 第 3 行 x+y=5.60 y+z=-2.20 z+x=-3.40:小数点后 2 位 → %.2f;三个式子之间是两个空格,格式串里照写两个空格。
  • 第 4 行 u=47215:直接 %d。
  • 第 5 行 c1='a' or 97(ASCII) c2='b' or 98(ASCII):同一个变量既要打字符又要打码值,所以 %c 和 %d 各取一遍,实参写成 c1, c1、c2, c2。

实测:这份程序在 gcc 15.2.0(-O2 -std=gnu23 -Wall)下编译运行,输出与题干样张逐行一字不差(按判题同款口径 normalize() 剥掉行尾空白后比对,5 行全部相同)。

易错:

  1. 第 1 行漏掉 -,写成 %5d 会变成右对齐,输出 1j=2 k=3
  2. 第 2 行用默认 %f,多打一位变 2.200000
  3. 第 5 行只写一个 c1,%d 就会去读后面的 c2

4 · %#x、%u、%o 的实际输出

设执行以下语句之前变量 i=100,k=-1,试确定以下格式输出函数调用的输出内容。

printf("i=%#x;k=%u,%o.\n",i,k,k);
答案
i=0x64;k=4294967295,37777777777.
解析

三个转换说明符对着两个变量:%#x 取 i,%u 取 k,%o 又取一遍 k。

逐行对照:

  • i=%#x:# 的作用是给十六进制加 0x 前缀。100 的十六进制是 64,加前缀就是 0x64。
  • k=%u:%u 把实参按 unsigned int 解释。int k = -1 的 32 位全是 1,当成无符号数就是 4294967295。
  • ,%o.:%o 同样按无符号解释,全 1 的 32 位写成八进制是 37777777777(11 个数字);后面那个 . 和换行是普通字符,照打。

真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):题干这行原样跑一次,输出为 i=0x64;k=4294967295,37777777777.,与上面逐项对上。

这个 # 只加前缀,不改数值

同一个 # 用在别的转换符上是加别的前缀:%#o 给八进制加前导 0(printf("%#o", 8) 得 010),%#X 把前缀和数字一起大写(printf("%#X", -1) 得 0XFFFFFFFF)。这三种写法都跑过(gcc 15.2.0,-O2 -std=gnu23 -Wall),数值部分一位都不变。

易错:

  1. 以为 %u 会打出 -1(%u 是"按无符号解释",不是"打符号")
  2. 以为 # 是补齐宽度的开关
  3. 看到 %d 与 %u 混用就觉得结果不确定——-1 按无符号解释是标准明确定义的行为,不是未定义

5 · printf 格式符与字段宽度

写出以下程序的输出结果。

#include<stdio.h>
int main()
{  int a=8,b=9;float x=127.895,y=-123.456;
   char c='B';long n=12345678L;unsigned u=65535u;
   printf("%d,%d\n",a,b);
   printf("%5d,%5d\n",a,b);
   printf("%f,%f\n",x,y);
   printf("%-12f,%-12f\n",x,y);
   printf("%8.3f,%8.3f,%.3f,%.3f,%4f,%5f\n",x,y,x,y,x,y);
   printf("%e,%10.4e\n",x,y);
   printf("%c,%d,%o,%x\n",c,c,c,c);
   printf("%ld,%lo,%lx\n",n,n,n);
   printf("%u,%o,%x,%d\n",u,u,u,u);
   printf("%s,%6.3s,%-10.5s\n","C language","C language","C language");
   return 0;
}
答案
8,9
    8,    9
127.894997,-123.456001
127.894997  ,-123.456001
 127.895,-123.456,127.895,-123.456,127.894997,-123.456001
1.278950e+02,-1.2346e+02
B,66,102,42
12345678,57060516,bc614e
65535,177777,ffff,65535
C language,   C l,C lan
解析

10 个 printf 各自独立,逐行把格式串和实参对一遍就行。题干程序在 gcc 15.2.0(-O2 -std=gnu23 -Wall)下原样编译无警告,下面是实测输出。

逐行对照:

  • 8,9:%d,%d,两个数都只有 1 位,宽度没起作用。
  • 8, 9:%5d 是"至少 5 位、右对齐",8 前面补 4 个空格。
  • 127.894997,-123.456001:%f 默认小数点后 6 位。这里不是 127.895000:float 存不下 127.895 的精确值,实际存进去的数略小于它,打到第 6 位小数就落在 127.894997 上。
  • 127.894997 ,-123.456001:%-12f 是左对齐、字段宽 12。127.894997 占 10 位补 2 个空格,-123.456001 占 11 位补 1 个空格。
  • 127.895,-123.456,127.895,-123.456,127.894997,-123.456001:前两个是 %8.3f,宽度 8、精度 3,127.895 占 7 位补 1 个空格,-123.456 正好 8 位不补;中间两个是 %.3f,只有精度没有宽度;最后两个是 %4f、%5f,宽度 4 和 5 比 %f 的实际长度(10 位、11 位)还小,宽度被忽略,退化成 %f 打出完整 6 位小数。
  • 1.278950e+02,-1.2346e+02:%e 默认 6 位小数;%10.4e 的精度是 4 位,-1.2346e+02 共 11 位已经超过宽度 10,宽度同样被忽略。
  • B,66,102,42:同一个 'B' 分别按字符、十进制、八进制、十六进制打,得到 B、66、102、42。
  • 12345678,57060516,bc614e:%ld、%lo、%lx。long 必须配 l 修饰符。
  • 65535,177777,ffff,65535:65535 没有超出 int 的正数范围,所以最后那个 %d 打出来还是 65535,不会变成负数。
  • C language, C l,C lan:%s 打全串;%6.3s 先按精度 3 截成 C l,再补到宽度 6(右对齐,前面 3 个空格);%-10.5s 先截成 5 个字符 C lan,再左对齐补到 10 位。

答案块里剥掉了两处行尾空白

两个左对齐的格式符会补空格:第 4 行 -123.456001 后面本来有 1 个空格,第 10 行 C lan 后面本来有 5 个空格。按判题口径 normalize(),行尾空白在比对时一律剥掉,所以这里也按剥掉后的样子列。

易错:

  1. 第 3 行容易错,float x=127.895 打出来是 127.894997,不是 127.895000
  2. 第 5 行以为 %4f 会把输出截成 4 个字符——宽度只管"不足时补齐",多了不截
  3. 第 10 行把 %6.3s 的 .3 当成宽度(它是精度,先截断再补宽度)
  4. 第 8 行漏写 l,用 %d 去打 long

手算:127.895 为什么打印成 127.894997

先对齐一下标准:

  • float → IEEE 754 binary32,单精度,32 位(1 位符号 + 8 位阶码 + 23 位尾数)
  • double → IEEE 754 binary64,双精度,64 位(1 位符号 + 11 位阶码 + 52 位尾数)
  • long double → 平台相关,x86 上通常是 80 位扩展精度,不完全是标准规定的

C 的 float 对应 IEEE 754 里的 binary32,也就是单精度二进制浮点数,占 32 位。这 32 位分三段:

字段 位数 含义
符号位 s 1 0 为正,1 为负
阶码 E 8 指数加上偏置 127
尾数 f 23 小数点后的有效位(隐含前导 1)

表示的数值是 (-1)^s × 1.f × 2^(E-127)。尾数前那个 1 是隐含的,不占这 23 位;阶码加偏置 127,是为了让正负指数都能按无符号数比较大小。

问题的根源是十进制小数未必是有限的二进制小数。

手算方法就是把小数部分不断乘 2、把整数位取出来:

0.895 × 2 = 1.79  → 取 1,余 0.79
0.790 × 2 = 1.58  → 取 1,余 0.58
0.580 × 2 = 1.16  → 取 1,余 0.16
0.160 × 2 = 0.32  → 取 0,余 0.32
0.320 × 2 = 0.64  → 取 0,余 0.64
0.640 × 2 = 1.28  → 取 1,余 0.28
                    …

取出来的整数位排起来是 0.1110010100011110…,往后每 20 位循环一次,不会终止。

加上整数部分 127 = 1111111,再规格化,得到 1.1111111110010100011110… × 2^6。于是阶码 E = 6 + 127 = 133,二进制是 10000101。

尾数只有 23 位,后面那些位只能舍入掉。这串尾数截到 23 位正好是 11111111100101000111101(第 24 位是 0,不必进位),存进去的数就是 127.8949966431,比字面的 127.895 小一点。

%f 默认保留 6 位小数,四舍五入到第 6 位就落在 127.894997 上,这就是打印出来的那行。

同理 float y = -123.456 打成 -123.456001,误差方向相反,来源完全一样。

所以本题整型那几行(%d、%o、%x)都是精确的,只有涉及 float 的两行被 IEEE 754 舍入过。

单次舍入的相对误差量级只有 2^-24,看着很小,但成千上万次运算会累积放大,两个相近的大数相减还可能把有效位数吃光(灾难性抵消),所以数值计算里常要改用 double,或者重排运算顺序。


6 · scanf 与 cin 输入语句

试按以下变量定义,分别用函数 scanf() 和 cin() 流编写为它们输入值的代码。

int i;char c;long k;float f;double x;
答案

scanf 版:

#include <stdio.h>

int main(void)
{
    int i; char c; long k; float f; double x;

    if (scanf("%d %c %ld %f %lf", &i, &c, &k, &f, &x) != 5)
        return 1;
    printf("i=%d c=%c k=%ld f=%f x=%f\n", i, c, k, f, x);
    return 0;
}

cin 版(C++):

#include <iostream>

int main()
{
    int i; char c; long k; float f; double x;

    std::cin >> i >> c >> k >> f >> x;
    std::cout << "i=" << i << " c=" << c << " k=" << k
              << " f=" << f << " x=" << x << std::endl;
    return 0;
}
解析

两组代码各干一件事:把 5 个变量按类型对应到转换说明符(scanf)或提取运算符(cin)。

关键点:

  1. scanf 的实参必须取地址(&i、&c……)。漏掉 & 就是把变量的值当地址传给 scanf,轻则读不到数据,重则写坏内存。
  2. 类型要和转换说明符一一对上:int 配 %d、char 配 %c、long 配 %ld、float 配 %f、double 配 %lf。scanf 里 %f 和 %lf 是有区别的,double 必须写 %lf。这一点和 printf 不同:printf 里 %lf 与 %f 等价。
  3. %c 前面留一个空格(" %c")。%c 不跳过空白,输入里 B 前面如果还有空格或换行,会被原样读进 c;格式串里加一个空格,由它先把空白跳掉,输入就能自由用空格分隔。
  4. cin 版不用管类型也不用取地址,>> 按变量的类型自动解析,而且默认跳过空白——所以 char 也不会误吃到空格。省事的地方就在这里。

实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):

真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):scanf 版喂入 2 B 123456 5.8 3.4,输出 i=2 c=B k=123456 f=5.800000 x=3.400000。

cin 版用 g++ 15.2.0(-O2 -std=gnu++23 -Wall)编译,同一份输入输出 i=2 c=B k=123456 f=5.8 x=3.4。这题混排了 C 和 C++:本站判题只收 C,所以以 scanf 版为准,cin 版知道怎么写就行。

易错:

  1. long 配 %d、double 配 %f(scanf 里 double 必须是 %lf)
  2. 漏掉 &
  3. 用 cin 时还去写格式串(cin 没有格式串)
  4. %c 前面不写空格,输入里一个空格就把 c 读成了空白字符

7 · scanf 格式串里的字面字符

若有以下变量定义:

int i;char c;long k;float f;double x;

要使这些变量分别有值:

i=2,c='B',k=123456,f=5.8,x=3.4

并有以下函数调用:

scanf("i=%d  c=%c",&i,&c);
scanf("k=%ld",&k);
scanf("f=%f  x=%lf",&f,&x);

试回答应如何输入。

答案

三段输入必须连成一行写,数字与后面的字面字符之间不能插空白:

i=2  c=Bk=123456f=5.8  x=3.4
解析

三段 scanf 的格式串里都带字面字符(i=、c=、k=、f=、x=)。

scanf 只有两种情况会跳过空白:格式串里的空白指令(一个或多个空格),以及数值转换说明符(%d、%ld、%f、%lf)自动跳过前面的空白。匹配字面字符时不跳过任何空白——这就是整道题的机关。

把 i=2 c=Bk=123456f=5.8 x=3.4 逐段走一遍:

  1. scanf("i=%d c=%c",&i,&c):字面 i 对上,%d 读走 2;格式串里那两个空格是空白指令,把输入里的两个空格跳掉;字面 c= 对上;%c 读走 B。返回 2。
  2. scanf("k=%ld",&k):输入指针这时停在 B 后面,下一个字符必须是 k,所以 k= 要紧接着 B 写。%ld 读走 123456 后停在 f 上。返回 1。
  3. scanf("f=%f x=%lf",&f,&x):同理,f= 必须紧接着 123456 写,对上后 %f 读走 5.8;%f 后面那两个空格是空白指令,把输入里剩下的空格跳掉;字面 x= 对上,%lf 读走 3.4。返回 2。

四种输入形式各跑一遍(每段后面打印 scanf 的返回值,返回值等于这一段的转换个数才算匹配成功):

输入 返回值 结果
i=2 c=B / k=123456 / f=5.8 x=3.4,分行写 r1=2 r2=0 r3=0 i=2 c=B k=0 f=0.000000 x=0.000000,后两段完全没读到
i=2 c=Bk=123456f=5.8 x=3.4 r1=2 r2=1 r3=2 5 个变量全部有值 ✅
i=2 c=Bk=123456f=5.8 x=3.4 r1=2 r2=1 r3=2 同上,i= 与 c= 之间少一个空格也行
i=2 c=Bk=123456 f=5.8 x=3.4 r1=2 r2=1 r3=0 123456 与 f= 之间多一个空格,第三段失配

真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):上表四组输入各跑一次,返回值与结果就是表里列的。另外试过把 i=2 与 c=B 之间的两个空格换成一个换行,同样通过(r1=2 r2=1 r3=2)——格式串里那两个空格是空白指令,会跳过任意多个空白,包括换行。

空白只能出现在两处

格式串里 %d 与 c= 之间、%f 与 x= 之间各有一对空格,它们是空白指令,输入在这两处放空格或换行都行。其余位置都是字面字符:B 后面必须紧跟 k=,123456 后面必须紧跟 f=,多一个空格就对不上,后面整段读不到。

易错:

  1. 照着题面的分段排版把输入写成三行,后两段直接失配
  2. 输入时习惯性在每个数后面敲一个空格,123456 f= 就会让第三段失配
  3. 以为 scanf 匹配字面字符前也会跳过空白

8 · ASCII 字符与码值对照表

试在计算机上运行以下程序。本程序输出 ASCII 码值为 31~126 的所有字符及其代码的程序,并记住一些常用字符的 ASCII 码。

#include<stdio.h>
int main()
{  int i,k=0;
   for(i=31;i<127;i++)
   {  if(k++%10==0) /*每行10个*/
         printf("\n");
      printf("%3c(%3d)",i,i);
   }
   printf("\n");
   return 0;
}
答案
  \x1f( 31)   ( 32)  !( 33)  "( 34)  #( 35)  $( 36)  %( 37)  &( 38)  '( 39)  (( 40)
  )( 41)  *( 42)  +( 43)  ,( 44)  -( 45)  .( 46)  /( 47)  0( 48)  1( 49)  2( 50)
  3( 51)  4( 52)  5( 53)  6( 54)  7( 55)  8( 56)  9( 57)  :( 58)  ;( 59)  <( 60)
  =( 61)  >( 62)  ?( 63)  @( 64)  A( 65)  B( 66)  C( 67)  D( 68)  E( 69)  F( 70)
  G( 71)  H( 72)  I( 73)  J( 74)  K( 75)  L( 76)  M( 77)  N( 78)  O( 79)  P( 80)
  Q( 81)  R( 82)  S( 83)  T( 84)  U( 85)  V( 86)  W( 87)  X( 88)  Y( 89)  Z( 90)
  [( 91)  \( 92)  ]( 93)  ^( 94)  _( 95)  `( 96)  a( 97)  b( 98)  c( 99)  d(100)
  e(101)  f(102)  g(103)  h(104)  i(105)  j(106)  k(107)  l(108)  m(109)  n(110)
  o(111)  p(112)  q(113)  r(114)  s(115)  t(116)  u(117)  v(118)  w(119)  x(120)
  y(121)  z(122)  {(123)  |(124)  }(125)  ~(126)
解析

96 个码值按每行 10 个打出来,每格固定 8 个字符。

逐行对照:

  • 循环跑 i = 31 到 i = 126,共 96 个字符。
  • 换行由 if(k++%10==0) printf("\n"); 控制:k 从 0 起,每轮先用 k 的原值判断、再自增;取值能被 10 整除时先打一个换行。第一轮 k = 0 就满足 0%10==0,所以输出以一个空行开头,此后每满 10 个字符换一行。
  • 每格 %3c(%3d):字符本身占 3 位(右对齐),码值占 3 位。一格 8 个字符,一行 10 格正好 80 个字符;96 个字符排成 9 整行加末行 6 个(y 到 ~)。
  • 常用区间记住就够用:32 是空格,48~57 是 0~9,65~90 是 A~Z,97~122 是 a~z。小写字母比对应的大写字母大 32,'a' - 'A' == 32 就是这么来的。
  • 几个容易看岔的格子:(( 40) 里前一个 ( 是字符本身、后一个 ( 是格式串带的括号;\( 92) 是反斜杠;^( 94) 是脱字符;'( 39) 是单引号。

真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):题干程序原样编译运行,输出共 779 字节,按行长度数是"1 个空行 + 9 行 × 80 字符 + 末行 48 字符",内容与上面答案块一致。

两个看着像空的格子,性质并不一样

码值 31 是控制字符 US,原样输出的那一个字节就是 0x1f,终端打不出来,这里按转义写法 \x1f 列出;码值 32 是空格字符,%3c 打出来确实是 3 个空格,其中只有最后一个位置是那个字符本身。两格都像是空的,但一个真有字符、一个真是空格。

易错:

  1. 把 k++%10==0 读成"先自增再判断"(是先用原值判断、再自增),据此数错换行位置
  2. 以为输出的第一行就是 ( 31),漏掉开头那个空行
  3. 把 %c 当成"打码值"(打字符用 %c,打码值用 %d)

9 · 给整型变量加 1 的多种写法

试尽量用多种不同的 C 代码,描述把整型变量 i 的值增加 1 的运算。

答案
i = i + 1;      /* 最直白:读出来加 1 再写回去 */
i += 1;         /* 复合赋值 */
i++;            /* 后缀自增 */
++i;            /* 前缀自增 */
i = 1 + i;      /* 加法交换律 */
i = i - (-1);   /* 减一个负数 */
i -= -1;        /* 复合赋值的减负数写法 */
i = -~i;        /* 按位取反两次:-~i 恒等于 i + 1 */
解析

让 i 增加 1,本质就是"读出 i、加 1、写回 i"三步,不同写法只是把这三步包装得不一样。

按包装方式分成三类,再加一个位运算的写法:

  1. 老老实实写全三步:i = i + 1;、i = 1 + i;、i = i - (-1);。
  2. 把"写回"这一步交给复合赋值:i += 1;、i -= -1;。op= 的语义本来就有"读出、按 op 算、写回"三件事。
  3. 用自增运算符:i++;、++i;。单独成一条语句时两者对 i 的效果一样,区别只在"表达式取到的值"上——i++ 取原值、++i 取新值。
  4. 位运算写法:i = -~i;。~i 是逐位取反,补码下 -~i 恒等于 i + 1。

真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):上面 8 种写法各跑一遍,i 的初值都设成 5,执行后全部是 6;整份写法合成一个程序送 -fsyntax-only,也是通过、无警告。

三种看着也能加 1 的写法都不能算

i = i++;、i = ++i;、i + 1; 都不要写进答案。前两种在同一个表达式里既读 i 又改 i、中间没有序列点,标准判为未定义行为(跟题 12 是同一回事),gcc 会给出 -Wsequence-point 警告;第三种算出来的值被丢掉了,i 一点没变。

易错:

  1. 写成 i + 1;(没有赋值,i 根本没变)
  2. 以为 i++; 与 ++i; 单独成语句时效果不同(一样)
  3. 把 i = -~i; 里的 - 当成减法(它是单目负号)

10 · 最紧凑的一条 C 代码

试分别用最紧凑的一条 C 代码描述完成下列要求的计算。

(1)把整型变量 i 和 j 的和赋给整型变量 k,并同时让 i 的值增加 1。

(2)把整型变量 x 值扩大一倍。

(3)在变量 i 减去 1 后,将变量 j 减去变量 i。

(4)计算变量 i 除以变量 j 的余数 r。

(5)将实型变量 x 精确到小数点后第三位四舍五入后的值赋给实型变量 y。

答案
序号 答案
(1) k = i++ + j;
(2) x *= 2;
(3) j -= --i;
(4) r = i % j;
(5) y = (int)(x * 1000 + 0.5) / 1000.0;
解析

五条都是一行,靠"赋值表达式的值"和"自增自减的前后缀"把两步并成一步。

逐项说明:

  • (1)k = i++ + j;:i++ 拿 i 的原值参与加法,这个式子求完值之后 i 自己加 1,一步到位。若写成 k = ++i + j;,i 会先把新值加进去,k 就多了 1。取 i = 3, j = 4 时得 i = 4, j = 4, k = 7。
  • (2)x *= 2; 就是 x = x * 2 的复合赋值写法。取 x = 2.5 时得 x = 5。
  • (3)j -= --i;:题干要求"先让 i 减 1,再用新的 i 去减 j",正对应前缀 --。取 i = 10, j = 3 时得 i = 9, j = -6。
  • (4)r = i % j;:取余只能用 %,两个操作数必须都是整型。取 i = 17, j = 5 时得 r = 2。
  • (5)y = (int)(x * 1000 + 0.5) / 1000.0;:乘 1000 把第三位小数顶到个位上,加 0.5 再用 (int) 截断,等价于对第三位四舍五入,最后除回 1000。

真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):x 取 1.23456、1.23444、2.34550,第(5)条分别得 y = 1.235、1.234、2.346,正是"看第三位小数四舍五入";五条式子合起来送 -fsyntax-only,通过、无警告。

第(5)条的两个边界

除回的时候必须写 1000.0,写成 1000 就变成整数除法,1.235 会被截成 1.0。

另外 (int) 的截断方向是"向零取整",负数要按绝对值四舍五入就得减 0.5:取 x = -1.23456 时,x * 1000 + 0.5 得 -1.234,x * 1000 - 0.5 得 -1.235。平时只考正数,知道这个边界就行。

易错:

  1. (1)用了 ++i 而不是 i++
  2. (3)前后缀搞反,写成 j -= i--;(那样减的是 i 的原值)
  3. (5)漏掉 0.5 只做截断,或者除回时用了整数 1000

11 · 数学式与逻辑条件的 C 表达式

试用 C 语言表达式描述以下数学计算式或逻辑条件。

(1)V=(4/3)πr³;

(2)R=1/(1/R1+1/R2);

(3)y=x⁵+x³+6;

(4)F=GM1M2/R2;

(5)sin(x)/x + |cos(πx/2)|;

(6)0<a<10;

(7)条件 x=1 与 y=2 有且只有一个成立。

答案

前置:#include <math.h>,圆周率自己定义 #define PI 3.1415926;链接时要带数学库(gcc ... -lm)。

序号 答案
(1) V = 4.0 / 3 * PI * r * r * r;
(2) R = 1.0 / (1.0 / R1 + 1.0 / R2);
(3) y = x * x * x * x * x + x * x * x + 6;
(4) F = G * M1 * M2 / (R * R);
(5) s = sin(x) / x + fabs(cos(PI * x / 2));
(6) a > 0 && a < 10
(7) (x == 1) != (y == 2)
解析

第(1)、(2)、(6)三个小题是同一个问题:C 的 / 在两个整型之间是整数除法,数学式子里的分数线不能照抄。

逐项说明:

  • (1)(4/3)πr³ 不能照抄成 4 / 3 * PI * r * r * r:4 / 3 是整数除法,结果是 1,整个体积就只剩 πr³。必须把其中一个操作数写成浮点,4.0 / 3 才是 1.333…。取 r = 3 时,写 4.0 / 3 得 113.097334(正好是 36π),而 4 / 3 单独算出来是 1。
  • (2)三个除法都得是浮点除法,所以写 1.0 /。也可以写成 R1 * R2 / (R1 + R2)(两电阻并联的常用形式),一样对。取 R1 = 10, R2 = 15 时得 6.000000。
  • (3)x 的五次方用连乘写,考试最稳;pow(x, 5) + pow(x, 3) + 6 也对,但要 #include <math.h>,而且 pow 返回 double。取 x = 2 时两种写法都得 46。
  • (4)题干转写为 F=GM1M2/R2,此处按万有引力公式 F = GM₁M₂/R² 理解,也就是 R 的平方。注意第(2)小题里 R1、R2 是电阻的变量名,所以 R2 也可能被读成"另一个变量";若按变量读,就直接写 G * M1 * M2 / R2。两种读法共同的一点是必须加括号:写成 G * M1 * M2 / R * R 会先除后乘,意思全变了。按 R * R 写、取 G = 6.67e-11, M1 = 2, M2 = 3, R = 4 时得 2.501250e-11。
  • (5)绝对值要用浮点版的 fabs(整型版是 abs,在 stdlib.h 里,对 double 用会截断)。π 用自己定的 PI。取 x = 2 时得 1.454649。
  • (6)0<a<10 在 C 里会被解析成 (0 < a) < 10:先算 0 < a 得 0 或 1,再拿这个 0 或 1 去和 10 比,结果永远是真。必须用 && 连起来写。取 a = -1 和 a = 11 时 0 < a < 10 都返回 1。
  • (7)"有且只有一个成立"就是异或,写成 (x == 1) != (y == 2) 最紧凑。也可以写 (x == 1) ^ (y == 2)(^ 是按位异或,两边都只有 0 和 1 时等同逻辑异或),或者展开成 (x == 1 && y != 2) || (x != 1 && y == 2)。(x, y) 取 (1, 2)、(1, 0)、(0, 2) 三组,!= 写法与 ^ 写法结果完全一致(0、1、1)。

真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):上面每个表达式都写进一个程序跑过一遍,数值就是正文里列的那些。第(6)条那行 gcc 还顺手给了两条警告:comparisons like 'X<=Y<=Z' do not have their mathematical meaning、comparison of constant '10' with boolean expression is always true——正好是从编译器角度印证"这么写没意义"。

易错:

  1. (1)的 4/3
  2. (2)漏掉括号写成 1.0 / 1.0 / R1 + 1.0 / R2
  3. (4)R * R 不加括号
  4. (5)用 abs 代替 fabs
  5. (6)把数学上的连写不等式直接搬到 C 里
  6. (7)用 && 去表达"有且只有一个"

12 · 同一表达式里两次自增

设在求以下表达式之前,整型变量 a 的值是 4,试指出在求了以下表达式之后,变量 a、b 和 c 的值。

(1)b=a*a++;

(2)c=++a+a;

答案
序号 答案
(1) a = 5,b = 20
(2) a = 5,c = 10
答案只是编译器的执行结果

需要注意:上面的答案只是编译器的执行结果.

按照C语言标准的规定,题目的两个语句其实都是未定义行为(Undefined Behavior)

解析

一、先讲结论

一个变量在同一个表达式里,既被"读"又被"改",而读和改之间没有先后顺序的保证,这种情况 C标准 规定为未定义行为。a*a++ 和 ++a+a 都属于这一类:

  • a*a++:左边的 a 是"读取",a++ 是"修改"
  • ++a+a:左边的 ++a 是"修改",右边的 a 是"读取"

读和改的先后顺序没有规定,写出来的结果自然没法保证。


二、直观:两种时间线

既然标准不管先后,就分别假设两种情况,看各自算出什么。

第(1)小题 b = a*a++,初始 a=4。关键在 a++ 里"加 1"这一步什么时候落地:

  • 时间线甲:先取左边的 a(=4),再执行 a++(取 4,再加 1),得到 4 × 4 = 16
  • 时间线乙:先执行 a++(a 变成 5),再取左边的 a(=5),得到 5 × 4 = 20

两条时间线都不违反 C 标准的约束,编译器选哪条都合法。答案栏的 b = 20 是时间线乙。


第(2)小题 c = ++a+a,初始 a=4。这里同样是两条时间线,差别在右边那个 a 什么时候被读到(++a 本身永远取值 5):

  • 时间线甲:先取右边的 a(=4),再执行 ++a(a 变成 5,取值 5),得到 4 + 5 = 9
  • 时间线乙:先执行 ++a(a 变成 5,取值 5),再取右边的 a(=5),得到 5 + 5 = 10

同样两条时间线都符合C标准,答案栏的 c = 10 是时间线乙。


三、语法树只定"怎么解析",不定"谁先算"

要说清怎么绑定,先看 C语言 从高到低的优先级顺序:

  1. 后缀类
    • 函数调用:foo()
    • 数组下标:arr[]
    • 成员访问:. ->
    • 后缀自增自减:i++ i--
  2. 一元类
    • 前缀自增自减:++i --i
    • 逻辑非和按位取反:! ~
    • 一元正负号:+ -
    • 解引用和取地址:* &
    • 求字节数:sizeof
    • 强制类型转换:(某类型)
  3. 乘除取余
    • 乘、除和取余:* / %
  4. 加减
    • 加和减:+ -
  5. 移位
    • 左移和右移:<< >>
  6. 关系比较
    • 小于和小于等于:< <=
    • 大于和大于等于:> >=
  7. 相等比较
    • 等于和不等于:== !=
  8. 按位与:&
  9. 按位异或:^
  10. 按位或:|
  11. 逻辑与:&&
  12. 逻辑或:||
  13. 条件运算符:?:
  14. 赋值运算符:= += -= *= /= %=
  15. 逗号运算符:,

本题用到的几个:

  • 后缀 i++ 在第 1 级
  • 前缀 ++i 在第 2 级
  • 乘法 * 在第 3 级
  • 加法 + 在第 4 级
  • 赋值运算符 = 在第 14 级

按这张表绑定,结果是唯一的:

b = a * a++    →    b = ( (a) * (a++) )
c = ++a + a    →    c = ( (++a) + (a) )

对应两棵语法树:

   =                      =
  / \                    / \
  b   *                  c   +
      / \                    / \
      a   ++                 ++  a
          |                   |
          a                   a

但要注意:语法树只描述表达式怎么拼起来,也就是计算顺序,而完全没有规定这些计算的实际读写顺序。

它不描述 a++ 对变量 a 的更新什么时候生效。


四、汇编:编译器确实在自己选顺序

下面给出编译器真实生成的汇编代码。

先解释几个基本概念:

  • 汇编是一条一条的指令,每条只干一件小事,CPU 从上往下逐条执行
  • 寄存器是 CPU 内部的临时读写空间,靠名字引用(a5、s0 这些),运算都需要在寄存器上做
  • 内存是程序放数据的地方;栈是内存里划出的一块,函数的临时变量放在这里
  • # 后面的是注释
int f(int a)
{
    int b;
    b = a * a++;
    return b;
}

得到汇编代码 RISC-V(gcc 15.2.0,-O0):

f:                              # 函数f
    ...                         # 函数对栈进行初始化(已省略)

    # 将参数(a)写入函数临时栈
    mv      a5,a0               # 参数(a) 由 寄存器[a0] 传进来,先搬到 寄存器[a5]
    sw      a5,-36(s0)          # 将 栈-36(s0) 作为 参数(a) 在当前函数作用域的存储位置

    # 从栈读参数(a),计算a++,然后将更新后的参数(a)写回函数栈
    lw      a5,-36(s0)          # 将 参数(a) 写入 寄存器[a5]
    addiw   a4,a5,1             # 寄存器[a4] = 寄存器[a5] + 1(5=4+1)
    sw      a4,-36(s0)          # 把更新后的 5 写回 参数(a)

    # 再从栈读参数(a),此时读到的是更新后的
    # 计算b=a*4,然后将参数(b)写回函数栈
    lw      a4,-36(s0)          # 再从内存读一次 参数(a)
    mulw    a5,a4,a5            # 寄存器[a5] 乘 寄存器[a4],也就是 4 × 5 = 20
    sw      a5,-20(s0)          # 把 20 存到 变量(b) 的位置

    # 从栈读变量(b),把它作为返回值放进寄存器[a0]
    lw      a5,-20(s0)          # 把 变量(b) 读进 寄存器[a5]
    mv      a0,a5               # 把约定返回值放进 寄存器[a0]

    ...                         # 函数对栈进行还原,并跳转回去(已省略)

可以看到,编译器先做了 a++,然后就马上把更新的值写入 参数 a。

之后就直接取了更新后的 参数 a,取到的值是 5,对应上面的"时间线乙"。

换一个编译器或换个优化等级,它完全可以先拿原始值计算,得到 b=16,然后再将自加 a++ 的更新写回,这正是"未定义"的实际含义。

平时自己写代码时,不要这样写

++、-- 尽量只用来做循环步进,比如 for (i = 0; i < n; i++),不要塞进赋值表达式里和别的运算混用。


13 · 前缀与后缀自增自减的区别

编写一个程序,示意前缀 ++ 和后缀 ++ 的区别,前缀 -- 和后缀 -- 的区别。

答案
#include <stdio.h>

int main(void)
{
    int i = 5, j;

    j = i++;
    printf("i++ : j=%d, i=%d\n", j, i);

    i = 5;
    j = ++i;
    printf("++i : j=%d, i=%d\n", j, i);

    i = 5;
    j = i--;
    printf("i-- : j=%d, i=%d\n", j, i);

    i = 5;
    j = --i;
    printf("--i : j=%d, i=%d\n", j, i);
    return 0;
}
解析

思路:四种写法各放进一次赋值,把"表达式取到的值"和"变量最后的值"分开打出来,区别就显示出来了。每轮开始前把 i 重置为 5,四行才有可比性。

关键点:

  1. i++(后缀):先把 i 的原值 5 交出去,i 再自增。所以 j 得 5,i 变成 6。
  2. ++i(前缀):i 先自增成 6,再把新值交出去。所以 j 和 i 都是 6。
  3. i--(后缀):先交出原值 5,i 再自减成 4。所以 j = 5,i = 4。
  4. --i(前缀):i 先自减成 4,再交出 4。所以 j = 4,i = 4。
  5. 单独成一条语句时(写成 i++;),前后缀对 i 的效果完全一样,都是加 1;区别只在"表达式取到的值"上。

真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):上面程序编译运行,输出四行 i++ : j=5, i=6、++i : j=6, i=6、i-- : j=5, i=4、--i : j=4, i=4,与前四条推演逐条吻合。

本章题干里 int main() 的写法照原样留着

教材是 C89/C90 年代的写法,题干里第 5、8 题写的是 int main()。标准从来只认 int main,而 C23 把空参数表定成了 (void),int main() 与 int main(void) 等价(gcc 15.2.0 实测:C23 下给这种函数传参会报 too many arguments to function,同一份代码在 C17 下不报),照原样在 -std=gnu23 下编译没有警告,所以那两题的题干不动。

本题没有给框架代码,答案按 int main(void) 写。

易错:

  1. 把两种写法塞进同一个表达式比较(比如 printf("%d %d", i++, ++i),那是未定义行为,见题 12)——比较前后缀要一次只放一个
  2. 忘了每轮重置 i,后几行的结果就不对了
  3. 以为后缀写法是"先自增再返回新值"

14 · 两整数的和差积商余

编写输入 2 个整数,输出这 2 个整数的和、差、积、商和余数的程序。

答案
#include <stdio.h>

int main(void)
{
    int a, b;

    printf("请输入两个整数: ");
    if (scanf("%d%d", &a, &b) != 2)
        return 0;

    printf("和: %d\n", a + b);
    printf("差: %d\n", a - b);
    printf("积: %d\n", a * b);
    if (b != 0) {
        printf("商: %d\n", a / b);
        printf("余数: %d\n", a % b);
    } else {
        printf("除数不能为 0,商与余数无意义\n");
    }
    return 0;
}
解析

思路:读两个整数,五项结果各打一行;商和余数要单独处理除数为 0 的情况。

关键点:

  1. scanf 的返回值是成功读到的项数,判断是不是 2 可以挡住空输入和非法输入。
  2. 商和余数用整型运算 a / b、a % b。别写成 a * 1.0 / b,那出来的是浮点数,和"商、余数"的整型语义不符。
  3. 除数可能为 0:整数除零在 C 里是未定义行为,实际运行通常直接崩溃,所以先 if (b != 0) 挡一道。

真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):输入 17 5,输出和 22、差 12、积 85、商 3、余数 2;输入 17 0,走到"除数不能为 0,商与余数无意义"那一支;输入 -17 5,输出和 -12、差 -22、积 -85、商 -3、余数 -2。

C 的整除是向零取整

负数上看得出这一点:-17 / 5 是 -3(不是数学上的 -4),-17 % 5 是 -2。这两条的结果都列在上面那次运行里。

所以恒等式 a == (a / b) * b + a % b 对负数也照样成立。

易错:

  1. 除数为 0 时直接做除法
  2. 把商写成浮点除法 a * 1.0 / b 却还用 %d 打(实参是 double、格式符是 %d,类型对不上)
  3. 以为 -17 / 5 会得到 -4(C 是向零截断,不是向下取整)

15 · 三整数的和、平均值与最值

编写输入 3 个整数,输出这 3 个数的和、平均值、最小值和最大值的程序。

答案
#include <stdio.h>

int main(void)
{
    int a, b, c, sum, max, min;

    printf("请输入三个整数: ");
    if (scanf("%d%d%d", &a, &b, &c) != 3)
        return 0;

    sum = a + b + c;

    max = a;
    if (b > max) max = b;
    if (c > max) max = c;

    min = a;
    if (b < min) min = b;
    if (c < min) min = c;

    printf("和: %d\n", sum);
    printf("平均值: %.2f\n", sum / 3.0);
    printf("最小值: %d\n", min);
    printf("最大值: %d\n", max);
    return 0;
}
解析

思路:和直接加;平均值用和除以 3.0;最大最小用逐个比较。

关键点:

  1. 平均值必须写 sum / 3.0,不能写 sum / 3。输入 1 2 2 时:sum / 3.0 得 1.67;写成 sum / 3 就成了整数除法 5 / 3 = 1,打出来是 1.00。
  2. 最值用逐个比较:先让 max 和 min 都等于 a,再依次和 b、c 比。三个数也可以直接两两比。
  3. 平均值用 %.2f 保留两位小数输出。

真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):输入 3 7 5,输出和 15、平均值 5.00、最小值 3、最大值 7;输入 1 2 2,输出和 5、平均值 1.67、最小值 1、最大值 2;输入 -3 -7 -5,输出和 -15、平均值 -5.00、最小值 -7、最大值 -3。

易错:

  1. 平均值写成 sum / 3(整数除法)
  2. 比较用的初值随手写成 0,输入全是负数时"最大值"就变成 0 了
  3. 求最大值只跟第一个数比一次(要依次和后面每一个比)

16 · 两整数的大小与整除关系

编写输入 2 个整数,输出它们之间的关系:小于、大于、等于、不等于、整除等。

答案
#include <stdio.h>

int main(void)
{
    int a, b;

    printf("请输入两个整数: ");
    if (scanf("%d%d", &a, &b) != 2)
        return 0;

    if (a < b)
        printf("%d 小于 %d\n", a, b);
    if (a > b)
        printf("%d 大于 %d\n", a, b);
    if (a == b)
        printf("%d 等于 %d\n", a, b);
    if (a != b)
        printf("%d 不等于 %d\n", a, b);

    if (b != 0) {
        if (a % b == 0)
            printf("%d 能被 %d 整除(%d / %d = %d)\n", a, b, a, b, a / b);
        else
            printf("%d 不能被 %d 整除(商 %d 余 %d)\n", a, b, a / b, a % b);
    } else {
        printf("%d 不能被 0 整除\n", a);
    }
    return 0;
}
解析

思路:几条"关系"各判各的、各打各的,最后单独判整除。

关键点:

  1. 每个关系用独立的 if,不要串成 else if。a > b 和 a != b 是可以同时成立的,用 else if 就会漏掉后面的结论。输入 12 5 时,"大于"和"不等于"两条都要打出来。
  2. 判整除要判 a % b == 0,不能拿 a / b 当判据——只要 b 不是 0,a / b 总能算出个整数,区分不出能不能整除。
  3. 判整除之前先把 b == 0 挡掉。

真机实测(gcc 15.2.0,-O2 -std=gnu23 -Wall):输入 12 5,输出"12 大于 5"、"12 不等于 5"、"12 不能被 5 整除(商 2 余 2)";输入 15 5,输出"15 大于 5"、"15 不等于 5"、"15 能被 5 整除(15 / 5 = 3)";输入 5 5,输出"5 等于 5"、"5 能被 5 整除(5 / 5 = 1)";输入 3 12,输出"3 小于 12"、"3 不等于 12"、"3 不能被 12 整除(商 0 余 3)";输入 12 0,输出"12 大于 0"、"12 不等于 0"、"12 不能被 0 整除"。

易错:

  1. 用 else if 把几条关系串成互斥
  2. 拿 a / b 当整除判据
  3. b == 0 的时候直接去算 a % b

习题自测登记:完成本章习题后在此登记