C语言总结项目和入门大作业——信息管理系统(多文件版)

本文最后更新于:3 年前

八、 C语言入门大作业——信息管理系统(多文件版)

C语言大作业学过的都知道这是一个对C语言综合应用的考核,也是对项目开发的实践,信息管理系统是C语言历史悠久的压轴,因为它用来综合使用C语言在合适不过了,今天我也用这个来做为这一系列的结尾,希望大家都能用C语言实现自己期望的目标和项目。
信息管理只要4个核心功能——增删查改。
信息管理系统的东西还是很多的,我这里精简一下,我们只要来实现5大功能:新建、查找、删除、修改、清空,并在文件中保存。同时我们将再次使用多文件编程(DevC++版),补充前面对多文件编程的漏洞,详细讲述条件编译和外部变量在多文件中的使用,我们将接触更多的系统函数,以及很多新鲜的用法。
大家或许会觉得程序之间的关联性很强,一下子理不清关系,这是正常的,因为这个程序是我写完之后我知道什么是什么,不同函数遵循什么的规定,这些是我规划了很长时间才出来的,我会尽量讲的详细一点,大家多看肯定能看懂的。

一、 功能模块的划分:

大型项目首先要知道我们需要实现什么功能,再把这些功能向下划分,形成一个个可重复利用的函数,这也是封装的一种(自顶向下)。
首先,我们有5个功能,最起码5个函数就有了吧,其次我们还要保存在文件中,文件的读写函数也要有,2+5=7个了,这是第一级划分,还可以再向下细分,这里就因人而异了,当然随时把现成的代码块封装成函数也是可以的,功能细分只是可以让你对这个项目有更好的认识。

二、 多文件的编写

