这一篇主要编写几个打字练习的小程序,然后通过这些小程序的实现来回顾复习我们之前学过的知识,然后通过这写打字练习的小程序来提升我们的打字技术和编程技术。
1. 打字练习
1.1 基本打字练习
1.1.1 基本实现
首先我们来制作一个用于计算并显示输入一个字符串所需时间的打字练习软件(在这里我们会用到上一篇博客中的"getputch.h"库):
cpp
#include<stdio.h>
#include<ctype.h>
#include<time.h>
#include<string.h>
#include"getputch.h"
int main()
{
int i;
char *str = "How do you do?";
int len = strlen(str);
clock_t start, end;
init_getputch();
printf("请跟着输入。\n");
printf(" % s\n", str);
fflush(stdout);
start = clock();
for (i = 0; i < len; i++)
{
int ch;
do
{
ch = getch();
if (isprint(ch))
{
putch(ch);
if (ch != str[i])//如果键按错了,把光标往前退一格
putch('\b');
}
} while (ch != str[i]);
}
end = clock();
printf("\n用时%.1f秒。\n", (double)(end - start) / CLOCKS_PER_SEC);
term_getputch();
return 0;
}
我们调试一下看看效果:
在这里我们要输入的是指针str指向的字符串"How do you do?"。根据指针和数组的可交换性,字符串内的字符'H','o','w',...'?'可以从前往后依次用str[0],str[1],....str[13]来表示。此外,变量len用于表示字符串str的长度,其初始值为14。现在我们来介绍一下打字练习的主体部分----for语句。这里的for语句把变量i的值按0,1,2....进行增量,同时通过len次循环从头到尾按顺序遍历字符串内的字符。循环过程中每次遍历的字符sr[i]分别是'H','o',...,'?',这些就是要输入的字符。这里的打字练习不接受输错了的字符(在玩家输入正确的字符之前,程序不会移动到下一个字符)。进行这项控制的是do...while语句。首先把输入的字符(getch函数的返回值)赋值给变量ch。字符ch如果是显示字符,就用putch函数来显示(不显示换行符和制表符等不可显示的字符)。如果字符ch不等于要输入的字符str[i],就输入退格符'\b',把光标的位置往前退一格。这项处理是为了能让下一个输入的字符再次显示在同一个位置上。这和上一篇博客中的"寻找重复数字"中的键盘输入处理是相同的原理。完成上述步骤后,对do语句的控制表达式ch!=str[i]进行求值表达。在输入了错误的字符(ch不等于str[i])时,就会开始do语句的循环。
1.1.2 消除已输入的字符
现在我们将程序稍加修改,每次输入正确字符时都会有一个字符消失,后面的字符会跟着前移。我们看下面这段代码:
cpp
#include<stdio.h>
#include<tome.h>
#include<string.h>
#include"getputch.h"
int main()
{
int i;
char* str = "How do you do?";
int len = strlen(str);
clock_t start, end;
init_getputch();
printf("请跟着输入。\n");
start = clock();
for (i = 0; i < len; i++)
{
printf("%s\r", &str[i]);
fflush(stdout);
while (getch() != str[i])
;
}
end = clock();
printf("\r用时%.1f秒。\n", (double)(end - start) / CLOCKS_PER_SEC);
term_getputch();
return 0;
}
跟上一个程序相同,除非玩家输入正确的值,否则程序不会移到下一个字符,当玩家正确输入完所有的字符,所有的字符都消失后,程序结束。尽管这里的操作比上一个程序高级了一些,但程序却变短了。for语句的主体仅有两个语句构成。因为变量i的值为0,所以指针&str[i]指向字符'H',这样一来画面上就会显示出以str[0]开头的字符串"How do you do?"。程序会在该字符串后紧接着输入空白字符和回车符\r,并把光标返回到本行开头'H'的位置。如果输入的字符(getch函数的返回值)不是str[i],即使输入的字符不是'H',这个while语句就会一直循环下去,知道玩家输入正确的字符,while语句才会结束。至此我们已经知道,通过把要显示的字符串的开始位置逐一向后方错一个,字符串看上去就像被逐个向左侧前移了一样。在字符串后面紧接着显示一个空白字符是为了不让最末尾的字符遗留在画面上。
1.1.3 输入多个字符串
我们来拓展一下上面的程序,让玩家可以在练习时输入多个字符串,输入完一个字符串后,同一行会显示下一个字符串供玩家输入,我们来实现一下:
cpp
#include<stdio.h>
#include<time.h>
#include<string.h>
#include"getputch.h"
#define QNO 12
int main()
{
char* str[QNO] = { "halt","reputation","supportive","junk","global",
"survive","multiple","coat","emphasis",
"capital","drop","novel" };
int i, stage;
clock_t start, end;
init_getputch();
printf("开始打字练习。\n");
printf("按下空格开始。\n");
while (_getch() != ' ')
;
start = clock();
for (stage = 0; stage < QNO; stage++)
{
int len = strlen(str[stage]);
for (i = 0; i < len; i++)
{
printf("%s\r", &str[stage][i]);
fflush(stdout);
while (_getch() != str[stage][i])
;
}
}
end = clock();
printf("\r用时%.1f秒。\n", (double)(end - start) / CLOCKS_PER_SEC);
term_getputch();
return 0;
}
有一个要注意的点就是,再上一个程序中,作为指针的str在这里变成了指针数组。元素str[0],str[1]...分别指向了"halt","reputation"...的开头字符"h","r"...的指针,在主体部分使用了两个for语句。因为题目中的单词从1个变成了12个,所以使用了双层for语句,外层的for语句会把变量stage的值从0开始循环QNO次(也就是12次),里面的for语句就相当于上的程序中的for语句,每次循环时要输入的字符串时str[stage](相当于上个程序的str),要输入的字符数量根据字符串的不同而有所差异。内层的for语句每次循环时,要输入的字符是str[stage][i],这里的str[stage][i]相当于上一个程序中的str[i]。
1.1.4 打乱出题顺序
用前面的程序反复多练习几次,题目下一个出现的字符串就会浮现在脑海里,从而削弱训练成果,为了把出题顺序打乱,我们有两个方法来打乱顺序。
1.1.4.1 方法1
程序新导入了一个元素类型为int型,元素个数为QNO(题目中的字符串的个数)的数组qno ,在开始练习打字之前通过两个for语句来设定数组qno各个元素的值,第一个for语句从前往后依次赋值0,1,2...11。第二个for语句我们用之前的方法把元素的顺序打乱并重新排列,打字练习的主体部分的for语句和上一个程序基本相同,但str[stage]的地方全部变成str[qno[atge]],这是因为在该程序的各次循环中用于出题的是str[qno[]stage]。我们看下面的实现代码:
cpp
#include<stdio.h>
#include<time.h>
#include<time.h>
#include<string.h>
#include"getputch.h"
#define QNO 12
#define swap(type,x,y) do{type t=x;x=y;y=t;}while(0)
int main()
{
char* str[QNO] = { "halt","reputation","supportive","junk","global",
"survive","multiple","coat","emphasis",
"capital","drop","novel" };
int i, stage;
int qno[QNO];
clock_t start, end;
init_getputch();
srand(time(NULL));
for (i = 0; i < QNO; i++)
qno[i] = i;
for (i = QNO - 1; i > 0; i--)
{
int j = rand() % (i + 1);
if (i != j)
swap(int, qno[i], qno[j]);
}
printf("开始打字练习。\n");
printf("按下空格开始。\n");
while (getch() != ' ')
;
start = clock();
for (stage = 0; stage < QNO; stage++)
{
int len = strlen(str[qno[stage]]);
for (i = 0; i < len; i++)
{
printf("%s\r", &str[qno[stage]][i]);
fflush(stdout);
while (getch() != str[qno[stage]][i])
;
}
}
end = clock();
printf("\r用时%.1f秒。\n", (double)(end - start) / CLOCKS_PER_SEC);
term_getputch();
return 0;
}
1.1.4.2 方法2
方法2就是大家最常想到的,重新排列指针,也就是数组str的各个元素的值,这样元素就被打乱了。这个方法有一个缺点:单词打乱顺序后无法恢复原样。 我们来实现一下:
cpp
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<string.h>
#include"getputch.h"
#define QNO 12
#define swap(type,x,y) do{type t=x;x=y;y=t;}while(0)
int main()
{
char* str[QNO] = { "halt","reputation","supportive","junk","global",
"survive","multiple","coat","emphasis",
"capital","drop","novel" };
int i, stage;
int qno[QNO];
clock_t start, end;
init_getputch();
srand(time(NULL));
for (i = QNO - 1; i > 0; i--)
{
int j = rand() % (i)+1;
if (i != j)
swap(char*, str[i], str[j]);
}
printf("开始打字练习。\n");
printf("按下空格开始。\n");
while (getch() != ' ')
;
start = clock();
for (stage = 0; stage < QNO; stage++)
{
int len = strlen(str[stage]);
for (i = 0; i < len; i++)
{
printf("%s\r", &str[stage][i]);
fflush(stdout);
while (getch() != str[stage][i])
;
}
}
end = clock();
printf("\r用时%.1f秒。\n", (double)(end - start) / CLOCKS_PER_SEC);
term_getputch();
return 0;
}
1.2 键盘布局联想打字
现在我们要编写一个稍微有些神奇的小程序,在这个小程序里能训练玩家在没有提示的情况下打出被隐藏的字符。玩家边回忆键盘上每个按键的位置边进行打字练习的软件,跟普通的打字练习不同,玩家需要输入的是没有提示符的字符。不同的键盘有不同的键盘布局,这里我们将键盘分为未按下shift键的状态和按下shift键的状态(这里的键盘我们取字母3层和其上面数字那一层)。关于该键盘的布局我们可以分成以下几个点来看:**1.由4层按构成。2.每层分为左手敲击键盘和右手敲击键盘。3.有不需要按shift键敲击的键和需要按shift键敲击的键。**这里我们用"?"来隐藏原本是这里的一个字符。我们实现一下这个程序:
cpp
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<time.h>
#include"getputch.h"
#define NO 30 //次数
#define KTYPE 16 //块数
int main()
{
char* str[] = { "12345","67890-=",
"!@#$%","^&*()_+",
"qwert","yuiop[]",
"QWERT","YUIOP{}",
"asdfg","hjkl;'\\",
"ASDFG","HJKL:\"|",
"zxcvb","nm,./",
"ZXCVB","NM<>?" };
int i, stage;
clock_t start, end;
init_getputch();
srand(time(NULL));
printf("开始键位联想打字练习。\n");
printf("请输入用?隐藏起来的字符。\n");
printf("按下空格开始。\n");
fflush(stdout);
while (getch() != ' ')
;
start = clock();
for (stage = 0; stage < NO; stage++)
{
int k, p, key;
char temp[10];
do
{
k = rand() % KTYPE;
p = rand() % strlen(str[k]);
key = str[k][p];
} while (key == ' ');
strcpy(temp, str[k]);
temp[p] = '?';
printf("%s",temp);
fflush(stdout);
while (getch() != key)
;
putchar('\n');
}
end = clock();
printf("用时%.1f秒。\n", (double)(end - start) / CLOCKS_PER_SEC);
term_getputch();
return 0;
}
我们看一下效果(这里我没有一直打完,只做了一部分):
变量k表示要用哪个块来出题,因为这个值对应数组str的下标,所以我们将其定义为一个大于等于0小于等于KTYPE的随机数。变量p表示要隐藏块中的哪一个字符来出题,因为这个值对应用于出题的块的字符串的下标,所以我们将其定义为一个大于等于0且小于用于出题的块的字符串数量的随机数。如果能显示出字符串temp,且能读取到从键盘输入的字符key,那么这个程序就对了,这里和之前的程序一样,不接受错误的字符,训练30次后,程序就会自动结束。
1.3 综合打字练习
现在我们编写一个具备多个练习的菜单,让玩家能够进行综合练习的打字小程序。那我们先来实现一下再进行介绍:
cpp
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<string.h>
#include"getputch.h"
#define NO 15
#define KTYPE 16
#define POS_LEN 10
//练习菜单
typedef enum
{
Term,
KeyPos,
KeyPosComp,
Clang,
Conversation,
InValid
}Menu;
//各个块的键
char* kstr[] = { "12345","67890-=",
"!@#$%","^&*()_+"
"qwert","yuiop[]",
"QWERT","YUIOP{}",
"asdfg","hjkl;'\\",
"ASDFG","HJKL:\"|",
"zxcvb","nm,./",
"ZXCVB","NM<>?",
};
//C语言的关键字和库
char* cstr[] = { "auto","break","case","char","const","continue",
"dafault","do","double","else","enum","extern",
"float","for","goto","if","int","long",
"register","return","short","signed","sizeof","static",
"struct","switch","typedef","union","unsigned","void",
"volatile","while",
"abort","abs","acos","asctime","asin","assert",
"atan","atan2","atexit","atof","atoi","atol",
"baserch","calloc","ceil","clearerr","clock","cos",
"cosh","ctime","difftime","div","exit","exp",
"fabs","fclose","feof","ferror","fflush","fgetc",
"fgetpos","fgets","floor","fmod","fopen","fprintf",
"fputc","fputs","fread","free","freopen","frexp",
"fscanf","fseek","fsetpos","ftell","fwrite","getc",
"getchar","getenv","gets","gmtine","isalnum","isalpha",
"iscntrl","isdigit","isgraph","islower","isprint","ispunct",
"isspace","isupper","isxdigit","labs","ldexp","ldiv",
"localeconv","localtime","log","log10","longjimp",
"malloc","memchr","memcmp","memcpy","memmove","memset",
"mktime","modf","perror","pow","printf","putc",
"putchar","puts","qsort","raise","rand","realloc",
"remove","rename","rewind","scanf","setbuf","setjmp",
"setlocale","setvbuf","signal","sin","sinh","sprintf",
"sqrt","srand","sscanf","strcat","strchr","strcmp",
"strcoll","strcpy","strcspn","strerror","strftime","strlen",
"strncat","strncmp","strncpy","strpbrk","strrchr","strspn",
"strstr","strtod","strtok","strtol","strtoul","strxfrm",
"system","tan","tanh","time","tmpfile","tmpnam",
"tolower","toupper","ungetc","va_arg","va_end","va_start",
"vfprintf","vprintf","vsprintf",
};
//英语对话
char* vstr[] = { "Hello!",
"How are you?",
"Fine thanks.",
"How do you do?",
"Good bye!",
"Good morning!",
"Good afternoon!",
"Good evening!",
"See you later!",
"Go ahead,please.",
"Thank you.",
"No,thank you.",
"May I have your name?",
"I'm glad to meet you.",
"What's time is it now?",
"I must go now.",
"Where is the restroom?",
"I don't know.",
"I have no change with me.",
"I will be back.",
"I hope I'm not disturbing you.",
"Just follow my lead.",
};
//字符串str的打字练习(返回错误次数)
int go(const char* str)
{
int i;
int len = strlen(str);
int mistake = 0;
for (i = 0; i < len; i++)
{
printf("%s\r", &str[i]);
fflush(stdout);
while (getch() != str[i])
{
mistake++;
}
}
return mistake;
}
//单一位置训练
void pos_traning()
{
int i;
int stage;
int temp, line;
int len;//用于出题块的键数
int qno, pno;//题目编号和上一个的题目编号
int tno, mno;//字符数量和错误次数
clock_t start, end;
printf("\n进行单一位置训练。\n");
printf("请选择要练习的块。\n");
printf("第一层 (1) 左 %-8s (2) 右 %-8s\n", kstr[0], kstr[1]);
printf("第二层 (1) 左 %-8s (2) 右 %-8s\n", kstr[4], kstr[5]);
printf("第三层 (1) 左 %-8s (2) 右 %-8s\n", kstr[8], kstr[9]);
printf("第四层 (1) 左 %-8s (2) 右 %-8s\n", kstr[12], kstr[13]);
do
{
printf("编号(停止练习为99): ");
scanf("%d", &temp);
if (temp == 99)
return;
} while (temp < 1 || temp>8);
line = 4 * ((temp - 1) / 2) + (temp - 1) % 2;
printf("练习%d次%s题目。\n",NO, kstr[line]);
printf("按下空格开始。\n");
while (getch() != ' ')
;
tno = mno = 0;
len = strlen(kstr[line]);
start = clock();
for (stage = 0; stage < NO; stage++)
{
char str[POS_LEN + 1];
for (i = 0; i < POS_LEN; i++)
str[i] = kstr[line][rand() % len];
str[i] = '\0';
mno += go(str);
tno += strlen(str);
}
end = clock();
printf("题目:%d字符/错误:%d次\n", tno, mno);
printf("用时%.1f秒。\n", (double)(end - start) / CLOCKS_PER_SEC);
}
//混合位置训练
void pos_training2()
{
int i;
int stage;
int temp, line;
int sno;//被选中的块数
int select[KTYPE];//被选中的块
int len[KTYPE];//用于出题的块的键数
int tno, mno;//字符数量和错误次数
clock_t start, end;
char* format = "第%d层 (%2d) 左 %-8s (%2d) 右 %-8s"
"(%2d) [左] %-8s (%2d) [右] %-8s\n";
printf("\n进行混合位置训练。\n");
printf("请选择要练习的块(可以多选)。\n");
for (i = 0; i < 4; i++)
{
int k = i * 4;
printf(format, i + 1, k + 1, kstr[k], k + 2, kstr[k + 1],
k + 3, kstr[k + 2], k + 4, kstr[k + 3]);
}
//不重复选择块
sno = 0;
while (1)
{
printf("编号(结束选择为50/停止练习为99)。");
do
{
scanf("%d", &temp);
if (temp == 99)
return;
} while ((temp<1 || temp>KTYPE) && temp != 50);
if (temp == 50)
break;
for (i = 0; i < sno; i++)
if (temp == select[i])
{
printf("\a这一层已经被选过了。\n");
break;
}
if (i == sno)
select[sno++] = temp;
}
if (sno == 0)
return;//一个都没选
printf("把下列块的题目练习%d次。\n", NO);
for (i = 0; i < sno; i++)
printf("%s", kstr[select[i] - 1]);
printf("\n按下空格开始。\n");
while (getch() != ' ')
;
tno = mno = 0;//清空字符数量和错误次数
for (i = 0; i < sno; i++)
len[i] = strlen(kstr[select[i] - 1]);
start = clock();
for (stage = 0; stage < NO; stage++)
{
char str[POS_LEN + 1];
for (i = 0; i < POS_LEN; i++)
{
int q = rand() % sno;
str[i] = kstr[select[q] - 1][rand() % len[q]];
}
str[i] = '\0';
mno += go(str);
tno += strlen(str);
}
end = clock();
printf("题目:%d字符/错误:%d次\n", tno, mno);
printf("用时%.1f秒。\n", (double)(end - start) / CLOCKS_PER_SEC);
}
//C语言/英语会话训练
void word_training(const char* mes, const char* str[], int n)
{
int stage;
int qno, pno;
int tno, mno;
clock_t start, end;
printf("\n练习%d个%s。\n", NO, mes);
printf("按下空格开始。\n");
while (getch() != ' ')
;
tno = mno = 0;
pno = n;
start = clock();
for (stage = 0; stage < NO; stage++)
{
do
{
qno = rand() % n;
mno += go(str[qno]);
tno += strlen(str[qno]);
} while (qno ==pno);
pno = qno;
}
end = clock();
printf("题目:%d字符/错误:%d次\n", tno, mno);
printf("用时%.1f秒。\n", (double)(end - start) / CLOCKS_PER_SEC);
}
//选择菜单
Menu Selectmenu()
{
int ch;
do
{
printf("\n请选择练习。\n");
printf("(1)单一位置 (2)混合位置\n");
printf("(3)C语言的的单词 (4)英语会话 (0)退出:");
scanf("%d", &ch);
} while (ch < Term || ch >= InValid);
return (Menu)ch;
}
int main()
{
Menu menu;
int cn = sizeof(cstr) / sizeof(cstr[0]);
int vn = sizeof(vstr) / sizeof(vstr[0]);
init_getputch();
srand(time(NULL));
do
{
switch (menu = Selectmenu())
{
case KeyPos:
pos_traning();
break;
case KeyPosComp:
pos_training2();
break;
case Clang:
word_training("C语言单词", cstr, cn);
break;
case Conversation:
word_training("英语会话文档", vstr, vn);
break;
}
} while (menu != Term);
term_getputch();
return 0;
}
我们实现一下看看效果:
函数SelectMenu用于显示训练的菜单,供练习者选择,该函数会从键盘读取0~4的整数值,把该值转换成枚举类型Menu的值然后返回。
单一位置训练是把一个块内的键组合起来供玩家练习的程序,首先,玩家需要选择练习哪个块,用于出题的字符串的元素下标是0,1,4,5,8,9,12,13,而作为选项显示在画面上,由练习者从键盘输入的块的编号是1,2,3,4,5,6,7,8。下面这段代码用于把从键盘读取到的值1~8转换为下标:
cpp
line=4*((temp-1)/2)+(temp-1)%2;
把通过右边计算的值赋值给变量line。这个值用于出题的块的下标。变量len用于表示被选中的块的字符串kstr[line]的字符数量。每次生成每次用于出题的字符串str时,从字符串kstr[line]中随机取出字符,然后赋值给str[0]~str[9],空字符赋值给str[POS_LEN]。生成题目后,程序就会调用go函数,这个函数负责显示接收到的字符串str,以供玩家进行打字练习,每次玩家输入错误时,变量mistake的值就会被增量,输入结束后,函数就会返回输入错误的次数。混合位置训练是在单一位置训练的基础上,将多个块的键进行组合练习,让练习者可以根据自己的喜好来组合。