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

    链表节点实现技巧–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…

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/499686.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

协议簇: Media Access Control(MAC) Frame 解析

Media Access Control(MAC) Frame 解析 前言 千里之行,始于足下。 因为个人从事网络协议开发,一直想深入的学习一下协议族,从这篇开始,我将开始记录分享我学习到的网络协议相关的知识 简介 引用百度百科的描述: 数…

协议簇:Ethernet Address Resolution Protocol (ARP) 解析

简介 前面的文章中,我们介绍了 MAC Frame 的帧格式。我们知道,在每个 Ethernet Frame 中都分别包含一个 48 bit 的源物理地址和目的物理地址. 对于源地址很容易理解,该地址可以直接从硬件上读取. 但是对于一个网络节点,他怎么知道…

协议簇:IPv4 解析

简介 IP 是一种无连接的协议. 操作在使用分组交换的链路层(如以太网)上。此协议会尽最大努力交付数据包。 尽最大努力意味着: IP 协议不保证数据的可靠传输, 没有流量控制机制, 不保证传输序列(意味着 IP 数据包会在传输过程中乱序), 没有…

协议簇:ICMP 解析

简介 ICMP 是 Internet Control Message Protocol 的简写. 它主要用来调试网络通信环境中存在的问题. 比如,当 IP 数据包总是无法正常的发送到目的地址, 当网关没有足够的 buffer 来转发对应的数据包 等问题. 值得一提的是,它属于网络层,不属…

协议簇:TCP 解析: 基础

简介 本文我们将从 RFC 学习一下 RFC793 中描述的 TCP 协议. 这将区别于通常讲解计算机网络书籍中所描述的 TCP. 但他们必然是相统一的,不会互相冲突. 系列文章 协议簇:TCP 解析:基础 协议簇:TCP 解析:建立连接 协议…

协议簇:TCP 解析: 建立连接

简介 接前文 协议簇:TCP 解析: 基础, 我们这篇文章来看看 TCP 连接建立的过程,也就是众所周知的”三次握手“的具体流程. 系列文章 协议簇:TCP 解析:基础 协议簇:TCP 解析:建立连接 协议簇&a…

协议簇:TCP 解析: 连接断开

简介 接前文 协议簇:TCP 解析: 建立连接, 我们这篇文章来看看 TCP 连接断开的过程,也就是众所周知的”四次挥手“的具体流程. 系列文章 协议簇:TCP 解析:基础 协议簇:TCP 解析:建立连接 协议…

协议簇:TCP 解析: Sequence Number

简介 序列号(Sequence Number) 是 TCP 协议中非常重要的一个概念,以至于不得不专门来学习一下。这篇文章我们就来解开他的面纱. 在 TCP 的设计中,通过TCP协议发送的每个字节都对应于一个序列号. 由于每个字节都有自己的序列号&a…

协议簇:TCP 解析:TCP 数据传输

简介 前面,我们分别介绍了 TCP 基础知识以及连接的建立和关闭,以及最重要的 Sequence Number 的概念. 本篇文章,我们来介绍一下 TCP 如何传输数据. 系列文章 协议簇:TCP 解析:基础 协议簇:TCP 解析&…

CodeTank iOS App Technical Support

CodeTank iOS App Technical Support For All Email: z253951598outlook.com TEL: 86-17782749061 App Screen Shoots

CentOS 7 防火墙命令

查看防火墙状态 systemctl status firewalld如果已经开启,状态为 active 如果未开启,状态为 inactive 开启防火墙 systemctl start firewalld关闭防火墙 systemctl stop firewalld查看当前防火墙的配置 firewall-cmd --list-all这里,我…

Go Concurrency Patterns: Context

Go Concurrency Patterns: Context 原文地址:https://blog.golang.org/context Introduction 在 Go 语言实现的服务器上,我们总是使用 goroutine 来处理与客户端建立的连接, 给每个连接分配一个独立的 goroutine. 在请求的 handler 中也通常…

Go Concurrency Patterns: Timing out, moving on

原文地址:https://blog.golang.org/concurrency-timeouts 并发变成有它自己的风格. 一个非常好的例子就是 timeout. 虽然 go 的 channel 没有直接支持 timeout 机制,但是要实现它非常容易. 比如说,我们想从一个 channel ch 中接收数据&#…

Go Concurrency Patterns: Pipelines and cancellation

原文地址: https://blog.golang.org/pipelines 简介 Go 语言提供的并发原语使得可以很方便的构建数据流 pipeline,使用这样的 pipeline 可以高效的利用 I/O 和多 cpu 的优势. 这篇文章我们将展示如何构建并使用 pipeline. 什么是 pipeline ? 在 go 语…

QTcpSocket connectToHost 建立连接速度慢问题

问题场景 在使用 QT 开发一个客户端 App 的时候,我们通过 QTcpSocket 与后台服务器进程通信。 后台程序使用其他语言编写。 问题: 在客户端启用之后尝试建立与后台程序的 TCP 连接的时候,发现连接速度非常慢(肉眼可见的慢&#x…

WinSock I/O 模型 -- Select 模型

简介 Select 模型是 WinSock 中最常见的 I/O 模型,这篇文章我们就来看看如何使用 Select api 来实现一个简单的 TCP 服务器. API 基础 Select 模型依赖 WinSock API Select 来检查当前 Socket 是否可写或者可读。 使用这个 API 的优点是我们不需要使用阻塞的 So…

WinSock I/O 模型 -- WSAEventSelect 模型

简介 WSAEventSelect 模型也是 WinSock 中最常见的异步 I/O 模型。 这篇文章我们就来看看如何使用 WSAEventSelect api 来实现一个简单的 TCP 服务器. API 基础 WSAEventSelect WSAEventSelect 用来把一个 SOCKET 对象和一个 WSAEVENT 对象关联起来。 lNetworkEvents 表示…

WinSock I/O 模型 -- WSAAsyncSelect 模型

简介 WSAAsyncSelect 模型也是 WinSock 中常见的异步 I/O 模型。 使用这个模型,网络应用程序通过接收以 Windows 消息为基础的网络事件通知来处理网络请求。 这篇文章我们就来看看如何使用 WSAAsyncSelect api 来实现一个简单的 TCP 服务器. API 基础 要使用 W…

WinSock I/O 模型 -- OVERLAPPED I/O 模型

简介 OVERLAPPED I/O 模型也是 WinSock 中常见的异步 I/O 模型,相比于我们之前提到的 Select 模型,WSAAsyncSelect 模型 和 WSAEventSelect 模型有更好的性能. 为了方便描述,下文我们将称 Overlapped I/O 模型为 “重叠模型”. 重叠模型的…

WinSock I/O 模型 -- IOCP 模型

前言 IOCP 全称 Input/Ouput Completion Ports,中文中翻译一般为“完成端口”,本文中我们使用 IOCP 简写. IOCP 模型是迄今为止最为复杂的一种 I/O 模型,但是同时通过使用 IOCP 我们往往可以达到最佳的系统性能. 当你的网络应用程序需要管理…