第四次作业
2026 高级语言程序设计 · 第四次作业
课程链接:https://edu.cnblogs.com/campus/fzu/2026gjyycxsj
作业要求链接:https://edu.cnblogs.com/campus/fzu/2026gjyycxsj/homework/16750
学号:052401231
姓名:黄思扬
〇、说明
本次作业使用教材《C Primer Plus(第 6 版)中文版》(Stephen Prata 著,人民邮电出版社)。
所有程序均在 Visual Studio 2022(MSVC,C11) 下编译运行,编译命令 cl /utf-8 /EHsc,无警告。
一、运行程序并给出结果
1.1 对用户输入的整数求和
题目:运行下列程序,给出程序结果。程序功能是对用户输入的整数求和,输入 q 结束。
运行过程(依次输入 1、2、3、q):
Please enter an integer to be summed (q to quit): 1
Please enter next integer (q to quit): 2
Please enter next integer (q to quit): 3
Please enter next integer (q to quit): q
Those integers sum to 6.

结果分析:
- 程序用
status = scanf("%ld", &num)的返回值控制循环。scanf成功读入一个整数时返回1,读不到(例如输入q)时返回0,因此while (status == 1)会一直循环。 - 输入
1 2 3后sum = 6,输入q时scanf失败,循环退出,打印总和。 - 需要注意:如果一开始就输入
q,sum初始化为0L,会输出Those integers sum to 0.,说明sum = 0L的初始化不能省。
本次遇到的问题及解决:
第一次运行时我按了回车换行输入,发现提示语连在一起看不清。后来明白控制台会把提示语和输入混排,只要在交互时按提示输入即可;批处理重定向输入时提示语会全部挤在一行,这是正常的缓冲表现,不是程序错误。
1.2 猜测 π 的值
题目:运行程序,给出结果。程序不断让用户猜 π 的值,直到猜的值与 3.14159 相差不超过 0.0001。
运行过程(第一次输入 3.14,提示 Try again!;第二次输入 3.14159,提示 Close enough!):
What is the value of pi?
3.14
Try again!
3.14159
Close enough!

结果分析:
const double ANSWER = 3.14159;定义了标准答案,fabs(response - ANSWER) > 0.0001是继续循环的条件。3.14与3.14159的差约为0.00159 > 0.0001,所以提示Try again!;输入3.14159时差为0,循环结束。- 若想验证精度的边界,可以输入
3.1415(差 0.00009 < 0.0001,会直接通过)和3.1416(差 0.00001,也通过),而3.1417(差 0.00011)会失败。
本次遇到的问题及解决:
程序开头 #include <math.h>,在 VS 里如果只写 #include <stdio.h> 会报 fabs 未声明,补上 math.h 即可。另外 scanf("%lf", &response) 中 %lf 不能写成 %f,否则读入 double 会出错。
二、用 while 和 do while 分别求 π 的近似值
题目:用 while 和 do while 分别设计程序实现:用公式
π/4 = 1 - 1/3 + 1/5 - 1/7 + 1/9 - ...
求 π 的近似值,直到某一项的绝对值小于 10⁻⁴ 为止,对两个程序的不同之处进行讨论。
2.1 while 版本
#include <stdio.h>
int main(void)
{
double pi4 = 0.0, term; /* pi4 存 π/4 的累加和 */
int i = 1, sign = 1; /* i 为分母,sign 为符号 */
term = 1.0 / i;
while (term >= 1e-4) /* 先判断:项不够小才进入循环 */
{
pi4 += sign * term;
sign = -sign; /* 符号交替 */
i += 2; /* 分母每次 +2,得到奇数序列 */
term = 1.0 / i;
}
printf("pi (while) = %.6f\n", pi4 * 4);
return 0;
}
运行结果:
pi (while) = 3.141393

2.2 do while 版本
#include <stdio.h>
int main(void)
{
double pi4 = 0.0, term;
int i = 1, sign = 1;
do
{
term = 1.0 / i;
pi4 += sign * term;
sign = -sign;
i += 2;
} while (1.0 / (i - 2) >= 1e-4); /* 先执行一次,再判断刚加进去的项 */
printf("pi (do-while) = %.6f\n", pi4 * 4);
return 0;
}
运行结果:
pi (do-while) = 3.141793

2.3 两个程序的不同之处(讨论)
| 对比项 | while 版本 | do while 版本 |
|---|---|---|
| 判断与执行的顺序 | 先判断,后执行 | 先执行,后判断 |
| 最少执行次数 | 0 次(条件一开始就不满足时) | 至少 1 次 |
| 判断条件写的位置 | 循环前先算一次 term,循环体末尾再更新 |
循环体末尾统一判断 |
| 本例运行结果 | 3.141393 |
3.141793 |
| 与真实值 π=3.141593 的误差 | 约 -2.0×10⁻⁴ |
约 +2.0×10⁻⁴ |
为什么结果不一样?
while 在进入循环前就判断 term >= 1e-4,当 1/i 第一次小于 10⁻⁴(即 i = 10001)时立刻退出,最后一项 1/9999 是加上去的,1/10001 没加。
do while 则是先把 1/10001 加进去,再判断这一项是否已经小于 10⁻⁴,等于多算了一项。由于这是交错级数,多算一项会让误差从真实值的一侧跑到另一侧,所以两个结果一个偏小、一个偏大,误差都约等于 2×10⁻⁴(正好是半个末项的量级)。
结论:这题想表达的是「求到某一项小于精度为止」——按通常的语义应该是 先判断、后累加,即 while 版本更符合题意;do while 版本会多累加一项。如果要用 do while 得到一致结果,应把判断条件改成「刚加进去的那一项」的绝对值:
do {
term = 1.0 / i;
if (term < 1e-4) break; /* 加上这一句,效果与 while 一致 */
pi4 += sign * term;
...
} while (1);
本次遇到的问题及解决:
最初把 i += 2 写在了 pi4 累加之前,导致分母跳过了 1,结果只有 3.0 左右。改成「先累加、再改分母」后结果正常。另外 1/i 中 i 是 int,必须写成 1.0 / i,否则整数除法结果全是 0,这是我调试时花了最久的一个点。
三、逻辑表达式中的短路求值
题目:设计一个程序,实现功能:逻辑表达式中的运算符,只有会影响表达式求值时,才会执行。
#include <stdio.h>
int main(void)
{
int a = 0, b = 5;
/* && 左边为假,右边不再计算,避免 b/a 除零 */
if (a != 0 && b / a > 1)
printf("a != 0 && b/a > 1 is true\n");
else
printf("short-circuit: b/a was NOT evaluated, no divide-by-zero\n");
/* || 左边为真,右边不再计算 */
if (a == 0 || b / a > 1)
printf("a == 0 || ... is true, right side skipped\n");
/* 条件运算符同样只求值一个分支 */
printf("result = %d\n", (a != 0) ? b / a : -1);
return 0;
}
运行结果:
short-circuit: b/a was NOT evaluated, no divide-by-zero
a == 0 || ... is true, right side skipped
result = -1

说明:
&&:左操作数为假(a != 0为假)时,整个表达式必为假,右操作数不会再计算。所以b / a根本不会执行,避免了除零崩溃。||:左操作数为真(a == 0为真)时,整个表达式必为真,右操作数同样被跳过。?:三目运算符也只对「被选中的那个分支」求值,a != 0为假,所以取-1,b / a不执行。- 如果把
a改成1,那么b / a就会真正参与计算,输出会变成a != 0 && b/a > 1 is true。这说明运算符是否被执行,取决于左边的结果。
本次遇到的问题及解决:
一开始把条件写成 if (a != 0 & b / a > 1)(少写一个 &),程序直接崩溃(除零)。查了才知道单个 & 是按位与,没有短路特性,两边都会求值。改成 && 后正常,这个问题让我真正理解了「逻辑与」和「按位与」的区别。
四、用 for 循环输出六行字符
题目:用 for 循环输出六行,第一行有 FEDCBA,第二行是 FEDCB,第三行是 FEDC,以此类推。
#include <stdio.h>
int main(void)
{
int row;
char ch;
for (row = 0; row < 6; row++) /* 控制 6 行 */
{
for (ch = 'F'; ch >= 'A' + row; ch--) /* 每行从 F 递减到 A+row */
putchar(ch);
putchar('\n');
}
return 0;
}
运行结果:
FEDCBA
FEDCB
FEDC
FED
FE
F

