结构指针强制转换

时间:2018-03-12 12:38:00

标签: c struct casting undefined-behavior c89

我试图实现这样的链接列表:

typedef struct SLnode
{
    void* item;
    void* next;
} SLnode;

typedef struct DLnode
{
    void* item;
    void* next;
    struct DLnode* prev;
} DLnode;

typedef struct LinkedList
{
    void* head; /*SLnode if doubly_linked is false, otherwise DLnode*/
    void* tail; /* here too */
    bool doubly_linked;
} LinkedList;

我想这样访问它:

void* llnode_at(const LinkedList* ll, size_t index)
{
    size_t i;
    SLnode* current;

    current = ll->head;

    for(i = 0; i < index; i++)
    {
        current = current->next;
    }

    return current;
}

所以我的问题是:

  • 只要我只访问普通会员,我是否可以在这些结构之间施放?我对此有不同意见。
  • 我还可以制作相应类型的下一个指针吗?或者是UB然后在我的示例函数中使用它,以防它真的是DLnode?

如果这不起作用,还有其他方法可以做这样的事吗?我读过工会可能会工作,但是这段代码也应该在C89中运行,并且afaik读取一个不同于上次写入的工会成员就是那里的UB。

3 个答案:

答案 0 :(得分:2)

如果您使用union来包含两个结构,则可以安全地执行此操作:

union Lnode {
    struct SLnode slnode;
    struct DLnode dlnode;
};

当前C standard的第6.5.2.3节以及C89标准的第6.3.2.3节规定如下:

  

6 为了简化联合的使用,我们做了一个特别的保证:如果一个联合包含几个共同的结构   初始序列(见下文),以及当前的联合对象   包含这些结构之一,允许检查   它们中任何一个的共同初始部分都是声明的   完成的联合类型是可见的。两种结构有共同之处   如果相应的成员具有兼容类型的初始序列(和,   对于一个或多个初始序列的位域,相同的宽度)   成员。

由于两个结构的前两个成员属于同一类型,因此您可以使用任一联盟成员自由访问这些成员。

答案 1 :(得分:2)

所以你试图在C中构建子类。一种可能的方法是使基本结构成为子结构的第一个元素,因为在这种情况下,C标准明确允许在这两种类型之间来回转换: / p>

  

6.7.2.1结构和联合说明符

     

§13...指向a的指针   结构对象,适当转换,指向其初始成员(或者如果该成员是a   比特字段,然后到它所在的单位),反之亦然......

缺点是需要对基类进行强制转换才能访问其成员:

示例代码:

typedef struct SLnode
{
    void* item;
    void* next;
} SLnode;

typedef struct DLnode
{
    struct SLnode base;
    struct DLnode* prev;
} DLnode;

然后你可以这样使用它:

    DLnode *node = malloc(sizeof(DLnode));
    ((SLnode*) node)->next = NULL;             // or node->base.next = NULL
    ((SLnode *)node)->item = val;
    node->prev = NULL;

答案 2 :(得分:0)

根据C标准,您应该允许您描述的内容。 Common Initial Sequence规则的混淆源于一个更大的问题:标准未能指定何时使用可视地从另一个获得的指针或左值被认为是原始的使用。如果答案是&#34; never&#34;,那么非字符类型的任何结构或联合成员都将毫无用处,因为该成员将是一个左值,其类型对于访问结构是无效的或者工会。这种观点显然是荒谬的。如果答案是&#34;只有当它是通过直接应用&#34;而形成的时候。&#34;或&#34; - &gt;&#34;在struct或union类型或指向这种类型的指针上,这将使得能够使用&#34;&amp;&#34;结构和联盟成员相当无用。我认为这种看法只是略显荒谬。

我认为很明显,为了有用,必须将C语言视为允许在至少某些情况下使用派生左值。您的代码或大多数依赖于公共初始序列规则的代码是否可用取决于这些情况。

如果代码无法可靠地使用派生左值来访问结构成员,那么语言会相当愚蠢。不幸的是,即使这个问题在1992年显而易见(它构成了当年公布的缺陷报告#028的基础),委员会也没有解决根本问题,而是基于完全荒谬的逻辑得出了正确的结论,从那以后,以“有效类型”和“有效类型”的形式增加了不必要的复杂性。没有费心去实际定义someStruct.member的行为。

因此,没有办法编写任何对结构或联合做任何事情的代码,而不依赖于比标准文字阅读实际保证的更多行为,无论这些访问是通过强制执行{{1或指向正确成员类型的指针。

如果读取6.5p7的意图是为了以某种方式允许使用从特定类型之一派生的左值的动作来访问该类型的对象,至少在不涉及实际混叠的情况下(请注意一个巨大的延伸,给出脚注#88和#34;此列表的目的是指定对象可能或可能没有别名的情况。&#34;),并认识到别名需要一个区域当存在另一个引用时,使用引用X访问存储,其中X未被明显导出将来将用于以冲突的方式访问存储,那么编译者应该尊重该意图能够毫无困难地处理像你这样的代码。

不幸的是,gcc和clang似乎都将p6.5p7解释为,即使在推导完全可见的情况下,从另一种类型派生的左值通常也应该被认为无法实际识别该类型的对象。

给出类似的东西:

void*

在访问struct s1 {int x;}; struct s2 {int x;}; union u {struct s1 v1; struct s2 v2;}; int test(union u arr[], int i1, int i2) { struct s1 *p1 = &arr[i1].v1; if (p1->x) { struct s2 *p2 = &arr[i2].v2; p2->x=23; } struct s1 *p3 = &arr[i1].v1; return p3->x; } 时,p1->x显然是从联合类型的左值派生的,因此应该能够访问这样的对象,以及唯一的其他现有引用用于访问存储的是对该联合类型的引用。同样,当访问p1p2->x时。不幸的是,gcc和clang都将N1570 6.5p7解释为他们应该忽略联盟和指向其成员的指针之间的关系。如果不能依赖gcc和clang来有效地允许上面的代码访问相同结构的公共初始序列,我就不会信任它们可靠地处理像你这样的结构。

除非或直到标准被更正为说明在什么情况下可以使用派生左值来访问结构或联合的成员,但是不清楚任何对结构或联合做任何远程异常的代码都应该特别期望在gcc和clang的p3->x方言下工作。另一方面,如果一个人认识到左值推导的概念是双向的,那么编译器可能有理由假设一个结构类型的指针不会以对别名引用的方式使用。 ,即使指针在使用前被转换为第二种类型。因此,我建议如果标准修订规则,使用-fstrict-aliasing将不太可能遇到麻烦。