目录
通讯录
通讯录是利用顺序表的特性——动态管理数组内存及各个元素。将每一个对象顺序地存储起来,当然创建通讯录这个项目也可以使用链表创建,但是相对于顺序表而言,链表存在着管理困难、对各个元素节点访问会更加繁琐等一些劣势。
如果使用顺序表就会方便许多,所有创建的联系人都井然有序的排列在一起对各个联系人信息管理更加高效。
其结构大致如下:
接下来就是对此项目的讲解。
使用顺序表创建通讯录,就用我们之前创建好的顺序表的结构。
#include<stdio.h>
#include<assert.h>
#include"Contact.h"
typedef struct Contact datetype;
typedef struct Seqlist
{
datetype* arr;
int size;//有效联系人
int capacity;//有效容量
}SL;
void initialize(SL* ps);//初始化顺序表
void destroyed(SL* ps);//销毁
void print(SL* ps);//内容打印
void endpush(SL* ps, datetype x);
由于我们之前我们将int类型数据用作元素类型示例,但是现在我们只需要将int类型改为一个结构体类型。至于这个结构体类型的构建我们可以放在新的头文件中。
#define NAME 20
#define ADDRESS 200
#define num 11
#define GENDER 8
typedef struct Contact
{
char name[NAME];
char gender[GENDER];
int age;
char address[ADDRESS];
char number[num];
}per;
由于内部的多个属性,比如名字地址一类就需要字符串设置。
联系人的具体信息,我在这里创建了联系人的姓名、年龄、性别、住址、电话号码,可以按照个人意愿添加。
由于有了之前顺序表的结构,在这里就会很简单,部分的讲解我将会用注释在代码中讲解。
新增联系人
只需创建好联系人对象,将对象的各个属性用scanf输入,最后尾插进顺序表。
void addcontact(SL* ps)//新增联系人
{
per a;
printf("请输入姓名\n");
scanf("%s",a.name);
printf("请输入年龄\n");
scanf("%d", &a.age);
printf("请输入性别\n");
scanf("%s",a.gender);
printf("请输入联系电话\n");
scanf("%s",a.number);
printf("请输入住址\n");
scanf("%s",a.address);
printf("输入完成\n\n\n");
endpush(ps,a);//尾插
}
查找联系人
输入字符串然后再在顺序表中逐一比对,如果输入字符串不存在此名称,返回不存在的下标,查找到就返回查找到的下标。
int findcontact(SL*ps,char name1[])//查找联系人姓名
{
for (int i=0;i<ps->size;i++)
{
if(strcmp(ps->arr->name, name1)==0)
return i;
}
return -1;
}
删除联系人
查找联系人实现后,删除联系人就简单了。查找到对应的联系人下标,然后进行顺序表的删除操作。
void delcontact(SL* ps)//删除联系人
{
char name[NAME];
printf("请输入要删除的联系人姓名\n");
scanf("%s", name);
int ret=findcontact(ps, name);
if (ret ==-1)
printf("无法找到此联系人\n");
else
{
posdel(ps, ret);
printf("已删除此人\n");
}
}
修改联系人
这里也是会用到查找的操作,然后修改。
void modcontact(SL* ps)//修改联系人
{
char name[NAME];
printf("请输入要修改的联系人姓名\n");
scanf("%s", name);
int ret=findcontact(ps, name);//返回对应下标
if (ret>0)
{
printf("请输入需要更改的姓名\n");
scanf("%s", ps->arr[ret].name);
printf("请输入年龄\n");
scanf("%d", &(ps->arr[ret].age));
printf("请输入性别\n");
scanf("%s", ps->arr[ret].gender);
printf("请输入联系电话\n");
scanf("%s", ps->arr[ret].number);
printf("请输入住址\n");
scanf("%s", ps->arr[ret].address);
printf("输入完成\n\n\n");
}
}
接下来一个简单的通讯录就成功了,接下来我们可以简单测试一下
#include "Seqlist.h"
#include "Contact.h"
int main()
{
SL con;
initialize(&con);
addcontact(&con);
modcontact(&con);
delcontact(&con);
return 0;
}
由于各个元素数组,测试时不宜将某个属性输入太大,否则会出现栈溢出的错误。
总结:创建通讯录只需要将顺序表元素类型改为自己创建好的结构体,结构体包含联系人的各个属性,联系人的增删查改类似于顺序表的增删查改,先是找,然后删改,增和改就需要对联系人的各个属性进行输入存储。切记输入的各个元素不能够超过对应数组的指定大小。
顺序表的小游戏讲完了,接下来就是贪吃蛇了
贪吃蛇小游戏
创建贪吃蛇,要首先在控制台“属性”这里设置控制台为“’Windows控制台主机”,这个黑框框就是控制台。而实现贪吃蛇需要用到win32API的知识。
Win32 API
Windows这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外,它同时也是⼀个很⼤ 的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程序达到开启 视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程序(Application),所以便 称之为ApplicationProgrammingInterface,简称API函数。WIN32API也就是MicrosoftWindows 32位平台的应⽤程序编程接⼝。在C语言中就需要包含windows.h的头文件。
首先我们可以用mode命令更改控制台的属性。
在这里我们只需要使用
system(“mode con cols =100 lines=30”);//30行100列
system(“title 贪吃蛇”);//控制台标题
这时的控制台参考如下:
(此为全屏下的编译器)
控制台屏幕上的坐标COORD COORD是WindowsAPI中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕幕缓冲区上的坐标,坐标系 (0,0) 的原点位于缓冲区的顶部左侧单元格。
例如这样写:
COORD pos={0,0};
此结构体声明如下:
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
GetStdHandle
GetStdHandle是⼀个WindowsAPI函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标 准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。可以看出句柄就类似于一个控制器。
句柄的类型就是HANDLE是一个指针,获得标准输出设备的句柄就可以用此类型生成一个变量:
HANDLE OUTPUT = NULL;//将变量初始化为指针
GetStdHandle的声明:HANDLE GetStdHandle(DWORD nStdHandle);
HANDLE OUTPUT = NULL;
OUTPUT = GetStdHandle(STD_OUTPUT_HANDLE);
这样就获得了句柄。
GetConsoleCursorInfo
检索有关指定控制台屏幕缓冲区的光标⼤⼩和可⻅性的信息,在语法上是:
BOOL WINAPI SetConsoleCursorInfo( _In_ HANDLE hConsoleOutput, _In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
hConsoleOutput [in]
控制台屏幕缓冲区的句柄。 该句柄必须具有 GENERIC_READ 访问权限。 有关详细信息,请参阅控制台缓冲区安全性和访问权限。
lpConsoleCursorInfo [out]
指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关控制台游标的信息。
CONSOLE_CURSOR_INFO
是一个结构体,内部包含了有关控制台光标的信息
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CO
- dwSize,由光标填充的字符单元格的百分⽐。此值介于1到100之间。光标外观会变化,范围从完 全填充单元格到单元底部的⽔平线条。
- bVisible,游标的可⻅性。如果光标可⻅,则此成员为TRUE。
SetConsoleCursorInfo
这就是设置光标的可见性和大小
BOOL WINAPI SetConsoleCursorInfo( HANDLE hConsoleOutput, const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo );
那么就可以设置光标:
HANDLE OUTPUT = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursor_info = { 0 };//创建cursor_info光标信息结构体
GetConsoleCursorInfo(OUTPUT, &cursor_info);
cursor_info.bVisible = 0;//也可以改为false//光标设置为不可见
SetConsoleCursorInfo(OUTPUT, &cursor_info);
这样光标就在控制台中隐藏了。
SetConsoleCursorPosition
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调 ⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置。
BOOL WINAPI SetConsoleCursorPosition( HANDLE hConsoleOutput, COORD pos )
这样就可以封装一个函数来专门在控制台上定位。
void setpos(short x , short y)
{
HANDLE OUTPUT = NULL;
OUTPUT = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = {x,y};
SetConsoleCursorPosition(OUTPUT, pos);
}
使用此函数后,就在控制台上定位成功了
GetAsyncKeyState
获取按键信息,将按键的虚拟值传给函数,函数通过此来分辨按键状态。
SHORT GetAsyncKeyState( int vKey );
GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果 返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬 起;如果最低位被置为1则说明,该按键被按过,否则为0。
因为此函数存在多次调用,在这里我们可以定义一个宏来直接判断某键是否按过。
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 1) ? 1 : 0 )
(下面是微软官网给出的键码,需要可自行查看)
虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn
宽字符介绍
在游戏中控制台的结构分布大致是这样的。
过去C语⾔并不适合⾮英语国家(地区)使⽤。 C语⾔最初假定字符都是单字节的。但是这些假定并不是在世界的任何地⽅都适⽤。后来为了使C语⾔适应国际化,C语⾔的标准中不断加⼊了国际化的⽀持。⽐如:加⼊了宽字符的类型 wchar_t 和宽字符的输⼊和输出函数,加⼊了<locale.h>头⽂件,其中提供了允许程序员针对特定 地区(通常是国家或者说某种特定语⾔的地理区域)调整程序⾏为的函数。
宽和列的单位长度分布比列为1:2,普通的字符是占⼀个字节的,这类宽字符和汉字字符是占⽤2个字节。大致的分布是这个样子:
(将字符这样分布大致是各自的文化不同,古时候汉字是写满田字格也田字格的宽就是两格,况且汉字的个数本就不计其数,将其设置为一个字节,表示有限,两个字节就更多了。宽字符和汉字符打印的大小相同,这里可用宽字节也可用汉字。)
又如前面所说宽和列的单位长度不同,横向一个字节长等于纵向两个字节,在这里用宽字符就会更好。在这里蛇的身体我们采用宽字符‘●’打印,食物采用‘★’(也可以先用中文字符试试)。
这里要注意的是蛇的X坐标必须是2的倍数,否则蛇的坐标不好对齐,出现地图残留多余的蛇身痕迹。
贪吃蛇游戏设计
游戏分为三个阶段:游戏开始、游戏分布、游戏结束,这三个阶段可封装为三个函数。(细节我将会写在代码注释里)
首先是贪吃蛇结构的创建(定义在头文件中):
#include<windows.h>
#include<stdbool.h>
#include<locale.h>
#include<time.h>
#define keypress(k) ((GetAsyncKeyState(k))&1 ? 1 : 0 )//判断一个键是否按过
enum dirction
{
up,down,left,right
};//用枚举来列举各个方向
enum snake_static{
normal,//正常运行
in_wall,//撞墙
eat_self,//碰到了自己
end//手动结束
};
//类型声明
typedef struct snakenode
{
int x;
int y;
struct snakenode* next;
}snakenode,*psnakenode;
typedef struct snake {
psnakenode psnake;//指向蛇头
psnakenode pfood;//指向食物
enum dirction dir;//蛇的方向
enum snake_static statics;//游戏状态
int food_score;//食物分数
int score;//获得分数
int sleep_time;//爬行速度,即睡眠速度
}Snake,*psnake;
游戏开始阶段
这里就是来打印游戏的场景还可以打印一些说明:
void Gamestart(psnake s)
{
//0.设置窗口大小,然后光标隐藏
system("mode con cols=100 lines=40");
system("title 贪吃蛇");
HANDLE OUTPUT = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursor_info = { 0 };//创建cursor_info光标信息结构体
GetConsoleCursorInfo(OUTPUT, &cursor_info);
cursor_info.bVisible = 0;//修改光标大小值范围为1-100
SetConsoleCursorInfo(OUTPUT, &cursor_info);
//1.打印游戏环境,功能介绍
Game_environment();
//2.绘制地图
Map_create();
//3.创建蛇
Snakebodycreate(s);
//5.创建食物
Foodcreate(s);
//6.设置游戏的相关信息
Inforgame();
}
这写打印信息我们可以在封装几个函数来打印,那么这里就可以使用之前我们创建的setpos函数来定位了。
场景打印:
void Game_environment()
{
setpos(39,14);
printf("欢迎来到贪吃蛇游戏\n");
setpos(40, 20);
system("pause");
system("cls");
setpos(39, 15);
printf("加速能够得到更高的分数\n");
//system("cls");
setpos(40, 20);
system("pause");
system("cls");
}
其中的system("pause");是暂停控制台屏幕,屏幕会显示出
的字样(后面是有光标的,只不过被隐藏了),而system("cls");是清理控制台中残留的打印信息。
地图打印:
void Map_create()
{
//对于坐标的计算需要精确,打印结果出现问题,大概就是坐标计算失误
//在这里可以看出地图打印的大小是27列58行
//上
for (int i = 0; i <= 29; i++) {//这里地图边框是宽字符,横占2格空间
wprintf(L"%lc", L'■');
}
//左
for (int i = 1; i <= 25; i++)
{
setpos(0, i);
wprintf(L"%lc", L'■');
}
//下
setpos(2,25);
for (int i=1;i<=28;i++)
{
wprintf(L"%lc", L'■');
}
//右
for (int i = 1; i <= 25; i++)
{
setpos(58,i);
wprintf(L"%lc", L'■');
}
//setpos(64, 13);
//printf("不能穿墙,不能咬到自己");
}
蛇身的创建
void Snakebodycreate(psnake s)
{
psnakenode cur=NULL;
for (int i=0;i<3;i++)
{
cur = (psnakenode)malloc(sizeof(snakenode));
if (cur == NULL)
{
perror("Snakebodycreate::malloc");
return;
}
cur->next = NULL;
cur->x=6+i*2;//蛇身位置的初始化
cur->y=1;
if (s== NULL)
{
s->psnake = cur;
}
else
{
cur->next = s->psnake;
s->psnake = cur;
}
}
cur = s->psnake;//在此利用cur指针
while (cur)
{
setpos(cur->x,cur->y);//根据位置打印
wprintf(L"%lc", BODY);
cur = cur->next;
}
//对蛇的身体的初始化,避免某个出现很大的随机值
s->dir = right;
s->score = 0;
s->statics = normal;
s->sleep_time = 200;
s->food_score = 10;
}
链表打印的每一个节点存在的每一个节点都打印为一个‘●’ ,这样我们可以试着定义一个
#define BODY L'●'
置于头文件中。
食物创建
void Foodcreate(psnake s)
{
int x = 0;
int y = 0;
again:
do {
x = rand() % 53 + 2;
y = rand() % 24 + 1;
} while (x % 2 != 0);
psnakenode cur =s->psnake;//指向蛇身,cur用来遍历蛇的链表
while (cur)
{
if (cur->x==x && cur->y==y)//如果与蛇身坐标相冲突,则跳到前面重新生成
{
goto again;
}
cur=cur->next;
}
psnakenode food = (psnakenode)malloc(sizeof(snakenode));
if (food==NULL)
{
perror("foodcreate::malloc");
return;
}
food->x = x;
food->y = y;
food->next = NULL;
setpos(x,y);
wprintf(L"☆");
s->pfood = food;
}
信息说明:
void Inforgame()
{
setpos(64,15);
printf("不能穿墙,不能咬到自己");
setpos(64, 16);
printf("用↑ ← ↓ →控制蛇移动");
setpos(64, 17);
printf("F1为加速,F2为减速");
setpos(64, 18);
printf("ESC退出游戏,space暂停游戏");
//这里不用多说
}
游戏运行阶段
这是整个游戏的核心,如果这一步成功,整个项目就成功了百分之八九十。在整个游戏运行中,需要考虑蛇的各种情况:蛇正常行走、蛇咬到了自己、蛇撞到了墙。而这些情况就可以再用函数封装(核心还是蛇的正常行走)。
void Gamerun(psnake s)
{
//此循环至少运行一次,可以do while 语句
do
{
setpos(64,11);
printf("当前的分数是:%2d",s->score);
setpos(64,12);
printf("当前食物分数是:%2d",s->food_score);
//使用个键值的状态,然后更新各个数据
if(keypress(VK_ESCAPE))
{
s->statics = end;
printf("游戏结束\n");
}
else if (keypress(VK_SPACE))
{
Pause();
}
else if (keypress(VK_F1))
{
if (s->sleep_time > 2)//这里需要确定最小值,不可能值为负数
{
s->sleep_time -= 30;//这里的蛇的爬行速度用睡眠时间代替
s->food_score += 2;
}
}
else if(keypress(VK_F2))
{
if (s->sleep_time >2 && s->food_score>2)//这里避免食物分数为0
{
s->food_score -= 2;
s->sleep_time += 30;
}
}
else if (keypress(VK_UP) && s->dir != down)
{
s->dir=up;
}
else if(keypress(VK_DOWN) && s->dir != up)
{
s->dir = down;
}
else if (keypress(VK_LEFT) && s->dir != right)
{
s->dir = left;
}
else if (keypress(VK_RIGHT) && s->dir!= left)
{
s->dir = right;
}
Sleep(s->sleep_time);//这是蛇的速度
snakemove(s);//蛇的行走
killedbywall(s);
killedbyself(s);
} while (s->statics == normal);//整个游戏的运行条件用之前我们创建好枚举类型
}
因为计算机的速度非常快,因此蛇的前进速度就需要睡眠时间Sleep函数来设置。
游戏暂停
游戏运行期间,会存在游戏中途暂停的选择,那么可封装个函数让游戏一直睡眠达到暂停的效果。
void Pause()//是游戏暂停
{
while (1)
{
if (keypress(VK_SPACE))
{
break;
}
Sleep(200);
}
}
只要再次使用空格键就可以解除。
蛇的行走方式
蛇的行走方式有”上下左右”几种。
void snakemove(psnake s)//这里是控制蛇的移动
{
psnakenode pnextnode = (psnakenode)malloc(sizeof(snakenode));
if (pnextnode == NULL)
{
perror("snakemove::malloc");
return;
}
switch (s->dir)
{
case up:
pnextnode->x = s->psnake->x;
pnextnode->y = s->psnake->y-1;
break;
case down:
pnextnode->x = s->psnake->x;
pnextnode->y = s->psnake->y+1;
break;
case left:
pnextnode->x = s->psnake->x-2;//-2正如上文所说
pnextnode->y = s->psnake->y;
break;
case right:
pnextnode->x = s->psnake->x+2;
pnextnode->y = s->psnake->y;
break;
default:
break;
}
if (Nextisfood(pnextnode,s))
{
eatfood(pnextnode,s);
pnextnode = NULL;//链表的结尾需要置空,避免出现野指针
}
else {
nofood(pnextnode,s);
}
}
由于蛇的是否吃到食物两种情况,在这里可以再封装两个函数。这两种情况需要判断。判断条件就可以写为:
int Nextisfood(psnakenode pn,psnake s)
{
return (s->pfood->x == pn->x && s->pfood->y == pn->y);
}
吃到食物时
void eatfood(psnakenode pn, psnake s)
{
s->pfood->next = s->psnake;
s->psnake = s->pfood;
free(pn);
pn = NULL;//
psnakenode pcur = s->psnake;//设置一个遍历链表的指针
while (pcur)
{
setpos(pcur->x,pcur->y);
/*printf("蛇");*/
wprintf(L"%lc",BODY);
pcur = pcur->next;
}
s->score += s->food_score;
Foodcreate(s);//重新生成食物
}
吃到了食物,那么食物的节点就会成为蛇的头,食物会释放,然后再重新生成,蛇身也会重新打印出来。
没吃到食物时
这时就是正常行走,蛇头到达指定的位置,让后将蛇身重新打印一遍,由于贪吃蛇是一步一步走,那么在控制台上残留的多余一格身体,就让空格覆盖。
void nofood(psnakenode pn,psnake s)
{
pn->next=s->psnake;
s->psnake = pn;
psnakenode pcur = s->psnake;
while (pcur->next->next!=NULL)
{
setpos(pcur->x,pcur->y);
wprintf(L"%lc", BODY);
pcur = pcur->next;
}
setpos(pcur->next->x, pcur->next->y);
printf(" ");//结尾打印的蛇的残留用空格覆盖
//释放最后的节点
free(pcur->next);
pcur->next=NULL;
}
蛇的死亡方式
在经典的贪吃蛇游戏中,失败方式就只有两个中:撞墙、咬到自己
蛇撞墙
只要蛇的头部进入了墙的坐标范围,就会那么游戏状态就设置为枚举中的in_wall,就可以结束游戏。
void killedbywall(psnake s)
{
if (s->psnake->y == 0 || s->psnake->x == 0 || s->psnake->x == 58 || s->psnake->y == 26)
{
Sleep(1000);//先暂停几秒
system("cls");//然后清空屏幕
setpos(42, 14);
printf("蛇撞到了墙上");
setpos(42, 16);
printf("您的当前分数为%d", s->score);
Sleep(3000);
s->statics = in_wall;
}
}
蛇咬到自己
这个需要对蛇的整体坐标进行判断这里就需要对各个节点的整体遍历,如果存在蛇头的坐标和身体的某个坐标冲突,就会更改游戏状态,结束游戏。
void killedbyself(psnake s)
{
psnakenode cur = s->psnake->next;
while (cur)
{
if (cur->x==s->psnake->x && cur->y==s->psnake->y)
{
Sleep(1000);
system("cls");
setpos(42, 14);
printf("您咬到了自己");
setpos(42, 16);
printf("您的当前分数为%d",s->score);
Sleep(3000);
s->statics = eat_self;
break;
}
cur = cur->next;
}
}
游戏结束阶段
这个阶段就会跟简单了,只要游戏的状态不再是normal那么就是游戏的结束阶段环节了。
void Gameend(psnake s)
{
system("cls");//清理游戏残留
setpos(42,14);
printf("游戏结束");
}
将游戏的三个阶段封装在一起。
void test()
{
//创建贪吃蛇
Snake snake = { 0 };
psnakenode pointsnake;//用来维护链表
//初始化游戏
/*1.打印游戏环境
2.功能介绍
3.绘制地图
4.创建蛇
5.创建食物
6.设置游戏的相关信息*/
Gamestart(&snake);
//运行游戏
Gamerun(&snake);
//结束游戏
Gameend(&snake);
}
主函数:
int main()
{
setlocale(LC_ALL, "");//"c"为默认模式,""为本地模式
srand((unsigned int)time(NULL)); //设置随机数
test();
return 0;
}
可以将test设置为循环,这样可以再次游玩贪吃蛇,就不需要再次重新启动程序。
画面流程:
总结:贪吃蛇的常见需要考虑贪吃蛇的开始、运行、结束三个阶段。游戏的开始和结束都是对界面的简单处理,而运行阶段是整个游戏的核心。
贪吃蛇需要设置好各个属性尤其是蛇的方向尤为重要,还要设置好蛇的头指针和食物的结构。
贪吃蛇需要使用到链表的知识,同时也需要使用到win32API的知识,控制台分了XY坐标,因此对坐标的精确计算是尤为关键的,坐标点的失误会导致游戏中途出现异常的打印信息,然后可以封装一个函数来取得对应的坐标位置,然后就可以打印信息。
游戏的运行阶段需要将蛇分为几种情况,蛇的行走,蛇的生存和死亡。根据虚拟键值使蛇进行对应的行为。
蛇的各个行为都需要一些代码量,因此可以将这些行为写成各自对应的函数,这同时也体现了“面向对象”的设计理念。
以上皆为我的学习成果,学习总结也只是粗略为之,如果有对文章有简介或者指正都会有我值得学习的地方,欢迎。