C语言不支持继承,但是它支持结构组合,可以对其进行调整以服务于需要父子关系的用例。在本文中,我们将了解结构组合如何帮助我们在C中模拟继承并保持代码的可扩展性。我们还将了解它如何驱动计算机科学领域有史以来发明的两个最重要的东西。
结构组合是指我们将一个结构放在另一个结构中,不是通过它的指针,而是作为一个本机成员-类似这样。
//此结构定义链表的一个节点,//它只保存指向链表中的下一个和//上一个//节点的指针。Struct{struct*;//指向当前struct下一个节点的指针*;//指向当前struct前一个节点的指针};//list_int保存LIST_HEAD和整数数据成员struct{struct;//公共Next和Prev指针值;//根据实现指定成员};//List_int保存LIST_HEAD和char*数据成员结构{struct;//公共Next和Prev指针char*str;//具体成员按实现};
在上面的示例中,我们使用结构组合定义了链表的一个节点。通常,一个链表节点有3个成员-两个指向相邻节点(下一个和前一个)的指针,第三个可以是数据或指向它的指针。链表的定义因素是逻辑上形成节点链的两个指针。为了保持抽象,我们创建了一个名为LIST_HEAD的结构,该结构包含NEXT和PREV这两个指针,并省略了细节,即数据。
使用LIST_HEAD结构,如果我们要定义包含整数值的链表节点,我们可以创建另一个名为LIST_INT的结构,该结构包含LIST_HEAD类型的成员和整数值。下一个和前一个指针通过LIST_HEAD LIST引入到此结构中,可以称为list.next和list.prev。
为链表节点和结构成员选择这样奇怪的名称是有非常真实的原因的;这样做的原因将在本文后面的部分中阐明。
由于上述结构定义,构建包含任何类型的链表节点变得轻而易举。例如,保存字符串的节点可以快速定义为具有list_head和char*的struct list_str。这种扩展LIST_HEAD并构建包含任何类型和任何细节的数据的节点的能力使低级代码变得简单、统一和可扩展。
C中的结构没有填充,它们甚至没有保存任何元信息,甚至没有成员名称的信息;因此,在分配期间,为它们分配的空间刚好足以保存实际数据。
在上图中,我们可以看到LIST_INT的成员如何映射到其单个成员所需的已分配空间上。它被分配了12字节的连续空间-两个指针中的每一个都是4字节,另外4字节是整数值。如下所示,可以通过打印出成员的地址来验证空间分配的邻接性和成员在分配期间的顺序。
Void(){//创建LIST_INT保持值41434结构*=(41434)的节点;//打印单个成员的地址printf(";%p:head\n&34;,head);printf(";%p:head-&>list.next\n&34;,&;((head-&>list).next));printf(";%p:head-&>;list.。,&;((head-gt;list).prev));printf(";%p:head->;value\n";,&;(head-gt;value));}~$make&;&;/a.out 0x4058f0:head 0x4058f0:head->;list.next 0x4058f4:head->;list.prev 0x4058f8。
我们清楚地看到了所有3个成员,按照它们在结构中的定义顺序占用了12个字节的连续内存段。
上面的代码是在整数和指针大小各为4字节的机器上执行的。根据机器和CPU体系结构的不同,结果可能会有所不同。
在C语言中,当指向一个结构的指针强制转换为指向另一个结构的指针时,引擎会根据目标结构类型的各个成员的顺序和偏移量,将它们映射到源结构实例的内存片上。
当我们将LIST_INT*转换为LIST_HEAD*时,引擎会将目标类型(即LIST_HEAD)所需的空间映射到LIST_INT占用的空间上。这意味着它将LIST_HEAD所需的8个字节映射到LIST_INT实例占用的前8个字节上。通过上面讨论的内存表示,我们发现list_int的前8个字节实际上是list_head,因此将list_int*转换为list_head*实际上只是通过一个新变量引用list_int的list_head成员。
这有效地在两个结构之间建立了父子关系,这样我们就可以安全地将子LIST_INT类型转换为其父LIST_HEAD。
这里需要注意的是,建立父子关系只是因为LIST_INT的第一个成员是LIST_HEAD类型。如果我们更改LIST_INT中成员的顺序,它将不起作用。
如上所述,通过将一个结构放入另一个结构中作为其第一个元素,我们可以有效地在这两个结构之间创建父子关系。由于这使我们能够安全地将子结构类型转换为其父结构,因此我们可以定义接受指向父结构的指针作为参数的函数,并执行实际上不需要处理细节的操作。这允许我们不必重写每个子扩展的功能逻辑,从而避免冗余代码。
从我们已经设置的上下文中,假设我们想要编写一个函数,在链表中的这两个节点之间添加一个节点。执行此操作的核心逻辑实际上不需要处理任何细节,只需对NEXT和PREV进行几次指针操作即可。因此,我们可以只定义接受LIST_HEAD*类型的参数的函数,并将该函数编写为。
/**在两个已知的连续条目之间插入新条目。**这仅用于内部列表操作,其中我们已知道*prev/next条目!*/static void__list_add(struct list_head*new,struct list_head*prev,struct list_head*next){next->;prev=new;new->;next=next;new->;prev=prev;prev->;next=new;}。
因为我们可以安全地将list_int*和list_str*类型化为list_head*,所以我们可以传递任何特定的实现函数__list_add,并且它仍然会无缝地添加其他两个之间的节点。
因为链表上的核心操作只需要指针操作,所以我们可以将这些操作定义为接受LIST_HEAD*的函数,而不是像LIST_INT*这样的特定类型。因此,我们不需要为具体内容编写类似的函数。删除节点的函数可以编写为。
/**通过使上一个/下一个条目*相互指向来删除列表条目。**这仅适用于已知道*prev/next条目的内部列表操作!*/static inline void__list_del(struct list_head*prev,struct list_head*next){next->;prev=prev;prev->;next=next;}。
其他链表实用程序,如向尾部添加节点、交换节点、拼接列表、旋转列表等,只需要操作NEXT和PREV指针。因此,它们也可以用非常相似的方式编写,即接受LIST_HEAD*,这样就不需要为每个单个子实现重新实现函数逻辑。
这种行为与现代OOP语言(如Python和Java)中允许子代调用任何父函数的继承的工作方式非常相似。
使用结构成分的实际用法有很多,但最著名的是。
为了保持事物的抽象性和可扩展性,Linux内核在多个地方使用了结构组合。它使用组合的最重要的地方之一是管理和维护链表,这正是我们在上面看到的。结构定义和代码片段按原样取自内核的源代码,因此结构和变量名看起来与通常不同。
Python是当今世界最重要的语言之一,它使用结构组合来构建类型层次结构。Python定义了一个名为PyObject的根结构,它保存引用计数,定义引用对象的位置数和对象类型,确定对象的类型,即int、str、list、dict等。
Tyfinf struct_{py_ssize_t ob_refcnt;//保存对象PyTypeObject*ob_type的引用计数;//保存对象的类型}PyObject;
由于Python希望这些字段出现在运行时创建的每个对象中,因此它使用结构组合来确保整数、浮点、字符串等对象将PyObject作为其第一个元素,从而建立父子关系。Python中的Float对象定义为。
现在,可以将每次访问任何对象时递增和递减引用计数的实用程序函数编写为接受PyObject的单个函数,如下所示。
因此,我们不再需要为每个单独的对象类型重写INCREF,只需为PyObject编写一次,它将适用于通过PyObject扩展的每个单独的Python对象类型。
如果你喜欢你所读的内容,订阅,你可以随时订阅我的时事通讯,并将帖子直接送到你的收件箱。我写各种工程主题的文章,并通过我的每周时事通讯👇与人分享。
MongoDB的cursor.skip()效率非常低,为什么呢?尽管它速度慢,效率低,但……。
写时复制用于建模时间旅行,构建没有锁的数据库,并使分叉系统...。
TF-IDF被广泛应用于搜索引擎以及各种文档分类和聚类分析中。