说明:外层循环控制行数,内层循环的终止条件 ch >= 'A' + row 随着行号增大而右移,所以每行少输出一个字符。字符在 C 语言里就是 ASCII 码整数,可以直接比较和自减。
本次遇到的问题及解决:
一开始内层写成 for (ch = 'A'; ch <= 'F'; ch++),每行都一样。后来意识到「每行的结尾在变化」,把 row 引入终止条件才对。
五、计算 s = 1+(1+2)+(1+2+3)+…+(1+2+…+n)
题目:编程输入 n,计算 s = 1 + (1+2) + (1+2+3) + … + (1+2+3+…+n)。
#include <stdio.h>
int main(void)
{
int n, i;
long long inner = 0, s = 0;
printf("n = ");
scanf("%d", &n);
for (i = 1; i <= n; i++)
{
inner += i; /* inner 就是 1+2+...+i */
s += inner; /* 把每个括号内的和累加起来 */
}
printf("s = %lld\n", s);
return 0;
}
运行结果(n = 5):
n = 5
s = 35

验证:1 + 3 + 6 + 10 + 15 = 35,结果正确。
说明:不需要嵌套两层循环,用 inner 这个「滚动变量」保存当前括号的和,每次加上 i 再累加到 s,时间复杂度 O(n)。注意 s 用 long long,因为 n 大时结果会溢出 int(n=100 时 s≈171700,n=1000 时已经超过 int 范围)。
本次遇到的问题及解决:
一开始写了双重循环,虽然结果对,但效率低。后来改用 inner 滚动累加,代码短了一半。另外用 %d 输出 long long 会出错,必须用 %lld。
六、用 for 循环给整型数组赋值、输出并求和
题目:用 for 循环给一组整型数组赋值、输出,以及求和,数组长度为 8。
#include <stdio.h>
int main(void)
{
int a[8];
int i, sum = 0;
for (i = 0; i < 8; i++) /* 赋值:3, 6, 9, ... */
a[i] = (i + 1) * 3;
for (i = 0; i < 8; i++) /* 输出并求和 */
{
printf("%4d", a[i]);
sum += a[i];
}
printf("\nsum = %d\n", sum);
return 0;
}
运行结果:
3 6 9 12 15 18 21 24
sum = 108

说明:数组下标从 0 开始,所以 a[0] 到 a[7] 共 8 个元素。循环条件必须是 i < 8 而不是 i <= 8,否则越界。
本次遇到的问题及解决:
一开始写 for (i = 1; i <= 8; i++) a[i] = ...,数组越界写到了 a[8],程序虽然没崩但结果乱掉。改成 i = 0; i < 8 后正常。这次让我记住了 C 语言数组不检查越界,必须自己控制。
七、多分支程序:if-else / switch / 条件运算符
题目:设计一个 if 和 else 的多分支程序(分支数目大于 3),再用 switch 语句改写,同时设计一个使用条件运算符 ?: 的双分支语句。
#include <stdio.h>
int main(void)
{
int score, level;
const char *name;
printf("score = ");
scanf("%d", &score);
/* ① if-else 多分支(5 个分支) */
if (score >= 90) level = 5;
else if (score >= 80) level = 4;
else if (score >= 70) level = 3;
else if (score >= 60) level = 2;
else level = 1;
/* ② switch 改写 */
switch (level)
{
case 5: name = "A"; break;
case 4: name = "B"; break;
case 3: name = "C"; break;
case 2: name = "D"; break;
default: name = "E"; break;
}
printf("if-else level = %d, switch grade = %s\n", level, name);
/* ③ 条件运算符的双分支 */
printf("ternary: %s\n", (score >= 60) ? "PASS" : "FAIL");
return 0;
}
运行结果(输入 85):
score = 85
if-else level = 4, switch grade = B
ternary: PASS

