以前学C语言的时候,写过几个小程序,还算蛮有意思的。先上程序截图,占个坑,然后再慢慢讲做这种小玩意的通用思路。
温馨提示:亮点在最后
1、贪吃蛇:
2、都市浮生记(以前有一个很老的小游戏叫“北京浮生记”,仿那个写的,去各种地方买卖商品):
3、背单词的软件(当年女朋友刚考上英语专业,写给女朋友记单词用的,然而被各种手机APP秒杀了,说实在的,如果不考虑界面的话,我觉得我这个功能还是蛮强大的……)
4、C语言结合WindosAPI实现的图形界面闹钟
首先我们需要知道,一款软件究竟有哪几个部分?
在这里我们不谈软件架构神马的专业知识,就站在入门水平能理解的角度思考,我觉得可以分为5个部分:
1、业务逻辑
指的是解决具体问题的思路。比如做一款背单词软件,你怎么随机抽取单词,用什么规则去判断用户是否掌握了这个单词,这就是业务算法。
2、控制算法
控制逻辑是除了业务逻辑之外,关于整体程序控制层面的算法,比如怎么去实现一个链表,怎么去实现图的搜索,或者如何处理线程同步,等等。
3、人机交互
简单来说就是界面。比如C语言的控制台(“黑框框”)最基本的人机交互就是输入和输出。图形化界面就复杂得多,标签、输入框、按钮、图形绘制、事件监听等等。如果做移动开发,还可能涉及到各种传感器。
4、数据存储
小程序不需要外部的数据存储,只有程序内部的变量、常量、静态数值。想要功能丰富一点,比如小游戏的排行榜、单词软件的单词库等等,就需要考虑数据存储的问题。简单一点可以用基本的文件读写,自己规定数据存储的格式。复杂一点就需要用到数据库了。
5、网络通信
普通单机程序用不到网络通信。但如果要做网络程序,比如局域网对战游戏、CS结构的企业管理软件、BS结构的商城平台,等等,就需要考虑网络通信的功能。有各种网络协议,底层一点可以是TCP/IP,往上走的话有封装好的Socket接口,再往上走还有HTTP、FTP等等具体的应用协议。
梳理清楚这五个部分,我们再来看看,入门阶段我们学C语言学了什么?
首先是基础的程序语言知识,从输入输出、变量、分支语句、循环语句,到数组、函数、指针、结构体、文件读写,基本就学完了。
然后可能还接触了一些简单的算法和数据结构,比如排序、递归、栈、队列等等。再复杂一些,可能会接触树的遍历、图的搜索、甚至是动态规划。
我们看看这些知识属于哪些模块?
1、它们解决业务逻辑不成问题,毕竟我们做的很多习题,都是真实情境抽象出来的算法。
2、它能解决一部分简单的控制逻辑。这主要看你算法与数据结构学的如何。当然,涉及到设计模式、多线程、事件监听、以及系统层面的控制内容,我们还没学到。
3、人机交互,只学了简单的输入输出。
4、数据存储,可以用文件读写。
5、网络通信,暂时没接触。
接下来,我们只需要有针对性的弥补这些模块,找到解决方案,就能做出有趣的应用。
1、业务算法
这个不需要额外的技术了,入门阶段学到的知识基本够用,但我们要学会归纳项目需求,并把它们抽象出来,转化为平常做的习题的形式,“能获取什么数据、进行怎样的计算、要得到什么结果”。当然了,思考的时候并不是这个顺序,而是“要得到什么结果,需要什么数据,要进行怎样的计算”。
2、控制逻辑
前面说到,首先这需要你的算法与数据结构基础。至少要学会数组、结构体、排序、链表、递归等等,掌握得越多,这块就越轻松一些。当然了,这毕竟不是竞赛,自己做项目实践的时候,没有人强制规定你“在1s内完成,内存空间不超过65535KB”,所以哪怕入门阶段会的少,效率低一些,也没关系,首先做到“能用”,再考虑优化。
那么复杂一些的控制逻辑问题怎么处理呢?
①多线程
需要调用系统接口。以windows系统为例,需要调用WindosAPI,也就是windows.h库中的函数。初学阶段,我们可以“不知其所以然”,会套用就行。
举例:
问题情境:在贪吃蛇游戏中,我们需要一遍不停的让蛇向当前的方向移动,一边获取用户输入的控制信息。我们知道,C语言在使用任何一个输入函数的时候,都会等待用户的输入,然后再进行下面的语句。所以我们必须在一个单独的线程里监听用户的输入,否则会导致“用户不输入内容,蛇就不移动”的情况。
实现方法(部分代码):
- #include <stdio.h>
- #include <windows.h>
- #include <conio.h>
- char c;//存储用户输入的按键字符的全局变量。
- DWORD WINAPI getOrder();//子线程调用的方法,用来等待用户输入控制命令
- int main()
- {
- CreateThread(NULL,NULL,getOrder,NULL,0,NULL);
- while(1){ //控制贪吃蛇不停的移动
- switch(c){
- //处理wsad四个字符的情况,像上下左右移动
- }
- }
- return 0;
- }
- DWORD WINAPI getOrder(){
- while(1){
- c=getch();//不停的等待用户的输入
- //此处默认用户按的肯定是wsad四个按键,没有处理错误情况。真正写代码需要考虑。
- }
- }
此处关于多线程的部分,是我当年写贪吃蛇程序时,临时上网搜索,直接按人家的格式套用的。说实话,我到现在也不明白CreateThread里面的几个NULL和0分别需要设置什么(后来深入研究Java去了,一入Java深似海,没再深究C语言WindowsAPI的问题)。
至于说CreateThread不稳定不安全,实际编程里不推荐使用,而是要用_beginthread。对于初学阶段,这有什么关系呢?就像我们小学、初中学数学的时候,课本里也把很多概念简化了,并不严谨。我们使用它,是为了帮助我们迈过项目实践里的拦路虎,实现自己想要的功能,真要是以后打算深入研究,再搞明白“为什么”、“什么好”也不迟。(当然了,如果愿意多花一些时间,按网上的说法,去学习_beginthread怎么使用,一步到位,也没有问题,此处给个链接: C语言多线程编程windows多线程CreateThread与_beginthreadex本质区别 )。
②实现一些与操作系统相关的功能
这个当然也可以通过WindowsAPI来实现。但还是那句话,初学阶段,没有必要。说起来有个更简单的方法,只要会用system("");函数就行了。别看一个小小的system函数,通过它,我们可以让系统执行各种dos命令,什么开机关机,文件删查,都不在话下。
当然了,要玩转system函数也有些技巧。首先是要学会拼接字符串,比如我们要实现定时关机的命令,让用户输入一个时间,我们就要把时间数字转换成字符串,再拼接到命令里面。
样例代码如下:
- #include <stdio.h>
- #include <stdlib.h>
- int main()
- {
- int x,t;
- char command[100]="shutdown -s -t ";
- char time[100];
- printf("输入1:设置定时自动关机 ");
- printf("输入2:取消自动关机 ");
- scanf("%d",&x);
- if(x==1){
- printf("请输入关机时间(分钟数):");
- scanf("%d",&t);
- t=t*60;//把分钟数化成秒数
- itoa(t,time,10);//把数字转换成字符串,存在time字符数组里
- strcat(command,time);//拼接命令
- system(command);//调用system函数来执行拼接好的命令
- }
- else if(x==2){
- system("shutdown -a");//取消自动关机的dos命令
- }
- system("pause");
- return 0;
- }
这段代码里,我们使用itoa函数,把数字转换为字符串,再是有那个strcat函数进行拼接,最后调用system函数执行命令。一定要深究的话,itoa并不是标准的C语言函数,但大多数编译器里都有它。
我们知道,system函数的返回值是数字,表示执行成功或具体什么错误。那么如果我们想分析它的输出结果,或者用它执行别的C程序,控制输入的内容呢?其实也很简单,就是用DOS命令中的重定向符“< > << >>”,让命令从文件中读取输入信息,或者把显示信息输出到文件。这样我们可以通过操作文件,来具体进行控制了。当年我担任C语言课程助教的时候,就用这个思路写了一个自动评测学生作业代码的程序。
就算这样效率比较低,还是那句话,“有什么关系呢?”我反对让新手一开始就纠结效率和优化的问题,这样会抹杀对编程的兴趣,或者变得不敢写代码。只有通过大量的实践,找到“成功实现一个功能”的成就感,积累足够的信心和经验,才能取得长足的进步。学得深了,再逐步探究更好的办法,我觉得这才是合适的顺序。
3、人机交互
①黑框框(控制台界面)
入门阶段,最受初学者反感的就是那个讨厌的黑框框了,看见它就想起无趣的scanf和printf,感觉相差了整整一个时代……其实吧,就算是黑框框,也能玩出花儿~
01. getch语句
getch语句是一个“无回显的、即时获取用户按键字符”的函数。也就是说,我们按一个按键,它不会显示在屏幕上,也不需要按回车键,就能直接被getch接收到。接收的方法是:
- char c;
- c=getch();
(最前面别忘了#include
这么一个小玩意儿,它能让我们实现很多的功能:游戏按键控制(有时需要结合上文提到的多线程)、菜单选择输入、输入密码的星号功能。此处我们来看看输入密码的函数实现吧:
- //输入密码的函数。传入一个字符数组,以及这个字符数组的大小
- void getPassword(char password[],int length){
- char c;
- int i=0;
- do{
- c=getch();//用getch来读取用户输入
- if(c==' '){//密码里是不能有空格的
- continue;
- }
- if(c==''){//退格键的处理
- if(i==0){
- continue;
- }
- printf(" ");
- i--;
- continue;
- }
- if(c==' '){//回车键的处理
- break;
- }
- if(i>=length-1){//达到最大长度时的处理
- continue;
- }
- password[i]=c;//存入数组
- printf("*");//显示一个星号
- i++;
- }while(c!=' ');
- password[i]='';//字符串末尾要添加''
- }
当我们需要输入密码时,直接调用这个函数就可以了。测试它的主函数此处就不写啦。效果如图(输入的内容自动变成星号,而且可以任意退格,按回车键完成输入)
02. system("cls");
还记得我们刚才说的,用system函数调用DOS命令吗。“cls”是DOS里的“清除控制台屏幕上的已有内容”的命令,可以清除我们已经输出的全部内容。这有什么用呢?
许多人小时候都玩过“连环画”,在一个本子的每一页画上变化的图案,快速翻动每一页,图像就动了起来。
我们也可以通过system("cls");实现简单的“动画”效果,当然了,刷新太快难免出现闪屏的现象,这个没办法,毕竟这就是个土办法……
举个例子,不知道大家有没有听说过“生命游戏”,也就是是英国数学家约翰·何顿·康威在1970年发明的细胞自动机。给个链接,大家去了解一下生命游戏(游戏作品) 我们用C语言来实现它:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <time.h>
- const int type_live=1;
- const int type_dead=0;
- const int map_size=20;
- int map[20][20];
- void initGame();//初始化
- void run();//每一轮的运行
- int getLivingNum(int x, int y);//判断某个格子周边有几个存活的细胞
- void show_map();//把地图的状态打印到屏幕上
- int main()
- {
- initGame();
- while(1>0){
- run();
- show_map();
- system("cls");
- }
- system("pause");
- return 0;
- }
- void initGame(){//初始化
- int i,j;
- srand((unsigned) time(NULL));
- for(i=0;i<map_size;i++){
- for(j=0;j<map_size;j++){
- map[i][j]=rand()%2;//每一个格子的细胞生死状态都是随机的
- }
- }
- }
- void run(){//每一轮的运行
- int i,j,num;
- for(i=0;i<map_size;i++){
- for(j=0;j<map_size;j++){
- num=getLivingNum(i,j);
- //按规则决定下一轮的生死状态
- if(num==3){
- map[i][j]=type_live;
- }
- else if(num!=2){
- map[i][j]=type_dead;
- }
- }
- }
- }
- //获取当前格子周边8个格子的活着的细胞数量
- int getLivingNum(int x, int y){
- int i,j;
- int num=0;
- for(i=x-1;i<=x+1;i++){
- if(i<0||i>=map_size){//防止数组下标越界
- continue;
- }
- for(j=y-1;j<=y+1;j++){
- if(j<0 || j>=map_size){//防止数组下标越界
- continue;
- }
- if(map[i][j]==type_live){
- num++;
- }
- }
- }
- if(map[x][y]==type_live){
- num--;
- }
- return num;
- }
- void show_map(){//把地图状态输出到屏幕上
- int i,j;
- for(i=0;i<map_size;i++){
- for(j=0;j<map_size;j++){
- if(map[i][j]==type_live){
- printf(" *");
- }
- else if(map[i][j]==type_dead){
- printf(" ");
- }
- }
- printf(" ");
- }
- }
这边最关键的界面控制原理,就是用system("cls");不停的清除之前输出的内容,输出一遍,清除一遍,输出一遍,清除一遍……就能让画面动起来了。给个截图,大家自己脑补一下动起来的样子……
03.其它一些小技巧
想让控制台的界面更美观一些,还有两个小方法。一个是system("color xy");控制控制台的背景色和字体颜色(这里的xy,x是背景色,y是前景色,不要直接填xy,而是如下的数值):
0=黑色 1=蓝色 2=绿色 3=湖蓝色 4=红色 5=紫色 6=黄色 7=白色 8=灰色
9=淡蓝色 A=淡绿色 B=淡蓝绿色 C=淡红色 D=淡紫色 E=淡黄色 F=亮白色
另一个是system("title 标题");,能把程序框框左上角显示的标题给替换了。来个简单的例子:
- #include <stdio.h>
- #include <stdlib.h>
- int main()
- {
- system("title hello,world");
- system("color B1");
- system("pause");
- return 0;
- }
运行效果:
②C语言里的图形库(graphics.h)
C语言也有自己的图形库,我知道的是graphics.h,应该还有别的吧,没研究过。graphics.h好像不是标准库,许多编程软件里都没有,要另外装。我这两天抽空研究研究,来给大家写个例子。graphics.h_百度百科
③图形界面
想要拿C语言实现真正的图形界面程序,那没什么好办法,去学WindowsAPI吧,当年我接触过一阵子,写了几个小东西(就像文章一开始的那个闹钟的截图),但没有深入研究,忘得差不多了,所以现在实在不敢给大家讲太多。而且我觉得吧,WindowsAPI实在不太适合新手去接触,何况根本没这个必要,有时间精力,还不如转而去学Java或者别的更容易做图形界面的语言呢。
4、数据存储
提到数据存储这块,大家第一反应就是“数据库”,想到SQL语言,以及眼花缭乱的一个个数据表,好像很麻烦的样子。其实咱们入门阶段不需要这么复杂嘛,完全可以用自定义的文件读写格式来代替。(话说就算是用SQL,也没有想象中那么复杂,这东西是“会用”容易,想“优化好”需要更深的学问)
①文件读写
大家在C语言入门阶段的学习中,大概是学到指针部分的前面或后面一点(不同的教程顺序不一样),就会学到文件读写的基本操作,咱们先简单复习一下:
fopen函数,以某种模式(读、写等等)打开一个文件流fopen_百度百科
fprintf函数,简单理解就是往文件里写入内容的“printf”函数fprintf_百度百科
fscanf函数,简单理解就是从文件里读取内容的“scanf"函数,注意“读字符串时遇到空格或换行结束”fscanf_百度百科
fgets函数,从文件里读字符串,一次读一行,遇到换行结束,遇到空格不结束fgets_百度百科
fclose函数,关闭文件流fclose_百度百科
feof函数,判断文件流是否到结束位置了feof(函数名)
这些函数就是咱们处理数据存储的基本工具~
说白了,数据存储,就是把我们想要保存的数据储存在硬盘上,留着下次(或者每次)使用,不会像那些临时存在内存空间里的变量那样,随着程序的关闭而Say Goodbye。在入门阶段的项目实践中,我们只需要自己规定好数据存储的格式,然后在程序里按照格式读取或写入文件,就OK了。
老规矩,拿例子说话~还记得开篇我做的那个“都市浮生记”吗?它涉及到用户游戏数据存档功能,玩游戏玩到一半,可以存档,然后下次接着玩~我们就来看看这部分功能的实现:
首先,设计一个文件存储结构:
我们来分析,在这个游戏中,玩家重要的临时数据有哪些:
01. 玩家名称Name
02. 当前金钱数额Money
03. 当前仓库容量Capacity
04. 游戏进行的天数Day
05. 库存货物数量Num
06. 这些具体库存货物的信息(货物编号ID,数量N,进货价格M)
07. 由于我这个游戏当时设计的思路,是支持别人更改数据,写扩展包的,所以增加了一个“游戏版本名称Version”的数据存储,位置放在文件开头。
怎么样,是不是有一种做输入输出练习题的既视感。其实这玩意儿改一改,添加一点需求,就可以是一道编程习题了。。。我们先来结合游戏和文件内容看一看效果
游戏天数不一样是因为“存档的时候是第4天,但再次开始游戏时直接进入了下一天”。
这边没有完整显示对应的数据,反正就是这个意思,大家意会一下~
接下来看看代码是怎么实现的(两年前的源码了,不是很规范,我大致加了一下注释,大家领会思路就好)(注意,我项目里用到了bool类型,C本身是没有的,需要引用stdbool.h头文件c语言中
- bool READ_USER(char *filename)
- {
- int i,n;
- FILE *fp;
- fp=fopen(user.filename,"r");//以只读模式打开文件
- if(fp==NULL) return false;//文件打开失败……
- fgets(user.bagname,100,fp);//读取版本号
- user.bagname[strlen(user.bagname)-1]='';/*我忘了当年写这句话是干嘛了,莫非fgets不会自动添加''吗,还是我自作多情?现在有点忘了,大家可以自己测试一下,评论里告诉我。*/
- if(strcmp(user.bagname,area[0])!=0)//对比存档的版本和当前游戏版本是否相同
- {
- printf("存档文件与当前扩展数据包不匹配! ");
- return false;//版本不同,再见吧~
- }
- fgets(user.name,100,fp);//读取玩家名字
- user.name[strlen(user.name)-1]='';//同上面那个''的注释
- fscanf(fp,"%lld %d %d ",&user.money,&user.storage,&user.day);//读取金钱、仓库容量、游戏天数
- fscanf(fp,"%d ",&user.cargo_amount);//读取库存商品数量
- user.be_used=0;//忘了是干嘛的了
- for(i=0;i<user.cargo_amount;i++)//循环读取每个商品的信息
- {
- fscanf(fp,"%d ",&n);//读取商品id
- fscanf(fp,"%d %d ",&user.cargo[n].amount,&user.cargo[n].total_price);//读取该商品的数量、价钱
- user.be_used=user.be_used+user.cargo[n].amount;//好像是计算已使用的库存容量?
- }
- fclose(fp);//关闭文件
- WRITE_RECORD();//自己定义的另一个函数,好像是写排行榜来着
- return true;//返回true,表示成功读取了存档数据文件
- }
然后再看看保存存档(写文件)的那个函数吧:
- void WRITE_USER(char *filename)
- {
- int i;
- FILE *fp;
- fp=fopen(user.filename,"w");//以写的模式打开文件流,如果文件不存在则新建一个。
- fprintf(fp,"%s ",user.bagname);//输出游戏版本名称
- fprintf(fp,"%s ",user.name);//输出玩家姓名
- fprintf(fp,"%lld %d %d ",user.money,user.storage,user.day);//金钱、仓库、天数
- fprintf(fp,"%d ",user.cargo_amount);//商品数量
- for(i=0;i<goods_amount;i++)//循环输出商品信息
- {
- if(user.cargo[i].amount!=0) fprintf(fp,"%d %d %lld ",i,user.cargo[i].amount,user.cargo[i].total_price);
- }
- fclose(fp);//关闭文件流
- }
就是这么简单粗暴的办法,自己规定文件结构,用简单的文件读写函数进行操作,就可以实现简单的数据存储功能。我另一个背单词的小软件也是用这个思路处理的,当时还特意写了一个转换程序,把我从百度文库搞下来的单词词库(复制到txt里的),转换成程序需要的格式。
②数据库操作
当然了,这种简单粗暴的方法,不适于大规模的数据存储,因为不方便查询和修改,只能是初学阶段的“权宜之计”(当然了,在实际开发中,小规模数据,尤其是允许用户自行修改的配置文件,也可以用类似的思路去处理)。如果要处理大规模数据,还是规范一点,操作数据库吧。
操作数据库,首先需要学习基本的SQL语法。这个不是很难,理解基本概念,然后照着格式写就行。SQL教程_w3cschool
其次,就要考虑如何与数据库连接。首先你要安装一个数据库,比如MySQL……然后需要学习C语言连接数据库的方法,这块我也没试过(我一般拿Java和PHP对接数据库,没试过直接用C写),所以抱歉没法详细介绍。给两个链接大家感受一下吧。c语言连接mysql数据库的实现方法_C 语言 , 用C语言操作MySQL数据库,进行连接、插入、修改、删除等操作 。个人认为,在初学阶段的项目实践中,不是非得死磕数据库。最好换个更方便的语言去学数据库,学明白了,真要深入探索,增加效率神马的,再换回C继续深入。
5、网络通信
入门阶段的项目实践中,用到网络通信的情况不多见,实在不建议大家刚上来就挑战CS架构(客户端-服务端的架构)甚至BS架构(浏览器前端-服务端的架构)的项目,要学的东西挺多的。
当然,如果只是想简单实现两个程序的联机通信,学习Socket编程接口,照着网上的样例代码改就可以了。今天本来想试试的,结果发现自己的IDE没有对应的库文件,按网上的方法折腾了一下没有搞定,过两天折腾清楚了再跟大家分享吧。先丢几个链接在这儿,感兴趣的也可以一块试一试。
socket(计算机专业术语)
C语言的Socket编程例子(TCP和UDP)
使用dev-c++做socket编程遇到的问题和解决过程
总之呢还是那句话,我觉得初学者可以暂时不接触C语言的网络通信,想做涉及网络通信的程序,可以转Java、PHP、Python之类的语言,更方便一些。然后需要辅以学习计算机网络原理之类的理论基础。初步掌握之后,再想深入底层原理,转回C语言也不迟。
使用C语言图形库写的“吃豆人”小游戏:
关于C语言Socket编程,从网上找的代码,调试通了,这是服务端,客户端没截图: