C语言趣味代码(三)

这一篇主要围绕写一个程序---寻找数字 来写,在这篇我会详细和大家介绍基本实现以及它的改良版,还有相关知识的拓展,干货绝对满满。

1. 寻找数字

在这一主题下,我们会编写一些代码,来锻炼玩家的反应力,同时起到锻炼右脑的作用,在这里我们会分为两个部分展开介绍实现。

1.1 寻找幸运数字

在寻找幸运数字的训练中,程序会从1到9抽掉一个数字然后再进行显示,玩家需要在一定时间内找出这个数字。程序分好几步来编写,首先我们看下面这段代码:

cpp 复制代码
#include<stdio.h>
int main()
{
  int i;
  int dgt[9]={1,2,3,4,5,6,7,8,9};
  int a[9]={0};
  for(i=0;i<9;i++)
   a[i]=dgt[i];
  for(i=0;i<9;i++)
   printf("%d",a[i]);
  putchar("\n");
  return 0;
}

这段代码把数组dgt的所有元素从前往后依次初始化为1,2,3....9,然后将这些元素复制到数组a中并显示出来。第一个for循环把数组的dgt的所有元素的值都赋值给了下标相同的数组a的元素,以1为单位对i的值进行增量操作,同时循环元素的赋值操作,这样就把dgt里的元素赋值到了数组a。复制数组时必须向上面一样,逐一复制每一个元素,不能直接一次性对数组复制。第二个for语句是为了显示已复制的数组a的所有元素的值。

复制数组时跳过一个值

在复制数组时如果能跳过一个元素,那它就更接近我们想要的程序了,我们看下面的代码:

cpp 复制代码
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
int main()
{
  int i,j,x;
  int dgt[9]={1,2,3,4,5,6,7,8,9};
  int a[8]={0};
  srand(time(NULL));
  x=rand()%8;
  i=j=0;
  while(i<9)
   {
     if(i!=x)
      a[j++]=dgt[i];
     i++;
   } 
  for(i=0;i<8;i++)
   printf("%d",a[i]);
  putchar("\n");
  rerturn 0;
}

我们运行一下看看:

数组dgt还和之前代码中的一样,但数组a的元素比之前少了一个,变成了8个。首先我们先生成一个随机值,在下面的while循环来跳过变量x为下标的元素dg[x],把数组dgt复制到数组a,用变量i来遍历数组dgt,用变量j来遍历数组a。在上面的示例中,有随机数决定的变量i的值为1,一开始变量i和变量j的值都是0,负责两个数组的开头元素,只有在if语句的判断成立的时候,程序才会进行复制元素的值,if语句判断不成立时,程序不会进行复制,变量i用于遍历数组dgt,每次通过while语句进行循环时,i的值都会增量,而用于遍历数组a的变量j的值只有在进行了元素的赋值时才会增量。这是因为只有在通过a[j++]=dgt[i]进行赋值之后,j才会增量。理解了这些内容,我们来实现这个程序的代码也就简单了:

cpp 复制代码
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#define MAX_STAGE 10
int main()
{
	int i, j, stage;
	int dgt[9] = { 1,2,3,4,5,6,7,8,9, };
	int a[8];
	double jikan;
	clock_t start, end;
	srand(time(NULL));
	printf("请输入缺失的数字。\n");
	start = clock();
	for (stage = 0; stage < 10; stage++)
	{
		int x = rand()% 9;
		int no;
		i = j = 0;
		while (i < 9)
		{
			if (i != x)
				a[j++] = dgt[i];
			i++;
		}
		for (i = 0; i < 8; i++)
			printf("%d ", a[i]);
		printf(": ");
		do
		{
			scanf("%d", &no);
		} while (no != dgt[x]);
	}
	end = clock();
	jikan = (double)(end-start) / CLOCKS_PER_SEC;
	printf("用时%.1f秒。\n", jikan);
	if (jikan > 25.0)
		printf("反应太慢了。\n");
	else if (jikan > 15.0)
		printf("还可以。\n");
	else
		printf("反应神速。\n");
	return 0;
}

我们调试看看效果:

