第四次作业

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.

01-sum

结果分析:

  • 程序用 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!

02-pi-guess

结果分析:

  • 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

03-pi-while

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

04-pi-dowhile

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

05-shortcircuit

说明:

  • &&:左操作数为假(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

06-letters

说明:外层循环控制行数,内层循环的终止条件 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

07-nestedsum

验证: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

08-array8

说明:数组下标从 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

09-grade

说明:

  • 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 

10-breakcontinue

讨论:

语句 作用 循环变量 本例表现
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 算法思想

  1. 依次取 n = 100, 101, …, 200。
  2. 对每个 n,用 i = 2, 3, …, √n 去试除:只要存在某个 i 能整除 n,则 n 不是素数,立即置标志 isPrime = 0 并 break 跳出内层循环。
  3. 内层循环结束后检查标志:如果 isPrime 仍为 1,说明没有找到因子,n 是素数,输出它并计数。
  4. 只需试到 √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

11-primes

9.3 传统流程图

flowchart TD A([开始]) --> B[n = 100] B --> C{n <= 200 ?} C -- 否 --> Z([结束,输出总数]) C -- 是 --> D[isPrime = 1<br/>i = 2] D --> E{i * i <= n ?} E -- 否 --> F{isPrime == 1 ?} E -- 是 --> G{n % i == 0 ?} G -- 是 --> H[isPrime = 0<br/>break] H --> F G -- 否 --> I[i = i + 1] I --> E F -- 是 --> J[输出 n<br/>cnt = cnt + 1] F -- 否 --> K[n = n + 1] J --> K K --> C

博文不支持 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

12-ch6-1

说明:字符常量 '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

13-ch6-5

说明:每行由「空格 + 递增字母 + 递减字母」三部分组成。空格数为 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

14-ch6-7

说明: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

15-ch6-10

说明:输出的 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 

16-ch6-13

说明:用「上一项乘 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

17-ch6-16

说明: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

18-ch6-18

说明:递推关系是 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

19-ch7-1

说明: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

20-ch7-2

说明:%-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

21-ch7-4

说明:输入里共有 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

22-ch7-5

对比讨论:两题结果完全一致,说明 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

23-ch7-7

计算验证:

  • 工资总额: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 个,均编译通过、运行正常
posted @ 2026-11-09 21:57  1103zh  阅读(2)  评论(0)    收藏  举报