有的時候,我們所遇到的數據結構,不僅僅是一群數字或者是字符串那么簡單。比如我們每一個人的學籍信息,學號是一個長整數,名字卻是字符;甚至有更復雜的情況,這種問題在現實生活中并不少見。我們之前學過一種叫數組的數據結構,它可以允許我們把很多同類型的數據集中在一起處理。相對于之前,這已經是一次極大的進步。但是,新的問題,往往又會出現,這個時候,我們就得上更高端的裝備——結構體。
相比于數組,結構體有以下的更強大的優勢:
批量存儲數據
存儲不同類型的數據
支持嵌套
結構體的聲明與定義
聲明
結構體的聲明使用struct關鍵字,如果我們想要把我們的學籍信息組織一下的話,可以這樣表示:
structInfo { unsignedlongidentifier;//學號,用無符號長整數表示 charname[20];//名字,用字符數組表示 unsignedintyear;//入學年份,用無符號整數表示 unsignedintyears;//學制,用無符號整數表示 }
這樣,我們就相當于描繪好了一個框架,以后要用的話直接定義一個這種類型的變量就好了。
定義
我們剛剛申請了一個名叫Info的結構體類型,那么理論上我們可以像聲明其他變量的操作一樣,去聲明我們的結構體操作,但是C語言中規定,聲明結構體變量的時候,struct關鍵字是不可少的。
struct 結構體類型名 結構體變量名
不過,你可以在某個函數里面定義:
#include structInfo { unsignedlongidentifier;//學號,用無符號長整數表示 charname[20];//名字,用字符數組表示 unsignedintyear;//入學年份,用無符號整數表示 unsignedintyears;//學制,用無符號整數表示 }; intmain(void) { /** *在main函數中聲明結構體變量 *結構體變量名叫info *struct關鍵字不能丟 */ structInfoinfo; ... }
也可以在聲明的時候就把變量名定義下來(此時這個變量是全局變量):
#include structInfo { unsignedlongidentifier;//學號,用無符號長整數表示 charname[20];//名字,用字符數組表示 unsignedintyear;//入學年份,用無符號整數表示 unsignedintyears;//學制,用無符號整數表示 }info; /** *此時直接定義了變量 *該變量是全局變量 *變量名叫info */ intmain(void) { ... }
訪問結構體成員
結構體成員的訪問有點不同于以往的任何變量,它是采用點號運算符.來訪問成員的。比如,info.name就是引用info結構體的name成員,是一個字符數組,而info.year則可以查到入學年份,是個無符號整型。
比如,下面開始錄入學生的信息:
//Example01 #include structInfo { unsignedlongidentifier;//學號,用無符號長整數表示 charname[20];//名字,用字符數組表示 unsignedintyear;//入學年份,用無符號整數表示 unsignedintyears;//學制,用無符號整數表示 }; intmain(void) { structInfoinfo; printf("請輸入學生的學號:"); scanf("%d",&info.identifier); printf("請輸入學生的姓名:"); scanf("%s",info.name); printf("請輸入學生的入學年份:"); scanf("%d",&info.year); printf("請輸入學生的學制:"); scanf("%d",&info.years); printf(" 數據錄入完畢 "); printf("學號:%d 姓名:%s 入學年份:%d 學制:%d 畢業時間:%d ", info.identifier,info.name,info.year,info.years,info.year+info.years); return0; }
運行結果如下:
//Consequence 01 請輸入學生的學號:20191101 請輸入學生的姓名:Harris 請輸入學生的入學年份:2019 請輸入學生的學制:4 數據錄入完畢 學號:20191101 姓名:Harris 入學年份:2019 學制:4 畢業時間:2023
初始化結構體
像數組一樣,結構體也可以在定義的時候初始化,方法也幾乎一樣:
structInfoinfo={ 20191101, "Harris", 2019, 4 };
在C99標準中,還支持給指定元素賦值(就像數組一樣):
structInfoinfo={ .name="Harris", .year=2019 };
對于沒有被初始化的成員,則「數值型」成員初始化為0,「字符型」成員初始化為‘’。
對齊
下面這個代碼,大家來看看會發生什么:
//EXample02V1 #include intmain(void) { structA { chara; intb; charc; }a={'a',10,'o'}; printf("sizeofa=%d ",sizeof(a)); return0; }
我們之前學過,char類型的變量占1字節,int類型的變量占4字節,那么這么一算,一個結構體A型的變量應該就是6字節了。別急,我們看運行結果:
//COnsequence 02 V1 size of a = 12
怎么變成12了呢?標準更新了?老師教錯了?都不是。我們把代碼改一下:
//EXample02V2 #include intmain(void) { structA { chara; charc; intb; }a={'a','o',10}; printf("sizeofa=%d ",sizeof(a)); return0; }
結果:
//Consequence 02 V2 size of a = 8
實際上,這是編譯器對我們程序的一種優化——內存對齊。在第一個例子中,第一個和第三個成員是char類型是1個字節,而中間的int卻有4個字節,為了對齊,兩個char也占用了4個字節,于是就是12個字節。
而在第二個例子里面,前兩個都是char,最后一個是int,那么前兩個可以一起占用4個字節(實際只用2個,第一個例子也同理,只是為了訪問速度更快,而不是為了擴展),最后的int占用4字節,合起來就是8個字節。
關于如何聲明結構體來節省內存容量,可以閱讀下面的這篇文章,作者是艾瑞克·雷蒙,時尚最具爭議性的黑客之一,被公認為開源運動的主要領導者之一:
英文原版,中文版
結構體嵌套
在學籍里面,如果我們的日期想要更加詳細一些,精確到day,這時候就可以使用結構體嵌套來完成:
#include structDate { unsignedintyear; unsignedintmonth; unsignedintday; }; structInfo { unsignedlongidentifier;//學號,用無符號長整數表示 charname[20];//名字,用字符數組表示 structDatedate;/*---入學日期,用結構體Date表示---*/ unsignedintyears;//學制,用無符號整數表示 }; intmain(void) { ... }
如此一來,比我們單獨聲明普通變量快多了。
不過,這樣訪問變量,就必須用點號一層層往下訪問。比如要訪問day這個成員,那就只能info.date.day而不能直接info.date或者info,day。
//Example03 #include structDate { unsignedintyear; unsignedintmonth; unsignedintday; }; structInfo { unsignedlongidentifier;//學號,用無符號長整數表示 charname[20];//名字,用字符數組表示 structDatedate;/*---入學日期,用結構體Date表示---*/ unsignedintyears;//學制,用無符號整數表示 }; intmain(void) { structInfoinfo; printf("請輸入學生的學號:"); scanf("%d",&info.identifier); printf("請輸入學生的姓名:"); scanf("%s",info.name); printf("請輸入學生的入學年份:"); scanf("%d",&info.date.year); printf("請輸入學生的入學月份:"); scanf("%d",&info.date.month); printf("請輸入學生的入學日期:"); scanf("%d",&info.date.day); printf("請輸入學生的學制:"); scanf("%d",&info.years); printf(" 數據錄入完畢 "); printf("學號:%d 姓名:%s 入學時間:%d/%d/%d 學制:%d 畢業時間:%d ", info.identifier,info.name, info.date.year,info.date.month,info.date.day, info.years,info.date.year+info.years); return0; }
運行結果如下:
//Consequence 03 請輸入學生的學號:20191101 請輸入學生的姓名:Harris 請輸入學生的入學年份:2019 請輸入學生的入學月份:9 請輸入學生的入學日期:7 請輸入學生的學制:4 數據錄入完畢 學號:20191101 姓名:Harris 入學時間:2019/9/7 學制:4 畢業時間:2023
結構體數組
剛剛我們演示了存儲一個學生的學籍信息的時候,使用結構體的例子。那么,如果要錄入一批學生,這時候我們就可以沿用之前的思路,使用結構體數組。
我們知道,數組的定義,就是存放一堆相同類型的數據的容器。而結構體一旦被我們聲明,那么你就可以把它看作一個類型,只不過是你自己定義的罷了。
定義結構體數組也很簡單:
struct結構體類型 { 成員; }數組名[長度]; /****或者這樣****/ struct結構體類型 { 成員; }; struct結構體類型數組名[長度];
結構體指針
既然我們可以把結構體看作一個類型,那么也就必然有對應的指針變量。
structInfo*pinfo;
但是在指針這里,結構體和數組就不一樣了。我們知道,數組名實際上就是指向這個數組第一個元素的地址,所以可以將數組名直接賦值給指針。而結構體的變量名并不是指向該結構體的地址,所以要使用取地址運算符&才能獲取地址:
pinfo=&info;
通過結構體指針來訪問結構體有以下兩種方法:
(*結構體指針).成員名
結構體指針->成員名
第一個方法由于點號運算符比指針的取值運算符優先級更高,因此需要加一個小括號來確定優先級,讓指針先解引用變成結構體變量,在使用點號的方法去訪問。
相比之下,第二種方法就直觀許多。
這兩種方法在實現上是完全等價的,但是點號只能用于結構體變量,而箭頭只能夠用于指針。
第一種方法:
#include ... intmain(void) { structInfo*p; p=&info; printf("學號: ",(*p).identifier); printf("姓名: ",(*p).name); printf("入學時間:%d/%d/%d ",(*p).date.year,(*p).date.month,(*p).date.day); printf("學制: ",(*p).years); return0; }
第二種方法:
#include ... intmain(void) { structInfo*p; p=&info; printf("學號: ",p->identifier); printf("姓名: ",p->name); printf("入學時間:%d/%d/%d ",p->date.year,p->date.month,p->date.day); printf("學制: ",p->years); return0; }
傳遞結構體信息
傳遞結構體變量
我們先來看看下面的代碼:
//Example04 #include intmain(void) { structTest { intx; inty; }t1,t2; t1.x=3; t1.y=4; t2=t1; printf("t2.x=%d,t2.y=%d ",t2.x,t2.y); return0; }
運行結果如下:
//Consequence 04 t2.x = 3, t2.y = 4
這么看來,結構體是可以直接賦值的。那么既然這樣,作為函數的參數和返回值也自然是沒問題的了。
先來試試作為參數:
//Example05 #include structDate { unsignedintyear; unsignedintmonth; unsignedintday; }; structInfo { unsignedlongidentifier; charname[20]; structDatedate; unsignedintyears; }; structInfogetInput(structInfoinfo); voidprintInfo(structInfoinfo); structInfogetInput(structInfoinfo) { printf("請輸入學號:"); scanf("%d",&info.identifier); printf("請輸入姓名:"); scanf("%s",info.name); printf("請輸入入學年份:"); scanf("%d",&info.date.year); printf("請輸入月份:"); scanf("%d",&info.date.month); printf("請輸入日期:"); scanf("%d",&info.date.day); printf("請輸入學制:"); scanf("%d",&info.years); returninfo; } voidprintInfo(structInfoinfo) { printf("學號:%d 姓名:%s 入學時間:%d/%d/%d 學制:%d 畢業時間:%d ", info.identifier,info.name, info.date.year,info.date.month,info.date.day, info.years,info.date.year+info.years); } intmain(void) { structInfoi1={}; structInfoi2={}; printf("請錄入第一個同學的信息... "); i1=getInput(i1); putchar(' '); printf("請錄入第二個學生的信息... "); i2=getInput(i2); printf(" 錄入完畢,現在開始打印... "); printf("打印第一個學生的信息... "); printInfo(i1); putchar(' '); printf("打印第二個學生的信息... "); printInfo(i2); return0; }
運行結果如下:
//Consequence 05 請錄入第一個同學的信息... 請輸入學號:20191101 請輸入姓名:Harris 請輸入入學年份:2019 請輸入月份:9 請輸入日期:7 請輸入學制:4 請錄入第二個學生的信息... 請輸入學號:20191102 請輸入姓名:Joy 請輸入入學年份:2019 請輸入月份:9 請輸入日期:8 請輸入學制:5 錄入完畢,現在開始打印... 打印第一個學生的信息... 學號:20191101 姓名:Harris 入學時間:2019/9/7 學制:4 畢業時間:2023 打印第二個學生的信息... 學號:20191102 姓名:Joy 入學時間:2019/9/8 學制:5 畢業時間:2024
傳遞指向結構體變量的指針
早期的C語言是不允許直接將結構體作為參數直接傳遞進去的。主要是考慮到如果結構體的內存占用太大,那么整個程序的內存開銷就會爆炸。不過現在的C語言已經放開了這方面的限制。
不過,作為一名合格的開發者,我們應該要去珍惜硬件資源。那么,傳遞指針就是一個很好的辦法。
將剛才的代碼修改一下:
//Example06 #include structDate { unsignedintyear; unsignedintmonth; unsignedintday; }; structInfo { unsignedlongidentifier; charname[20]; structDatedate; unsignedintyears; }; voidgetInput(structInfo*info); voidprintInfo(structInfo*info); voidgetInput(structInfo*info) { printf("請輸入學號:"); scanf("%d",&info->identifier); printf("請輸入姓名:"); scanf("%s",info->name); printf("請輸入入學年份:"); scanf("%d",&info->date.year); printf("請輸入月份:"); scanf("%d",&info->date.month); printf("請輸入日期:"); scanf("%d",&info->date.day); printf("請輸入學制:"); scanf("%d",&info->years); } voidprintInfo(structInfo*info) { printf("學號:%d 姓名:%s 入學時間:%d/%d/%d 學制:%d 畢業時間:%d ", info->identifier,info->name, info->date.year,info->date.month,info->date.day, info->years,info->date.year+info->years); } intmain(void) { structInfoi1={}; structInfoi2={}; printf("請錄入第一個同學的信息... "); getInput(&i1); putchar(' '); printf("請錄入第二個學生的信息... "); getInput(&i2); printf(" 錄入完畢,現在開始打印... "); printf("打印第一個學生的信息... "); printInfo(&i1); putchar(' '); printf("打印第二個學生的信息... "); printInfo(&i2); return0; }
此時傳遞的就是一個指針,而不是一個龐大的結構體。
動態申請結構體
結構體也可以在堆里面動態申請:
//Example01 #include ... intmain(void) { structInfo*i1; structInfo*i2; i1=(structInfo*)malloc(sizeof(structInfo)); i2=(structInfo*)malloc(sizeof(structInfo)); if(i1==NULL||i2==NULL) { printf("內存分配失?。?"); exit(1); } printf("請錄入第一個同學的信息... "); getInput(i1); putchar(' '); printf("請錄入第二個學生的信息... "); getInput(i2); printf(" 錄入完畢,現在開始打印... "); printf("打印第一個學生的信息... "); printInfo(i1); putchar(' '); printf("打印第二個學生的信息... "); printInfo(i2); free(i1); free(i2); return0; }
實戰:建立一個圖書館數據庫
實際上,我們建立的數組可以是指向結構體指針的數組。
代碼實現如下:
//Example02 #include #include #defineMAX_SIZE100 structDate { intyear; intmonth; intday; }; structBook { chartitle[128]; charauthor[48]; floatprice; structDatedate; charpublisher[48]; }; voidgetInput(structBook*book);//錄入數據 voidprintBook(structBook*book);//打印數據 voidinitLibrary(structBook*lib[]);//初始化結構體 voidprintLibrary(structBook*lib[]);//打印單本書數據 voidreleaseLibrary(structBook*lib[]);//釋放內存 voidgetInput(structBook*book) { printf("請輸入書名:"); scanf("%s",book->title); printf("請輸入作者:"); scanf("%s",book->author); printf("請輸入售價:"); scanf("%f",&book->price); printf("請輸入出版日期:"); scanf("%d-%d-%d",&book->date.year,&book->date.month,&book->date.day); printf("請輸入出版社:"); scanf("%s",book->publisher); } voidprintBook(structBook*book) { printf("書名:%s ",book->title); printf("作者:%s ",book->author); printf("售價:%.2f ",book->price); printf("出版日期:%d-%d-%d ",book->date.year,book->date.month,book->date.day); printf("出版社:%s ",book->publisher); } voidinitLibrary(structBook*lib[]) { for(inti=0;i
運行結果如下:
//Consequence 02 請問是否要錄入圖書信息(Y/N):Y 請輸入書名:人類簡史 請輸入作者:尤瓦爾·赫拉利 請輸入售價:32.25 請輸入出版日期:2016-3-4 請輸入出版社:中信出版集團 請問是否要錄入圖書信息(Y/N):N 數據錄入完畢,開始打印驗證... 書名:人類簡史 作者:尤瓦爾·赫拉利 售價:32.25 出版日期:2016-3-4 出版社:中信出版集團
單鏈表
我們知道,數組變量在內存中,是連續的,而且不可拓展。顯然在一些情況下,這種數據結構擁有很大的局限性。比如移動數據的時候,會牽一發而動全身,尤其是反轉這種操作更加令人窒息。那么,需要需要一種數據結構來弄出一種更加靈活的“數組”,那么這,就是「鏈表」。
本節我們只講講單鏈表。
所謂鏈表,就是由一個個「結點」組成的一個數據結構。每個結點都有「數據域」和「指針域」組成。其中數據域用來存儲你想要存儲的信息,而指針域用來存儲下一個結點的地址。如圖:
單鏈表
當然,鏈表最前面還有一個頭指針,用來存儲頭結點的地址。
這樣一來,鏈表中的每一個結點都可以不用挨個存放,因為有了指針把他們串起來。因此結點放在哪都無所謂,反正指針總是能夠指向下一個元素。我們只需要知道頭指針,就能夠順藤摸瓜地找到整個鏈表。
因此對于學籍數據庫來說,我們只需要在Info結構體中加上一個指向自身類型的成員即可:
structInfo { unsignedlongidentifier; charname[20]; structDatedate; unsignedintyears; structInfo*next; };
在單鏈表中插入元素
頭插法
這種每次都將數據插入單鏈表的頭部(頭指針后面)的插入法就叫頭插法。
如果要把學生信息加入到單鏈表,可以這么寫:
voidaddInfo(structInfo**students)//students是頭指針 { structInfo*info,*temp; info=(structInfo*)malloc(sizeof(structInfo)); if(info==NULL) { printf("內存分配失?。?"); exit(1); } getInput(info); if(*students!=NULL) { temp=*students; *students=info; info->next=temp; } else { *students=info; info->next=NULL; } }
?
由于students存放的是頭指針,因此我們需要傳入它的地址傳遞給函數,才能夠改變它本身的值。而students本身又是一個指向Info結構體的指針,所以參數的類型應該就是struct Info**。
?
往單鏈表里面添加一個結點,也就是先申請一個結點,然后判斷鏈表是否為空。如果為空,那么直接將頭指針指向它,然后next成員指向NULL。若不為空,那么先將next指向頭指針原本指向的結點,然后將頭指針指向新結點即可。
那么,打印鏈表也變得很簡單:
voidprintStu(structInfo*students) { structInfo*info; intcount=1; info=students; while(book!=NULL) { printf("Student%d: ",count); printf("姓名:%s ",info->name); printf("學號:%d ",info->identifier); info=info->next; count++; } }
想要讀取單鏈表里面的數據,只需要迭代單鏈表中的每一個結點,直到next成員為NULL,即表示單鏈表的結束。
最后,當然還是別忘了釋放空間:
voidreleaseStu(structInfo**students) { structInfo*temp; while(*students!=NULL) { temp=*students; *students=(*students)->next; free(temp); } }
尾插法
與頭插法類似,尾插法就是把每一個數據都插入到鏈表的末尾。
voidaddInfo(structInfo**students) { structInfo*info,*temp; info=(structInfo*)malloc(sizeof(structInfo)); if(info==NULL) { printf("內存分配失??! "); exit(1); } getInput(info); if(*students!=NULL) { temp=*students; *students=info; //定位到鏈表的末尾的位置 while(temp->next!=NULL) { temp=temp->next; } //插入數據 temp->next=info; info->next=temp; } else { *students=info; info->next=NULL; } }
這么一來,程序執行的效率難免要降低很多,因為每次插入數據,都要先遍歷一次鏈表。如果鏈表很長,那么對于插入數據來說就是一次災難。不過,我們可以給程序添加一個指針,讓它永遠都指向鏈表的尾部,這樣一來,就可以用很少的空間換取很高的程序執行效率。
代碼更改如下:
voidaddInfo(structInfo**students) { structInfo*info,*temp; staticstructInfo*tail;//設置靜態指針 info=(structInfo*)malloc(sizeof(structInfo)); if(info==NULL) { printf("內存分配失??! "); exit(1); } getInput(info); if(*students!=NULL) { tail->next=info; info->next=NULL; } else { *students=info; info->next=NULL; } }
搜索單鏈表
單鏈表是我們用來存儲數據的一個容器,那么有時候需要快速查找信息就需要開發相關搜索的功能。比如說輸入學號,查找同學的所有信息。
structInfo*searchInfo(structInfo*students,long*target) { structInfo*info; info=students; while(info!=NULL) { if(info->identifier==target) { break; } info=info->next; } returnbook; }; voidprintInfo(structInfo*info) { ... } ... intmain(void) { ... printf(" 請輸入學生學號:"); scanf("%d",input); info=searchInfo(students,input); if(info==NULL) { printf("抱歉,未找到相關結果! "); } else { do { printf("相關結果如下: "); printInfo(book); }while((info=searchInfo(info->next,input))!=NULL); } releaseInfo(...); return0; }
插入結點到指定位置
到了這里,才體現出鏈表真正的優勢。
設想一下,如果有一個有序數組,現在要求你去插入一個數字,插入完成之后,數組依然保持有序。你會怎么做?
沒錯,你應該會挨個去比較,然后找到合適的位置(當然這里也可以使用二分法,比較節省算力),把這個位置后面的所有數都往后移動一個位置,然后將我們要插入的數字放入剛剛我們騰出來的空間里面。
你會發現,這樣的處理方法,經常需要移動大量的數據,對于程序的執行效率來說,是一個不利因素。那么鏈表,就無所謂。反正在內存中,鏈表的存儲毫無邏輯,我們只需要改變指針的值就可以實現鏈表的中間插入。
//Example03 #include #include structNode { intvalue; structNode*next; }; voidinsNode(structNode**head,intvalue) { structNode*pre; structNode*cur; structNode*New; cur=*head; pre=NULL; while(cur!=NULL&&cur->valuenext; } New=(structNode*)malloc(sizeof(structNode)); if(New==NULL) { printf("內存分配失敗! "); exit(1); } New->value=value; New->next=cur; if(pre==NULL) { *head=New; } else { pre->next=New; } } voidprintNode(structNode*head) { structNode*cur; cur=head; while(cur!=NULL) { printf("%d",cur->value); cur=cur->next; } putchar(' '); } intmain(void) { structNode*head=NULL; intinput; printf("開始插入整數... "); while(1) { printf("請輸入一個整數,輸入-1表示結束:"); scanf("%d",&input); if(input==-1) { break; } insNode(&head,input); printNode(head); } return0; }
運行結果如下:
//Consequence 03 開始插入整數... 請輸入一個整數,輸入-1表示結束:4 4 請輸入一個整數,輸入-1表示結束:5 4 5 請輸入一個整數,輸入-1表示結束:3 3 4 5 請輸入一個整數,輸入-1表示結束:6 3 4 5 6 請輸入一個整數,輸入-1表示結束:2 2 3 4 5 6 請輸入一個整數,輸入-1表示結束:5 2 3 4 5 5 6 請輸入一個整數,輸入-1表示結束:1 1 2 3 4 5 5 6 請輸入一個整數,輸入-1表示結束:7 1 2 3 4 5 5 6 7 請輸入一個整數,輸入-1表示結束:-1
刪除結點
刪除結點的思路也差不多,首先修改待刪除的結點的上一個結點的指針,將其指向待刪除結點的下一個結點。然后釋放待刪除結點的空間。
... voiddelNode(structNode**head,intvalue) { structNode*pre; structNode*cur; cur=*head; pre=NULL; while(cur!=NULL&&cur->value!=value) { pre=cur; cur=cur->next; } if(cur==NULL) { printf("未找到匹配項! "); return; } else { if(pre==NULL) { *head=cur->next; } else { pre->next=cur->next; } free(cur); } }
內存池
C語言的內存管理,從來都是一個讓人頭禿的問題。要想更自由地管理內存,就必須去堆中申請,然后還需要考慮何時釋放,萬一釋放不當,或者沒有及時釋放,造成的后果都是難以估量的。
當然如果就這些,那倒也還不算什么。問題就在于,如果大量地使用malloc和free函數來申請內存,首先使要經歷一個從應用層切入系統內核層,調用完成之后,再返回應用層的一系列步驟,實際上使非常浪費時間的。更重要的是,還會產生大量的內存碎片。比如,先申請了一個1KB的空間,緊接著又申請了一個8KB的空間。而后,這個1KB使用完了,被釋放,但是這個空間卻只有等到下一次有剛好1KB的空間申請,才能夠被重新調用。這么一來,極限情況下,整個堆有可能被弄得支離破碎,最終導致大量內存浪費。
那么這種情況下,我們解決這類問題的思路,就是創建一個內存池。
內存池,實際上就是我們讓程序創建出來的一塊額外的緩存區域,如果有需要釋放內存,先不必使用free函數,如果內存池有空,那么直接放入內存池。同樣的道理,下一次程序申請空間的時候,先檢查下內存池里面有沒有合適的內存,如果有,則直接拿出來調用,如果沒有,那么再使用malloc。
其實內存池我們就可以使用單鏈表來進行維護,下面通過一個通訊錄的程序來說明內存池的運用。
普通的版本:
//Example04V1 #include #include #include structPerson { charname[40]; charphone[20]; structPerson*next; }; voidgetInput(structPerson*person); voidprintPerson(structPerson*person); voidaddPerson(structPerson**contects); voidchangePerson(structPerson*contacts); voiddelPerson(structPerson**contacts); structPerson*findPerson(structPerson*contacts); voiddisplayContacts(structPerson*contacts); voidreleaseContacts(structPerson**contacts); voidgetInput(structPerson*person) { printf("請輸入姓名:"); scanf("%s",person->name); printf("請輸入電話:"); scanf("%s",person->phone); } voidaddPerson(structPerson**contacts) { structPerson*person; structPerson*temp; person=(structPerson*)malloc(sizeof(structPerson)); if(person==NULL) { printf("內存分配失??! "); exit(1); } getInput(person); //將person添加到通訊錄中 if(*contacts!=NULL) { temp=*contacts; *contacts=person; person->next=temp; } else { *contacts=person; person->next=NULL; } } voidprintPerson(structPerson*person) { printf("聯系人:%s ",person->name); printf("電話:%s ",person->phone); } structPerson*findPerson(structPerson*contacts) { structPerson*current; charinput[40]; printf("請輸入聯系人:"); scanf("%s",input); current=contacts; while(current!=NULL&&strcmp(current->name,input)) { current=current->next; } returncurrent; } voidchangePerson(structPerson*contacts) { structPerson*person; person=findPerson(contacts); if(person==NULL) { printf("找不到聯系人! "); } else { printf("請輸入聯系電話:"); scanf("%s",person->phone); } } voiddelPerson(structPerson**contacts) { structPerson*person; structPerson*current; structPerson*previous; //先找到待刪除的節點的指針 person=findPerson(*contacts); if(person==NULL) { printf("找不到該聯系人! "); } else { current=*contacts; previous=NULL; //將current定位到待刪除的節點 while(current!=NULL&¤t!=person) { previous=current; current=current->next; } if(previous==NULL) { //若待刪除的是第一個節點 *contacts=current->next; } else { //若待刪除的不是第一個節點 previous->next=current->next; } free(person);//將內存空間釋放 } } voiddisplayContacts(structPerson*contacts) { structPerson*current; current=contacts; while(current!=NULL) { printPerson(current); current=current->next; } } voidreleaseContacts(structPerson**contacts) { structPerson*temp; while(*contacts!=NULL) { temp=*contacts; *contacts=(*contacts)->next; free(temp); } } intmain(void) { intcode; structPerson*contacts=NULL; structPerson*person; printf("|歡迎使用通訊錄管理程序| "); printf("|---1:插入新的聯系人---| "); printf("|---2:查找現有聯系人---| "); printf("|---3:更改現有聯系人---| "); printf("|---4:刪除現有聯系人---| "); printf("|---5:顯示當前通訊錄---| "); printf("|---6:退出通訊錄程序---| "); while(1) { printf(" 請輸入指令代碼:"); scanf("%d",&code); switch(code) { case1:addPerson(&contacts);break; case2:person=findPerson(contacts); if(person==NULL) { printf("找不到該聯系人! "); } else { printPerson(person); } break; case3:changePerson(contacts);break; case4:delPerson(&contacts);break; case5:displayContacts(contacts);break; case6:gotoEND; } } END://此處直接跳出恒循環 releaseContacts(&contacts); return0; }
運行結果如下:
//Consequence 04 V1 | 歡迎使用通訊錄管理程序 | |--- 1:插入新的聯系人 ---| |--- 2:查找現有聯系人 ---| |--- 3:更改現有聯系人 ---| |--- 4:刪除現有聯系人 ---| |--- 5:顯示當前通訊錄 ---| |--- 6:退出通訊錄程序 ---| 請輸入指令代碼:1 請輸入姓名:HarrisWilde 請輸入電話:0101111 請輸入指令代碼:1 請輸入姓名:Jack 請輸入電話:0101112 請輸入指令代碼:1 請輸入姓名:Rose 請輸入電話:0101113 請輸入指令代碼:2 請輸入聯系人:HarrisWilde 聯系人:HarrisWilde 電話:0101111 請輸入指令代碼:2 請輸入聯系人:Mike 找不到該聯系人! 請輸入指令代碼:5 聯系人:Rose 電話:0101113 聯系人:Jack 電話:0101112 聯系人:HarrisWilde 電話:0101111 請輸入指令代碼:3 請輸入聯系人:HarrisWilde 請輸入聯系電話:0101234 請輸入指令代碼:5 聯系人:Rose 電話:0101113 聯系人:Jack 電話:0101112 聯系人:HarrisWilde 電話:0101234 請輸入指令代碼:6
下面加入內存池:
//Example04V2 #include #include #include #defineMAX1024 structPerson { charname[40]; charphone[20]; structPerson*next; }; structPerson*pool=NULL; intcount; voidgetInput(structPerson*person); voidprintPerson(structPerson*person); voidaddPerson(structPerson**contects); voidchangePerson(structPerson*contacts); voiddelPerson(structPerson**contacts); structPerson*findPerson(structPerson*contacts); voiddisplayContacts(structPerson*contacts); voidreleaseContacts(structPerson**contacts); voidreleasePool(void); voidgetInput(structPerson*person) { printf("請輸入姓名:"); scanf("%s",person->name); printf("請輸入電話:"); scanf("%s",person->phone); } voidaddPerson(structPerson**contacts) { structPerson*person; structPerson*temp; //如果內存池不是空的,那么首先從里面獲取空間 if(pool!=NULL) { person=pool; pool=pool->next; count--; } //內存池為空,則直接申請 else { person=(structPerson*)malloc(sizeof(structPerson)); if(person==NULL) { printf("內存分配失敗! "); exit(1); } } getInput(person); //將person添加到通訊錄中 if(*contacts!=NULL) { temp=*contacts; *contacts=person; person->next=temp; } else { *contacts=person; person->next=NULL; } } voidprintPerson(structPerson*person) { printf("聯系人:%s ",person->name); printf("電話:%s ",person->phone); } structPerson*findPerson(structPerson*contacts) { structPerson*current; charinput[40]; printf("請輸入聯系人:"); scanf("%s",input); current=contacts; while(current!=NULL&&strcmp(current->name,input)) { current=current->next; } returncurrent; } voidchangePerson(structPerson*contacts) { structPerson*person; person=findPerson(contacts); if(person==NULL) { printf("找不到聯系人! "); } else { printf("請輸入聯系電話:"); scanf("%s",person->phone); } } voiddelPerson(structPerson**contacts) { structPerson*person; structPerson*current; structPerson*previous; structPerson*temp; { }; //先找到待刪除的節點的指針 person=findPerson(*contacts); if(person==NULL) { printf("找不到該聯系人! "); } else { current=*contacts; previous=NULL; //將current定位到待刪除的節點 while(current!=NULL&¤t!=person) { previous=current; current=current->next; } if(previous==NULL) { //若待刪除的是第一個節點 *contacts=current->next; } else { //若待刪除的不是第一個節點 previous->next=current->next; } //判斷內存池中有沒有空位 if(countnext=temp; } else { pool=person; person->next=NULL; } count++; } //沒有空位,直接釋放 else { free(person);//將內存空間釋放 } } } voiddisplayContacts(structPerson*contacts) { structPerson*current; current=contacts; while(current!=NULL) { printPerson(current); current=current->next; } } voidreleaseContacts(structPerson**contacts) { structPerson*temp; while(*contacts!=NULL) { temp=*contacts; *contacts=(*contacts)->next; free(temp); } } voidreleasePool(void) { structPerson*temp; while(pool!=NULL) { temp=pool; pool=pool->next; free(temp); } } intmain(void) { intcode; structPerson*contacts=NULL; structPerson*person; printf("|歡迎使用通訊錄管理程序| "); printf("|---1:插入新的聯系人---| "); printf("|---2:查找現有聯系人---| "); printf("|---3:更改現有聯系人---| "); printf("|---4:刪除現有聯系人---| "); printf("|---5:顯示當前通訊錄---| "); printf("|---6:退出通訊錄程序---| "); while(1) { printf(" 請輸入指令代碼:"); scanf("%d",&code); switch(code) { case1:addPerson(&contacts);break; case2:person=findPerson(contacts); if(person==NULL) { printf("找不到該聯系人! "); } else { printPerson(person); } break; case3:changePerson(contacts);break; case4:delPerson(&contacts);break; case5:displayContacts(contacts);break; case6:gotoEND; } } END://此處直接跳出恒循環 releaseContacts(&contacts); releasePool(); return0; }
typedef
給數據類型起別名
C語言是一門古老的語言,它是在1969至1973年間,由兩位天才丹尼斯·里奇和肯·湯普遜在貝爾實驗室以B語言為基礎開發出來的,用于他們的重寫UNIX計劃(這也為后來UNIX系統的可移植性打下了基礎,之前的UNIX是使用匯編語言編寫的,當然也是這兩位為了玩一個自己設計的游戲而編寫的)。天才就是和咱常人不一樣,不過他倆的故事,在這篇里面不多啰嗦,我們回到話題。
雖然C語言誕生的很早,但是卻依舊不是最早的高級編程語言。目前公認的最早的高級編程語言,是IBM公司于1957年開發的FORTRAN語言。C語言誕生之時,FORTRAN已經統領行業數十年之久。因此,C語言要想快速吸納FORTRAN中的潛在用戶,就必須做出一些妥協。
我們知道,不同的語言的語法,一般來說是不同的,甚至還有較大的差距。比如:
C:
inta,b,c; floati,j,k;
而FORTRAN語言是這樣的:
integer :: a, b, c; real :: i, j, k;
如果讓FORTRAN用戶使用原來的變量名稱進行使用,那么就能夠快速遷移到C語言上面來,這就是typedef的用處之一。
我們使用FORTRAN語言的類型名,那就這么辦:
typedefintinteger; typedeffloatreal; integera,b,c; reali,j,k;
結構體的搭檔
雖然結構體的出現能夠讓我們有一個更科學的數據結構來管理數據,但是每次使用結構體都需要struct...,未免顯得有些冗長和麻煩。有了typedef的助攻,我們就可以很輕松地給結構體類型起一個容易理解的名字:
typedefstructdate { intyear; intmonth; intday; }DATE;//為了區分,一般用全大寫 intmain(void) { DATE*date; ... }
甚至還可以順便給它的指針也定義一個別名:
typedefstructdate { intyear; intmonth; intday; }DATE,*PDATE;
進階
我們還可以利用typedef來簡化一些比較復雜的命令。
比如:
int(*ptr)[5];
我們知道這是一個數組指針,指向一個5元素的數組。那么我們可以改寫成這樣:
typedefint(*PTR_TO_ARRAY)[3];
這樣就可以把很復雜的聲明變得很簡單:
PTR_TO_ARRAYa=&array;
取名的時候要盡量使用容易理解的名字,這樣才能達到使用typedef的最終目的。
共用體
共用體也稱聯合體。
聲明
和結構體還是有點像:
union共用體名稱 { 成員1; 成員2; 成員3; };
但是兩者有本質的不同。共用體的每一個成員共用一段內存,那么這也就意味著它們不可能同時被正確地訪問。如:
//Example05 #include #include unionTest { inti; doublepi; charstr[9]; }; intmain(void) { unionTesttest; test.i=10; test.pi=3.14; strcpy(test.str,"TechZone"); printf("test.i:%d ",test.i); printf("test.pi:%.2f ",test.pi); printf("test.str:%s ",test.str); return0; }
執行結果如下:
//Consequence 05 test.i: 1751344468 test.pi: 3946574856045802736197446431383475413237648487838717723111623714247921409395495328582015991082102150186282825269379326297769425957893182570875995348588904500564659454087397032067072.00 test.str: TechZone
可以看到,共用體只能正確地展示出最后一次被賦值的成員。共用體的內存應該要能夠滿足最大的成員能夠正常存儲。但是并不一定等于最大的成員的尺寸,因為還要考慮內存對齊的問題。
共用體可以類似結構體一樣來定義和聲明,但是共用體還可以允許不帶名字:
union { inti; charch; floatf; }a,b;
初始化
共用體不能在同一時間存放多個成員,所以不能批量初始化
uniondata { inti; charch; floatf; }; uniondataa={520};//初始化第一個成員 uniondatab=a;//直接使用一個共用體初始化另一個共用體 uniondatac={.ch='C'};//C99的特性,指定初始化成員
枚舉
枚舉是一個基本的數據類型,它可以讓數據更簡潔。
如果寫一個判斷星期的文章,我們當然可以使用宏定義來使代碼更加易懂,不過:
#defineMON1 #defineTUE2 #defineWED3 #defineTHU4 #defineFRI5 #defineSAT6 #defineSUN7
這樣的寫法有點費鍵盤。那么枚舉就簡單多了:
enumDAY { MON=1,TUE,WED,THU,FRI,SAT,SUN };
?
**注意:**第一個枚舉成員的默認值為整型的 0,后續枚舉成員的值在前一個成員上加 1。我們在這個實例中把第一個枚舉成員的值定義為 1,第二個就為 2,以此類推。
?
枚舉變量的定義和聲明方法和共用體一樣,也可以省略枚舉名,直接聲明變量名。
//Example06 #include #include intmain() { enumcolor{red=1,green,blue}; enumcolorfavorite_color; printf("請輸入你喜歡的顏色:(1.red,2.green,3.blue):"); scanf("%d",&favorite_color); //輸出結果 switch(favorite_color) { casered: printf("你喜歡的顏色是紅色"); break; casegreen: printf("你喜歡的顏色是綠色"); break; caseblue: printf("你喜歡的顏色是藍色"); break; default: printf("你沒有選擇你喜歡的顏色"); } return0; }
執行結果如下:
//Consequence 06 請輸入你喜歡的顏色: (1. red, 2. green, 3. blue): 3 你喜歡的顏色是藍色
也可以把整數轉換為枚舉類型:
//Example07 #include #include intmain() { enumday { saturday, sunday, monday, tuesday, wednesday, thursday, friday }workday; inta=1; enumdayweekend; weekend=(enumday)a;//使用強制類型轉換 //weekend=a;//錯誤 printf("weekend:%d",weekend); return0; }
運行結果如下:
//Consequence 07 weekend:1
位域
C語言除了開發桌面應用等,還有一個很重要的領域,那就是「單片機」開發。單片機上的硬件資源十分有限,容不得我們去肆意揮灑。單片機使一種集成電路芯片,使采用超大規模集成電路技術把具有數據處理能力的CPU、RAM、ROM、I/O、中斷系統、定時器/計數器等功能(有的還包括顯示驅動電路、脈寬調制電路、模擬多路轉換器、A/D轉換器等電路)集成到一塊硅片上構成的一個小而完善的微型計算機系統,在工控領域使用廣泛。
對于這樣的設備,通常內存只有256B,那么能夠給我們利用的資源就十分珍貴了。在這種情況下,如果我們只需要定義一個變量來存放布爾值,一般就申請一個整型變量,通過1和0來間接存儲。但是,顯然1和0只用1個bit就能夠放完,而一個整型卻是4個字節,也就是32bit。這就造成了內存的浪費。
好在,C語言為我們提供了一種數據結構,稱為「位域」(也叫位端、位字段)。也就是把一個字節中的二進制位劃分,并且你能夠指定每個區域的位數。每個域有一個域名,并允許程序中按域名進行單獨操作。
使用位域的做法是在結構體定義的時候,在結構體成員后面使用冒號(:)和數字來表示該成員所占的位數。
//Example08 #include intmain(void) { structTest { unsignedinta:1; unsignedintb:1; unsignedintc:2; }test; test.a=0; test.b=1; test.c=2; printf("a=%d,b=%d,c=%d ",test.a,test.b,test.c); printf("sizeoftest=%d ",sizeof(test)); return0; }
運行結果如下:
//Consequence 08 a = 0, b = 1, c = 2 size of test = 4
如此一來,結構體test只用了4bit,卻存放下了0、1、2三個整數。但是由于2在二進制中是10,因此占了2個bit。如果把test.b賦值為2,那么:
//Consequence 08 V2 a = 0, b = 0, c = 2 size of test = 4
可以看到,b中的10溢出了,只剩下0。
當然,位域的寬度不能夠超過本身類型的長度,比如:
unsignedinta:100;
那么就會報錯:
錯誤C2034“main::a”: 位域類型對位數太小
位域成員也可以沒有名稱,只要給出類型和寬度即可:
structTest { unsignedintx:1; unsignedinty:2; unsignedintz:3; unsignedint:26; };
無名位域一般用來作為填充或者調整成員的位置,因為沒有名稱,所以無名位域并不能夠拿來使用。
?
C語言的標準只說明unsigned int和signed int支持位域,然后C99增加了_Bool類型也支持位域,其他數據類型理論上是不支持的。不過大多數編譯器在具體實現時都進行了擴展,額外支持了signed char、unsigned char以及枚舉類型,所以如果對char類型的結構體成員使用位域,基本上也沒什么問題。但如果考慮到程序的可移植性,就需要謹慎對待了。另外,由于內存的基本單位是字節,而位域只是字節的一部分,所以并不能對位域進行取地址運算。
?
雖然科技發展日新月異,但是秉承著節約成本這個放之四海而皆準的原則,還是要注意使用!畢竟5毛錢可能是小錢,但是乘以5000萬呢?
評論