在上面这个程序中训练次数有10次,程序不接受错误的答案,其中的do...while循环负责读取从键盘输入的答案,斌判断答案是否正确,变量no读取到的值只要不等于之前在复制时跳过的dgt[x],do...while语句就会循环,因此,只要玩家没有输入正确答案,就无法进入下一个问题。10次训练结束后,程序会显示出玩家所用的时间和对玩家的评价(反应快还是反应慢)。

重新排列数组元素

在刚才的程序中,数字1~9时按顺序排列显示的,所以我们可以轻易找到缺失的那个数字,因为只要和上一行比较哪里出现了错位就可以了。下面我们把数字的顺序打乱,增加寻找数字的难度:

cpp 复制代码
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#define MAX_STAGE 10
int main()
{
	int i, j, stage;
	int dgt[9] = { 1,2,3,4,5,6,7,8,9, };
	int a[8];
	double jikan;
	clock_t start, end;
	srand(time(NULL));
	printf("请输入缺失的数字。\n");
	start = clock();
	for (stage = 0; stage < 10; stage++)
	{
		int x = rand()% 9;
		int no;
		i = j = 0;
		while (i < 9)
		{
			if (i != x)
				a[j++] = dgt[i];
			i++;
		}
		for (i = 7; i > 0; i--)
		{
			int j = rand() % (i + 1);
			if (i != j)
			{
				int tmp = a[i];
				a[i] = a[j];
				a[j] = tmp;
			}
		}
		for (i = 0; i < 8; i++)
			printf("%d ", a[i]);
		printf(": ");
		do
		{
			scanf("%d", &no);
		} while (no != dgt[x]);
	}
	end = clock();
	jikan = (double)(end-start) / CLOCKS_PER_SEC;
	printf("用时%.1f秒。\n", jikan);
	if (jikan > 25.0)
		printf("反应太慢了。\n");
	else if (jikan > 15.0)
		printf("还可以。\n");
	else
		printf("反应神速。\n");
	return 0;
}

我们调试看看效果:

1.2 寻找重复数字

现在我们再写一个程序,这次不抽出数字,而是重复显示数字,然后让玩家找到这个重复的数字。显示的数字一共有10个,比找幸运数字时要多,不过一旦习惯了,反而更容易找到,我们来实现一下:

cpp 复制代码
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#define MAX_STAGE 10
int main()
{
	int i, j, stage;
	int dgt[9] = { 1,2,3,4,5,6,7,8,9, };
	int a[10];
	double jikan;
	clock_t start, end;
	srand(time(NULL));
	printf("请输入重复的数字。\n");
	start = clock();
	for (stage = 0; stage < 10; stage++)
	{
		int x = rand()% 9;
		int no;
		i = j = 0;
		while (i < 9)
		{
			a[j++] = dgt[i];
			if (i == x)
				a[j++] = dgt[i];
			i++;
		}
		for (i = 9; i > 0; i--)
		{
			int j = rand() % (i + 1);
			if (i != j)
			{
				int tmp = a[i];
				a[i] = a[j];
				a[j] = tmp;
			}
		}
		for (i = 0; i < 10; i++)
			printf("%d ", a[i]);
		printf(": ");
		do
		{
			scanf("%d", &no);
		} while (no != dgt[x]);
	}
	end = clock();
	jikan = (double)(end-start) / CLOCKS_PER_SEC;
	printf("用时%.1f秒。\n", jikan);
	if (jikan > 25.0)
		printf("反应太慢了。\n");
	else if (jikan > 15.0)
		printf("还可以。\n");
	else
		printf("反应神速。\n");
	return 0;
}

我们调试看看效果:

程序大部分和我们上面寻找幸运数字的第二段代码相同,除了里面的while语句其他都相同,只要有以下两点不同:数组a的元素数量从8变成了10,用于遍历数组a的for语句的循环次数从8变成了10。

键盘的输入和操作性能的提升(MS-Windows/MS-DOS)

寻找幸运数字和找重复数字的程序都是由scanf函数负责读取从键盘输入的数字。对该函数而言,只要回车键(输入键)没有被按下,就无法获得已输入的字符的信息。因此,训练时玩家需要在数字后面按下回车键,这样就增加运动手指的次数,也失去了操作的实时性。即便是每次读取一个字符的getchar函数也同样需要按回车键。我们可以利用编程环境单独提供的函数(C语言标准库中未定义的函数)来解决这个问题。首先我们分成以下两个环境来学习,之后再把他们结合在一起。

  1. MS-Windows/MS-DOS
  2. UNIX/Linux/OS X

