如果看到一个声明:type **********************ptr;你会怎么想?估计一半人都疯了,如此声明一个变量的人本身要么是一个高手,要么是一个低能。这样的一排*事实上表示的是一个链表,链表上的每一个元素可以分布在内存的任意一个位置,它们之间每两个通过一个*相联系。*p定义一个指针,p指向一个内存位置,该位置中保存p声明的数据类型,而**p表示一个指针的指针,p指向一个位置,该位置存放一个指针,后者指向p声明的数据类型,类似的***p也一样表示一个指针,****p也没什么区别,这么一大堆指针真的就是一个链表了,以下是一个例子:
void test()
{
int *l1,*l2,*l3,*l4;
int ba = 0x1234;
l4 = &ba;
l3 = &l4;
l2 = &l3;
l1 = &l2;
int *p = l1;
printf("? value:%X\n", (*(int *)p)); //1
printf("? value:%X\n", (**(int **)p)); //2
printf("? value:%X\n", (***(int ***)p)); //3
printf("? value:%X\n", (****(int ****)p)); //4
}
会打印什么呢?l1,l2,l3,l4都在栈上分配,其地址当然也在栈上,1处将打印l2的地址,由于l2指向的也是一个地址,2处将打印l3的地址,同样3处打印l4的地址,4处最终解码出了0x1234这个数字,是这样吗?测试一下就知道了。由此可见**********...确实可以当成链表的next来使用,每增加一个*,链表就往前推进一个元素,相当于引用了当前节点的next节点。由于指针实际上是一个地址,因此构造这个******...链表的时候一定要注意每一个节点都必须是通过&取到的真正的地址,而不能任意赋值,否则就会造成访存违规,程序出错。既然使用了*****...这么一大堆指针,你就要保证使用(*****...)p解码节点元素的时候每一级都是一个地址,上述的例子通过&取到了地址,然后赋值给一个指针,最终的那个l1其实就是一个4级指针,****p最终指向0x1234。
理解了多重指针的本质,下面需要将其构造成一个真正的链表。上面的test函数仅仅将lx声明成int类型的指针,一个int型的数据存放在内存的一个位置,既然通过*相联系的节点没有必要相邻,那么一个int型的指针除了存放指针值之外,该值所在地址下面或者上面的一部分相邻的地址空间也是可以被利用的,比如:
int *p1;
int *p2 = &p1;
int *p3 = &p2;
这里的p1,p2,p3都是指针,也就是一个地址,那么px+/-Y的区域都可以被使用,每一个p将得到一个Y+sizeof(int *)的连续空间范围,这个范围内可以存储自己的数据,最终通过****...可以解码到某个节点,然后再该节点附近大小为Y的范围内就可以找到自己的数据,由此可以设计出以下的数据结构:
struct lst;
struct lst {
struct lst* fuc;
int value;
};
可见,lst是一个结构体,携带了一个value数据,这里不要太在意fuc(?)字段,它的地址其实就是lst的地址,而它的内容则是next的地址,通过该字段可以将几个lst结构体链接在一起,这是c语言的要求,和上述int类型测试时的不断&取地址是一致的。接下来看一下新的test2函数,它演示了***...和链表的同一性:
void test2()
{
struct lst* l1 = (struct lst*)calloc(1, sizeof(struct lst));
struct lst* l2 = (struct lst*)calloc(1, sizeof(struct lst));
struct lst* l3 = (struct lst*)calloc(1, sizeof(struct lst));
struct lst* l4 = (struct lst*)calloc(1, sizeof(struct lst));
l1->fuc = l2;
l1->value = 0x111;
l2->fuc = l3;
l2->value = 0x222;
l3->fuc = l4;
l3->value = 0x333;
l4->fuc = NULL;
l4->value = 0x444;
struct lst *p = (struct lst*)l1;
printf("? value:%X\n", (*(struct lst*)p).value);
printf("? value:%X\n", (struct lst*)(**(struct lst**)p).value);
printf("? value:%X\n", (struct lst*)(***(struct lst***)p).value);
printf("? value:%X\n", (struct lst*)(****(struct lst****)p).value)
}
显然,打印结果如下:
? value:0X111
? value:0X222
? value:0X333
? value:0X444
结构体lst实际上和上述的int类型的指针是一样的,只不过在其指针地址的下面又携带了一个int型的字段value,这样的话一个链表就可以携带数据了。
进一步思考,既然链表现在可以携带数据了,那么携带些什么数据呢?是不是每一个携带有数据的链表都要构造一个结构体呢?这样是不是太浪费了,链表操作API又不能统一。我们反过来想这个问题,既然链表可以携带数据,然后通过链表中的fuc(next)字段或者通过***...找到这个数据,那么数据是不是也可以携带一个链表呢?这是可以的,它们都在连续的内存空间内,对于地址空间而言,谁携带谁是无所谓的,我们需要做的仅仅是管理好链表的next指针即可,比方说一个数据结构data:
struct data {
int other;
struct lst* lt;
int value;
}
由此,value字段进入了data,value就没有必要存在于lst了,由此lst成了以下的样子:
struct lst {
struct lst* fuc;
}
如果我们现在拥有一个data实例,可以在它的首地址下面偏移4字节(other的大小)处取出它的lt字段,也就是一个链表,反过来如果我们拥有一个链表节点实例,也可以在这个上面上面偏移4字节处取到一个data实例。看一下这像什么?是不是linux内核的list_head?只不过list_head是一个双向链表,还有一个prev字段,这个通过list找data的过程也就是linux内核的container_of宏:
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
可以看到,必须指定一个数据类型和该list_head在该数据类型中的字段名。正是虚拟内存空间的连续性使得我们可以抽出list_head这个结构,使嵌入式链表成为可能,这样就可以单独为list_head设计一套api了,而不用关心它携带的是什么数据,实际上它从来不携带数据,而是其它数据结构实例携带了它。
*****...是一个链表,任意的多级指针都是一个链表,理解了这个之后,不但可以单独设计出诸如list_head之类的linux内核的核心数据结构,对于hlist_head的理解也加深了一步:
struct hlist_head {
struct hlist_node *first;
};
struct hlist_node {
struct hlist_node *next, **pprev;
};
hlist_head中只有一个指向hlist_node的指针,而hlist_node中却有一个**类型的指针,也就是一个指针的指针,证明它指向的一个元素是一个指针指向另一个元素,指向谁呢?其实是指向该hlist_node自己的,指向自己?岂不多此一举?事实上它在删除节点的时候十分有用,它可以直接修改前一个节点的next字段,使之指向当前被删除节点的后一个节点,整个过程不用考虑前一个节点到底是hlist_node还是hlist_head,只要它有一个hlist_node类型的指针字段就可以了,这么一来就解放了hlist_head,使得一个和hlist_node截然不同的数据类型可以连接进哈希桶,而这个hlist_head中只有一个字段,大大节省了空间(为何hlist_node不去掉pprev呢,这样不更节省空间?是节省了空间,然而删除的时候就麻烦了,需要遍历操作,找到这个需要删除节点的位置)。
从*******...一路上到达了hlist,事实上全部可以用指针来说话,指针真的是一个了不起的特性,c语言正是因为有了指针才可以访问地址空间任意的地址,正是这种便利性,操作系统内核用c写是最好的,c提供了机器的一个最好的抽象,不多也不少,而这个最好是通过指针来实现的。
分享到:
相关推荐
多级双向链表中,除了指向下一个节点和前一个节点指针之外,它还有一个子链表指针,可能指向单独的双向链表。这些子列表也可能会有一个或多个自己的子项,依此类推,生成多级数据结构,如下面的示例所示。 给你位于...
题目您将获得一个双向链表,除了下一个和前一个指针之外,它还有一个子指针,可能指向单独的双向链表。这题主要还要维护双向链表,所以要维护好prev和next,同时一
空间换时间,多级索引来提高查询的效率,实现了基于链表的“二分查找”,是一种动态数据结构,支持快速的插入、删除、查找操作,时间复杂度为O(nlogn) 283.移动零 解法一 双指针 (j始终记录下一个非零元素的位置) ...
14_传统链表和非传统链表 15_链表的技术体系推演 16_通用链表库集成和测试 17_C提高课程_day05-day07_知识体系梳理_传智扫地僧 源码及文档 第二部分 C++基础目录 01_C++基础课程的安排和需要持之以恒的学习态度 02_...
6.1.5 指向指针变量的指针与多级指针 6.1.6 指向void类型的指针 6.2 指针与数组 6.2.1 数组元素的指针引用 6.2.2 多字符串的存储与处理 6.2.3 内存的动态分配与动态数组的建立 6.3 指针与函数 6.3.1...
10.7 指针数组和指向指针的指针 161 10.7.1 指针数组的概念 161 10.7.2 指向指针的指针 164 10.7.3 main 函数的参数 166 10.8 有关指针的数据类型和指针运算的小结 167 10.8.1 有关指针的数据类型的小结 167 10.8.2 ...
10.7 指针数组和指向指针的指针 161 10.7.1 指针数组的概念 161 10.7.2 指向指针的指针 164 10.7.3 main 函数的参数 166 10.8 有关指针的数据类型和指针运算的小结 167 10.8.1 有关指针的数据类型的小结 167 10.8.2 ...
在多级存储体系中,"Cache-主存"结构的作用是解决( )的问题。 A.主存容量不足 B.辅存与CPU速度不匹配 C.主存与辅存速度不匹配 D.主存与CPU速度不匹配 【答案】D 32.请从下面浮点运算器的描述...
25.c //多级指针的应用 26.c //位运算 27.c //结构体变量 28.c //结构体指针 29.c //静态链表 30.c //动态链表 xiao3 31.c //共用体 32.c //文件的打开与关闭 33.c //文件的读和写 34.c //文件的块读 35.c //逆序...
扁平化多级双向链表 【day-09】109.有序链表转换二叉搜索树 树 【day-18】124.二叉树中的最大路径和 哈希表 【day-21】36.有效的数独 【day-24】149.直线上最多的点数 双指针 【day-25】11.盛最多水的容器 进阶篇 ...
展平多级双向链表 子集 反转位 同一棵树 时钟指针之间的角度 反转字符串中的单词 Pow(x, n) 前 K 个频繁元素 课程表二 添加二进制 删除链表元素 词搜索 二叉树之字形水平顺序遍历 单号 III 从源到目标的所有路径 在...
本书全面介绍了应用C语言进行开发的各种技术和技巧,全书共分12章,内容包括基础知识、指针、数据结构、算法、数学应用、文件操作、库函数应用、图形图像、系统调用、加解密与安全性、游戏、综合应用等。全书共提供...
C语言视频培训教程,本课程属于C语言编码技能提高篇,帮助学习过C语言的人,更上一个...课程内容涉及:C语言类型转化、深入理解二维数组、指针、二级指针及多级指针、回调函数、双向链表、排序、贪吃蛇项目案例实战等。
天:展平多级双向链表 () 第 11 天:子集 () 第 12 天:反转位 () 第 13 天:同一棵树 () 第 14 天:时钟指针之间的角度 () 第 15 天:反转字符串中的单词 () 第 16 天:Pow(x,n) () 第17天:前K个频繁元素() 第 ...
1.1.2 二进制、十六进制和八进制...........................................................................2 1.1.3 原码、反码与补码..........................................................................
信号量的数据结构为一个值和一个指针,指针指向等待该信号量的下一个进程。信号量的值与相应资源的使用情况有关。当它的值大于0时,表示当前可用资源的数量;当它的值小于0时,其绝对值表示等待使用该资源的进程...
支持静态链接其它编程语言(如C/C++、汇编等)编译生成的静态库(.LIB或.OBJ),但仅限于COFF格式,支持cdecl和stdcall两种函数调用约定。 使用说明如下:函数声明和调用方法与DLL命令一致;“库文件名”以.lib...
子程序 创建多级目录, 逻辑型, 公开, 成功返回真,失败返回假 .参数 目录路径, 文本型 .子程序 创建进程, 整数型, 公开, 创建一个程序进程(成功返回进程ID,失败返回0) .参数 程序路径, 文本型, , 欲创建进程的执行...