说明:
if-else适合区间判断(score >= 90这种范围条件),switch只能对整型/字符型的等值判断,所以这里先把分数映射成level,再用switch。- 每个
case后面的break不能丢,否则会「穿透」执行下一个分支。 ?:只适合两个分支的情况,写起来比if-else简洁。
本次遇到的问题及解决:
switch 里一开始漏了 break,输入 85 时输出了 B C D E 四个结果(穿透)。补上 break 后正确。另外 switch 的表达式不能是 double,这也是它和 if 的一个重要区别。
八、循环中 break 与 continue 的区别
题目:设计一个循环程序,在循环中用 break 和 continue 语句,实现不同的程序效果,给出结果并讨论。
#include <stdio.h>
int main(void)
{
int i;
printf("break demo: ");
for (i = 1; i <= 10; i++)
{
if (i == 6) break; /* 到 6 直接结束整个循环 */
printf("%d ", i);
}
printf("\ncontinue demo: ");
for (i = 1; i <= 10; i++)
{
if (i % 2 == 0) continue; /* 偶数跳过本次循环,进入下一次 */
printf("%d ", i);
}
printf("\n");
return 0;
}
运行结果:
break demo: 1 2 3 4 5
continue demo: 1 3 5 7 9

讨论:
| 语句 | 作用 | 循环变量 | 本例表现 |
|---|---|---|---|
break |
终止整个循环,直接跳出 | 不再递增 | 输出 1 2 3 4 5,到 6 就彻底停了 |
continue |
跳过本次循环剩下的语句,直接进入下一次判断 | 照常递增 | 偶数被跳过,输出所有奇数 |
break影响的是循环本身,continue只影响当前这一轮。- 在
for循环里,continue之后仍会执行i++和条件判断,所以不会死循环;但在while循环里如果continue前没有更新条件变量,就会死循环,这是常见的坑。
本次遇到的问题及解决:
把 break 和 continue 混用过一次,写成了 if (i == 6) continue;,结果只是少输出 6,循环还在继续跑。对比输出才明白两者的区别。
九、求 100~200 之间的全部素数
题目:设计一个程序求 100~200 间的全部素数,给出算法思想、传统流程图、程序 NS 流程图。
9.1 算法思想
- 依次取
n = 100, 101, …, 200。 - 对每个
n,用i = 2, 3, …, √n去试除:只要存在某个i能整除n,则n不是素数,立即置标志isPrime = 0并break跳出内层循环。 - 内层循环结束后检查标志:如果
isPrime仍为 1,说明没有找到因子,n是素数,输出它并计数。 - 只需试到 √n,因为若
n = a × b且a ≤ b,必有a ≤ √n。这把内层循环从 O(n) 降到 O(√n),是本题最关键的一步优化。
9.2 程序代码
#include <stdio.h>
int main(void)
{
int n, i, cnt = 0;
for (n = 100; n <= 200; n++)
{
int isPrime = 1; /* 先假设是素数 */
for (i = 2; i * i <= n; i++) /* 只试到 √n */
{
if (n % i == 0)
{
isPrime = 0; /* 找到因子,不是素数 */
break;
}
}
if (isPrime)
{
printf("%d ", n);
cnt++;
}
}
printf("\ntotal = %d\n", cnt);
return 0;
}
运行结果:
101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199
total = 21