首先要学习的是在MS-Windows/MS-DOS中该如何解决这个问题。此时我们需要用到Visual C++等编程环境中特有的getch函数和putch函数。我们通过下面的代码来学习这两个函数的作用:

cpp 复制代码
#include<stdio.h>
#include<ctype.h>
#include<conio.h>
int main()
{
	int ch;
	int retry;
	do
	{
		printf("请按键。");
		ch = getch();
		printf("\n按下的键是%c,值是%d。\n", isprint(ch) ? ch : ' ', ch);
		printf("再来一次吗?(Y/N):");
		retry = getch();
		if (isprint(retry))
			putch(retry);
		putchar('\n');
	} while (retry == 'Y' || retry == 'y');
	return 0;
}

我们调试看看效果:

getch函数:获取按下的键

getch函数用于获取玩家从键盘输入的字符。它与getchar函数的不同之处就在于,无需使用回车键就可以立即获取信息。

|-----|---------------------|
| 函数名 | getch |
| 头文件 | #include<conio.h> |
| 格式 | int getch(void); |
| 功能 | 直接从键盘读取字符而不回显 |
| 返回值 | 返回到读取到的字符的值 |

使用getch函数进行读取时,输入的字符不会显示在画面上。上面的代码用十进制表示getch函数的字符和该字符的编码。通过isprint函数判断读取的字符为不可见字符时,则显示空白字符以代替该字符。当确认是否要再来一次时,也会调用getch函数。因此,只要按"Y"或"y"键就能够进行循环了(也就是说没必要按回车键)。

putch函数:输出到控制台

putch函数负责把字符显示在控制台上。函数输出字符串后,字符会立即显示在画面上,因此需要通过fflush函数(用于强制输出)进行清空操作。

|-----|--------------------------------------------|
| 函数名 | putch |
| 头文件 | #include<conio.h> |
| 格式 | int putch(int c); |
| 功能 | 在画面上显示字符c(在一些特殊的编程环境中,如果c时换行符就只换行而不进行返回操作) |
| 返回值 | 显示成功后返回输出的字符c,错误则会返回EOF |

在上面的代码中只有当ch(询问是否再来一次时输入的字符)是能显示的字符时,才会用putch函数来显示该字符。这是因为,如果输出了换行符和制表符等不可显示的字符时,才会使用putch函数来显示该字符。这是因为,如果输出了换行符和制表符等不可显示的字符,画面就会混乱。另外,输入字符"Y"或者"y"后,程序会一直循环,直到输入"Y"或者"y"以外的字符。

键盘输入和操作性能的提升(UNIX/Linux/OS X)

UNIX和Linux通过Curses库来提供getch函数,我们来看下面的代码:

cpp 复制代码
#include<curses.h>
#include<ctype.h>
#include<stdio.h>
int main()
{
  int ch;
  int retry;
  initscr();
  cbreak();
  noecho();
  refresh();
  do
   {
      printf("请按键。");
      fflush(stdout);
      ch=getch();
      printf("\n\r按下的键是%c,值是%d。\n\r",isprint(ch)?ch:'',ch);
      printf("再来一次?(Y/N):");
      fflush(stdout);
      retry=getch();
      if(isprint(retry))
       putchar(retry);
      putchar('\n');
      fflush(stdout);
   }while(retry=='Y'||retry=='y');
  endwin();
  return 0;
}

