C结构继承指针对齐

Posted

技术标签:

【中文标题】C结构继承指针对齐【英文标题】:C struct inheritance pointer alignment 【发布时间】:2015-03-26 14:32:07 【问题描述】:

背景

我创建了一个基本的链表数据结构,主要用于学习目的。该列表的一个目标是它可以处理不同的数据结构。因此,我尝试使用结构组合来模拟 C 中的“继承”。以下是构成我的链表基础的结构。

typedef struct Link 
    struct Link* next;
    struct Link* prev;
 Link;

typedef Link List;

在我的实现中,我选择了一个哨兵节点作为列表的头部和尾部(这就是为什么 Link == List)。

为了让列表真正处理数据,结构简单地将 Link 结构作为第一个成员:

typedef struct 
    Link link;
    float data;
 Node;

所以链表看起来像这样。

         ┌───┬───┬───┐     ┌───┬───┐     ┌───┬───┬───┐     
... <--->│ P │ N │ D │<--->│ P │ N │<--->│ P │ N │ D │<---> ... 
         └───┴───┴───┘     └───┴───┘     └───┴───┴───┘
         End Node          myList        First Node

List myList;
Node node1 = , 1.024;
....
Node nodeN = , 3.14;

list_init(&myList) // myList.next = &myList; myList.prev = &myList;
list_append(&myList, &node1);
....
list_append(&myList, &nodeN);

问题

要遍历这个列表,Node 指针最初指向第一个节点。然后它沿着列表遍历,直到它再次指向哨兵然后停止。

void traverse()

    Node* ptr;
    for(ptr = myList.next; ptr != &myList; ptr = ptr->link.next)
    
        printf("%f ", ptr->data);
    

我的问题是ptr != &amp;myList此行是否存在指针对齐问题?

for 循环正确地产生警告:(warning: assignment from incompatible pointer typewarning: comparison of distinct pointer types lacks a cast)可以通过执行它所说的并强制转换为 Node* 来消除这些警告。然而,这是一个 DumbThingToDo™ 吗?当ptr-&gt;data 指向&amp;myList 时,我永远不会访问它,因为循环会在ptr == &amp;myList 终止一次。

TLDR

在 C 结构中,如果 BaseDerived 中的第一个成员,则 Base* 可以指向 Derived。如果没有Derived 特定成员被访问,Derived* 是否可以指向 Base

编辑:用等效的内联代码替换相关的函数调用。

【问题讨论】:

与问题本身无关,我只想说演示文稿是 stellar 我认为您不会遇到指针对齐问题,但我会问:为什么不将 List 设为 Node 的别名,而忽略 data 成员的 @987654343 @?或者直接将其设为Node(定义Node myList; 而不是List myList;)这将是避免指针转换问题的一种更简洁的方法。我同意另一条评论:干得好,清楚地说明问题。 (+1) &amp;myList 不是Node*,它是指向Node *Node ** 的指针。我认为您需要将第二个变量设置为 list_first(&amp;myList) 或者只是在 for 中调用 list_first(&amp;myList) 并希望编译器能够优化。 我的问题中有内联 list_first() 和 link_next()。 @lurker 我考虑定义 Node myList 但它只是似乎列表应该是List 类型;)。 【参考方案1】:

感谢您的演示。

我认为您的实现应该可以正常工作,因为 C 保证结构的地址是其初始成员的地址。撇开 C 关于结构成员对齐的声明不谈,这个保证应该意味着,只要您的实现始终将 Link 作为第一个成员,这不会导致对齐问题。

来自here:C99 §6.7.2.1:

13 在结构对象中,非位域成员和单元 位域所在的地址按顺序增加 在其中声明它们。指向结构对象的指针,适当地 转换后,指向其初始成员(或者如果该成员是 位域,然后到它所在的单元),反之亦然。 结构对象中可能有未命名的填充,但在其 开始

这应该是您对 Base *Derived * 的意思,尽管在纯 C 中不存在这样的东西。它们只是恰好具有相同内存布局的结构。

不过我觉得这样实现有点脆弱,因为Node和Link直接相互依赖。如果你改变 Node 的结构,你的代码就会失效。目前我看不出有一个额外的struct Link 有什么意义,除了你可以为一个新的类型编写一个新的节点来重用链接。

当我看到你的帖子时,我立即想到了一个链表实现,它的工作方式与你打算使用列表的方式非常相似:kernel list

它使用相同的列表元素 (list_head):

struct list_head 
    struct list_head *next, *prev;
;

它包含这个函数宏:

#define list_for_each_entry(pos, head, member)                          \
      for (pos = list_first_entry(head, typeof(*pos), member);        \
           &pos->member != (head);                                    \
           pos = list_next_entry(pos, member))

如果您查看宏的实现方式,您会发现它提供了对列表条目的迭代,而无需了解列表所包含条目的布局。假设我正确理解您的意图,我认为这就是你想要的样子。

【讨论】:

赞成,尽管 C99 没有说明哪种转换是“合适的”转换让我很恼火。 (是的,它可能意味着“当转换为适当的类型时”,但它为什么不直接说呢?) 你也这么想很有趣。很长一段时间以来,我一直在考虑对此发表评论。但是对于 OPs 问题,我认为指向初始成员的指针和指向结构本身的指针应该相同这一事实才是真正重要的。 在实践中,是的。不过,OP 的问题中的头/尾存在问题。因为这个初始的Link 对象不是Node 的一部分(与列表的其余部分不同),所以没有定义ptr = ptr-&gt;link.next 中的指针转换(6.7.2.1p13 不能应用)。在实践中,我怀疑这会导致问题。【参考方案2】:

