C 语言笔记: 链表节点实现技巧--struct的妙用
????鏈表節(jié)點實現(xiàn)技巧–struct的妙用
作者能力有限, 如果您在閱讀過程中發(fā)現(xiàn)任何錯誤, 還請您務(wù)必聯(lián)系本人,指出錯誤, 避免后來讀者再學(xué)習(xí)錯誤的知識.謝謝!
廢話
C 語言雖然只提供了非常簡單的語法,但是絲毫不影響 C 語言程序員使用 C 來實現(xiàn)很多讓人嘆為觀止的高級功能.
本文介紹一項在 C 語言中非常常見的鏈表節(jié)點實現(xiàn)的一個技巧.
也許你看過了好幾本 C 語言的書籍,也看到過相關(guān)的介紹,但是你卻沒有很在意,那么這里我們來詳細的學(xué)習(xí)一下.
接下來,我們將描述一個鏈表節(jié)點的實現(xiàn),先不要失望,它的實現(xiàn)可能并不像所想的那么簡單.
節(jié)點的定義
typedef struct _LIST_ENTRY {struct _LIST_ENTRY *Next; } LIST_ENTRY, *PLIST_ENTRY;LIST_ENTRY 代表雙向鏈表的一個節(jié)點. Next 是指向下一個節(jié)點的指針.
但是對于上述節(jié)點,我們沒法使用它, 因為它除了能表示一個節(jié)點之外, 無法包含其他任何額外的信息.
好,這里我們假設(shè)我們想創(chuàng)建一個表示學(xué)生的鏈表,我們先定義一下學(xué)生結(jié)構(gòu)吧.
typedef struct _STUDENT {char name[64];int age; } STUDENT, *PSTUDENT;我們隨手就寫出來一個表示學(xué)生的結(jié)構(gòu)體,它很簡單,是因為這里我們只是用它來說明我們?nèi)绻褂?LIST_ENTRY, 而并不想講解如果構(gòu)建一個學(xué)生管理系統(tǒng).
為了讓 STUDENT 結(jié)構(gòu)可以成為鏈表的一個節(jié)點,我們需要將他們合并一下. 然后我們的 STUDENT 結(jié)構(gòu)就變成了這樣:
typedef struct _STUDENT {LIST_ENTRY list_entry;char name[64];int age; } STUDENT, *PSTUDENT;注意,我們將 LIST_ENTRY 結(jié)構(gòu)嵌套在 STUDENT 結(jié)構(gòu)的開始位置,這將使得后續(xù)的實現(xiàn)簡單很多. 放在其他位置當(dāng)然也是可以的,但是卻會把事情搞得復(fù)雜起來.
使用
既然結(jié)構(gòu)體定義好了,下面我們就來看看,我們?nèi)绾问褂眠@個結(jié)構(gòu)體,以及這個結(jié)構(gòu)體體的巧妙之處,這也是本文想要表達的東西.
再次重申一下,本文是想描述這個結(jié)構(gòu)體用法的妙處,無意于實現(xiàn)一個完整的鏈表. 因此只給出了最簡陋的版本.
#define GET_STUDENT(address, type, field) ((type *)( \(char *)(address) - \(char *)(&((type *)0)->field)))PLIST_ENTRY list_header = NULL; // 鏈表頭// 在鏈表的尾部添加一個新的節(jié)點 int add_student(char* name, int age) {// create a student with the given parametersPSTUDENT student = malloc(sizeof(STUDENT));if (student == NULL)return -1;memset(student, 0, sizeof(STUDENT));strcpy(student->name, name);student->age = age;if (list_header == NULL) {list_header = &student->list_entry;} else {PLIST_ENTRY p = list_header;while (p->Next) {p = p->Next;}p->Next = &student->list_entry;// student->list_entry.Next is NULL} }int main() {// 添加兩個節(jié)點add_student("student abc", 22);add_student("student ijk", 25);// 遍歷整個鏈表請注意這里!!!!/for (p = list_header; p != NULL; p = p->Next) {// get the student structPSTUDENT student = GET_STUDENT(p, STUDENT, list_entry);// PSTUDENT student = (PSTUDENT)(((char*)p - (char*)(&((PSTUDENT)0)->list_entry)));printf("student name: %s, student age: %d\n", student->name, student->age);}/// 省略釋放內(nèi)存的代碼return 0; }解析
如果至此,你已經(jīng)看到了它的巧妙之處,就不需要再浪費時間看接下來的部分了.
上述代碼的重點在哪兒呢?
重點就是 GET_STUDENT 那個宏.
為了方便調(diào)試,我們在提供了 42 行的宏展開之后的形式以方便調(diào)試.
首先,在 42 行加個斷點調(diào)試一下,我們得到了如下結(jié)果:
請注意,我們此時 p 的地址和 student 的地址是一樣的. 這是因為我們 LIST_ENTRY 放在 STUDENT 結(jié)構(gòu)體的第一個位置,而我們在往鏈表中添加新節(jié)點的時候添加的都是 STUDETN 結(jié)構(gòu),在這種情況下,我們可以將一個 STUDENT 結(jié)構(gòu)的指針賦值給一個 LIST_ENTRY 的指針.
這里我們在看一樣整個 student 結(jié)構(gòu)的內(nèi)部布局:
這里首先看到我們 student 的內(nèi)存開始地址為 0x00000000600049fb0, 這個值和我們上圖中顯示的一致的,因為我的計算機是 64 位機器,因此地址占用 8 的字節(jié).
這里我們詳細解析一下這些字節(jié)的意義:
(1) 因為我們 student 的第一個字段是 LIST_ENTRY,而 LIST_ENTRY 中僅包含一個指向鏈表下一個節(jié)點的 Next 指針. 因此毫無疑問前八個字 10a00400 06000000表示當(dāng)前字節(jié)的下一個節(jié)點的首地址. 注意這里是小段表示,因此和第一張圖片中看到的地址字節(jié)序列正好相反, 最高有效位在最后.
(2) 解析來的字節(jié)值止倒數(shù)第三個字節(jié)的內(nèi)存都是用來保存 student->name
(3) 最后兩個字節(jié)表示 student->age,它的值是小段編碼的 16.
4. 接著,我們讓循環(huán)繼續(xù),定位鏈表的第二個節(jié)點,看一下內(nèi)存布局:
驗證一下我們上面說的對不對. 第二個節(jié)點的地址為 0x0000000060004a010, 它正好與 10a00400 06000000 吻合,因為我們知道第一個節(jié)點的 list_entry->Next 指向的正是當(dāng)前節(jié)點. 它的前兩個字節(jié)為 0,是因為當(dāng)前節(jié)點的 Next 是空. 接下來的字段與 (2)(3) 小結(jié)相同,這里我們不再解釋.
總結(jié), LIST_ENTRY 與 STUDENT 的結(jié)構(gòu)體巧妙的使用了 C 語言結(jié)構(gòu)體內(nèi)存布局的特點, 將 STUDENT 結(jié)構(gòu)體置入一個 LIST_ENTRY 的鏈表. 它的優(yōu)點是什么呢?
這就使得我們可以將任何結(jié)構(gòu)放入我們使用 LIST_ENTRY 定義的鏈表中,而我們不必為每一個需要放入鏈表的結(jié)構(gòu)單獨定義相關(guān)的字段使得他們得以互聯(lián). 這樣做之后我們等于將鏈表相關(guān)的邏輯從真正用來保存信息的結(jié)構(gòu)體中抽離出來,我們在編寫操作鏈表的方法時幾乎可以不關(guān)注存入鏈表中的真正的 student 類型.
歡迎交流任何想法.
End…
總結(jié)
以上是生活随笔為你收集整理的C 语言笔记: 链表节点实现技巧--struct的妙用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python字符串筛选输出_如何在Pyt
- 下一篇: STM32与MS5837压力传感器的I2