因为我才开始学习Linux,还不会在Linux环境下运行这段代码,这里运行结果等后面再补上。这段代码只能在提供Curses库的环境下使用(MAS的OS X内部也是UNIX,也提供了(Curses库),Curses库是一个用于进行控制台画面的控制操作等的综合库,在上面只使用了其中的6个函数,下面是这些函数的概要:

|---------|-------------------------------|
| initscr | 生成屏幕并初始化库,使用Curses库时必须最先调用该函数 |
| cbreak | 禁止行缓冲 |
| noecho | 禁止输入的字符显示在画面上 |
| refresh | 刷新画面 |
| getch | 返回输入的字符 |
| endwin | 使用库时用于最后的收尾函数,使用Curses |

因为Curses库中没有提供putch函数,所以在上面采用的是标准库的putchar函数来显示一个字节。Curses库有单独的输出机制,因此规格和C语言标准库的printf函数和putchar函数等兼容性不强,大家尤其需要注意以下两点:

  1. **换行符的操作不同:**即便使用printf函数和putchar函数输出换行符'\n',光标也只会移动到下一行,而不是移动到下一行的开头。想要把光标移动到下一行的开头,就需要输出换行符\n和回车符\r,所以在上面的代码的输出中输出了\n\r。
  2. **即使输出换行符也无法清除缓存:**一般来说,输出换行符后,堆积在缓冲区中未输出的字符就会显示在画面上,然而使用Curses库时不然。因此在上面的代码中为了确保能正常输出就调用了fflush函数。

通用头文件

在两个不同环境中使用程序,实现的方法也大相径庭。因为分环境来编写程序非常麻烦,所以我们来生成一个能吸收两个环境的差异的库。作为头文件来实现的话,只要包含头文件就可以使用,非常方便,我们看下面这段代码:

cpp 复制代码
#ifndef __GETPUTCH
#define __GETPUTCH
#if defined(_MSC_VER)||(__TURBOC__)||(LSI_C)
#include<conio.h>
static void init_getputch(void){}
static void term_getputch(void) {}
#else
#include<curses.h>
#undef putchar
#undef puts
#undef printf
static char __buf[4096]
static int __putchar(int ch)//相当于putchar函数(用"换行符+回车符"代替换行符进行输出)
{
	if (ch == '\n')
		putchar('\r');
	return putchar(ch);
}
static int putch(int ch)显示一个字符,清除缓存区
{
	int result = putchar(ch);
	fflush(stdout);
	return result;
}
static int __printf(const char *format,...)//相当于printf函数(用"换行符+回车符"代替换行符进行输出)
{
	va_list ap;
	int count;
	va_start(ap, format);
	vsprintf(__buf, format, ap);
	va_end(ap);
	for (count = 0; __buf[count]; count++)
	{
		putchar(__buf[count]);
		if (buf[count] == '\n')
			putchar('\r');
	}
	return count;
}
static int __puts(const char* s)//相当于puts函数(用"换行符+回车符"代替换行符进行输出)
{
	int i, j;
	for (i = 0, j = 0; s[i]; i++)
	{
		__buf[j++] = s[i];
		if (s[i] == '\n')
			__buf[j++] = '\r';
	}
	return puts(__buf);
}
static void init_getputch(void)//库初始化处理
{
	initscr();
	cbreak();
	noecho();
	refresh();
}
static void term_getputch(void)//库终止处理
{
	endwin();
}
#define putchar __putchar
#define printf __printf
#define puts __puts
#endif // defined(_MSC_VER)||(__TURBOC__)||(LSI_C)

#endif // !__GETPUTCH

包含头文件保护的头文件的设计

头文件"getputch.h"包括函数(不只是声明)的定义。如果多次包含这种包括函数定义的头文件,就会因重复定义函数而发生编译错误。大家可能会想:哪有人会把同一个头文件包含两三次啊。但实际却不像大家想的那样。例如,假设头文件"abc.h"中包含了"curses.h"。这样一来下面这种情况下,"curses.h"就会被包含两次。

cpp 复制代码
#include "curses.h"
#include "abc.h"

因此,为了让头文件"getputch.h"无论被包含多少次都不会令程序发生故障,我们使用了一个被称为头文件保护的方法,该方法通常表示为如下形式:

cpp 复制代码
#ifndef __HEADERXX
#define __HEADERXX
/* 声明和定义等 */
#endif

此处所示的头文件第一次被包含时,宏__HEADERXX处于未定义状态,因此计算机会读取被#ifndef和#endif括起来的部分,并定义宏__HEADERXX。但是,自第二次被包含起,由于宏__HEADERXX已经被定义了,因此计算机会跳过这部分。此外,宏的名称__HEADERXX必须对应不同头文件来分别设定,在上面我们就将"getputch.h"头文件的宏名称设成了__GETPUTCH。头文件"getputch.h"的机制为:在判断编程环境为Windows类还是UNIX类后再去切换应采用的范围。

改良后的程序:

cpp 复制代码
#include<stdio.h>
#include<time.h>
#include<ctype.h>
#include<stdlib.h>
#include"getputch.h"
#define MAX_STAGE 10
#define swap(type,x,y) do{type t=x;x=y;y=t;}while(0)
int main()
{
	int i, j, x, stage;
	int dgt[9] = { 1,2,3,4,5,6,7,8,9 };
	int a[10];
	double jikan;
	clock_t start, end;
	init_getputch();
	srand(time(NULL));
	printf("请输入重复的数字。\n");
	printf("按下空格键开始。\n");
	fflush(stdout);
	while (getch() != ' ')
		;
	start = clock();
	for (stage = 0; stage < MAX_STAGE; stage++)
	{
		int x = rand() % 9;
		int no;
		i = j = 0;
		while (i < 9)
		{
			a[j++] = dgt[i];
			if (i == x)
				a[j++] = dgt[i];
			i++;
		}
		for (i = 9; i > 0; i--)
		{
			int j = rand() % (i + 1);
			if (i != j)
				swap(int, a[i], a[j]);
		}
		for (i = 0; i < 10; i++)
			printf("%d ", a[i]);
		printf(":");
		fflush(stdout);
		do
		{
			no = getch();
			if (isprint(no))
			{
				putch(no);
				if (no != dgt[x] + '0')
					putch('\b');
				else
					printf("\n");
				fflush(stdout);
			}
		} while (no != dgt[x] + '0');
	}
		end = clock();
	jikan = (double)(end-start) / CLOCKS_PER_SEC;
	printf("用时%.1f秒。\n", jikan);
	if (jikan > 25.0)
		printf("反应太慢了。\n");
	else if (jikan > 15.0)
		printf("还可以。\n");
	else
		printf("反应神速。\n");
	return 0;
}

我们调试一下看看效果:

跟之前的程序不同,在这个程序中,只要不按下空格程序就不会进行开始,用于实现这一操作的是while语句,只要getch函数返回的字符不是空白字符,这个while语句就会持续循环,这个while语句控制的循环体";"是仅由分号构成的空语句。在do...while语句负责处理输入的字符时,这部分和改良前使用scanf函数不同,较为复杂。首先,getch函数读取从键盘输入的值,并把该值赋值给no,这个过程中输入的字符不会显示在画面上,下面的if...else语句部分只会在已读取的字符为显示字符时运行,此处会进行如下操作:

  1. 首先,通过putch函数来显示已读取的字符no。
  2. 其次,根据对错分别执行不同的处理。

当字符no不是正确答案时

输出退格符'\b',把光标位置往前退一格。这项处理是为了让接下来输入的字符能够再次显示在同一个位置上。

当字符no是正确答案时

输出换行符"\n",这是为了进入到下一个问题而做的准备。

相关推荐
尘浮生4 分钟前
Java项目实战II基于微信小程序的电影院买票选座系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
hopetomorrow17 分钟前
学习路之PHP--使用GROUP BY 发生错误 SELECT list is not in GROUP BY clause .......... 解决
开发语言·学习·php
小牛itbull27 分钟前
ReactPress vs VuePress vs WordPress
开发语言·javascript·reactpress
请叫我欧皇i36 分钟前
html本地离线引入vant和vue2(详细步骤)
开发语言·前端·javascript
闲暇部落39 分钟前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
GIS瞧葩菜1 小时前
局部修改3dtiles子模型的位置。
开发语言·javascript·ecmascript
chnming19871 小时前
STL关联式容器之set
开发语言·c++
带多刺的玫瑰1 小时前
Leecode刷题C语言之统计不是特殊数字的数字数量
java·c语言·算法
爱敲代码的憨仔1 小时前
《线性代数的本质》
线性代数·算法·决策树
熬夜学编程的小王1 小时前
【C++篇】深度解析 C++ List 容器:底层设计与实现揭秘
开发语言·数据结构·c++·stl·list