在 C 结构中,如果 Base 是 Derived 中的第一个成员,则 Base* 可以指向 Derived。如果没有访问 Derived 的特定成员,Derived* 是否可以指向 Base?

如果您的意思是“Derived* 可以指向任意 Base(包括不是 Derived 对象的第一个成员)”,那么:从技术上讲,不能。我对Defect Report #74 的理解是对齐要求可以不同。

Q:如果一个结构有一个类型为t的字段,可以对齐吗? 现场要求与对齐要求不同 不是结构成员的相同类型的对象?如果 (a) 的答案是“是”,那么在适用的情况下,其余的 应该假设已经针对其中的两个对象提出了问题 结构和结构外的对象。

A:子条款 6.1.2.5 说,'... 指向合格或不合格的指针 兼容类型的版本应具有相同的表示和 对齐要求。子条款 6.5.2.1 说,‘每个 结构或联合对象的非位域成员在 适合其类型的实现定义的方式。然后, '因此在结构对象中可能存在未命名的填充, ...根据需要实现适当的对齐。 a) 它是 一个实现可以说明一般性要求 满足子条款 6.1.2.5。这些要求可能会进一步 使用可用的实现定义的行为进行强化 在子条款 6.5.2.1 中。是的,对齐要求可以是 不同。

【讨论】:

虽然这对于最符合标准的代码来说可能是正确的,但我非常怀疑是否存在一个实用甚至半流行的架构,其 ABI 确实这样做。 @davmac “未命名的填充”行是 DR 中所有答案的介绍的一部分,我认为“未命名的填充”引用用于该 DR 的其他与填充特别相关的答案.关于您提到的更强要求,我在当前的DR中看不到任何朝着这个方向发展的东西,只是写了它们可以不同 我认为如果缺陷报告#74 确实适用于引用的具体问题,那么它是错误的。请记住 C99 §6.7.2.1 还包括短语“反之亦然”,我看不出第二个问题是否或如何用“如果没有访问派生特定成员”来改变这一点。 @GregA.Woods 访问“派生特定成员”需要派生对象存在;问题是询问“Derived*”是否可以“指向 Base”(不存在 Derived 对象)。我相信有资格明确说明不会通过Derived* 进行访问,问题纯粹是关于这样的指针是否可以存在,而不是关于它是否可以被取消引用。 §6.7.2.1 没有说明从“Derived*”到“Base*”的转换是否始终是“合适的转换”,例如。是否必须满足 §6.3.2.3 对齐要求。 首先,这不是问题要问的。其次,我们必须假设,给定问题,Derived 对象暗示存在,因为指向它的指针具有非 NULL 值。访问是否发生在问题的范围内都没有关系。此外,任何值得其盐的现代优化器都可以推断出将指针转换为指向Base 对象,然后大概使用它来访问Base 对象显然意味着 the 的第一个成员Derived 对象实际上正在被访问,因为指针的唯一操作是强制转换它。【参考方案3】:

我在 cmets 中为 ouah 的回答写了这个,但我将把它作为一个单独的答案呈现出来。

正如 ouah 所写,如果您按照 C 标准,理论上您的问题中的代码可能存在对齐问题。但是,我不认为您的代码可能(或不太可能)在其 ABI 表现出这种对齐模式的单一架构上运行。至少我不知道,当然,这包括使用几种不同的桌面和嵌入式架构的经验。我也无法真正想象一个架构会从这种对齐属性中获得什么。

同样值得注意的是,这种模式在实践中确实使用得相当普遍;这有点像你总是可以将ints 放入指针中的事实,因为任何跨平台标准都不能保证它,但几乎没有平台是不正确的,人们一直都在这样做。

【讨论】:

“我也无法想象一个架构会从这种对齐属性中获得什么”——考虑到字长的增加有时需要相应的对齐。在 32 位 x86 上,SSE2 扩展需要 8 字节对齐以有效处理 64 位值;但指针只需要 4 字节对齐。因此,目前很有可能拥有具有不同对齐要求的非平凡结构。 (另一方面,当然,x86 不强制指针加载对齐,所以 OP 的代码可能仍然没问题)。 @davmac:虽然这在更一般意义上是正确的,但我认为它不适用于 OP 的情况,他只通过 Node 指针访问 Link 成员,而不是其他绕路。通过Link 指针访问Node 成员时可能会出现您提到的那种对齐问题,但OP 明确避免了这种情况。 我说的是对齐要求,而不是别名限制。不管访问与否,Node 可能比List 有更严格的对齐要求,将ptr = ptr-&gt;link.next 呈现为non-strictly-conforming。然而,正如您所说,这不太可能在大多数实际架构上引起问题(我在上面提到过:x86 不会强制指针加载对齐)。 @davmac:这正是我的意思。 Node 可能比List 有更严格的对齐要求,但不是相反,因为没有按照List 对齐的数据结构被访问为Node,这应该不是问题。不?即使ptr-&gt;link.next 通过Link 指针取消引用,它仍将根据Node 对齐分配。 "因为没有按照 List 对齐的数据结构作为节点访问" - 不正确;检查 OP 的问题 - 在我的实现中,我选择了一个哨兵节点作为列表的头部和尾部;此哨兵节点被定义为List 而不是Node (List myList;),然后可能通过traverse 方法的for 循环中的ptr = ptr-&gt;link.next 寻址。

以上是关于C结构继承指针对齐的主要内容,如果未能解决你的问题,请参考以下文章

c++数据对齐/成员顺序&继承

具有类私有成员和函数指针的继承结构

C语言的多态是不是是类似于用结构指针的强制转换来实现的,具体实现方式类似于Linux万能链表???

C++继承设计链表

go语言之多继承 匿名结构体与匿名结构体指针

C - cast 问题中的指针和继承?