9.3 传统流程图
博文不支持 mermaid 时,请把上面这段替换成自己画的流程图截图。
9.4 NS 流程图(Nassi-Shneiderman 图)
┌──────────────────────────────────────────────┐
│ n = 100, cnt = 0 │
├──────────────────────────────────────────────┤
│ while n <= 200 │
│ ┌──────────────────────────────────────────┐ │
│ │ isPrime = 1 , i = 2 │ │
│ ├──────────────────────────────────────────┤ │
│ │ while i * i <= n │ │
│ │ ┌──────────────────────────────────────┐ │ │
│ │ │ if n % i == 0 │ │ │
│ │ │ ┌───────────────┬──────────────────┐ │ │ │
│ │ │ │ 是 │ 否 │ │ │ │
│ │ │ ├───────────────┼──────────────────┤ │ │ │
│ │ │ │ isPrime = 0 │ i = i + 1 │ │ │ │
│ │ │ │ break │ │ │ │ │
│ │ │ └───────────────┴──────────────────┘ │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ ├──────────────────────────────────────────┤ │
│ │ if isPrime == 1 │ │
│ │ ┌───────────────┬──────────────────────┐ │ │
│ │ │ 是 │ 否 │ │ │
│ │ ├───────────────┼──────────────────────┤ │ │
│ │ │ 输出 n │ (空) │ │ │
│ │ │ cnt = cnt + 1 │ │ │ │
│ │ └───────────────┴──────────────────────┘ │ │
│ ├──────────────────────────────────────────┤ │
│ │ n = n + 1 │ │
│ └──────────────────────────────────────────┘ │
├──────────────────────────────────────────────┤
│ 输出 cnt │
└──────────────────────────────────────────────┘
流程图见上方 mermaid 代码块;如博客不支持 mermaid,可在 https://mermaid.live 粘贴代码导出 PNG 后插入这里。
本次遇到的问题及解决:
最初内层循环写成 i <= n / 2,结果虽然正确但循环次数多了一倍多;改成 i * i <= n 后效率明显提升。注意这里用 i * i <= n 而不是 i <= sqrt(n),是为了避免引入 math.h 和浮点误差,同时也不会溢出(i 最大只到 15 左右)。
十、教材第 6 章 6.16 编程练习 第 1、5、7、10、13、16、18 题
10.1 习题 6.1 —— 26 个字母的数组
题目:创建一个包含 26 个元素的数组,存储 26 个小写字母,然后打印数组的所有内容。
#include <stdio.h>
int main(void)
{
char letters[26];
int i;
for (i = 0; i < 26; i++) /* 存入 a~z */
letters[i] = 'a' + i;
for (i = 0; i < 26; i++) /* 一行打印 */
printf("%c", letters[i]);
printf("\n");
for (i = 0; i < 26; i++) /* 分两行打印 */
{
printf("%c", letters[i]);
if (i % 13 == 12) printf("\n");
}
return 0;
}
运行结果:
abcdefghijklmnopqrstuvwxyz
abcdefghijklm
nopqrstuvwxyz

说明:字符常量 'a' 是 int 类型,'a' + i 得到下一个字母的 ASCII 码,赋给 char 数组时自动转换。
10.2 习题 6.5 —— 字母金字塔
题目:提示用户输入一个大写字母,使用嵌套循环以下面的格式打印字母金字塔(以 E 为例):
A
ABA
ABCBA
ABCDCBA
ABCDEDCBA
#include <stdio.h>
int main(void)
{
char ch;
int i, j, rows;
printf("Enter a capital letter: ");
scanf("%c", &ch);
rows = ch - 'A' + 1; /* 总行数 */
for (i = 0; i < rows; i++)
{
for (j = 0; j < rows - i - 1; j++) /* 前导空格 */
putchar(' ');
for (j = 0; j <= i; j++) /* 递增部分 A.. */
putchar('A' + j);
for (j = i - 1; j >= 0; j--) /* 递减部分 ..A */
putchar('A' + j);
putchar('\n');
}
return 0;
}
运行结果(输入 E):
Enter a capital letter: E
A
ABA
ABCBA
ABCDCBA
ABCDEDCBA

说明:每行由「空格 + 递增字母 + 递减字母」三部分组成。空格数为 rows - i - 1,递减部分比递增部分少一个字符,所以从 i - 1 开始。
遇到的问题:第一次写时递减循环用了 j = i; j >= 0,导致每行末尾多一个字母,变成 ABAA 这种。改成 j = i - 1 后正确。
10.3 习题 6.7 —— 倒序打印单词
题目:把一个单词读入一个字符数组中,然后倒序打印这个单词。
#include <stdio.h>
#include <string.h>
int main(void)
{
char word[64];
int i;
printf("Enter a word: ");
scanf("%63s", word); /* 限制长度,防止越界 */
for (i = (int)strlen(word) - 1; i >= 0; i--)
putchar(word[i]);
putchar('\n');
return 0;
}
运行结果:
Enter a word: hello
olleh

