Python 版打字测速做完后,要不要试试用 C 再来一遍?C 没有现成的字符串切片和格式化,一切都要自己动手,反而能让你看清"输入到底是怎么进到程序里的"。本文用纯标准库实现:显示目标句子、fgets 读输入、clock() 计时、逐字符比对,最后输出 WPM 和正确率。

1. 游戏规则

程序先显示一句英文——我们选经典的练习句 "the quick brown fox jumps over the lazy dog",它几乎包含全部 26 个字母,是打字练习的标配。玩家照着敲,敲完按回车。程序测量从显示到回车之间的用时,逐字符比对,算出三个指标:

为什么用 fgets 而不是 scanf("%s")?因为 scanf("%s") 读到空格就停,根本没法输入带空格的句子,而 fgets 能读一整行——字符串输入的两个函数差别,从这里就能体会出来。

为什么拿英文句子练打字?因为英文单词由字母组成,逐字符比对简单直观,而且字母键是键盘的"主战区",练好英文打字,中文输入的速度也会跟着涨——毕竟拼音输入也要先敲字母。等基础版跑通,再考虑中文、代码、数字混排的进阶模式。

2. 模块拆解

按数据流拆成四步:输入 → 计时 → 比对 → 统计。

先看最关键的"去换行"。fgets 会把回车键的 \n 也存进数组,比对前必须去掉,strcspn 是处理这个问题的标准武器:

#include <stdio.h>
#include <string.h>

int main(void) {
    /* 模拟 fgets 读进来的一行,末尾带着换行符 */
    char typed[] = "hello\n";

    /* strcspn 返回第一个换行符的下标,把它替换成字符串结束符 */
    typed[strcspn(typed, "\n")] = '\0';
    printf("处理后的输入:[%s]\n", typed);
    return 0;
}

strcspn(typed, "\n") 扫描字符串,返回第一个 \n 的位置;如果整行没有换行符(比如用户输入的字符把缓冲区塞满了),它返回字符串长度,此时把 '\0' 写到末尾也完全安全。这个一行写法比手写循环找换行更简洁,是 C 社区公认的惯用法。

顺便说一下 MAX_LEN 128 这个缓冲区大小:它必须比目标句子长,否则 fgets 会把句子截断。我们的目标句 45 个字符,128 绰绰有余。缓冲区大小是 C 字符串编程里最需要"心里有数"的数字——太小截断,太大浪费内存,写 #define 常量而不是魔法数字,就是为了一次改到位。

3. 逐步实现:计时

#include <stdio.h>
#include <time.h>

int main(void) {
    clock_t start = clock();
    volatile long sum = 0;
    for (long i = 0; i < 10000000; i++)
        sum += i;              /* 模拟一点计算耗时 */
    clock_t end = clock();

    double seconds = (double)(end - start) / CLOCKS_PER_SEC;
    printf("耗时: %.3f 秒\n", seconds);
    return 0;
}

clock() 返回程序启动以来消耗的 CPU 时间,单位是"时钟滴答";除以 CLOCKS_PER_SEC(标准规定为每秒 1000000)就换算成秒。clock_t 本质上是整数类型,相减后必须强转成 double 再做除法,否则整数除法会把小数截掉——打字再慢也可能显示 0 秒。真实程序里,把 start 放在打印目标句之后、end 放在 fgets 返回之后,夹住的就是玩家打字的时间。

clock() 测量的是 CPU 时间,而不是墙上的真实时间——对打字测速这种"程序全程在等键盘输入"的场景,两者几乎相等,直接用没问题。但如果程序里还有其他线程或大量的等待操作,墙钟时间更准确,那时该用 time() 或平台相关的 clock_gettime。

4. 核心算法:逐字符比对

#include <stdio.h>
#include <string.h>

int main(void) {
    const char *target = "hello world";
    char typed[] = "hello worl";       /* 故意少打一个 d */
    int correct = 0, wrong = 0;
    int len_target = (int)strlen(target);
    int len_typed = (int)strlen(typed);
    int i;

    for (i = 0; i < len_target; i++) {
        if (i < len_typed && typed[i] == target[i])
            correct++;
        else
            wrong++;
    }
    /* 输入比目标长时,多出来的字符也算错 */
    if (len_typed > len_target)
        wrong += len_typed - len_target;

    printf("correct=%d wrong=%d\n", correct, wrong);
    return 0;
}

比对逻辑:以目标句长度为循环边界,每个位置先判断"输入里有没有这个位置"(i < len_typed),有且字符相同才算对,否则算错;最后补一段,输入更长时把多打的字符全部计错。这样"漏打"和"多打"都被公平扣分。strlen 返回 size_t(无符号类型),赋值给 int 时显式强转,可以避免与负数比较时出现诡异行为。

