C語言鏈表案例學習之通訊錄的實現(xiàn)
一、通訊錄需要實現(xiàn)的功能
1,通訊錄可以存儲編號,聯(lián)系人的姓名,電話號碼和家庭住址。
2,通訊錄最基本的功能是添加聯(lián)系人,用戶可以隨時添加聯(lián)系人。
3,通訊錄可以展示已經(jīng)添加的所有聯(lián)系人。
4,通訊錄中用戶可以根據(jù)聯(lián)系人的姓名刪除對應(yīng)通訊錄中的信息。
5,通訊錄中姓名可以重復,所以為了刪除準確的信息,需要實現(xiàn)按位置刪除的功能。
6,通訊錄中用戶可以根據(jù)聯(lián)系人的姓名找到聯(lián)系人在通訊錄中的信息,因為聯(lián)系人可以重名,所以如果有重名的聯(lián)系人的時候就需要返回兩個或兩個以上的聯(lián)系人信息到一個查找表單中。
7,通訊錄中用戶可以根據(jù)通訊錄的位置修改該位置的聯(lián)系人信息,而修改這樣的功能就只需要按照位置來進行修改,不需要再按照姓名進行修改,因為存在重名的情況,并且可以根據(jù)返回的表單看到具體的聯(lián)系人位置,所以就不需要再 設(shè)計像按照名字進行修改這樣冗余的功能了。
8,通訊錄可以按照位置插入聯(lián)系人
二、項目目的
制作本項目是為了將所學到的鏈表的知識進行鞏固學習,做到學以致用,并通過做這樣的小項目來增強理解開發(fā),算法和C語言的指針,結(jié)構(gòu)體等知識,同時收獲開發(fā)經(jīng)驗,項目重點是能夠使用鏈表的知識做出的小項目,所以該項目不會考慮到實現(xiàn)數(shù)據(jù)持久化的操作和GUI編程,只是基于DOS的命令行程序。
三、項目開發(fā)
開發(fā)IDE:Visual Studio 2019
IDE注意:
1,在該IDE中不能夠直接使用scanf()函數(shù),因為它可能會存在一些不安全的因素,所以在該IDE中使用的是scanf_s()函數(shù),但是scanf和scanf_s函數(shù)具有本質(zhì)的區(qū)別,并且scanf_s函數(shù)只能在該IDE中使用,不廣泛,所以還是推薦使用scanf()函數(shù),為了能正常使用scanf()函數(shù),需要將聲明#define _CRT_SECURE_NO_WARNINGS放在項目的最頂部。
2,為了更方便的開發(fā)該項目,所以使用了一些c++的庫函數(shù),所以為了能夠正確運行程序,建議將后綴改為.cpp,其實c++是c的升級版本,解決了c不能面向?qū)ο箝_發(fā)的模式,它的編譯器既可以運行.c的程序也可以運行.cpp的程序,但是.c的編譯器是不能夠運行.cpp的程序的,它并不能識別一些.cpp的源碼。
首先,根據(jù)需求,選擇合適的數(shù)據(jù)結(jié)構(gòu),這里選擇的數(shù)據(jù)結(jié)構(gòu)是鏈表為主體,采用帶有頭結(jié)點的單鏈表的形式,通過傳入指向頭結(jié)點的指針進行添加結(jié)點,遍歷結(jié)點,刪除結(jié)點,插入結(jié)點等對結(jié)點的操作,這樣每次對鏈表進行操作就只需要傳入指向頭結(jié)點的指針就可以了,可以這樣理解:
當程序運行在內(nèi)存中的時候,首先先使用一個指針指向一個頭結(jié)點
將該指針傳入到添加結(jié)點的函數(shù)中,在該函數(shù)中通過指針從頭結(jié)點開始遍歷,使用頭插法或尾插法,將生成的結(jié)點插入到頭結(jié)點之后
然后再次傳入指向頭結(jié)點的指針到添加結(jié)點的函數(shù),此時該鏈表已經(jīng)有兩個結(jié)點,頭結(jié)點和一個結(jié)點,內(nèi)部函數(shù)使用指針進行遍歷,然后添加結(jié)點,形成頭結(jié)點為起點的后面帶有兩個結(jié)點的單向鏈表
依次如此....
理解這里最重要的是對于指針的理解
然后我們根據(jù)需要在通訊錄中的內(nèi)容可以存儲編號,聯(lián)系人的姓名,電話號碼和家庭住址定義一個結(jié)構(gòu)體,如下:
typedef struct Node { Number num;//編號 Name name[23];//聯(lián)系人姓名 Phone phone[33];//聯(lián)系人電話 Addr addr[50];//聯(lián)系人地址 struct Node* next;//next指針 }LNode;
在這里需要考慮到的問題是對于編號的實現(xiàn),編號可以在程序中由程序自主的實現(xiàn),也就是在程序中可以定義一個全局變量number,并且賦初值為0,當調(diào)用添加結(jié)點的函數(shù)時,number自增并將number的值的賦給結(jié)構(gòu)體成員num,但是它只能夠不斷的自增,當調(diào)用刪除功能時,它的序號就會變得混亂,比如,現(xiàn)在已經(jīng)添加了5個聯(lián)系人,編號分別為1,2,3,4,5,如果刪除編號為3的聯(lián)系人的話,那么此時通訊錄中的編號只剩下1,2,4,5,這樣就會造成編號無序的情況,就沒有意義也不便于管理操作。
如果編號在添加結(jié)點的時候由用戶輸入實現(xiàn)的話,對于用戶來說可能會增加操作的負擔,同時也不便于管理,可能它會無序甚至是重復。
所以最后可以解決的方式就是將編號不作為結(jié)構(gòu)體成員的變量而是作為功能函數(shù)體中的一部分,也就是說我們傳入頭結(jié)點之后添加結(jié)點形成的單鏈表,如果要將信息輸出到屏幕上時,可以定義一個指針指向單鏈表的首元結(jié)點并定義一個num為1的計數(shù)器,指針遍歷到的個數(shù)就是計數(shù)器上的數(shù)字,這樣如果刪除了某個結(jié)點,它都會重新遍歷一次,這樣編號就是有序的,也就是說在打印聯(lián)系人名單的函數(shù)中每次傳入頭結(jié)點都需要重新遍歷一下,編號由函數(shù)中的num給出,所以此時重新修改結(jié)構(gòu)體為:
typedef struct Node { Name name[23];//聯(lián)系人姓名 Phone phone[33];//聯(lián)系人電話 Addr addr[50];//聯(lián)系人地址 struct Node* next;//next指針 }LNode;
然后定義函數(shù)printNode(Node* head)用來打印通訊錄的名單,這里可以更好的理解如何解決編號的問題
void printNode(Node* head) { Node* move; move = head->next; int num = 1; printf("================================通訊錄頁面=============================\n"); while (move) { printf("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n"); printf("編號:%d 姓名:%s 電話:%s 住址:%s\n", num, move->name, move->phone, move->addr); printf("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n"); move = move->next; num++; } printf("================================通訊錄頁面=============================\n"); }
接下來,我們來定義完整的鏈表實現(xiàn):
首先,可以定義一個函數(shù)Node* initList()用來初始化鏈表,也就是創(chuàng)建一個頭結(jié)點并讓它的指針域指向NULL,定義一個指針函數(shù),該指針函數(shù)返回一個指向頭結(jié)點的指針。
Node* initList() { Node* head; head = (Node*)malloc(sizeof(Node)); head->next = NULL; return head; }
然后,定義一個函數(shù)用來添加聯(lián)系人也就是創(chuàng)建結(jié)點并使用頭插法添加到單向鏈表中,此時我們需要傳入指向頭結(jié)點的指針。
void addListNode(Node* head) { Node* node; node = (Node*)malloc(sizeof(Node)); printf("請輸入聯(lián)系人信息:\n"); printf("姓名:"); scanf("%s", &node->name); printf("電話號碼:"); scanf("%s", &node->phone); printf("家庭地址:"); scanf("%s", &node->addr); //使用頭插法將結(jié)點鏈接到頭結(jié)點之后 node->next = head->next; head->next = node; if (head->next != NULL) { printf("添加聯(lián)系人成功\n"); } else { printf("添加聯(lián)系人失敗\n"); } }
此時,可以在主方法中進行測試,此時已經(jīng)能夠打印出三個聯(lián)系人了
int main() { Node* head; head=initList(); addListNode(head); addListNode(head); printNode(head); return 0; }
我們已經(jīng)實現(xiàn)了通訊錄的存儲要求,添加聯(lián)系人和打印出通訊錄的聯(lián)系人信息了。
此時我們會考慮到如果該通訊錄為空的話,我們直接進行打印可能會造成nullptr,所以需要實現(xiàn)一個判斷表空的函數(shù),該函數(shù)返回bool值便于調(diào)用判斷布爾類型。
bool isempty(Node* head) { if (head->next == NULL) { return false; } else { return true; } }
此時在打印聯(lián)系人目錄的函數(shù)中使用該函數(shù)在函數(shù)的最開始進行判斷,并在main中進行測試
if (!isempty(head)) { printf("檢測到通訊錄為空,請先添加聯(lián)系人再進行操作\n"); return; }
接下來需要實現(xiàn)的是通訊錄的其他功能。
首先實現(xiàn)用戶可以根據(jù)聯(lián)系人的姓名刪除對應(yīng)通訊錄中的信息,根據(jù)聯(lián)系人的姓名刪除對應(yīng)通訊錄中的信息,我們可以想到,聯(lián)系人的姓名是可以重復的,所以在刪除時需要判斷是否有重名的情況,所以可以先實現(xiàn)第6個需求,也就是通訊錄中用戶可以根據(jù)聯(lián)系人的姓名找到聯(lián)系人在通訊錄中的信息,因為聯(lián)系人可以重名,所以如果有重名的聯(lián)系人的時候就需要返回兩個或兩個以上的聯(lián)系人信息到一個查找表單中。
首先我們知道該功能是通過用戶輸入聯(lián)系人的姓名也就是字符串然后在鏈表中找到相應(yīng)字符串的位置進行返回,那么我們就需要實現(xiàn)一個字符串匹配的函數(shù),它應(yīng)當返回一個bool類型的值,當指針在鏈表中不斷遍歷并取出聯(lián)系人的姓名進行比較直到找到這個聯(lián)系人為止也就是該函數(shù)返回false的時候循環(huán)結(jié)束,所以可以定義一個字符匹配函數(shù)bool isBatch(Name n1[], Name n2[]);它的具體實現(xiàn)如下:
bool isBatch(Name n1[], Name n2[]) { Name* n11, * n22;//定義兩個char類型的指針 n11 = n1;//讓n11指向字符串n1的首地址 n22 = n2;//讓n22指向字符串n2的首地址 int num1 = 1;//定義長度器用來計量n1的長度 int num2 = 1;//定義長度器用來計量n2的長度 //定義兩個長度器的原因是如果是n1:NUM,n2:NUM2的話,沒有計數(shù)器的情況下當跳出循環(huán)后它依然是返回true的 while (1) { if (*n11 == *n22) { n11++; n22++; num1++; num2++; if (*n11 == '\0' && *n22 == '\0') { break; } } else { return false; } } if (num1 != num2) { return false; } return true; }
然后我們需要返回聯(lián)系人在鏈表中的位置通過再打印查找單的函數(shù)中遍歷打印出信息,聯(lián)系人可能重名,所以位置可能會返回多個,一個或者是無聯(lián)系人的情況,因此這個返回位置的函數(shù)可以想到使用返回一個int數(shù)組的方式來操作,在C語言中如果想要返回數(shù)組的話需要返回的是指向這個數(shù)組的指針,也就是說C語言不能夠直接就返回一個數(shù)組類型的數(shù)據(jù),所以可以定義一個函數(shù)int* lookupByname(Node* head, Name name[]);傳入頭結(jié)點和輸入的名字,它的具體實現(xiàn)如下:
int* lookupByname(Node* head, Name name[]) { Node* target;//定義一個目標指針,該目標指針是用來獲取每一個結(jié)點中的name并與輸入的name進行比較 target = head->next;//讓其指向第一個結(jié)點 int* summary = NULL;//定義一個指向返回數(shù)組的指針 summary = (int*)calloc(NUM, sizeof(int));//該指針指向一片大小為NUM的int類型的數(shù)組,該數(shù)組中所有值為0 int loc = 1;//獲取位置從1開始,放在所申請的數(shù)組中 int numd = 0;//數(shù)組的下標 while (target)//遍歷完整個鏈表 { if (isBatch(target->name, name))//如果匹配 { summary[numd] = loc;//位置放入數(shù)組 numd++;//只要放入numd就會加1,所以只要有一個聯(lián)系人numd是1不是0 } target = target->next; loc++; } if (numd == 0) {//如果最后遍歷完整個鏈表numd還是0就說明沒有這個人 loc = -1;//讓位置為-1 summary[0] = loc; } return summary;//返回指向整型數(shù)組的指針 }
最后,我們來實現(xiàn)遍歷打印出查找單的函數(shù)void printsMenu(Node* head, int* loc);傳入鏈表的頭結(jié)點和指向整型數(shù)組的指針,它的具體實現(xiàn)如下:
void printsMenu(Node* head, int* loc) { if (!isempty(head)) { printf("檢測到通訊錄為空,請先添加聯(lián)系人再進行操作\n"); return; } Node* ptr;//定義一個指針用來獲取位置并打印信息 ptr = head->next; int num = 1;//定義位置尋找器用來找到指定的位置 printf("================================查找人匯總=============================\n"); for (int i = 0; i < NUM; i++)//遍歷返回的數(shù)組,數(shù)組最大值為NUM其實不必要遍歷到NUM,找到0的時候就可以跳出循環(huán) { if (loc[i] == -1) {//如果位置返回的是-1的話就表明無此人 printf(" 未查詢到此人\n"); break; } while (num!=loc[i]) {//指針指到相應(yīng)位置的結(jié)點 num++; ptr = ptr->next; } //打印信息 printf("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n"); printf("編號:%d 姓名:%s 電話:%s 住址:%s\n", num, ptr->name, ptr->phone, ptr->addr); printf("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n"); if (ptr->next == NULL) {//如果指針指到結(jié)尾就結(jié)束方法 return; } } return; }
此時我們可以來測試一下這個功能,在main方法中:
int main() { Node* head; Name name[23]="李白"; //添加兩個李白,一個李白,沒有李白和一個叫李白一個叫李白1進行測試 head=initList(); addListNode(head); addListNode(head); addListNode(head); addListNode(head); int* loc; loc=lookupByname(head,name); printsMenu(head, loc) return 0; }
接下來,既然現(xiàn)在已經(jīng)能夠按照姓名查到位置并輸出了,那么實現(xiàn)用戶可以根據(jù)聯(lián)系人的姓名刪除對應(yīng)通訊錄中的信息,這里可以考慮到姓名重名的話我們可以再設(shè)計一個按照位置刪除聯(lián)系人的函數(shù),這樣在按姓名刪除的函數(shù)中需要通過返回的數(shù)組的大小提醒用戶是否有重名的聯(lián)系人,并提供建議如果有則建議結(jié)束函數(shù)然后采用按位置刪除聯(lián)系人的方式解決,沒有就可以直接刪除還有就是沒有此聯(lián)系人的情況,所以定義函數(shù)void deleteByname(Node* head, Name name[]),依然傳入頭結(jié)點和要刪除的名字
void deleteByname(Node* head, Name name[]) { if (!isempty(head)) { printf("檢測到通訊錄為空,請先添加聯(lián)系人再進行操作\n"); return; } Node* p, * q;//定義兩個指針,p指針指向的是頭結(jié)點,q指針指向的是首元結(jié)點 p = head; q = head->next; //當q查詢到名字時就將q所指結(jié)點進行釋放,然后將p所指結(jié)點也就是q所指結(jié)點的前一個結(jié)點連接到q所指結(jié)點的后一個結(jié)點 while (q) { if (q->next == NULL && !isBatch(q->name, name)) {//當遍歷完整個鏈表并且最后一個結(jié)點的名字與輸入的名字不符合的情況下結(jié)束遍歷 printf("刪除失敗,未找到該聯(lián)系人\n"); return; } if (isBatch(q->name, name))//如果遍歷的過程中找到與輸入的名字相對于的名字時 { int* ptr = lookupByname(head, name);//調(diào)用查找名字位置的函數(shù)用于查找該名稱是否有重名的情況 int li=1;//定義計數(shù)器記錄有無重名的情況 for (int i = 0; i < NUM; i++) { if (ptr[i] == 0) { break; } li++; } if (li-1 == 1) {//計數(shù)器為1,也就是說只有一個名字無重名 printf("沒有重名的聯(lián)系人\n"); } else { printf("有%d個重名的聯(lián)系人,建議查詢后使用位置刪除\n",li-1); printf("按1選擇繼續(xù)刪除,按2選擇結(jié)束本次刪除:"); int input; scanf("%d", &input); if (input == 1) { goto loop; } else { return; } } break; } p = p->next; q = q->next; } loop: //刪除結(jié)點的過程 p->next = q->next; q->next = NULL; free(q); printf("刪除聯(lián)系人成功\n"); }
然后在main方法寫測試的用例:
int main() { Node* head; Name deletename[23]="李白"; //添加兩個李白,一個李白,沒有李白和一個叫李白一個叫李白1進行測試 head=initList(); addListNode(head); addListNode(head); addListNode(head); addListNode(head); deleteByName(head,deletename); return 0; }
添加兩個李白的情況:
添加一個李白的情況:
沒有李白的情況:
接下來,既然能夠按照聯(lián)系人的名稱進行刪除了,那么在重名的情況下,還需要一種刪除的方法,也就是按照位置的刪除方法,所以可以定義函數(shù)void deleteByLoc(Node* head, int loc),按照位置刪除聯(lián)系人,它的具體實現(xiàn)如下:
void deleteByLoc(Node* head, int loc) { if (!isempty(head)) { printf("檢測到通訊錄為空,請先添加聯(lián)系人再進行操作\n"); return; } Node* move,*q,*choic; choic = head->next;//choic指向首元結(jié)點,用來判斷鏈表中結(jié)點的數(shù)量,避免程序出現(xiàn)nullptr //刪除操作的兩個指針 q = head; move = head->next; int num = 1;//位置計數(shù)器,一直自增到對應(yīng)的位置loc int t = 1;//鏈表數(shù)量器,用來遍歷獲取鏈表的結(jié)點數(shù)量 while (choic) {//獲取鏈表的實際長度 if (choic->next == NULL) { break; } choic = choic->next; t++; } while (num!=loc&&move) {//尋找到要刪除的位置 q = q->next; move = move->next; num++; } if (num >= t) {//如果要刪除的位置比鏈表的長度都長就說明刪除錯誤 printf("查詢錯誤,已經(jīng)超出已有人數(shù)上限,會造成程序異常\n"); return; } else { q->next = move->next; move->next = NULL; free(move); printf("刪除成功\n"); } }
再次使用main方法進行測試:
int main() { Node* head; int loc=1; head=initList(); addListNode(head); deleteByLoc(head,1); return 0; }
到目前位置,整個通訊錄的功能已經(jīng)完成了,接下來完成修改聯(lián)系人的信息和插入新的聯(lián)系人的功能,先完成修改聯(lián)系人信息的功能,既然可以查找到重名的聯(lián)系人,所以此時需要按照位置修改聯(lián)系人信息,所以定義函數(shù)void modifyByName(Node* head, int loc),闖入頭結(jié)點和需要修改的位置,在修改的時候,我們希望可以展示修改人原先的信息,有些算法的思想和按位刪除聯(lián)系人的思想一致,具體實現(xiàn)如下:
void modifyByName(Node* head, int loc) { if (!isempty(head)) { printf("檢測到通訊錄為空,請先添加聯(lián)系人再進行操作\n"); return; } Node* move, * choic; choic = head->next; move = head->next; int num = 1; int t = 1; while (choic) { choic = choic->next; t++; if (choic->next == NULL) { break; } } while (num != loc && move) { move = move->next; num++; } if (num > t) { printf("位置錯誤,已經(jīng)超出已有人數(shù)上限,會造成程序異常\n"); return; } else { printf("找到聯(lián)系人信息\n"); printf("正在檢測聯(lián)系人信息...........\n"); printf("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n"); printf("編號:%d 姓名:%s 電話:%s 住址:%s\n", num, move->name, move->phone, move->addr); printf("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n"); printf("按1鍵更改聯(lián)系人信息,按2鍵不更改聯(lián)系人信息并結(jié)束:"); int inputs; scanf("%d", &inputs); if (inputs==1) { int istrue; do { printf("請輸入聯(lián)系人信息:\n"); printf("姓名:"); scanf("%s", &move->name); printf("電話號碼:"); scanf("%s", &move->phone); printf("家庭地址:"); scanf("%s", &move->addr); printf("按1鍵確認更改,按2鍵重新更改:"); scanf("%d", &istrue); } while (istrue!=1); printf("修改聯(lián)系人信息成功\n"); } else { printf("ERROR"); return; } } }
同樣在main函數(shù)中進行測試
最后完成最后一個功能,按位置插入聯(lián)系人,這個函數(shù)的意義不大,但是為了鞏固鏈表的插入而設(shè)計的,定義一個函數(shù)void insertNodeByLoc(Node* head, int loc)算法的實現(xiàn)思路與按位刪除的一致,只是找到后是將新的結(jié)點插入而已,具體實現(xiàn)如下:
void insertNodeByLoc(Node* head, int loc) { if (!isempty(head)) { printf("檢測到通訊錄為空,請先添加聯(lián)系人再進行操作\n"); return; } Node* p, * q,*m; m = head->next; p = head->next; q = head; int num=1; int i=1; while (m) { i++; if (m->next == NULL) { break; } } while (num!=loc&&p) { p = p->next; q = q->next; num++; } if (num >= i) { printf("插入位置錯誤,已經(jīng)超出已有數(shù)量上限,會造成程序異常\n"); return; } else { Node* node; node = (Node*)malloc(sizeof(Node)); printf("請輸入聯(lián)系人信息:\n"); printf("姓名:"); scanf("%s", &node->name); printf("電話號碼:"); scanf("%s", &node->phone); printf("家庭地址:"); scanf("%s", &node->addr); node->next = p->next; p->next = node; printf("插入成功\n"); } }
同樣進行測試:
以上就是C語言鏈表案例學習之通訊錄的實現(xiàn)的詳細內(nèi)容,更多關(guān)于C語言鏈表實現(xiàn)通訊錄的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C++實現(xiàn)一個簡易版的事件(Event)的示例代碼
之前在?windows系統(tǒng)中開發(fā)應(yīng)用時,?遇到需要進行線程同步的時候幾乎都是使用的事件內(nèi)核對象?Event。本文為大家整理了C++實現(xiàn)一個簡易版的事件(Event)的相關(guān)資料,需要的可以參考一下2022-11-11深入解析C++中的函數(shù)模板和函數(shù)的默認參數(shù)
這篇文章主要介紹了深入解析C++中的函數(shù)模板和函數(shù)的默認參數(shù),是C++入門學習中的基礎(chǔ)知識,需要的朋友可以參考下2015-09-09