说明:strlen 返回字符个数(不含 '\0'),最后一个字符下标是 strlen - 1。scanf("%63s") 中的 63 是宽度限制,保证最多读 63 个字符再加 '\0',不会溢出 word[64]。
遇到的问题:一开始直接写 scanf("%s", word),输入超长字符串时会越界。加上宽度限制后安全了。
10.4 习题 6.10 —— 上下限之间整数的平方和
题目:要求用户输入上限和下限整数,计算从下限到上限之间所有整数的平方和;当用户输入的上限小于下限时结束。
#include <stdio.h>
int main(void)
{
int lower, upper, i;
long sum;
printf("Enter lower and upper integer limits: ");
while (scanf("%d %d", &lower, &upper) == 2 && lower < upper)
{
sum = 0;
for (i = lower; i <= upper; i++)
sum += (long)i * i;
printf("The sums of the squares from %d to %d is %ld\n",
lower * lower, upper * upper, sum);
printf("Enter next set of limits: ");
}
printf("Done\n");
return 0;
}
运行结果:
Enter lower and upper integer limits: 5 9
The sums of the squares from 25 to 81 is 255
Enter next set of limits: 3 25
The sums of the squares from 9 to 625 is 5520
Enter next set of limits: 5 5
Done

说明:输出的 25 和 81 是上下限的平方(不是输入本身),这是教材示例要求的格式。sum 用 long 是因为 25² 到 625² 累加会超过 int 的范围。循环条件里的 && lower < upper 保证了「上限小于下限时退出」。
10.5 习题 6.13 —— 2 的前 8 次幂
题目:创建一个包含 8 个元素的 int 数组,把元素分别设为 2 的前 8 次幂,用 for 循环赋值,用 do while 循环显示。
#include <stdio.h>
int main(void)
{
int p[8], i;
p[0] = 2;
for (i = 1; i < 8; i++) /* 后一项 = 前一项 × 2 */
p[i] = p[i - 1] * 2;
i = 0;
do
{
printf("%d ", p[i]);
i++;
} while (i < 8);
printf("\n");
return 0;
}
运行结果:
2 4 8 16 32 64 128 256

说明:用「上一项乘 2」递推比调用 pow 更高效,也不涉及浮点。数组元素最大 256,int 完全够用。
10.6 习题 6.16 —— Daphne 与 Deirdre 的投资
题目:Daphne 以 10% 单利投资 100 美元;Deirdre 以 5% 复利投资 100 美元。编写程序计算多少年后 Deirdre 的投资额会超过 Daphne,并显示那时的投资额。
#include <stdio.h>
int main(void)
{
double daphne = 100.0, deirdre = 100.0;
int year = 0;
while (deirdre <= daphne)
{
year++;
daphne += 100.0 * 0.10; /* 单利:每年固定 +10 */
deirdre *= 1.05; /* 复利:每年 ×1.05 */
}
printf("%d years later: Daphne = %.2f, Deirdre = %.2f\n",
year, daphne, deirdre);
return 0;
}
运行结果:
27 years later: Daphne = 370.00, Deirdre = 373.35

说明:Daphne 是等差数列(每年 +10),Deirdre 是等比数列(每年 ×1.05)。第 26 年时 Deirdre 为 351.09,仍小于 Daphne 的 360;第 27 年才反超。
10.7 习题 6.18 —— Rabnud 博士的朋友数
题目:Rabnud 博士起初有 5 个朋友,之后每周朋友数按「上一周人数减 1 后翻倍再加 1」增长。编写程序显示每周的人数,并求出人数超过 150(Dunbar 数)是在第几周。
#include <stdio.h>
#define DUNBAR 150
int main(void)
{
int week = 1, friends = 5;
while (friends <= DUNBAR)
{
printf("week %2d: %4d friends\n", week, friends);
week++;
friends = (friends - 1) * 2 + 1;
}
printf("week %2d: %4d friends <- exceeds Dunbar number\n", week, friends);
return 0;
}
运行结果:
week 1: 5 friends
week 2: 9 friends
week 3: 17 friends
week 4: 33 friends
week 5: 65 friends
week 6: 129 friends
week 7: 257 friends <- exceeds Dunbar number