再细看那个判断 i < len_typed && typed[i] == target[i]:&& 是短路运算符,左边为假时右边根本不会执行——所以当输入比目标短时,访问 typed[i] 是安全的,不会越界。把"边界检查"和"比较"写在一个条件里,是 C 里非常典型的防御式写法。

5. 统计输出

#include <stdio.h>

int main(void) {
    int correct = 40, wrong = 5;
    double seconds = 30.0;
    double wpm, accuracy;

    /* 防御:用时太短就兜底,防止除零 */
    if (seconds < 0.001)
        seconds = 0.001;
    wpm = (correct / 5.0) / (seconds / 60.0);
    accuracy = 100.0 * correct / (correct + wrong);

    printf("速度: %.0f WPM\n", wpm);
    printf("正确率: %.1f%%\n", accuracy);
    return 0;
}

两个公式和 Python 版完全一致,但 C 里更要小心类型:correct / 5.0 必须写 5.0 而不是 5,否则整数除法直接得 0,整个 WPM 就废了。正确率的分母 correct + wrong 永远大于 0,但 seconds 理论上可能为 0,所以先做个下限保护。

6. 完整代码

把四步串起来,保存为 typing.c,内容如下:

#include <stdio.h>
#include <string.h>
#include <time.h>

#define MAX_LEN 128

int main(void) {
    const char *target = "the quick brown fox jumps over the lazy dog";
    char typed[MAX_LEN];
    clock_t start, end;
    double seconds, wpm, accuracy;
    int i, correct = 0, wrong = 0;
    int len_target, len_typed;

    printf("目标句子:\n%s\n\n", target);
    printf("照着输入,按回车结束:\n");

    start = clock();
    if (fgets(typed, MAX_LEN, stdin) == NULL)
        return 1;                 /* 读入失败(如 EOF),直接退出 */
    end = clock();

    seconds = (double)(end - start) / CLOCKS_PER_SEC;
    typed[strcspn(typed, "\n")] = '\0';

    len_target = (int)strlen(target);
    len_typed = (int)strlen(typed);

    for (i = 0; i < len_target; i++) {
        if (i < len_typed && typed[i] == target[i])
            correct++;
        else
            wrong++;
    }
    if (len_typed > len_target)
        wrong += len_typed - len_target;

    if (seconds < 0.001)
        seconds = 0.001;
    wpm = (correct / 5.0) / (seconds / 60.0);
    accuracy = 100.0 * correct / (correct + wrong);

    printf("\n用时: %.1f 秒\n", seconds);
    printf("正确字符: %d / %d\n", correct, len_target);
    printf("速度: %.0f WPM\n", wpm);
    printf("正确率: %.1f%%\n", accuracy);
    return 0;
}

整个程序不到 50 行,却把 C 字符串处理的核心点都过了一遍:字符数组、strlen、strcspn 截断、逐字符 == 比较。target 声明成 const char * 是有讲究的——字符串字面量是只读数据,尝试修改它属于未定义行为;而 typed 是玩家输入,必须用可写的字符数组。另外 fgets 返回 NULL 表示读入失败(比如从管道运行、没有输入),这时直接退出比继续处理未初始化的数组安全得多。

编译运行:

gcc typing.c -o typing
./typing

编译时如果报 implicit declaration 之类的警告,先检查三个头文件是否齐全:stdio.h(输入输出)、string.h(字符串函数)、time.h(计时)。另外,把 gcc typing.c -o typing 里的 -o typing 去掉也能编译,但会生成默认的 a.out;养成指定输出名的习惯,多文件项目时才不会乱。

跑起来之后可以自测一下:故意打错几个字母,看正确率是不是按预期下降;输入比目标长的情况,可以用复制粘贴多打几个字符,验证"多打计错"的逻辑是否生效。把边界情况都试一遍,你对程序的信任感会完全不同。

7. 扩展想法

8. 总结与练习

这篇的核心收获是:C 里一切都要自己动手——读整行要用 fgets 并手动去换行,计时要注意整数除法陷阱,字符串比较要 strlen 加逐字符循环。把 Python 版和 C 版对照着看,你会深刻理解"高级语言到底替你做了什么"。

如果某个环节结果不对,建议用 printf 在关键位置打印中间值(比如 len_target、len_typed),这是 C 调试最朴素也最有效的手段——比起瞎猜,数据会告诉你答案。

练习建议:

把 C 版和 Python 版放在一起对比,你会看到同一个需求两种语言的解法差异:Python 用 input() 一行读入、strip() 去空白,还自带无限长字符串;C 要自己定缓冲区、自己剥换行、自己数长度。没有孰优孰劣,只是"默认行为"不同——理解这种差异,是同时掌握多门语言的关键。

💡 学 C 的正确姿势是"故意不用库函数重写一遍":手写 strlen、手写 strcmp,写过一次,你就再也不会被指针和字符串搞晕。