首頁/ 汽車/ 正文

C語言之結構體就這樣被攻克了

寫在前面:本文兩萬五千多字,預計閱讀時間8分鐘。同時,含金量也是非常高的,建議收藏!

有的時候,我們所遇到的資料結構,不僅僅是一群數字或者是字串那麼簡單。比如我們每一個人的學籍資訊,學號是一個長整數,名字卻是字元;甚至有更復雜的情況,這種問題在現實生活中並不少見。我們之前學過一種叫陣列的資料結構,它可以允許我們把很多同類型的資料集中在一起處理。相對於之前,這已經是一次極大的進步。但是,新的問題,往往又會出現,這個時候,我們就得上更高階的裝備——結構體。

相比於陣列,結構體有以下的更強大的優勢:

批次儲存資料

儲存不同型別的資料

支援巢狀

結構體的宣告與定義

宣告

結構體的宣告使用

struct

關鍵字,如果我們想要把我們的學籍資訊組織一下的話,可以這樣表示:

struct Info{ unsigned long identifier;//學號,用無符號長整數表示 char name[20];//名字,用字元陣列表示 unsigned int year;//入學年份,用無符號整數表示 unsigned int years;//學制,用無符號整數表示}

這樣,我們就相當於描繪好了一個框架,以後要用的話直接定義一個這種型別的變數就好了。

定義

我們剛剛申請了一個名叫

Info

的結構體型別,那麼理論上我們可以像宣告其他變數的操作一樣,去宣告我們的結構體操作,但是C語言中規定,宣告結構體變數的時候,

struct

關鍵字是不可少的。

struct 結構體型別名 結構體變數名

不過,你可以在某個函數里面定義:

#include struct Info{ unsigned long identifier;//學號,用無符號長整數表示 char name[20];//名字,用字元陣列表示 unsigned int year;//入學年份,用無符號整數表示 unsigned int years;//學制,用無符號整數表示};int main(void){ /** *在main函式中宣告結構體變數 *結構體變數名叫info *struct關鍵字不能丟 */ struct Info info; 。。。}

也可以在宣告的時候就把變數名定義下來(此時這個變數是全域性變數):

#include struct Info{ unsigned long identifier;//學號,用無符號長整數表示 char name[20];//名字,用字元陣列表示 unsigned int year;//入學年份,用無符號整數表示 unsigned int years;//學制,用無符號整數表示} info;/** *此時直接定義了變數 *該變數是全域性變數 *變數名叫info */int main(void){ 。。。}

訪問結構體成員

結構體成員的訪問有點不同於以往的任何變數,它是採用點號運算子

來訪問成員的。比如,

info。name

就是引用

info

結構體的

name

成員,是一個字元陣列,而

info。year

則可以查到入學年份,是個無符號整型。

比如,下面開始錄入學生的資訊:

//Example 01#include struct Info{ unsigned long identifier;//學號,用無符號長整數表示 char name[20];//名字,用字元陣列表示 unsigned int year;//入學年份,用無符號整數表示 unsigned int years;//學制,用無符號整數表示};int main(void){ struct Info info; printf(“請輸入學生的學號:”); scanf(“%d”, &info。identifier); printf(“請輸入學生的姓名:”); scanf(“%s”, info。name); printf(“請輸入學生的入學年份:”); scanf(“%d”, &info。year); printf(“請輸入學生的學制:”); scanf(“%d”, &info。years); printf(“\n資料錄入完畢\n\n”); printf(“學號:%d\n姓名:%s\n入學年份:%d\n學制:%d\n畢業時間:%d\n”, \ info。identifier, info。name, info。year, info。years, info。year + info。years); return 0;}

執行結果如下:

//Consequence 01請輸入學生的學號:20191101請輸入學生的姓名:Harris請輸入學生的入學年份:2019請輸入學生的學制:4資料錄入完畢學號:20191101姓名:Harris入學年份:2019學制:4畢業時間:2023

初始化結構體

像陣列一樣,結構體也可以在定義的時候初始化,方法也幾乎一樣:

struct Info info = { 20191101, “Harris”, 2019, 4};

在C99標準中,還支援給指定元素賦值(就像陣列一樣):

struct Info info = { 。name = “Harris”, 。year = 2019};

對於沒有被初始化的成員,則

「數值型」

成員初始化為0,

「字元型」

成員初始化為‘\0’。

對齊

下面這個程式碼,大家來看看會發生什麼:

//EXample 02 V1#include int main(void){ struct A { char a; int b; char c; } a = {‘a’, 10, ‘o’}; printf(“size of a = %d\n”, sizeof(a)); return 0;}

我們之前學過,

char

型別的變數佔1位元組,

int

型別的變數佔4位元組,那麼這麼一算,一個結構體A型的變數應該就是6位元組了。別急,我們看執行結果:

//COnsequence 02 V1size of a = 12

怎麼變成12了呢?標準更新了?老師教錯了?都不是。我們把程式碼改一下:

//EXample 02 V2#include int main(void){ struct A { char a; char c; int b; } a = {‘a’, ‘o’, 10}; printf(“size of a = %d\n”, sizeof(a)); return 0;}

結果:

//Consequence 02 V2size of a = 8

實際上,這是編譯器對我們程式的一種最佳化——記憶體對齊。在第一個例子中,第一個和第三個成員是

char

型別是1個位元組,而中間的

int

卻有4個位元組,為了對齊,兩個

char

也佔用了4個位元組,於是就是12個位元組。

而在第二個例子裡面,前兩個都是

char

,最後一個是

int

,那麼前兩個可以一起佔用4個位元組(實際只用2個,第一個例子也同理,只是為了訪問速度更快,而不是為了擴充套件),最後的

int

佔用4位元組,合起來就是8個位元組。

關於如何宣告結構體來節省記憶體容量,可以閱讀下面的這篇文章,作者是艾瑞克·雷蒙,時尚最具爭議性的駭客之一,被公認為開源運動的主要領導者之一:

英文原版,中文版

結構體巢狀

在學籍裡面,如果我們的日期想要更加詳細一些,精確到day,這時候就可以使用結構體巢狀來完成:

#include struct Date{ unsigned int year; unsigned int month; unsigned int day;};struct Info{ unsigned long identifier;//學號,用無符號長整數表示 char name[20];//名字,用字元陣列表示 struct Date date;/*——-入學日期,用結構體Date表示——-*/ unsigned int years;//學制,用無符號整數表示};int main(void){ 。。。}

如此一來,比我們單獨宣告普通變數快多了。

不過,這樣訪問變數,就必須用點號一層層往下訪問。比如要訪問

day

這個成員,那就只能

info。date。day

而不能直接

info。date

或者

info,day

//Example 03#include struct Date{ unsigned int year; unsigned int month; unsigned int day;};struct Info{ unsigned long identifier;//學號,用無符號長整數表示 char name[20];//名字,用字元陣列表示 struct Date date;/*——-入學日期,用結構體Date表示——-*/ unsigned int years;//學制,用無符號整數表示};int main(void){ struct Info 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); printf(“\n資料錄入完畢\n\n”); printf(“學號:%d\n姓名:%s\n入學時間:%d/%d/%d\n學制:%d\n畢業時間:%d\n”,\ info。identifier, info。name,\ info。date。year, info。date。month, info。date。day,\ info。years, info。date。year + info。years); return 0;}

執行結果如下:

//Consequence 03請輸入學生的學號:20191101請輸入學生的姓名:Harris請輸入學生的入學年份:2019請輸入學生的入學月份:9請輸入學生的入學日期:7請輸入學生的學制:4資料錄入完畢學號:20191101姓名:Harris入學時間:2019/9/7學制:4畢業時間:2023

結構體陣列

剛剛我們演示了儲存一個學生的學籍資訊的時候,使用結構體的例子。那麼,如果要錄入一批學生,這時候我們就可以沿用之前的思路,使用結構體陣列。

我們知道,陣列的定義,就是存放一堆相同型別的資料的容器。而結構體一旦被我們宣告,那麼你就可以把它看作一個型別,只不過是你自己定義的罷了。

定義結構體陣列也很簡單:

struct 結構體型別{ 成員;} 陣列名[長度];/****或者這樣****/struct 結構體型別{ 成員;};struct 結構體型別 陣列名[長度];

結構體指標

既然我們可以把結構體看作一個型別,那麼也就必然有對應的指標變數。

struct Info* pinfo;

嵌入式物聯網需要學的東西真的非常多,千萬不要學錯了路線和內容,導致工資要不上去!

無償分享大家一個資料包,差不多150多G。裡面學習路線、面經、專案都比較新也比較全面!某魚上買估計至少要好幾十。

點選這裡找小助理0元領取:嵌入式物聯網學習資料(頭條)

C語言之結構體就這樣被攻克了

C語言之結構體就這樣被攻克了

但是在指標這裡,結構體和陣列就不一樣了。我們知道,陣列名實際上就是指向這個陣列第一個元素的地址,所以可以將陣列名直接賦值給指標。而結構體的變數名並不是指向該結構體的地址,所以要使用取地址運算子

&

才能獲取地址:

pinfo = &info;

透過結構體指標來訪問結構體有以下兩種方法:

(*結構體指標)。成員名

結構體指標->成員名

第一個方法由於點號運算子比指標的取值運算子優先順序更高,因此需要加一個小括號來確定優先順序,讓指標先解引用變成結構體變數,在使用點號的方法去訪問。

相比之下,第二種方法就直觀許多。

這兩種方法在實現上是完全等價的,但是點號只能用於結構體變數,而箭頭只能夠用於指標。

第一種方法:

#include 。。。int main(void){ struct Info *p; p = &info; printf(“學號:\n”, (*p)。identifier); printf(“姓名:\n”, (*p)。name); printf(“入學時間:%d/%d/%d\n”, (*p)。date。year, (*p)。date。month, (*p)。date。day); printf(“學制:\n”, (*p)。years); return 0;}

第二種方法:

#include 。。。int main(void){ struct Info *p; p = &info; printf(“學號:\n”, p -> identifier); printf(“姓名:\n”, p -> name); printf(“入學時間:%d/%d/%d\n”, p -> date。year, p -> date。month, p -> date。day); printf(“學制:\n”, p -> years); return 0;}

傳遞結構體資訊

傳遞結構體變數

我們先來看看下面的程式碼:

//Example 04#include int main(void){ struct Test { int x; int y; }t1, t2; t1。x = 3; t1。y = 4; t2 = t1; printf(“t2。x = %d, t2。y = %d\n”, t2。x, t2。y); return 0;}

執行結果如下:

//Consequence 04t2。x = 3, t2。y = 4

這麼看來,結構體是可以直接賦值的。那麼既然這樣,作為函式的引數和返回值也自然是沒問題的了。

先來試試作為引數:

//Example 05#include struct Date{ unsigned int year; unsigned int month; unsigned int day;};struct Info{ unsigned long identifier; char name[20]; struct Date date; unsigned int years;};struct Info getInput(struct Info info);void printInfo(struct Info info);struct Info getInput(struct Info 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); return info;}void printInfo(struct Info info){ printf(“學號:%d\n姓名:%s\n入學時間:%d/%d/%d\n學制:%d\n畢業時間:%d\n”, \ info。identifier, info。name, \ info。date。year, info。date。month, info。date。day, \ info。years, info。date。year + info。years);}int main(void){ struct Info i1 = {}; struct Info i2 = {}; printf(“請錄入第一個同學的資訊。。。\n”); i1 = getInput(i1); putchar(‘\n’); printf(“請錄入第二個學生的資訊。。。\n”); i2 = getInput(i2); printf(“\n錄入完畢,現在開始列印。。。\n\n”); printf(“列印第一個學生的資訊。。。\n”); printInfo(i1); putchar(‘\n’); printf(“列印第二個學生的資訊。。。\n”); printInfo(i2); return 0;}

執行結果如下:

//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語言已經放開了這方面的限制。

不過,作為一名合格的開發者,我們應該要去珍惜硬體資源。那麼,傳遞指標就是一個很好的辦法。

將剛才的程式碼修改一下:

//Example 06#include struct Date{ unsigned int year; unsigned int month; unsigned int day;};struct Info{ unsigned long identifier; char name[20]; struct Date date; unsigned int years;};void getInput(struct Info *info);void printInfo(struct Info *info);void getInput(struct Info *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);}void printInfo(struct Info *info){ printf(“學號:%d\n姓名:%s\n入學時間:%d/%d/%d\n學制:%d\n畢業時間:%d\n”, \ info->identifier, info->name, \ info->date。year, info->date。month, info->date。day, \ info->years, info->date。year + info->years);}int main(void){ struct Info i1 = {}; struct Info i2 = {}; printf(“請錄入第一個同學的資訊。。。\n”); getInput(&i1); putchar(‘\n’); printf(“請錄入第二個學生的資訊。。。\n”); getInput(&i2); printf(“\n錄入完畢,現在開始列印。。。\n\n”); printf(“列印第一個學生的資訊。。。\n”); printInfo(&i1); putchar(‘\n’); printf(“列印第二個學生的資訊。。。\n”); printInfo(&i2); return 0;}

此時傳遞的就是一個指標,而不是一個龐大的結構體。

動態申請結構體

結構體也可以在堆裡面動態申請:

//Example 01#include 。。。int main(void){ struct Info *i1; struct Info *i2; i1 = (struct Info *)malloc(sizeof(struct Info)); i2 = (struct Info *)malloc(sizeof(struct Info)); if (i1 == NULL || i2 == NULL) { printf(“記憶體分配失敗!\n”); exit(1); } printf(“請錄入第一個同學的資訊。。。\n”); getInput(i1); putchar(‘\n’); printf(“請錄入第二個學生的資訊。。。\n”); getInput(i2); printf(“\n錄入完畢,現在開始列印。。。\n\n”); printf(“列印第一個學生的資訊。。。\n”); printInfo(i1); putchar(‘\n’); printf(“列印第二個學生的資訊。。。\n”); printInfo(i2); free(i1); free(i2); return 0;}

實戰:建立一個圖書館資料庫

實際上,我們建立的陣列可以是指向結構體指標的陣列。

程式碼實現如下:

//Example 02#include #include #define MAX_SIZE 100struct Date{ int year; int month; int day;};struct Book{ char title[128]; char author[48]; float price; struct Date date; char publisher[48];};void getInput(struct Book* book);//錄入資料void printBook(struct Book* book);//列印資料void initLibrary(struct Book* lib[]);//初始化結構體void printLibrary(struct Book* lib[]);//列印單本書資料void releaseLibrary(struct Book* lib[]);//釋放記憶體void getInput(struct Book* 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);}void printBook(struct Book* book){ printf(“書名:%s\n”, book->title); printf(“作者:%s\n”, book->author); printf(“售價:%。2f\n”, book->price); printf(“出版日期:%d-%d-%d\n”, book->date。year, book->date。month, book->date。day); printf(“出版社:%s\n”, book->publisher);}void initLibrary(struct Book* lib[]){ for (int i = 0; i < MAX_SIZE; i++) { lib[i] = NULL; }}void printLibrary(struct Book* lib[]){ for (int i = 0; i < MAX_SIZE; i++) { if (lib[i] != NULL) { printBook(lib[i]); putchar(‘\n’); } }}void releaseLibrary(struct Book* lib[]){ for (int i = 0; i < MAX_SIZE; i++) { if (lib[i] != NULL) { free(lib[i]); } }}int main(void){ struct Book* lib[MAX_SIZE]; struct Book* p = NULL; int ch, index = 0; initLibrary(lib); while (1) { printf(“請問是否要錄入圖書資訊(Y/N):”); do { ch = getchar(); } while (ch != ‘Y’ && ch != ‘N’); if (ch == ‘Y’) { if (index < MAX_SIZE) { p = (struct Book*)malloc(sizeof(struct Book)); getInput(p); lib[index] = p; index++; putchar(‘\n’); } else { printf(“資料庫已滿!\n”); break; } } else { break; } } printf(“\n資料錄入完畢,開始列印驗證。。。\n\n”); printLibrary(lib); releaseLibrary(lib); return 0;}

執行結果如下:

//Consequence 02請問是否要錄入圖書資訊(Y/N):Y請輸入書名:人類簡史請輸入作者:尤瓦爾·赫拉利請輸入售價:32。25請輸入出版日期:2016-3-4請輸入出版社:中信出版集團請問是否要錄入圖書資訊(Y/N):N資料錄入完畢,開始列印驗證。。。書名:人類簡史作者:尤瓦爾·赫拉利售價:32。25出版日期:2016-3-4出版社:中信出版集團

單鏈表

我們知道,陣列變數在記憶體中,是連續的,而且不可拓展。顯然在一些情況下,這種資料結構擁有很大的侷限性。比如移動資料的時候,會牽一髮而動全身,尤其是反轉這種操作更加令人窒息。那麼,需要需要一種資料結構來弄出一種更加靈活的“陣列”,那麼這,就是

「連結串列」

本節我們只講講單鏈表。

所謂連結串列,就是由一個個

「結點」

組成的一個數據結構。每個結點都有

「資料域」

「指標域」

組成。其中資料域用來儲存你想要儲存的資訊,而指標域用來儲存下一個結點的地址。如圖:

單鏈表

當然,連結串列最前面還有一個頭指標,用來儲存頭結點的地址。

這樣一來,連結串列中的每一個結點都可以不用挨個存放,因為有了指標把他們串起來。因此結點放在哪都無所謂,反正指標總是能夠指向下一個元素。我們只需要知道頭指標,就能夠順藤摸瓜地找到整個連結串列。

因此對於學籍資料庫來說,我們只需要在Info結構體中加上一個指向自身型別的成員即可:

struct Info{ unsigned long identifier; char name[20]; struct Date date; unsigned int years; struct Info* next;};

在單鏈表中插入元素

頭插法

這種每次都將資料插入單鏈表的頭部(頭指標後面)的插入法就叫頭插法。

如果要把學生資訊加入到單鏈表,可以這麼寫:

void addInfo(struct Info** students)//students是頭指標{ struct Info* info, *temp; info = (struct Info*)malloc(sizeof(struct Info)); if (info == NULL) { printf(“記憶體分配失敗!\n”); 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

指向頭指標原本指向的結點,然後將頭指標指向新結點即可。

那麼,列印連結串列也變得很簡單:

void printStu(struct Info* students){ struct Info* info; int count = 1; info = students; while (book != NULL) { printf(“Student%d:\n”, count); printf(“姓名:%s\n”, info->name); printf(“學號:%d\n”, info->identifier); info = info->next; count++; }}

想要讀取單鏈表裡面的資料,只需要迭代單鏈表中的每一個結點,直到

next

成員為

NULL

,即表示單鏈表的結束。

最後,當然還是別忘了釋放空間:

void releaseStu(struct Info** students){ struct Info* temp; while (*students != NULL) { temp = *students; *students = (*students)->next; free(temp); }}

尾插法

與頭插法類似,尾插法就是把每一個數據都插入到連結串列的末尾。

void addInfo(struct Info** students){ struct Info* info, *temp; info = (struct Info*)malloc(sizeof(struct Info)); if (info == NULL) { printf(“記憶體分配失敗!\n”); 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; }}

這麼一來,程式執行的效率難免要降低很多,因為每次插入資料,都要先遍歷一次連結串列。如果連結串列很長,那麼對於插入資料來說就是一次災難。不過,我們可以給程式新增一個指標,讓它永遠都指向連結串列的尾部,這樣一來,就可以用很少的空間換取很高的程式執行效率。

程式碼更改如下:

void addInfo(struct Info** students){ struct Info* info, *temp; static struct Info* tail;//設定靜態指標 info = (struct Info*)malloc(sizeof(struct Info)); if (info == NULL) { printf(“記憶體分配失敗!\n”); exit(1); } getInput(info); if (*students != NULL) { tail->next = info; info->next = NULL; } else { *students = info; info->next = NULL; }}

搜尋單鏈表

單鏈表是我們用來儲存資料的一個容器,那麼有時候需要快速查詢資訊就需要開發相關搜尋的功能。比如說輸入學號,查詢同學的所有資訊。

struct Info *searchInfo(struct Info* students, long* target){ struct Info* info; info = students; while (info != NULL) { if (info->identifier == target) { break; } info = info->next; } return book;};void printInfo(struct Info* info){ 。。。}。。。int main(void){ 。。。 printf(“\n請輸入學生學號:”); scanf(“%d”, input); info = searchInfo(students, input); if (info == NULL) { printf(“抱歉,未找到相關結果!\n”); } else { do { printf(“相關結果如下:\n”); printInfo(book); } while ((info = searchInfo(info->next, input)) != NULL); } releaseInfo(。。。); return 0;}

插入結點到指定位置

到了這裡,才體現出連結串列真正的優勢。

設想一下,如果有一個有序陣列,現在要求你去插入一個數字,插入完成之後,陣列依然保持有序。你會怎麼做?

沒錯,你應該會挨個去比較,然後找到合適的位置(當然這裡也可以使用二分法,比較節省算力),把這個位置後面的所有數都往後移動一個位置,然後將我們要插入的數字放入剛剛我們騰出來的空間裡面。

你會發現,這樣的處理方法,經常需要移動大量的資料,對於程式的執行效率來說,是一個不利因素。那麼連結串列,就無所謂。反正在記憶體中,連結串列的儲存毫無邏輯,我們只需要改變指標的值就可以實現連結串列的中間插入。

//Example 03#include #include struct Node{ int value; struct Node* next;};void insNode(struct Node** head, int value){ struct Node* pre; struct Node* cur; struct Node* New; cur = *head; pre = NULL; while (cur != NULL && cur->value < value) { pre = cur; cur = cur->next; } New = (struct Node*)malloc(sizeof(struct Node)); if (New == NULL) { printf(“記憶體分配失敗!\n”); exit(1); } New->value = value; New->next = cur; if (pre == NULL) { *head = New; } else { pre->next = New; }}void printNode(struct Node* head){ struct Node* cur; cur = head; while (cur != NULL) { printf(“%d ”, cur->value); cur = cur->next; } putchar(‘\n’);}int main(void){ struct Node* head = NULL; int input; printf(“開始插入整數。。。\n”); while (1) { printf(“請輸入一個整數,輸入-1表示結束:”); scanf(“%d”, &input); if (input == -1) { break; } insNode(&head, input); printNode(head); } return 0;}

執行結果如下:

//Consequence 03開始插入整數。。。請輸入一個整數,輸入-1表示結束:44請輸入一個整數,輸入-1表示結束:54 5請輸入一個整數,輸入-1表示結束:33 4 5請輸入一個整數,輸入-1表示結束:63 4 5 6請輸入一個整數,輸入-1表示結束:22 3 4 5 6請輸入一個整數,輸入-1表示結束:52 3 4 5 5 6請輸入一個整數,輸入-1表示結束:11 2 3 4 5 5 6請輸入一個整數,輸入-1表示結束:71 2 3 4 5 5 6 7請輸入一個整數,輸入-1表示結束:-1

刪除結點

刪除結點的思路也差不多,首先修改待刪除的結點的上一個結點的指標,將其指向待刪除結點的下一個結點。然後釋放待刪除結點的空間。

。。。void delNode(struct Node** head, int value){ struct Node* pre; struct Node* cur; cur = *head; pre = NULL; while (cur != NULL && cur->value != value) { pre = cur; cur = cur->next; } if (cur == NULL) { printf(“未找到匹配項!\n”); return ; } else { if (pre == NULL) { *head = cur->next; } else { pre->next = cur->next; } free(cur); }}

記憶體池

C語言的記憶體管理,從來都是一個讓人頭禿的問題。要想更自由地管理記憶體,就必須去堆中申請,然後還需要考慮何時釋放,萬一釋放不當,或者沒有及時釋放,造成的後果都是難以估量的。

當然如果就這些,那倒也還不算什麼。問題就在於,如果大量地使用

malloc

free

函式來申請記憶體,首先使要經歷一個從應用層切入系統核心層,呼叫完成之後,再返回應用層的一系列步驟,實際上使非常浪費時間的。更重要的是,還會產生大量的記憶體碎片。比如,先申請了一個1KB的空間,緊接著又申請了一個8KB的空間。而後,這個1KB使用完了,被釋放,但是這個空間卻只有等到下一次有剛好1KB的空間申請,才能夠被重新呼叫。這麼一來,極限情況下,整個堆有可能被弄得支離破碎,最終導致大量記憶體浪費。

那麼這種情況下,我們解決這類問題的思路,就是建立一個記憶體池。

記憶體池,實際上就是我們讓程式創建出來的一塊額外的快取區域,如果有需要釋放記憶體,先不必使用

free

函式,如果記憶體池有空,那麼直接放入記憶體池。同樣的道理,下一次程式申請空間的時候,先檢查下記憶體池裡面有沒有合適的記憶體,如果有,則直接拿出來呼叫,如果沒有,那麼再使用

malloc

其實記憶體池我們就可以使用單鏈表來進行維護,下面透過一個通訊錄的程式來說明記憶體池的運用。

普通的版本:

//Example 04 V1#include #include #include struct Person{ char name[40]; char phone[20]; struct Person* next;};void getInput(struct Person* person);void printPerson(struct Person* person);void addPerson(struct Person** contects);void changePerson(struct Person* contacts);void delPerson(struct Person** contacts);struct Person* findPerson(struct Person* contacts);void displayContacts(struct Person* contacts);void releaseContacts(struct Person** contacts);void getInput(struct Person* person){ printf(“請輸入姓名:”); scanf(“%s”, person->name); printf(“請輸入電話:”); scanf(“%s”, person->phone);}void addPerson(struct Person** contacts){ struct Person* person; struct Person* temp; person = (struct Person*)malloc(sizeof(struct Person)); if (person == NULL) { printf(“記憶體分配失敗!\n”); exit(1); } getInput(person); //將person新增到通訊錄中 if (*contacts != NULL) { temp = *contacts; *contacts = person; person->next = temp; } else { *contacts = person; person->next = NULL; }}void printPerson(struct Person* person){ printf(“聯絡人:%s\n”, person->name); printf(“電話:%s\n”, person->phone);}struct Person* findPerson(struct Person* contacts){ struct Person* current; char input[40]; printf(“請輸入聯絡人:”); scanf(“%s”, input); current = contacts; while (current != NULL && strcmp(current->name, input)) { current = current->next; } return current;}void changePerson(struct Person* contacts){ struct Person* person; person = findPerson(contacts); if (person == NULL) { printf(“找不到聯絡人!\n”); } else { printf(“請輸入聯絡電話:”); scanf(“%s”, person->phone); }}void delPerson(struct Person** contacts){ struct Person* person; struct Person* current; struct Person* previous; //先找到待刪除的節點的指標 person = findPerson(*contacts); if (person == NULL) { printf(“找不到該聯絡人!\n”); } else { current = *contacts; previous = NULL; //將current定位到待刪除的節點 while (current != NULL && current != person) { previous = current; current = current->next; } if (previous == NULL) { //若待刪除的是第一個節點 *contacts = current->next; } else { //若待刪除的不是第一個節點 previous->next = current->next; } free(person);//將記憶體空間釋放 }}void displayContacts(struct Person* contacts){ struct Person* current; current = contacts; while (current != NULL) { printPerson(current); current = current->next; }}void releaseContacts(struct Person** contacts){ struct Person* temp; while (*contacts != NULL) { temp = *contacts; *contacts = (*contacts)->next; free(temp); }}int main(void){ int code; struct Person* contacts = NULL; struct Person* person; printf(“| 歡迎使用通訊錄管理程式 |\n”); printf(“|——- 1:插入新的聯絡人 ——-|\n”); printf(“|——- 2:查詢現有聯絡人 ——-|\n”); printf(“|——- 3:更改現有聯絡人 ——-|\n”); printf(“|——- 4:刪除現有聯絡人 ——-|\n”); printf(“|——- 5:顯示當前通訊錄 ——-|\n”); printf(“|——- 6:退出通訊錄程式 ——-|\n”); while (1) { printf(“\n請輸入指令程式碼:”); scanf(“%d”, &code); switch (code) { case 1:addPerson(&contacts); break; case 2:person = findPerson(contacts); if (person == NULL) { printf(“找不到該聯絡人!\n”); } else { printPerson(person); } break; case 3:changePerson(contacts); break; case 4:delPerson(&contacts); break; case 5:displayContacts(contacts); break; case 6:goto END; } }END://此處直接跳出恆迴圈 releaseContacts(&contacts); return 0;}

執行結果如下:

//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

下面加入記憶體池:

//Example 04 V2#include #include #include #define MAX 1024struct Person{ char name[40]; char phone[20]; struct Person* next;};struct Person* pool = NULL;int count;void getInput(struct Person* person);void printPerson(struct Person* person);void addPerson(struct Person** contects);void changePerson(struct Person* contacts);void delPerson(struct Person** contacts);struct Person* findPerson(struct Person* contacts);void displayContacts(struct Person* contacts);void releaseContacts(struct Person** contacts);void releasePool(void);void getInput(struct Person* person){ printf(“請輸入姓名:”); scanf(“%s”, person->name); printf(“請輸入電話:”); scanf(“%s”, person->phone);}void addPerson(struct Person** contacts){ struct Person* person; struct Person* temp; //如果記憶體池不是空的,那麼首先從裡面獲取空間 if (pool != NULL) { person = pool; pool = pool->next; count——; } //記憶體池為空,則直接申請 else { person = (struct Person*)malloc(sizeof(struct Person)); if (person == NULL) { printf(“記憶體分配失敗!\n”); exit(1); } } getInput(person); //將person新增到通訊錄中 if (*contacts != NULL) { temp = *contacts; *contacts = person; person->next = temp; } else { *contacts = person; person->next = NULL; }}void printPerson(struct Person* person){ printf(“聯絡人:%s\n”, person->name); printf(“電話:%s\n”, person->phone);}struct Person* findPerson(struct Person* contacts){ struct Person* current; char input[40]; printf(“請輸入聯絡人:”); scanf(“%s”, input); current = contacts; while (current != NULL && strcmp(current->name, input)) { current = current->next; } return current;}void changePerson(struct Person* contacts){ struct Person* person; person = findPerson(contacts); if (person == NULL) { printf(“找不到聯絡人!\n”); } else { printf(“請輸入聯絡電話:”); scanf(“%s”, person->phone); }}void delPerson(struct Person** contacts){ struct Person* person; struct Person* current; struct Person* previous; struct Person* temp; { }; //先找到待刪除的節點的指標 person = findPerson(*contacts); if (person == NULL) { printf(“找不到該聯絡人!\n”); } else { current = *contacts; previous = NULL; //將current定位到待刪除的節點 while (current != NULL && current != person) { previous = current; current = current->next; } if (previous == NULL) { //若待刪除的是第一個節點 *contacts = current->next; } else { //若待刪除的不是第一個節點 previous->next = current->next; } //判斷記憶體池中有沒有空位 if (count < MAX) { //使用頭插法將person指向的空間插入記憶體池中 if (pool != NULL) { temp = pool; pool = person; person->next = temp; } else { pool = person; person->next = NULL; } count++; } //沒有空位,直接釋放 else { free(person);//將記憶體空間釋放 } }}void displayContacts(struct Person* contacts){ struct Person* current; current = contacts; while (current != NULL) { printPerson(current); current = current->next; }}void releaseContacts(struct Person** contacts){ struct Person* temp; while (*contacts != NULL) { temp = *contacts; *contacts = (*contacts)->next; free(temp); }}void releasePool(void){ struct Person* temp; while (pool != NULL) { temp = pool; pool = pool->next; free(temp); }}int main(void){ int code; struct Person* contacts = NULL; struct Person* person; printf(“| 歡迎使用通訊錄管理程式 |\n”); printf(“|——- 1:插入新的聯絡人 ——-|\n”); printf(“|——- 2:查詢現有聯絡人 ——-|\n”); printf(“|——- 3:更改現有聯絡人 ——-|\n”); printf(“|——- 4:刪除現有聯絡人 ——-|\n”); printf(“|——- 5:顯示當前通訊錄 ——-|\n”); printf(“|——- 6:退出通訊錄程式 ——-|\n”); while (1) { printf(“\n請輸入指令程式碼:”); scanf(“%d”, &code); switch (code) { case 1:addPerson(&contacts); break; case 2:person = findPerson(contacts); if (person == NULL) { printf(“找不到該聯絡人!\n”); } else { printPerson(person); } break; case 3:changePerson(contacts); break; case 4:delPerson(&contacts); break; case 5:displayContacts(contacts); break; case 6:goto END; } }END://此處直接跳出恆迴圈 releaseContacts(&contacts); releasePool(); return 0;}

typedef

給資料型別起別名

C語言是一門古老的語言,它是在1969至1973年間,由兩位天才丹尼斯·裡奇和肯·湯普遜在貝爾實驗室以B語言為基礎開發出來的,用於他們的重寫UNIX計劃(這也為後來UNIX系統的可移植性打下了基礎,之前的UNIX是使用匯編語言編寫的,當然也是這兩位為了玩一個自己設計的遊戲而編寫的)。天才就是和咱常人不一樣,不過他倆的故事,在這篇裡面不多囉嗦,我們回到話題。

雖然C語言誕生的很早,但是卻依舊不是最早的高階程式語言。目前公認的最早的高階程式語言,是IBM公司於1957年開發的FORTRAN語言。C語言誕生之時,FORTRAN已經統領行業數十年之久。因此,C語言要想快速吸納FORTRAN中的潛在使用者,就必須做出一些妥協。

我們知道,不同的語言的語法,一般來說是不同的,甚至還有較大的差距。比如:

C:

int a, b, c;float i, j, k;

而FORTRAN語言是這樣的:

integer :: a, b, c;real :: i, j, k;

如果讓FORTRAN使用者使用原來的變數名稱進行使用,那麼就能夠快速遷移到C語言上面來,這就是

typedef

的用處之一。

我們使用FORTRAN語言的型別名,那就這麼辦:

typedef int integer;typedef float real;integer a, b, c;real i, j, k;

結構體的搭檔

雖然結構體的出現能夠讓我們有一個更科學的資料結構來管理資料,但是每次使用結構體都需要

struct。。。

,未免顯得有些冗長和麻煩。有了

typedef

的助攻,我們就可以很輕鬆地給結構體型別起一個容易理解的名字:

typedef struct date{ int year; int month; int day;} DATE;//為了區分,一般用全大寫int main(void){ DATE* date; 。。。}

甚至還可以順便給它的指標也定義一個別名:

typedef struct date{ int year; int month; int day;} DATE, *PDATE;

進階

我們還可以利用

typedef

來簡化一些比較複雜的命令。

比如:

int (*ptr) [5];

我們知道這是一個數組指標,指向一個5元素的陣列。那麼我們可以改寫成這樣:

typedef int(*PTR_TO_ARRAY)[3];

這樣就可以把很複雜的宣告變得很簡單:

PTR_TO_ARRAY a = &array;

取名的時候要儘量使用容易理解的名字,這樣才能達到使用

typedef

的最終目的。

共用體

共用體也稱聯合體。

宣告

和結構體還是有點像:

union 共用體名稱{ 成員1; 成員2; 成員3;};

但是兩者有本質的不同。共用體的每一個成員共用一段記憶體,那麼這也就意味著它們不可能同時被正確地訪問。如:

//Example 05#include #include union Test{ int i; double pi; char str[9];};int main(void){ union Test test; test。i = 10; test。pi = 3。14; strcpy(test。str, “TechZone”); printf(“test。i: %d\n”, test。i); printf(“test。pi: %。2f\n”, test。pi); printf(“test。str: %s\n”, test。str); return 0;}

執行結果如下:

//Consequence 05test。i: 1751344468test。pi: 3946574856045802736197446431383475413237648487838717723111623714247921409395495328582015991082102150186282825269379326297769425957893182570875995348588904500564659454087397032067072。00test。str: TechZone

可以看到,共用體只能正確地展示出最後一次被賦值的成員。共用體的記憶體應該要能夠滿足最大的成員能夠正常儲存。但是並不一定等於最大的成員的尺寸,因為還要考慮記憶體對齊的問題。

共用體可以類似結構體一樣來定義和宣告,但是共用體還可以允許不帶名字:

union{ int i; char ch; float f;} a, b;

初始化

共用體不能在同一時間存放多個成員,所以不能批次初始化

union data{ int i; char ch; float f;};union data a = {520}; //初始化第一個成員union data b = a; //直接使用一個共用體初始化另一個共用體union data c = {。ch = ‘C’}; //C99的特性,指定初始化成員

列舉

列舉是一個基本的資料型別,它可以讓資料更簡潔。

如果寫一個判斷星期的文章,我們當然可以使用宏定義來使程式碼更加易懂,不過:

#define MON 1#define TUE 2#define WED 3#define THU 4#define FRI 5#define SAT 6#define SUN 7

這樣的寫法有點費鍵盤。那麼列舉就簡單多了:

enum DAY{ MON=1, TUE, WED, THU, FRI, SAT, SUN};

**注意:**第一個列舉成員的預設值為整型的 0,後續列舉成員的值在前一個成員上加 1。我們在這個例項中把第一個列舉成員的值定義為 1,第二個就為 2,以此類推。

列舉變數的定義和宣告方法和共用體一樣,也可以省略列舉名,直接宣告變數名。

//Example 06#include #include int main(){ enum color { red = 1, green, blue }; enum color favorite_color; printf(“請輸入你喜歡的顏色: (1。 red, 2。 green, 3。 blue): ”); scanf(“%d”, &favorite_color); //輸出結果 switch (favorite_color) { case red: printf(“你喜歡的顏色是紅色”); break; case green: printf(“你喜歡的顏色是綠色”); break; case blue: printf(“你喜歡的顏色是藍色”); break; default: printf(“你沒有選擇你喜歡的顏色”); } return 0;}

執行結果如下:

//Consequence 06請輸入你喜歡的顏色: (1。 red, 2。 green, 3。 blue): 3你喜歡的顏色是藍色

也可以把整數轉換為列舉型別:

//Example 07#include #include int main(){ enum day { saturday, sunday, monday, tuesday, wednesday, thursday, friday } workday; int a = 1; enum day weekend; weekend = (enum day) a; //使用強制型別轉換 //weekend = a; //錯誤 printf(“weekend:%d”, weekend); return 0;}

執行結果如下:

//Consequence 07weekend:1

位域

C語言除了開發桌面應用等,還有一個很重要的領域,那就是

「微控制器」

開發。微控制器上的硬體資源十分有限,容不得我們去肆意揮灑。微控制器使一種積體電路晶片,使採用超大規模積體電路技術把具有資料處理能力的CPU、RAM、ROM、I/O、中斷系統、定時器/計數器等功能(有的還包括顯示驅動電路、脈寬調製電路、模擬多路轉換器、A/D轉換器等電路)整合到一塊矽片上構成的一個小而完善的微型計算機系統,在工控領域使用廣泛。

對於這樣的裝置,通常記憶體只有256B,那麼能夠給我們利用的資源就十分珍貴了。在這種情況下,如果我們只需要定義一個變數來存放布林值,一般就申請一個整型變數,透過1和0來間接儲存。但是,顯然1和0只用1個bit就能夠放完,而一個整型卻是4個位元組,也就是32bit。這就造成了記憶體的浪費。

好在,C語言為我們提供了一種資料結構,稱為

「位域」

(也叫位端、位欄位)。也就是把一個位元組中的二進位制位劃分,並且你能夠指定每個區域的位數。每個域有一個域名,並允許程式中按域名進行單獨操作。

使用位域的做法是在結構體定義的時候,在結構體成員後面使用冒號(:)和數字來表示該成員所佔的位數。

//Example 08#include int main(void){ struct Test { unsigned int a : 1; unsigned int b : 1; unsigned int c : 2; } test; test。a = 0; test。b = 1; test。c = 2; printf(“a = %d, b = %d, c = %d\n”, test。a, test。b, test。c); printf(“size of test = %d\n”, sizeof(test)); return 0;}

執行結果如下:

//Consequence 08a = 0, b = 1, c = 2size of test = 4

如此一來,結構體

test

只用了4bit,卻存放下了0、1、2三個整數。但是由於2在二進位制中是10,因此佔了2個bit。如果把

test。b

賦值為2,那麼:

//Consequence 08 V2a = 0, b = 0, c = 2size of test = 4

可以看到,b中的10溢位了,只剩下0。

當然,位域的寬度不能夠超過本身型別的長度,比如:

unsigned int a : 100;

那麼就會報錯:

錯誤 C2034 “main::test::a”: 位域型別對位數太小

位域成員也可以沒有名稱,只要給出型別和寬度即可:

struct Test{ unsigned int x : 1; unsigned int y : 2; unsigned int z : 3; unsigned int : 26;};

無名位域一般用來作為填充或者調整成員的位置,因為沒有名稱,所以無名位域並不能夠拿來使用。

C語言的標準只說明unsigned int和signed int支援位域,然後C99增加了_Bool型別也支援位域,其他資料型別理論上是不支援的。不過大多數編譯器在具體實現時都進行了擴充套件,額外支援了signed char、unsigned char以及列舉型別,所以如果對char型別的結構體成員使用位域,基本上也沒什麼問題。但如果考慮到程式的可移植性,就需要謹慎對待了。另外,由於記憶體的基本單位是位元組,而位域只是位元組的一部分,所以並不能對位域進行取地址運算。

雖然科技發展日新月異,但是秉承著節約成本這個放之四海而皆準的原則,還是要注意使用!畢竟5毛錢可能是小錢,但是乘以5000萬呢?

原文連結:C語言之結構體就這樣被攻克了!

轉載自:微控制器愛好者

原文連結:https://mp。weixin。qq。com/s/fgAZ3zL8ikaqKMB7wydepw

版權宣告:本文來源網路,免費傳達知識,版權歸原作者所有。如涉及作品版權問題,請聯絡我進行刪除

相關文章

頂部