第二章 · 基本数据类型及其运算¶
本页最后更新于 ,内容可能过时
《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。
本题问的是"用八进制整数和十六进制数表示这个数",属于数学上的表示法,所以答案按"绝对值转换 + 负号"给;机器里的位模式是另一回事,两种口径别混。
易错:
- 拆 2 499 时漏掉 8 位上的 0,把
4703写成473- 负数去查补码,把
-53写成37777777713- 十六进制里 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 给答案。
易错:
- 把
0xABCL当成"写错的常量"划到两者都不是(它完全合法)- 把
-1E-31里那个负号算进常量本身,进而以为"常量不能带负号"- 把
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 行漏掉
-,写成%5d会变成右对齐,输出1j=2 k=3- 第 2 行用默认
%f,多打一位变2.200000- 第 5 行只写一个
c1,%d就会去读后面的c2
4 · %#x、%u、%o 的实际输出¶
设执行以下语句之前变量 i=100,k=-1,试确定以下格式输出函数调用的输出内容。
解析
三个转换说明符对着两个变量:%#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),数值部分一位都不变。
易错:
- 以为
%u会打出-1(%u是"按无符号解释",不是"打符号")- 以为
#是补齐宽度的开关- 看到
%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;
}
答案
解析
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(),行尾空白在比对时一律剥掉,所以这里也按剥掉后的样子列。
易错:
- 第 3 行容易错,
float x=127.895打出来是127.894997,不是127.895000- 第 5 行以为
%4f会把输出截成 4 个字符——宽度只管"不足时补齐",多了不截- 第 10 行把
%6.3s的.3当成宽度(它是精度,先截断再补宽度)- 第 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() 流编写为它们输入值的代码。
答案
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++):
解析
两组代码各干一件事:把 5 个变量按类型对应到转换说明符(scanf)或提取运算符(cin)。
关键点:
scanf的实参必须取地址(&i、&c……)。漏掉&就是把变量的值当地址传给scanf,轻则读不到数据,重则写坏内存。- 类型要和转换说明符一一对上:
int配%d、char配%c、long配%ld、float配%f、double配%lf。scanf里%f和%lf是有区别的,double必须写%lf。这一点和printf不同:printf里%lf与%f等价。 %c前面留一个空格(" %c")。%c不跳过空白,输入里B前面如果还有空格或换行,会被原样读进c;格式串里加一个空格,由它先把空白跳掉,输入就能自由用空格分隔。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版知道怎么写就行。易错:
long配%d、double配%f(scanf里double必须是%lf)- 漏掉
&- 用
cin时还去写格式串(cin没有格式串)%c前面不写空格,输入里一个空格就把c读成了空白字符
7 · scanf 格式串里的字面字符¶
若有以下变量定义:
要使这些变量分别有值:
并有以下函数调用:
试回答应如何输入。
解析
三段 scanf 的格式串里都带字面字符(i=、c=、k=、f=、x=)。
scanf 只有两种情况会跳过空白:格式串里的空白指令(一个或多个空格),以及数值转换说明符(%d、%ld、%f、%lf)自动跳过前面的空白。匹配字面字符时不跳过任何空白——这就是整道题的机关。
把 i=2 c=Bk=123456f=5.8 x=3.4 逐段走一遍:
scanf("i=%d c=%c",&i,&c):字面i对上,%d读走2;格式串里那两个空格是空白指令,把输入里的两个空格跳掉;字面c=对上;%c读走B。返回 2。scanf("k=%ld",&k):输入指针这时停在B后面,下一个字符必须是k,所以k=要紧接着B写。%ld读走123456后停在f上。返回 1。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=,多一个空格就对不上,后面整段读不到。
易错:
- 照着题面的分段排版把输入写成三行,后两段直接失配
- 输入时习惯性在每个数后面敲一个空格,
123456 f=就会让第三段失配- 以为
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 个空格,其中只有最后一个位置是那个字符本身。两格都像是空的,但一个真有字符、一个真是空格。
易错:
- 把
k++%10==0读成"先自增再判断"(是先用原值判断、再自增),据此数错换行位置- 以为输出的第一行就是
( 31),漏掉开头那个空行- 把
%c当成"打码值"(打字符用%c,打码值用%d)
9 · 给整型变量加 1 的多种写法¶
试尽量用多种不同的 C 代码,描述把整型变量 i 的值增加 1 的运算。
答案
解析
让 i 增加 1,本质就是"读出 i、加 1、写回 i"三步,不同写法只是把这三步包装得不一样。
按包装方式分成三类,再加一个位运算的写法:
- 老老实实写全三步:
i = i + 1;、i = 1 + i;、i = i - (-1);。 - 把"写回"这一步交给复合赋值:
i += 1;、i -= -1;。op=的语义本来就有"读出、按op算、写回"三件事。 - 用自增运算符:
i++;、++i;。单独成一条语句时两者对i的效果一样,区别只在"表达式取到的值"上——i++取原值、++i取新值。 - 位运算写法:
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 一点没变。
易错:
- 写成
i + 1;(没有赋值,i根本没变)- 以为
i++;与++i;单独成语句时效果不同(一样)- 把
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)用了
++i而不是i++- (3)前后缀搞反,写成
j -= i--;(那样减的是i的原值)- (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)的
4/3- (2)漏掉括号写成
1.0 / 1.0 / R1 + 1.0 / R2- (4)
R * R不加括号- (5)用
abs代替fabs- (6)把数学上的连写不等式直接搬到 C 里
- (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语言 从高到低的优先级顺序:
- 后缀类
- 函数调用:
foo() - 数组下标:
arr[] - 成员访问:
.-> - 后缀自增自减:
i++i--
- 函数调用:
- 一元类
- 前缀自增自减:
++i--i - 逻辑非和按位取反:
!~ - 一元正负号:
+- - 解引用和取地址:
*& - 求字节数:
sizeof - 强制类型转换:
(某类型)
- 前缀自增自减:
- 乘除取余
- 乘、除和取余:
*/%
- 乘、除和取余:
- 加减
- 加和减:
+-
- 加和减:
- 移位
- 左移和右移:
<<>>
- 左移和右移:
- 关系比较
- 小于和小于等于:
<<= - 大于和大于等于:
>>=
- 小于和小于等于:
- 相等比较
- 等于和不等于:
==!=
- 等于和不等于:
- 按位与:
& - 按位异或:
^ - 按位或:
| - 逻辑与:
&& - 逻辑或:
|| - 条件运算符:
?: - 赋值运算符:
=+=-=*=/=%= - 逗号运算符:
,
本题用到的几个:
- 后缀
i++在第 1 级 - 前缀
++i在第 2 级 - 乘法
*在第 3 级 - 加法
+在第 4 级 - 赋值运算符
=在第 14 级
按这张表绑定,结果是唯一的:
对应两棵语法树:
但要注意:语法树只描述表达式怎么拼起来,也就是计算顺序,而完全没有规定这些计算的实际读写顺序。
它不描述 a++ 对变量 a 的更新什么时候生效。
四、汇编:编译器确实在自己选顺序
下面给出编译器真实生成的汇编代码。
先解释几个基本概念:
- 汇编是一条一条的指令,每条只干一件小事,CPU 从上往下逐条执行
- 寄存器是 CPU 内部的临时读写空间,靠名字引用(
a5、s0这些),运算都需要在寄存器上做 - 内存是程序放数据的地方;栈是内存里划出的一块,函数的临时变量放在这里
#后面的是注释
得到汇编代码 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 · 前缀与后缀自增自减的区别¶
编写一个程序,示意前缀 ++ 和后缀 ++ 的区别,前缀 -- 和后缀 -- 的区别。
答案
解析
思路:四种写法各放进一次赋值,把"表达式取到的值"和"变量最后的值"分开打出来,区别就显示出来了。每轮开始前把 i 重置为 5,四行才有可比性。
关键点:
i++(后缀):先把i的原值 5 交出去,i再自增。所以j得 5,i变成 6。++i(前缀):i先自增成 6,再把新值交出去。所以j和i都是 6。i--(后缀):先交出原值 5,i再自减成 4。所以j = 5,i = 4。--i(前缀):i先自减成 4,再交出 4。所以j = 4,i = 4。- 单独成一条语句时(写成
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) 写。
易错:
- 把两种写法塞进同一个表达式比较(比如
printf("%d %d", i++, ++i),那是未定义行为,见题 12)——比较前后缀要一次只放一个- 忘了每轮重置
i,后几行的结果就不对了- 以为后缀写法是"先自增再返回新值"
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 的情况。
关键点:
scanf的返回值是成功读到的项数,判断是不是 2 可以挡住空输入和非法输入。- 商和余数用整型运算
a / b、a % b。别写成a * 1.0 / b,那出来的是浮点数,和"商、余数"的整型语义不符。 - 除数可能为 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 对负数也照样成立。
易错:
- 除数为 0 时直接做除法
- 把商写成浮点除法
a * 1.0 / b却还用%d打(实参是double、格式符是%d,类型对不上)- 以为
-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;最大最小用逐个比较。
关键点:
- 平均值必须写
sum / 3.0,不能写sum / 3。输入1 2 2时:sum / 3.0得1.67;写成sum / 3就成了整数除法5 / 3 = 1,打出来是1.00。 - 最值用逐个比较:先让
max和min都等于a,再依次和b、c比。三个数也可以直接两两比。 - 平均值用
%.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。易错:
- 平均值写成
sum / 3(整数除法)- 比较用的初值随手写成 0,输入全是负数时"最大值"就变成 0 了
- 求最大值只跟第一个数比一次(要依次和后面每一个比)
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;
}
解析
思路:几条"关系"各判各的、各打各的,最后单独判整除。
关键点:
- 每个关系用独立的
if,不要串成else if。a > b和a != b是可以同时成立的,用else if就会漏掉后面的结论。输入12 5时,"大于"和"不等于"两条都要打出来。 - 判整除要判
a % b == 0,不能拿a / b当判据——只要b不是 0,a / b总能算出个整数,区分不出能不能整除。 - 判整除之前先把
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 整除"。易错:
- 用
else if把几条关系串成互斥- 拿
a / b当整除判据b == 0的时候直接去算a % b
习题自测登记:完成本章习题后在此登记