链表节点实现技巧–struct的妙用

作者能力有限, 如果您在阅读过程中发现任何错误, 还请您务必联系本人,指出错误, 避免后来读者再学习错误的知识.谢谢!

废话

C 语言虽然只提供了非常简单的语法,但是丝毫不影响 C 语言程序员使用 C 来实现很多让人叹为观止的高级功能.

本文介绍一项在 C 语言中非常常见的链表节点实现的一个技巧.

也许你看过了好几本 C 语言的书籍,也看到过相关的介绍,但是你却没有很在意,那么这里我们来详细的学习一下.

接下来,我们将描述一个链表节点的实现,先不要失望,它的实现可能并不像所想的那么简单.

节点的定义

typedef struct _LIST_ENTRY {struct _LIST_ENTRY *Next;
} LIST_ENTRY, *PLIST_ENTRY;

LIST_ENTRY 代表双向链表的一个节点. Next 是指向下一个节点的指针.

但是对于上述节点,我们没法使用它, 因为它除了能表示一个节点之外, 无法包含其他任何额外的信息.

好,这里我们假设我们想创建一个表示学生的链表,我们先定义一下学生结构吧.

typedef struct _STUDENT {char name[64];int  age;
} STUDENT, *PSTUDENT;

我们随手就写出来一个表示学生的结构体,它很简单,是因为这里我们只是用它来说明我们如果使用 LIST_ENTRY, 而并不想讲解如果构建一个学生管理系统.

为了让 STUDENT 结构可以成为链表的一个节点,我们需要将他们合并一下. 然后我们的 STUDENT 结构就变成了这样:

typedef struct _STUDENT {LIST_ENTRY list_entry;char name[64];int  age;
} STUDENT, *PSTUDENT;

注意,我们将 LIST_ENTRY 结构嵌套在 STUDENT 结构的开始位置,这将使得后续的实现简单很多. 放在其他位置当然也是可以的,但是却会把事情搞得复杂起来.

使用

既然结构体定义好了,下面我们就来看看,我们如何使用这个结构体,以及这个结构体体的巧妙之处,这也是本文想要表达的东西.

再次重申一下,本文是想描述这个结构体用法的妙处,无意于实现一个完整的链表. 因此只给出了最简陋的版本.

#define GET_STUDENT(address, type, field) ((type *)( \(char *)(address) - \(char *)(&((type *)0)->field)))PLIST_ENTRY list_header = NULL; // 链表头// 在链表的尾部添加一个新的节点
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() {// 添加两个节点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);}/// 省略释放内存的代码return 0;
}

解析

如果至此,你已经看到了它的巧妙之处,就不需要再浪费时间看接下来的部分了.

上述代码的重点在哪儿呢?
重点就是 GET_STUDENT 那个宏.

为了方便调试,我们在提供了 42 行的宏展开之后的形式以方便调试.

  1. 首先需要注意的时,我们的链表中每一个节点的类型 STUDENT,而不是 LIST_ENTRY.
  2. 但是需要注意,我们的 STUDENT 结构中第一个字段是 LIST_ENTRY,这是我们 GET_STUDETN 正常工作的前提.
  3. 那么为什么这样子就能工作呢?
    首先,在 42 行加个断点调试一下,我们得到了如下结果:

请注意,我们此时 p 的地址和 student 的地址是一样的. 这是因为我们 LIST_ENTRY 放在 STUDENT 结构体的第一个位置,而我们在往链表中添加新节点的时候添加的都是 STUDETN 结构,在这种情况下,我们可以将一个 STUDENT 结构的指针赋值给一个 LIST_ENTRY 的指针.

这里我们在看一样整个 student 结构的内部布局:

这里首先看到我们 student 的内存开始地址为 0x00000000600049fb0, 这个值和我们上图中显示的一致的,因为我的计算机是 64 位机器,因此地址占用 8 的字节.
这里我们详细解析一下这些字节的意义:
(1) 因为我们 student 的第一个字段是 LIST_ENTRY,而 LIST_ENTRY 中仅包含一个指向链表下一个节点的 Next 指针. 因此毫无疑问前八个字 10a00400 06000000表示当前字节的下一个节点的首地址. 注意这里是小段表示,因此和第一张图片中看到的地址字节序列正好相反, 最高有效位在最后.
(2) 解析来的字节值止倒数第三个字节的内存都是用来保存 student->name
(3) 最后两个字节表示 student->age,它的值是小段编码的 16.
4. 接着,我们让循环继续,定位链表的第二个节点,看一下内存布局:

验证一下我们上面说的对不对. 第二个节点的地址为 0x0000000060004a010, 它正好与 10a00400 06000000 吻合,因为我们知道第一个节点的 list_entry->Next 指向的正是当前节点. 它的前两个字节为 0,是因为当前节点的 Next 是空. 接下来的字段与 (2)(3) 小结相同,这里我们不再解释.

总结, LIST_ENTRY 与 STUDENT 的结构体巧妙的使用了 C 语言结构体内存布局的特点, 将 STUDENT 结构体置入一个 LIST_ENTRY 的链表. 它的优点是什么呢?