一般来说,同一类功能的函数(math.h)或者同一文件中的函数放在同一个文件中。我这里使用了2个文件,一个文件放和文件读写相关的函数,另一个放和这个项目有关的所有函数。当然这样还是笼统的分,分多少文件也看个人习惯,这里做简单处理,就分2个吧,再加上对应的头文件和main函数文件,本项目共5个文件。
Dev c++多文件编写:
首先,打开。
在这里插入图片描述
文件—新建—项目(所有的多文件工程都要新建项目
在这里插入图片描述
我们选第二个:控制台程序,并选择C语言
在这里插入图片描述
选择一个保存项目的地方。最好新建一个属于这个项目的文件夹,方便后续添加文件。
保存,OK,项目就好了。
它会自动提供一个main函数模板

在这里插入图片描述
咱们用自己写的,不用它这个。
在这里插入图片描述
左侧项目管理可以管理这个项目的文件,咱们先把它自带的这个移除,右键移除就OK。
然后就可以添加自己的文件了,我们先新建一个文件
在这里插入图片描述
第一个,然后把main函数写进去,保存为main.c(前面的章节讲过了,这里略)
右键项目(就上面这个5(你自己命名的项目名))-添加,找到刚刚的文件就OK了。
用这种方法把需要的5个文件添加进去。
这里我已经规划好了,所以文件是直接全部新建导入的,实际在开发中文件是随时整理,随时编写的,什么时候觉得需要就可以搞。
在这里插入图片描述

三、 基本函数的实现(重点)

我们把所有函数都进行封装,使main函数得到精简。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int main()
{
int temp=1;
while(temp)
{
switch(Menu())
{
case 1:
Build();
break;
case 2:
Seek();
break;
case 3:
Amend();
break;
case 4:
Delete();
break;
case 5:
Clear();
break;
case 6:
temp = 0;
}
getchar();
}
return 0;
}

这是最终main函数的样子。其余所有功能都靠函数实现,这些东西都会讲到,莫急莫急。

  • 菜单函数
    首先我们来实现菜单函数
    菜单函数很简单,显示选择项,输入选项,OK了,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int Menu()
{
int n;
do{
system("cls");
printf("\n\n\n\n");
printf("\t\t\t \n");
printf("\t\t|----------信息管理系统----------|\n");
printf("\t\t|\t1.新建联系人 |\n");
printf("\t\t|\t2.查看联系人 |\n");
printf("\t\t|\t3.修改联系人 |\n");
printf("\t\t|\t4.删除联系人 |\n");
printf("\t\t|\t5.清空联系人 |\n");
printf("\t\t|\t6.退出 |\n");
printf("\t\t|--------------------------------|\n");
printf("\t\t请输入选项<1~6>:");

scanf("%d",&n);
printf("\n");
if(n>6||n<1)
{
printf("输入有误,请重新输入\a");
getchar();
getchar();
system("cls");
}
}while(n>6||n<1);
getchar();
return n;
}

需要注意的是,我们通过菜单函数的返回值来判断用户输入了几,同时对于非法输入,应当提示重新输入,保证完整性。
这个system(“cls”);是什么,这是一个系统函数,看system就知道了,cls是它的参数,实现的功能是小黑窗清屏,就是清除之前小黑窗(控制台命令行)的所有内容,由于我们这个程序会不断的循环执行,时间长了东西会很多,这里采用每次显示菜单的时候就进行清屏刷新的方式。
大家会发现这里用了很多好像没啥用的getchar();前面说了,这是读取一个字符的函数,但没有接收它的返回值,所以它相当于白读,实际上它起到的作用是阻碍程序进行,当程序运行到这里,它必须等待一个输入才能继续,相当于按任意键继续的感觉,由于我们有清屏函数的存在,如果程序不停的话,它printf的东西我们还没看呢,清屏了,啥也没有,就很难受。
有时候会连续出现2个getchar();这是因为上一次的键盘输入会产生输入的缓存,需要先用一个来消除,然后才能正确接收,只用一个的话就直接从缓存中读了一个字符,而和我们的输不输入没啥关系了,所以要先消耗缓存中的字符,这一点在输入字符串的时候尤为关键。
菜单函数和main函数放在一起了。

好了,接下来重头戏来了,我们写一下主体功能的实现
首先,我们写一下我们管理的东西的一些信息,我这里是用的人。

1
2
3
4
5
6
7
8
9
typedef struct{
char phone[15]; //手机
char name[100]; //名字
char num[20]; //工号
char ad[50]; //地址
char sex; //性别
int age; //年龄
}People;

同时用typedef重新定义这个结构体为People,现在我们就有了People这个数据类型了。
我们需要不断输入,通过输入判断是否结束,这意味着我们需要用链表来存储,我们再写一个链表的结构体。

1
2
3
4
5
6
7
typedef struct node{
People peo;
int id;
struct node* p_next;
}Node;


这是链表中的节点结构体,包含这个节点存放的信息peo,这个节点的id,下一个节点的指针。并重定义为Node。

  • 新建函数

接下来,我们将编写“增”——新建函数,在此之前先来说说我的思路。编写程序最重要的就是思路,思路有了编不出来就是对这个语言的了解不够,当你不知道如何实现,没有思路的时候,数据结构和算法就是时候去学习一下了。
我们用链表结构存放我们输入的数据,如果我们没有输入特定的结束字符,就一直新建,同时我们用一个全局变量来记录我们生成了多少个节点(数据),这个数字将存在文件中表示文件中存放了多少个数据。
Ps:虽然按道理我们只要一块一块的写入,再一块一块的读出,是能够正好读完的,用文件的结束标志就能完全读入,本应没必要再记录,但我写的程序总是会多载入一些乱码数据,以我的实力难以解决,只能出此下策。如有大神能指点迷津,甚是感谢!
需要注意的是,数据新建的时候是分为2种情况的,之前有数据和之前没有数据,要分开处理。我们做如下规定:当没有数据时,head指针为空,当存在数据时,head指针指向数据.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//新建函数,返回头节点 
Node* Create(Node* head)
{
Node* p_old,*p_new;
People t;

if(!Input(&t))
{
return head; //没有新建
}
if(!head)
{
head = (Node*)malloc(LEN);
p_old = head;
p_new = head;
p_new->id = 0;
p_new->peo = t;
p_new->p_next = NULL;
}else{
p_new = (Node*)malloc(LEN);
for(p_old = head;p_old->p_next != NULL;p_old = p_old->p_next);
p_old->p_next = p_new;
p_new->id = p_old->id+1;
p_old = p_new;
p_new->peo = t;
p_new->p_next = NULL;
}
Number++;
while(1)
{
if(!Input(&t))
{
return head;
}

p_new = (Node*)malloc(LEN);
Number++;
p_new->id = p_old->id+1;
p_new->peo = t;
p_old->p_next = p_new;
p_new->p_next = NULL;
p_old = p_new;

}

}

我们用一个局部变量t来接收每次的输入。
我还编了一个输入函数,实现每次的输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//输入函数 
int Input(People* p)
{
printf("\n输入姓名(输入#中止):");
scanf("%s",&p->name);
if(p->name[0] == '#')
{
return 0;
}
printf("输入年龄:");
scanf("%d",&p->age);
printf("输入性别(m-男,f-女):");
getchar();
scanf("%c",&p->sex);
printf("输入手机号:");
scanf("%s",&p->phone);
printf("输入地址:");
scanf("%s",&p->ad);
printf("输入工号:(没有输入-1)");
scanf("%s",&p->num);
printf("\n");
return 1;
}

通过一个People的指针的传入,直接对对应位置的结构体赋值。并规定退出符号——名字输入#,则返回0,表示输入结束符号,否则返回1,表示正常输入。其他的都是小玩意,可有可无的。我这里是考虑到不是人人都有编号,所以对编号的输入进行了规定,这些都是小问题。
当输入结束时,就返回头指针。对于第一次输入就退出的话,相当于没有新建,就把头指针原封不动的返回去。对于有新输入的话,我们会在后面进行合适的处理,保证头指针指向的链表是完整的。
当输入有效时,我们就可以建立空间存放这个新数据了,这里分2中情况,第一次新建和之前有数据,这个可以根据头指针来区分。
对于新建的,就好说了,我们直接新建一个空间,放刚刚输入的数据(**结构体可以直接赋值p_new->peo = t;**)并让id=0。对于之前存在数据的话,我们先新建一个节点空间,并通过
for(p_old = head;p_old->p_next != NULL;p_old = p_old->p_next);
这一个for循环定位原来数据的最后一个,这是一个循环体为空的for循环,大家知道for循环的机制应该是可以明白的,当p_old指向的下一个节点为NULL时,即为最后一个节点,此时p_old就定位在最后一个节点上。
之后就是关联,id递加,赋值,老节点跟进,和前面链表的时候一样的。

由于我们开始的时候新建了一个,全局变量Number+1,表示多了一个,很好理解。
之后循环输入,用死循环+条件退出的形式。每次新建,关联,id递加,赋值,老节点跟进,并更新Number,没啥好说的。
重点是:每次新建的节点,我都会让它的下一个节点指向默认为NULL,这样如果输入结束的话,直接返回头指针,也可以保证链表最后一个指针指向NULL,从而保证链表的完整性。

  • 查找函数:

我们提供除id以外的全因素查找,我这里id是当作内部资源不对外开放的,虽然也有id的查找函数但是内部使用的。缺点是只能返回查找到的第一个,原本我希望的是能返回所有符合结果的id号构成的数组,但这样的话需要一个动态数组来实现,工程量又大了不少,等我以后有空再更新吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//查找函数,head-头指针,k-方式选择 
int Find(Node* head,int k)
{
char n[50];
int m;
Node *p = head;
switch(k)
{
case 0: //名字查
printf("输入查找姓名:");
scanf("%s",n);
FindName(n,head);
break;
case 1: //年龄查
printf("输入查看的年龄:");
scanf("%d",&m);
FindAge(m,head);
break;
case 2: //手机号查
printf("输入查找的手机号:");
scanf("%s",n);
FindPhone(n,head);
break;
case 3: //地址查
printf("输入查找地址:");
scanf("%s",n);
FindAd(n,head);
break;
case 4: //性别查
printf("输入查找性别m-男,f-女:");
getchar();
scanf("%c",&n[0]);
FindSex(n[0],head);
break;
case 5: //工号查
printf("输入查找工号:");
scanf("%s",n);
FindNum(n,head);
break;
case 6: //全部显示
ShowAll(head);
break;
default:
printf("输入错误\a\n");
}

}

这里我对于每种方式的查找都封装了自己函数,其实它们大同小异。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
//想实现能全部找到返回各个序号的,但这需要动态数组,比较麻烦 
int FindName(char name[],Node* head)
{
Node *p = head;
for(;p;p = p->p_next)
{
if(!strcmp(p->peo.name,name))
{
Print(&p->peo);
return 1;
}
}
printf("查无此人\a");
return 0;

}

void FindAge(int age,Node *head)
{
Node *p = head;
for(;p;p = p->p_next)
{
if(p->peo.age == age)
{
Print(&p->peo);
return;
}
}
printf("查无此人\a");
}

void FindPhone(char Phone[],Node* head)
{
Node *p = head;
for(;p;p = p->p_next)
{
if(!strcmp(p->peo.phone,Phone))
{
Print(&p->peo);
return;
}
}

printf("查无此人\a");

}

void FindAd(char ad[],Node* head)
{
Node *p = head;
for(;p;p = p->p_next)
{
if(!strcmp(p->peo.ad,ad))
{
Print(&p->peo);
return;
}
}
printf("查无此人\a");
}

void FindSex(char sex,Node* head)
{
Node *p = head;
for(;p;p = p->p_next)
{
if(sex == p->peo.sex)
{
Print(&p->peo);
return;
}
}
printf("查无此人\a");
}

void FindNum(char num[],Node* head)
{
Node *p = head;
for(;p;p = p->p_next)
{
if(!strcmp(p->peo.num,num))
{
Print(&p->peo);
return;
}
}
printf("查无此人\a");
}

//id查找
Node* FindId(int id,Node* head)
{
Node *p = head;
for(;p;p = p->p_next)
{
if(p->id == id)
{
return p;
}
}
if(!p)
{
return NULL;
}
}

通过for循环遍历,找到就打印,没有找到就输出提示,大体都是这个思路,只不过字符串比较要用strcmp函数。
所有查找函数中,只有姓名查找和id查找有返回值,姓名查找返回是否存在,id查找返回节点指针,为什么这样设计,这是和我之后的安排相关的,因为之后的改和删都需要这两个查找函数。所以它们要顶一点。
同时,我也编写了打印函数,作为基础的输入输出函数使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//打印函数 
void Print(People *p)
{
printf("\n姓名:%s\n",p->name);
printf("年龄:%d\n",p->age);
printf("性别:%c\n",p->sex);
printf("电话:%s\n",p->phone);
printf("地址:%s\n",p->ad);
if((p->num)[0] == '-')
{
printf("\n");
return;
}
printf("工号:%s\n",p->num);
printf("\n");
}


void ShowAll(Node* head)
{
Node* temp = head;
while(temp)
{
Print(&temp->peo);
temp = temp->p_next;
}
}

Print函数是对一个People类型的打印,ShowAll是输出所有people的数据,按照前面规定的,如果没有工号,就不输出。

  • 修改函数:

这个有了前面查找函数的思路很简单,就是找到对应的节点数据,然后重新写值就OK,如果没有找到,就提示并退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//现在只能支持直接查找,等动态数组出来就可以随意处理了
//修改函数 (名字)
void Correct(char a[],Node *head)
{
Node *p = head;
int choice;
for(;p;p = p->p_next)
{
if(!strcmp(p->peo.name,a))
{
Print(&p->peo);
break;
}
}
if(!p)
{
printf("查无此人\a");
return;
}
printf("查到这个人,要修改什么:");
printf("1.姓名\t2.性别\t3.年龄\t4.地址\t5.工号\t6.电话7.全部重写\n请输入:");
scanf("%d",&choice);
switch(choice)
{
case 1:
printf("输入新姓名:");
scanf("%s",&p->peo.name);
break;
case 2:
printf("输入新性别:");
getchar();
scanf("%c",&p->peo.sex);
break;
case 3:
printf("输入新年龄:");
scanf("%d",&p->peo.age);
break;
case 4:
printf("输入新地址:");
scanf("%s",&p->peo.ad);
break;
case 5:
printf("输入新工号:");
scanf("%s",&p->peo.num);
break;
case 6:
printf("输入新电话:");
scanf("%s",&p->peo.phone);
break;
case 7:
Input(&p->peo);
break;
default:
printf("输入错误\a");
}
}

这个函数需要头指针指向的链表数据,和待查找的名字。

  • 删除函数:

通过查找,找寻待删除的信息,如果删除,就进行链表的操作。
链表的删除前面讲过了,这里再说一下,如果是头节点,就让头节点的下一个节点当头节点,如果是尾节点,就让倒数第二个节点的下一个节点指向空,如果是中间,就让前一个结点的下一节点指向后一个结点,需要注意的是,最后一定要释放删除的结点。
每删除一个节点,Number就--。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//删除函数_目前只能看人名 
void Det(Node* head,char name[])
{
Node *p = head;
char temp;
for(;p;p = p->p_next)
{
if(!strcmp(p->peo.name,name))
{
Print(&p->peo);
break;
}
}


printf("确认删除吗:y-是,n-否");
getchar();
scanf("%c",&temp);
if(temp == 'y')
{
if(p == head)
{
head = p->p_next;
}else if(p->p_next == NULL){
Node *p1 = FindId(p->id-1,head);
p1->p_next =NULL;
}else{
Node *p1 = FindId(p->id-1,head);
p1->p_next = p->p_next;
}
free(p);
printf("OK\a\n");
Number--;
}
}

我们还是通过和名字查找一样的方法得到指向待删除元素的指针。
我们首先提示一下,防止误操作,当得到确定的删除命令后,对上面的3中情况判断,然后释放删除空间,Number--,Ok。
大家可能会发现我这里没有查找失败,没有找到的情况,这是因为现在介绍的都是功能的核心程序,真正使用的话还要一个辅助程序,这个我们后面会讲。

  • 清空函数:

大家或许有很多想法,但这个实现其实很简单,我们运用w方式打开文件时会格式化文件这个特性,就很方便了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void Clear()
{
char temp;
printf("确定吗\a:y-确定,n-手滑了:");
scanf("%c",&temp);
if(temp == 'y')
{
FILE* fp ;
if(!(fp = fopen(TXT,"wb")))
{
printf("Error\a\n");
exit(0);
}
fclose(fp);
printf("OK");
Number = 0;
getchar();

}
}

我们打开,什么都不写立刻关闭文件,文件就清空了,是不是很方便。
这里的TXT是一个宏定义,表示文件名,后面文件操作函数会介绍。
要注意的是,Number同时置0(这个BUG我找了好久……)
大家或许发现我只是把文件清空了,但程序运行的时候,其实数据已经从文件中读到程序里了,这就意味着程序中仍然可以通过头指针访问这些数据。实际上,我的构想是每次进行数据操作时,都是先从文件中读出,再修改,再写入。所以下次的任何操作都会先读取文件,这样将会刷新head。
注:这样的构造思路存在一个BUG,就是每次刷新都会开辟新内存空间,而原来的空间得不到释放,造成内存的严重浪费,虽然好像不怎么影响程序运行,但这对于C语言程序其实比较BUG的,虽然现在电脑性能有很大的提升,这点空间不足以挂齿,但这种思维的疏忽应当是写C程序的人所应当注意的,或许更好的解决方法是每次只在打开程序时读入,在结束程序时写入,而不是在每次操作中刷新,后面有时间我会更新这个BUG。

四、文件操作函数

好了,这些基本核心函数就完成了,下面是文件操作函数。
文件的操作,最简单的就是读写,所以我们来编写这两个函数。

  • 文件加载

首先是文件加载,将文件中的数据加载到程序中。
我们用#define TXT “dat.txt”这个宏定义语句来定义打开的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
Node* Load()//失败返回空,没有返回空,有返回首地址 
{
FILE *fp;
int i;
Node *head = (Node*)malloc(LEN);
Node *p_old = head;
Node *p_new = head;
if(access(TXT,0))
{
fp = fopen(TXT,"w");
fclose(fp);
if(!(fp = fopen(TXT,"rb")))
{
printf("error");
return NULL;
}
}else if(!(fp = fopen(TXT,"rb")))//存在,打开
{
printf("Error\a\n");
return NULL;
}
//空文件?
rewind(fp);
if(fgetc(fp) == EOF)
{
free(head);
return NULL;
}else
{
rewind(fp);
}

fread(&Number,sizeof(int),1,fp);

if(!Number)
{
free(head);
return NULL;
}

for(i=1;i<=Number;i++)
{
fread(p_new,LEN,1,fp);
p_new = (Node*)malloc(LEN);
p_old->p_next = p_new;
p_old->id = i;
p_old = p_new;
}
free(p_new);
p_old = head;

for(;p_old->id != Number;p_old = p_old->p_next);

p_old->p_next = NULL;
fclose(fp);
return head;
}

Load函数遵循规定,没有数据时就返回空。
access(TXT,0)这是一个新函数,其声明在头文件io.h中,这个函数用来判断文件的状态,我们这里用参数0的功能,来判断文件是否存在,如果文件不存在,就用w方式打开不存在文件时会新建文件这个功能来新建。如果文件本身存在,直接打开就OK。
之后我们判断是否是空文件,先将文件位置标志归位(文件开头)(其实这一步可以不用,但这样写更好理解),然后读取一个字符,判断是否是文件结束标志,如果是,说明文件是空的,就释放刚新建的空间,返回空指针,如果不是,就归位文件标志。
首先读入文件中数据个数Number(我们写的时候也是先写这个),如果个数为0,说明没有数据,就返回空指针。(文件空和没有数据是两种情况,因为我们的文件中还有一个数据个数参数)
之后就读入Number个数据,并用链表存放。
读完后,由于我们是开辟的下一个待读取的单元,所以会多开一个,我们把它释放掉,并找到读取的最后一个结点,让它的下一个指向空,关闭文件,返回头指针。

  • 保存函数:
    先保存个数,再保存数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void Save(Node* head)
{
FILE *fp;
Node* p = head;
if(!head) //没有
{
printf("kong");
return;
}
if(!(fp = fopen(TXT,"wb")))//打开文件
{
printf("error");
return;
}
fwrite(&Number,sizeof(int),1,fp);//写

for(;p;p = p->p_next)//写
{
fwrite(p,LEN,1,fp);
}
fclose(fp);
}

如果头文件为空说明没有数据,就不报错,并提示,有数据的话,打开文件,写个数,循环写结点信息,OK。

五、函数的辅助函数

我们对核心函数提供外围服务,使之成为可以真正使用的函数。

  • 新建函数:
1
2
3
4
5
6
7
8
9
10
void Build()
{

Node* head = Load();
head = Create(head);
Save(head);
printf("\nOK\n");
getchar();
}

这个很简单,先加载,再新建刷新头指针,之后再保存。

  • 查找函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void Seek()
{
Node* head = Load();
int temp;
if(!head)
{
printf("列表空");
return;
}
printf("输入查找方式:\n\t1.姓名\t2.年龄\t3.手机号\n\t4.地址\t5.性别\t6.工号\t7.全部显示:\n");
scanf("%d",&temp);
Find(head,temp-1);
getchar();
}

同样的先加载,如果为空,还找啥,输出提示完事,否则选择查找方式,调用查找函数,OK

  • 修改函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void Amend()
{
char a[50];
Node* head = Load();
if(!head)
{
printf("列表空");
return;
}
printf("输入修改的姓名:");
scanf("%s",a);
Correct(a,head);
Save(head);
printf("\nOK\n");
getchar();
}

和查找函数大同小异,载入,获取修改姓名,调用函数,保存,OK

  • 删除函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void Delete()
{
Node* head = Load();
char a[50];
if(!head)
{
printf("列表空");
return;
}
printf("输入删除的姓名:");
scanf("%s",a);
if(FindName(a,head))
{
Det(head,a);
Save(head);
}

getchar();
}

和查找函数一样,载入,获取姓名,如果没有找到(这里就用到了姓名查找函数的返回值),就提示(这个提示在查找函数中就有了),啥也不干。如果找到了,就删除,保存,OK。
修改函数好像没有对未找到函数的判断,这是因为在correct函数中内含了,而det函数没有内涵没有名字的处理,所以要用外围辅助判断。这是两种函数的区别。
清空函数本身就能干大事,不用外围函数。

六、多文件编程和联系

这一块我们将编写头文件,并对各个头文件之间进行关联。
先来写fun的头文件,头文件中包含所有fun中的函数的声明,这个就不用说,头文件中还可以包含结构体的定义,宏定义。

这里我们先介绍头文件编写的格式,

1
2
3
4
5
6
7
#ifndef BASE
#define BASE

#include<stdio.h>

#endif

#ifndef XXX……#endif,这是预处理指令,预处理指令是用来协调多文件的编译工作的,这个格式的意思是如果没有编译过XXX,就编译下面的指令。

这个XXX是一个标志,要符合命名规则,我这里用BASE这个标志来标注下面这段代码(只有一个头文件包含指令)

这个的用途是什么,当我们有多个文件都要用的时候,如这个头文件,很常用,每个文件都要用,如果你不包含这个头文件,你这个文件的函数就出错了,但如果直接包含,多个文件一起编译的时候,由于每个都包含一遍这个头文件,就会出现头文件重复包含的错误,这个结构就是解决这个问题的,对于每个单个文件,我本身没有定义BASE,当然会包含下面的头文件,对于多文件编译,当编译器编译了第一个BASE后,后面的BASE就会由于编译过而不在编译这一块,也是实现单次编译,OK。

应当说明,实现这个效果,同一块编译的标志应该一样,不然你前面是BASE,后面编程BASS,编译器集体编译,发现没讲过BASS,一编译,发现里面一样,重复定义,没有用了

这个标志也有讲究,一般来说,会把本文件对应的所有编译块用和本文件相关的标志框起来。对于通用的编译文件,用统一的标识标注(上面的BASE)。

说起来比较难理解,大家看实例就OK。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
fun.h头文件:
#ifndef BASE
#define BASE

#include<stdio.h>


#endif

#ifndef FUN_H
#define FUN_H

#include<stdlib.h>

#define LEN sizeof(Node)

#define TXT "dat.txt"

extern int Number;

typedef struct{
char phone[15]; //手机
char name[100]; //名字
char num[20]; //工号
char ad[50]; //地址
char sex; //性别
int age; //年龄
}People;

typedef struct node{
People peo;
int id;
struct node* p_next;
}Node;


Node* New(Node *);
int Input(People* p);
void Print(People *p);

int FindName(char name[],Node* head);
void FindAge(int age,Node *head);
void FindPhone(char Phone[],Node* head);
void FindAd(char ad[],Node* head);
void FindSex(char sex,Node* head);
void FindNum(char num[],Node* head);
Node* FindId(int id,Node* head);

void ShowAll(Node* head);

void Det(Node* head,char name[]);
void Correct(char a[],Node *head);

Node* Create(Node* head);

void Seek();
void Build();
void Clear();
void Delete();
void Amend();

#endif


我们用FUN_H标注这个fun文件的编译代码。
大家发现一个问题:我们哪里都用到了Number这个变量,fun和save的文件中的函数都有用到,但这个变量定义在哪里呢?我这里把它定义在了main函数的文件中,这意味着,如果不做处理,fun和save的文件中的函数将无法识别到这个变量

这个变量跨过多个文件,还要保证是同一个变量,这是什么,外部变量
关于外部变量的多文件使用,比较麻烦,首先我们可以在头文件中声明外部变量,如上面的extern int Number;这就是对Number进行外部声明,但头文件中不能定义变量,所以不能对Number赋值,赋值操作必须在c文件中,这也是我们为什么要定义在main文件中的一个原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Save.h头文件
#ifndef BASE
#define BASE

#include<stdio.h>

#endif

#ifndef SAVE_H
#define SAVE_H

#include"fun.h"

extern int Number;

Node* Load();//失败返回空,没有返回空,有返回首地址
void Save(Node* head);
#endif

由于我们文件操作中也有用到fun文件中的函数,所以干脆包含进来吧。
在main函数文件中:
在这里插入图片描述

虽然我们save.h文件包含了fun.h文件,但由于我们的预编译的作用,它们可以同时在main函数文件中包含而不会发生重定义,预编译标志的重要性可见一斑。
当然我们知道fun头文件在save中,我们就直接包含save就OK了。
大家会发现,Number外部变量声明其实是声明了两次,一次在fun中,一次在save中,但没有问题,因为这是声明,只有声明不冲突,声明几次都没啥问题。

七、文件编译

在这里插入图片描述
用了这么久,我们终于用到了最后一个编译按钮,全部重新编译,这个就是针对多文件项目的。点击这个按钮,就可以对所有文件进行编译。
编译之后运行,一个成功的程序,信息管理系统就OK了。
运行情况:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这个系统还是很简陋的,高级的系统更为复杂。

八、全部代码:

希望大家能有所收获,如果是做作业的话,尽量不要CV大法,看看前面的分析过程,自己写出来的代码更有成就感!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
Main.c
#include"save.h"

int Menu();
int Number = 0;

int main()
{
int temp=1;
while(temp)
{
switch(Menu())
{
case 1:
Build();
break;
case 2:
Seek();
break;
case 3:
Amend();
break;
case 4:
Delete();
break;
case 5:
Clear();
break;
case 6:
temp = 0;
}
getchar();
}

return 0;
}


int Menu()
{
int n;
do{
system("cls");
printf("\n\n\n\n");
printf("\t\t\t \n");
printf("\t\t|----------信息管理系统----------|\n");
printf("\t\t|\t1.新建联系人 |\n");
printf("\t\t|\t2.查看联系人 |\n");
printf("\t\t|\t3.修改联系人 |\n");
printf("\t\t|\t4.删除联系人 |\n");
printf("\t\t|\t5.清空联系人 |\n");
printf("\t\t|\t6.退出 |\n");
printf("\t\t|--------------------------------|\n");
printf("\t\t请输入选项<1~6>:");

scanf("%d",&n);
printf("\n");
if(n>6||n<1)
{
printf("输入有误,请重新输入\a");
getchar();
getchar();
system("cls");
}
}while(n>6||n<1);
getchar();
return n;
}

Fun.c
#include"fun.h"
#include<string.h>


//查找函数,head-头指针,k-方式选择
int Find(Node* head,int k)
{
char n[50];
int m;
Node *p = head;
switch(k)
{
case 0: //名字查
printf("输入查找姓名:");
scanf("%s",n);
FindName(n,head);
break;
case 1: //年龄查
printf("输入查看的年龄:");
scanf("%d",&m);
FindAge(m,head);
break;
case 2: //手机号查
printf("输入查找的手机号:");
scanf("%s",n);
FindPhone(n,head);
break;
case 3: //地址查
printf("输入查找地址:");
scanf("%s",n);
FindAd(n,head);
break;
case 4: //性别查
printf("输入查找性别m-男,f-女:");
getchar();
scanf("%c",&n[0]);
FindSex(n[0],head);
break;
case 5: //工号查
printf("输入查找工号:");
scanf("%s",n);
FindNum(n,head);
break;
case 6: //全部显示
ShowAll(head);
break;
default:
printf("输入错误\a\n");
}

}

//想实现能全部找到返回各个序号的,但这需要动态数组,比较麻烦
int FindName(char name[],Node* head)
{
Node *p = head;
for(;p;p = p->p_next)
{
if(!strcmp(p->peo.name,name))
{
Print(&p->peo);
return 1;
}
}
printf("查无此人\a");
return 0;

}

void FindAge(int age,Node *head)
{
Node *p = head;
for(;p;p = p->p_next)
{
if(p->peo.age == age)
{
Print(&p->peo);
return;
}
}
printf("查无此人\a");
}

void FindPhone(char Phone[],Node* head)
{
Node *p = head;
for(;p;p = p->p_next)
{
if(!strcmp(p->peo.phone,Phone))
{
Print(&p->peo);
return;
}
}

printf("查无此人\a");

}

void FindAd(char ad[],Node* head)
{
Node *p = head;
for(;p;p = p->p_next)
{
if(!strcmp(p->peo.ad,ad))
{
Print(&p->peo);
return;
}
}
printf("查无此人\a");
}

void FindSex(char sex,Node* head)
{
Node *p = head;
for(;p;p = p->p_next)
{
if(sex == p->peo.sex)
{
Print(&p->peo);
return;
}
}
printf("查无此人\a");
}

void FindNum(char num[],Node* head)
{
Node *p = head;
for(;p;p = p->p_next)
{
if(!strcmp(p->peo.num,num))
{
Print(&p->peo);
return;
}
}
printf("查无此人\a");
}

//id查找
Node* FindId(int id,Node* head)
{
Node *p = head;
for(;p;p = p->p_next)
{
if(p->id == id)
{
return p;
}
}
if(!p)
{
return NULL;
}
}



//输入函数
int Input(People* p)
{
printf("\n输入姓名(输入#中止):");
scanf("%s",&p->name);
if(p->name[0] == '#')
{
return 0;
}
printf("输入年龄:");
scanf("%d",&p->age);
printf("输入性别(m-男,f-女):");
getchar();
scanf("%c",&p->sex);
printf("输入手机号:");
scanf("%s",&p->phone);
printf("输入地址:");
scanf("%s",&p->ad);
printf("输入工号:(没有输入-1)");
scanf("%s",&p->num);
printf("\n");
return 1;
}


//打印函数
void Print(People *p)
{
printf("\n姓名:%s\n",p->name);
printf("年龄:%d\n",p->age);
printf("性别:%c\n",p->sex);
printf("电话:%s\n",p->phone);
printf("地址:%s\n",p->ad);
if((p->num)[0] == '-')
{
printf("\n");
return;
}
printf("工号:%s\n",p->num);
printf("\n");
}

void Amend()
{
char a[50];
Node* head = Load();
if(!head)
{
printf("列表空");
return;
}
printf("输入修改的姓名:");
scanf("%s",a);
Correct(a,head);
Save(head);
printf("\nOK\n");
getchar();
}

//现在只能支持直接查找,等动态数组出来就可以随意处理了
//修改函数 (名字)
void Correct(char a[],Node *head)
{
Node *p = head;
int choice;
for(;p;p = p->p_next)
{
if(!strcmp(p->peo.name,a))
{
Print(&p->peo);
break;
}
}
if(!p)
{
printf("查无此人\a");
return;
}
printf("查到这个人,要修改什么:");
printf("1.姓名\t2.性别\t3.年龄\t4.地址\t5.工号\t6.电话7.全部重写\n请输入:");
scanf("%d",&choice);
switch(choice)
{
case 1:
printf("输入新姓名:");
scanf("%s",&p->peo.name);
break;
case 2:
printf("输入新性别:");
getchar();
scanf("%c",&p->peo.sex);
break;
case 3:
printf("输入新年龄:");
scanf("%d",&p->peo.age);
break;
case 4:
printf("输入新地址:");
scanf("%s",&p->peo.ad);
break;
case 5:
printf("输入新工号:");
scanf("%s",&p->peo.num);
break;
case 6:
printf("输入新电话:");
scanf("%s",&p->peo.phone);
break;
case 7:
Input(&p->peo);
break;
default:
printf("输入错误\a");
}
}
//删除函数_目前只能看人名
void Det(Node* head,char name[])
{
Node *p = head;
char temp ;
for(;p;p = p->p_next)
{
if(!strcmp(p->peo.name,name))
{
Print(&p->peo);
break;
}
}


printf("确认删除吗:y-是,n-否");
getchar();
scanf("%c",&temp);
if(temp == 'y')
{
if(p == head)
{
head = p->p_next;
}else if(p->p_next == NULL){
Node *p1 = FindId(p->id-1,head);
p1->p_next =NULL;
}else{
Node *p1 = FindId(p->id-1,head);
p1->p_next = p->p_next;
}
free(p);
printf("OK\a\n");
Number--;
}
}

void Build()
{

Node* head = Load();
head = Create(head);
Save(head);
printf("\nOK\n");
getchar();
}


void ShowAll(Node* head)
{
Node* temp = head;
while(temp)
{
Print(&temp->peo);
temp = temp->p_next;
}
}


Node* Create(Node* head)
{
Node* p_old,*p_new;
People t;

if(!Input(&t))
{
return head; //没有新建
}
if(!head)
{
head = (Node*)malloc(LEN);
p_old = head;
p_new = head;
p_new->id = 0;
p_new->peo = t;
p_new->p_next = NULL;
}else{
p_new = (Node*)malloc(LEN);
for(p_old = head;p_old->p_next != NULL;p_old = p_old->p_next);
p_old->p_next = p_new;
p_new->id = p_old->id+1;
p_old = p_new;
p_new->peo = t;
p_new->p_next = NULL;
}
Number++;
while(1)
{
if(!Input(&t))
{
return head;
}

p_new = (Node*)malloc(LEN);
Number++;
p_new->id = p_old->id+1;
p_new->peo = t;
p_old->p_next = p_new;
p_new->p_next = NULL;
p_old = p_new;

}

}

void Seek()
{
Node* head = Load();
int temp;
if(!head)
{
printf("列表空");
return;
}
printf("输入查找方式:\n\t1.姓名\t2.年龄\t3.手机号\n\t4.地址\t5.性别\t6.工号\t7.全部显示:\n");
scanf("%d",&temp);
Find(head,temp-1);
getchar();
}


void Clear()
{
char temp;
printf("确定吗\a:y-确定,n-手滑了:");
scanf("%c",&temp);
if(temp == 'y')
{
FILE* fp ;
if(!(fp = fopen(TXT,"wb")))
{
printf("Error\a\n");
exit(0);
}
fclose(fp);
printf("OK");
Number = 0;
getchar();

}
}


void Delete()
{
Node* head = Load();
char a[50];
if(!head)
{
printf("列表空");
return;
}
printf("输入删除的姓名:");
scanf("%s",a);
if(FindName(a,head))
{
Det(head,a);
Save(head);
}

getchar();
}

Fun.h
#ifndef BASE
#define BASE

#include<stdio.h>


#endif

#ifndef FUN_H
#define FUN_H

#include<stdlib.h>

#define LEN sizeof(Node)

#define TXT "dat.txt"

extern int Number;

typedef struct{
char phone[15]; //手机
char name[100]; //名字
char num[20]; //工号
char ad[50]; //地址
char sex; //性别
int age; //年龄
}People;

typedef struct node{
People peo;
int id;
struct node* p_next;
}Node;


Node* New(Node *);
int Input(People* p);
void Print(People *p);

int FindName(char name[],Node* head);
void FindAge(int age,Node *head);
void FindPhone(char Phone[],Node* head);
void FindAd(char ad[],Node* head);
void FindSex(char sex,Node* head);
void FindNum(char num[],Node* head);
Node* FindId(int id,Node* head);

void ShowAll(Node* head);

void Det(Node* head,char name[]);
void Correct(char a[],Node *head);

Node* Create(Node* head);

void Seek();
void Build();
void Clear();
void Delete();
void Amend();

#endif

Save.c
#include"save.h"

#include<io.h>

void Save(Node* head)
{
FILE *fp;
Node* p = head;
if(!head) //没有
{
printf("kong");
return;
}
if(!(fp = fopen(TXT,"wb")))//打开文件
{
printf("error");
return;
}
fwrite(&Number,sizeof(int),1,fp);//写

for(;p;p = p->p_next)//写
{
fwrite(p,LEN,1,fp);
}
fclose(fp);
}


Node* Load()//失败返回空,没有返回空,有返回首地址
{
FILE *fp;
int i;
Node *head = (Node*)malloc(LEN);
Node *p_old = head;
Node *p_new = head;
if(access(TXT,0))
{
fp = fopen(TXT,"w");
fclose(fp);
if(!(fp = fopen(TXT,"rb")))
{
printf("error");
return NULL;
}
}else if(!(fp = fopen(TXT,"rb")))//存在,打开
{
printf("Error\a\n");
return NULL;
}
//空文件?
rewind(fp);
if(fgetc(fp) == EOF)
{
free(head);
return NULL;
}else
{
rewind(fp);
}

fread(&Number,sizeof(int),1,fp);

if(!Number)
{
free(head);
return NULL;
}

for(i=1;i<=Number;i++)
{
fread(p_new,LEN,1,fp);
p_new = (Node*)malloc(LEN);
p_old->p_next = p_new;
p_old->id = i;
p_old = p_new;
}
free(p_new);
p_old = head;

for(;p_old->id != Number;p_old = p_old->p_next);

p_old->p_next = NULL;
fclose(fp);
return head;
}

Save.h
#ifndef BASE
#define BASE

#include<stdio.h>

#endif

#ifndef SAVE_H
#define SAVE_H

#include"fun.h"

extern int Number;

Node* Load();//失败返回空,没有返回空,有返回首地址
void Save(Node* head);
#endif

OK,C语言入门总结就到这里结束了,感谢大家的观看,希望大家能有所收获,如果有空我会更新其他系列的东西,欢迎大家观看,如果本栏目有什么BUG,大家可以在下面评论,我能改的尽量改(我也不是高手……)。


C语言总结项目和入门大作业——信息管理系统(多文件版)
https://357meng.github.io/2021/09/01/C07/
作者
Mengki
发布于
2021年9月1日
更新于
2022年10月31日
许可协议