Python 版打字测速做完后,要不要试试用 C 再来一遍?C 没有现成的字符串切片和格式化,一切都要自己动手,反而能让你看清"输入到底是怎么进到程序里的"。本文用纯标准库实现:显示目标句子、fgets 读输入、clock() 计时、逐字符比对,最后输出 WPM 和正确率。
1. 游戏规则
程序先显示一句英文——我们选经典的练习句 "the quick brown fox jumps over the lazy dog",它几乎包含全部 26 个字母,是打字练习的标配。玩家照着敲,敲完按回车。程序测量从显示到回车之间的用时,逐字符比对,算出三个指标:
- 用时:总秒数,保留 1 位小数
- 正确字符:目标句子里打对的字符数
- WPM 与正确率:和 Python 版同一套公式(5 字符折 1 词)
为什么用 fgets 而不是 scanf("%s")?因为 scanf("%s") 读到空格就停,根本没法输入带空格的句子,而 fgets 能读一整行——字符串输入的两个函数差别,从这里就能体会出来。
为什么拿英文句子练打字?因为英文单词由字母组成,逐字符比对简单直观,而且字母键是键盘的"主战区",练好英文打字,中文输入的速度也会跟着涨——毕竟拼音输入也要先敲字母。等基础版跑通,再考虑中文、代码、数字混排的进阶模式。
2. 模块拆解
按数据流拆成四步:输入 → 计时 → 比对 → 统计。
- 输入:
fgets(typed, MAX, stdin)读入一行,末尾带着\n,要手动去掉 - 计时:
clock()返回 CPU 时钟数,除以CLOCKS_PER_SEC得到秒 - 比对:
strlen拿长度,for循环逐字符==比较 - 统计:套 WPM 公式,
printf格式化输出
先看最关键的"去换行"。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. 扩展想法
- 多轮测试:准备 3~5 个目标句,用
rand()随机抽,每轮结算一次,最后算平均成绩 - 实时反馈:在 Linux / macOS 上用
termios.h关闭终端回显和行缓冲,配合getchar()逐字符读取,做到"打错立刻提示"——注意这是 POSIX 接口,Windows 要用conio.h的_getch() - 词库模式:像 Python 版那样随机出单词而不是整句,把逐字符比对换成
strcmp整词比较 - 排行榜文件:用
fprintf把成绩追加进records.txt,再fscanf读回找出历史最佳
8. 总结与练习
这篇的核心收获是:C 里一切都要自己动手——读整行要用 fgets 并手动去换行,计时要注意整数除法陷阱,字符串比较要 strlen 加逐字符循环。把 Python 版和 C 版对照着看,你会深刻理解"高级语言到底替你做了什么"。
如果某个环节结果不对,建议用 printf 在关键位置打印中间值(比如 len_target、len_typed),这是 C 调试最朴素也最有效的手段——比起瞎猜,数据会告诉你答案。
练习建议:
- 删掉"多打计错"的逻辑,只比对
min(len_target, len_typed)个字符,观察成绩有什么变化 - 用
for循环手写一个my_strlen,再和库函数对比结果 - 加一个"从第一次按键开始计时"的功能,提示:第一次
getchar()成功返回前不启动计时
把 C 版和 Python 版放在一起对比,你会看到同一个需求两种语言的解法差异:Python 用 input() 一行读入、strip() 去空白,还自带无限长字符串;C 要自己定缓冲区、自己剥换行、自己数长度。没有孰优孰劣,只是"默认行为"不同——理解这种差异,是同时掌握多门语言的关键。
💡 学 C 的正确姿势是"故意不用库函数重写一遍":手写strlen、手写strcmp,写过一次,你就再也不会被指针和字符串搞晕。