这就使得我们可以将任何结构放入我们使用 LIST_ENTRY 定义的链表中,而我们不必为每一个需要放入链表的结构单独定义相关的字段使得他们得以互联. 这样做之后我们等于将链表相关的逻辑从真正用来保存信息的结构体中抽离出来,我们在编写操作链表的方法时几乎可以不关注存入链表中的真正的 student 类型.

欢迎交流任何想法.

End…

C 语言笔记: 链表节点实现技巧--struct的妙用相关推荐

  1. c语言求链表节点的删除,C语言实现链表节点的删除

    对链表节点进行增删改查是最基本的操作,这篇博客将会来实现对节点的删除.其他的操作可参考<c语言实现链表的基本操作>这篇博客.删除某个节点有两个类型: (1)删除i某个位置的节点: (2)判 ...

  2. c语言 链表_小陈的C语言笔记---链表(详细讲解基本操作和概念)

    关于链表的TIPS: 链表中各结点在内存中可以不是连续存放的,各数据接点的存储顺序与数据元素之间的逻辑关系可以不一致,而数据元素之间的逻辑关系是由指针域来确定的. 在链表结点 的数据结构中,结构体内的 ...

  3. C语言 线性链表 节点的创建、添加和删除

    注释说明以后再补,先给程序 #include <stdio.h> #include <stdlib.h>typedef struct svit {int x ; struct ...

  4. 嵌入式C语言基础链表

    什么是链表? 链表其实就是一种数据结构,所谓的数据结构就是数据存放的思想. 数组.链表优缺点: 增加一个元素或者删除一个元素都很难,因为地址是连续的,删除一个元素可能会挪动多个元素,不灵活.但是对于链 ...

  5. c语言统计链表值的总合,C语言链表综合操作

    /*----------------------------------预处理命令-----------------------------------------*/ #include #inclu ...

  6. 【C语言】 链表 超详细解析

    目录 一:静态存储和动态存储 二:存储类别 三:malloc函数 四: free函数 五:内存初始化函数memset 六:calloc函数 七:realloc函数 八:线性表 九:链式存储结构 十:线 ...

  7. 不带头结点的C语言单链表操作,头插法及尾插法

    接上篇带头结点的单链表操作,不带头节点真的比带头结点的麻烦好多,主要是我自己指针指的有点晕,搞了这几天的不带头结点的单链表,算是弄清楚指针的玩法了..老样子上代码. # include "s ...

  8. C语言打印链表的中间节点的算法(附完整源码)

    C语言打印链表的中间节点的算法 C语言打印链表的中间节点的算法完整源码(定义,实现,main函数测试) C语言打印链表的中间节点的算法完整源码(定义,实现,main函数测试) #include < ...

  9. C语言通过链表指针删除链表节点的算法(附完整源码)

    C语言通过链表指针删除链表节点的算法 C语言通过链表指针删除链表节点的算法完整源码(定义,实现,main函数测试) C语言通过链表指针删除链表节点的算法完整源码(定义,实现,main函数测试) #in ...

最新文章

  1. 第九章 性能监控诊断
  2. 如何自学python爬虫-Python爬虫:零基础该如何学习爬虫
  3. freemarker程序开发
  4. 白话阿里巴巴Java开发手册(编程规约)
  5. c linux time微秒_qt linux系统获取当前时间(精确到毫秒、微秒)-Go语言中文社区...
  6. 【渝粤教育】国家开放大学2018年春季 0675-21T中级财务会计(2) 参考试题
  7. linux 串口读取陀螺仪,stm32读取陀螺仪MPU6050发送数据到串口
  8. 云漫圈 | 什么是字符串匹配算法?
  9. mysql binlog备份_MySQL mysqldump + mysqlbinlog 备份和还原
  10. 吴昊品游戏核心算法 Round 17 —— M*N PUZZLE 与 N PUZZLE 的解的唯一性定理(由特殊到一般)...
  11. git rev-parse 和 git cat-file
  12. Java程序设计基础笔记 • 【第7章 Java中的类和对象】
  13. android游戏开发方向初探
  14. 访问oracle数据库语句,Oracle数据库SQL ——Select 语句使用方法
  15. 施耐德somachine4.1注册工具
  16. PCB封装尺寸-0402-0603-0805
  17. 移动端H5 QQ在线客服链接代码
  18. 2021-01-12
  19. 企业邮箱的优势有哪些
  20. ASFF的TensorFlow2实现

热门文章

  1. Object.prototype的成员介绍
  2. StringBuilder String string.Concat 字符串拼接速度再议
  3. OVS+DPDK Datapath 包分类技术
  4. 在JS中如何判断所输入的是一个数、整数、正数、非数值?
  5. c#扩展方法的理解(二:接口)
  6. linux的基础知识——shell语法
  7. 操作系统 —— 设备管理
  8. 牛客21783 牛牛的星际旅行
  9. www万维网和HTTP协议
  10. linux内核等价多路径路由,Linux内核分析 - 网络[四]:路由表