说明:递推关系是 a(n) = (a(n-1) - 1) × 2 + 1,也就是 a(n) = 2ⁿ + 1(n 从 1 开始,依次为 5, 9, 17, 33, 65, 129, 257)。第 6 周 129 人还没超过 150,第 7 周 257 人超过。
十一、教材第 7 章 7.12 编程练习 第 1、2、4~5、7 题
11.1 习题 7.1 —— 统计空格、换行和其他字符
题目:读取输入,读到 # 为止,报告读取的空格数、换行符数和所有其他字符的数量。
#include <stdio.h>
int main(void)
{
char ch;
int spaces = 0, newlines = 0, others = 0;
while ((ch = getchar()) != '#')
{
if (ch == ' ') spaces++;
else if (ch == '\n') newlines++;
else others++;
}
printf("spaces = %d, newlines = %d, other characters = %d\n",
spaces, newlines, others);
return 0;
}
运行结果(输入两行文字后以 # 结束):
Hello world
this is a test#spaces = 4, newlines = 1, other characters = 21

说明:getchar() 一次读一个字符,返回值是 int,这样才能用 != EOF 判断结束。这里用 # 作为结束标志。统计结果为:空格 4 个(Hello world 1 个 + this is a test 3 个)、换行 1 个、其他字符 21 个(即 21 个字母)。
11.2 习题 7.2 —— 打印字符及其 ASCII 码
题目:读取输入,读到 # 为止,报告读取到的每个字符及其对应的 ASCII 码(十进制),每行打印 8 个「字符-码」对。
#include <stdio.h>
int main(void)
{
char ch;
int n = 0;
while ((ch = getchar()) != '#')
{
printf("'%c' = %-4d ", (ch == '\n') ? ' ' : ch, ch);
if (++n % 8 == 0) putchar('\n');
}
if (n % 8 != 0) putchar('\n');
printf("total = %d\n", n);
return 0;
}
运行结果(输入 abc#):
'a' = 97 'b' = 98 'c' = 99
total = 3

说明:%-4d 中的负号表示左对齐,宽度 4,用来让每列对齐。换行符本身显示成空格((ch == '\n') ? ' ' : ch),否则会把输出打乱。++n % 8 == 0 控制每 8 个换一行。
11.3 习题 7.4 —— 用 if else 替换字符
题目:读入输入,读到 # 为止,把句号替换成感叹号,把感叹号替换成两个感叹号,并报告一共进行了多少次替换。
#include <stdio.h>
int main(void)
{
char ch;
int count = 0;
while ((ch = getchar()) != '#')
{
if (ch == '.')
{
putchar('!');
count++;
}
else if (ch == '!')
{
printf("!!");
count++;
}
else
putchar(ch);
}
printf("\nreplacements = %d\n", count);
return 0;
}
运行结果(输入 Hi. Are you ok! Yes.#):
Hi! Are you ok!! Yes!
replacements = 3

说明:输入里共有 2 个句号(Hi. 和 Yes.)和 1 个感叹号,每处替换计数加 1,所以 replacements = 3。句号变成 !,感叹号变成 !!,所以输出 Hi! ... ok!! Yes!。
11.4 习题 7.5 —— 用 switch 改写 7.4
题目:用 switch 语句重写上一题。
#include <stdio.h>
int main(void)
{
char ch;
int count = 0;
while ((ch = getchar()) != '#')
{
switch (ch)
{
case '.':
putchar('!');
count++;
break;
case '!':
printf("!!");
count++;
break;
default:
putchar(ch);
}
}
printf("\nreplacements = %d\n", count);
return 0;
}
运行结果(同样的输入):
Hi! Are you ok!! Yes!
replacements = 3

对比讨论:两题结果完全一致,说明 if-else 和 switch 在这类等值判断上等价。但 switch 更清晰、更易扩展;if-else 更灵活,能处理范围判断和复杂条件。注意 switch 每个分支的 break 不能漏。
11.5 习题 7.7 —— 工资、税金与净收入
题目:提示用户输入一周的工作小时数,打印工资总额、税金和净收入。假设:基本工资 10 美元/小时;超过 40 小时的部分按 1.5 倍计算;税率:前 300 美元 15%,接下来的 150 美元 20%,其余 25%。用 #define 定义常量。
#include <stdio.h>
#define RATE1 0.15
#define RATE2 0.20
#define RATE3 0.25
int main(void)
{
double hours, gross, tax, net;
printf("Enter the number of hours worked in a week: ");
scanf("%lf", &hours);
/* ① 计算工资总额:40 小时以内 10 美元/小时,超出部分 1.5 倍 */
if (hours <= 40.0)
gross = hours * 10.0;
else
gross = 400.0 + (hours - 40.0) * 15.0;
/* ② 分段计税 */
if (gross <= 300.0)
tax = gross * RATE1;
else if (gross <= 450.0)
tax = 300.0 * RATE1 + (gross - 300.0) * RATE2;
else
tax = 300.0 * RATE1 + 150.0 * RATE2 + (gross - 450.0) * RATE3;
net = gross - tax;
printf("gross = %.2f tax = %.2f net = %.2f\n", gross, tax, net);
return 0;
}
运行结果(输入 45 小时):
Enter the number of hours worked in a week: 45
gross = 475.00 tax = 81.25 net = 393.75

计算验证:
- 工资总额:
40 × 10 + 5 × 15 = 400 + 75 = 475美元 - 税金:
300 × 15% + 150 × 20% + 25 × 25% = 45 + 30 + 6.25 = 81.25美元 - 净收入:
475 - 81.25 = 393.75美元
三者与程序输出完全一致。
本次遇到的问题及解决:
一开始税率直接写成 0.15、0.2、0.25 散落在代码里,后来按题目要求改成 #define 常量,修改税率时只改一处即可。另外输入 40 时 gross = 400,正好落在第二档边界,用 <= 判断才不会漏掉。
十二、总结与思考
这次作业题目量比较大,覆盖了循环结构、分支结构、数组、字符输入输出、函数调用这些 C 语言最核心的语法,做下来有几个比较深的体会:
1. 循环的三种写法各有适用场景
for 适合「次数已知」的循环(比如 6 行、8 个元素),while 适合「条件驱动」的循环(比如读到 # 结束、猜 π),do while 适合「至少执行一次」的场景(比如菜单)。第二题求 π 时,while 和 do while 结果差了一点点(3.141393 vs 3.141793),一开始以为是精度问题,后来逐项打印才明白是 do while 多累加了一项——这让我真正理解了「先判断」和「先执行」的区别。
2. 整数除法和浮点除法是最容易踩的坑
1 / i 在 i 为 int 时结果是 0,必须写 1.0 / i。同样,sum 用 int 还是 long long 直接决定程序在 n 变大后会不会溢出。这次第五题和第十题都栽在这上面,改过来之后对类型转换的敏感度高了很多。
3. 数组越界不会报错,但结果一定错
第六题写成 i <= 8 时程序照常运行,只是结果不对。C 语言不检查越界,这既是它的「自由」,也是它的「危险」,只能靠严格的循环边界来保证。
4. 短路求值是逻辑运算符的重要特性
第三题让我第一次直观感受到 && 和 & 的区别:前者右边可能根本不执行,后者一定会执行。理解了这一点,才能写出 if (p != NULL && p->next != NULL) 这种安全的判断。
5. 流程图不是形式主义
第九题画流程图时,我才发现「先假设是素数、再逐个试除、找到因子就 break」这个结构用文字描述很容易漏掉 break,画出来之后逻辑漏洞一目了然。NS 图把循环和分支嵌在方框里,比传统流程图更适合表达嵌套结构。
6. 对教材习题的理解
《C Primer Plus》的编程练习设计得很细,每道题都在训练一个具体的语法点:6.5 练嵌套循环和字符运算,6.7 练字符串和 strlen,6.10 练循环条件的设计,6.16 对比单利复利、训练 while 的终止条件,7.1~7.5 系统地训练 getchar 和循环读取,7.7 训练多分支和常量定义。做完这一批,对「循环 + 分支 + 数组」的组合使用已经比较熟练了。
7. 还需要改进的地方
- 代码里还有不少魔法数字(比如
10.0、15.0),应该都抽成#define; - 输入没有做充分的健壮性检查,比如输入字母时
scanf会失败,应该处理返回值; - 缩写和命名还可以更规范,比如
pi4可以叫quarter_pi。
接下来打算把 OJ 上的题目坚持刷下去(本学期的 200 题目标),同时把这次作业里写过的程序再重构一遍,练习把重复逻辑封装成函数。
附录:运行环境
| 项目 | 内容 |
|---|---|
| 操作系统 | Windows 11 |
| 编译器 | Visual Studio 2022(MSVC 14.43,C11) |
| 编译命令 | cl /utf-8 /EHsc 源文件.c |
| 教材 | 《C Primer Plus(第 6 版)中文版》,Stephen Prata |
| 全部程序 | 共 23 个,均编译通过、运行正常 |

浙公网安备 33010602011771号