代码之家  ›  专栏  ›  技术社区  ›  Kona98

结构指针强制转换

  •  2
  • Kona98  · 技术社区  · 7 年前

    我正在尝试实现如下链接列表:

    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;
    }
    

    所以我的问题是:

    • 只要我只访问公共成员,我就可以在这些结构之间进行强制转换吗?我在这方面读到了不同的意见。
    • 我是否也可以创建相应类型的下一个指针?如果它真的是DLnode,那么在我的示例函数中使用它是UB吗?

    如果这不起作用,还有其他方法可以这样做吗?我读到工会可能会工作,但这段代码也应该在C89中运行,而afaik读取的工会成员与上次写入的工会成员不同。

    3 回复  |  直到 7 年前
        1
  •  2
  •   Community CDub    5 年前

    因此,您试图在C中构建子类。一种可能的方法是将基结构作为子结构的第一个元素,因为在这种情况下,C标准明确允许在这两种类型之间来回转换:

    6.7.2.1结构和联合规范

    §13。。。指向 经过适当转换的结构对象指向其初始成员(或者如果该成员是 位字段,然后是它所在的单元),反之亦然。。。

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

    示例代码:

    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
  •  2
  •   dbush    7 年前

    如果使用 union 包含两个结构:

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

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

    6. 为了简化联合体的使用,有一个特殊的保证:如果一个联合体包含多个共享一个公共的结构 初始序列(见下文),以及如果联合对象当前 包含其中一个结构,允许检查 其中任何一个的公共首字母部分 完整的联合类型可见。两个结构共用一个 如果相应的成员具有兼容的类型(和, 对于位字段,对于一个或多个初始 成员。

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

        3
  •  0
  •   supercat    7 年前

    您所描述的内容在C标准下应该是允许的。常见的初始序列规则的混乱源于一个更大的问题:该标准未能规定使用明显源自另一个的指针或左值被视为使用了原始值。如果答案是“从不”,那么任何非字符类型的结构或联合成员都将毫无用处,因为该成员将是一个左值,其类型对于访问结构或联合无效。这种观点显然是荒谬的。如果答案是“仅当通过直接应用形成时”。“或”->在结构或联合类型上,或在指向此类类型的指针上,可以使用“&“在结构和工会成员身上是毫无用处的。我认为这种观点没有那么荒谬。

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

    如果代码不能可靠地使用派生的左值来访问结构成员,那么这种语言将相当愚蠢。不幸的是,尽管这个问题在1992年很明显(它构成了当年发布的缺陷报告#028的基础),但委员会没有解决根本问题,而是基于完全荒谬的逻辑得出了正确的结论,从那以后,它以“有效类型”的形式增加了不必要的复杂性,而从未真正定义 someStruct.member

    因此,如果不依赖于更多的行为,而不是通过标准的字面阅读来保证,那么就无法编写任何对结构或联合做很多事情的代码,无论这种访问是通过强制 void* 或指向正确成员类型的指针。

    如果有人认为6.5p7的意图是以某种方式允许使用从某一特定类型派生的左值的操作访问该类型的对象,至少在不涉及实际别名的情况下(请注意,脚注#88“此列表的意图是指定对象可能被别名或不被别名的情况”),并认识到别名要求在存在另一个引用时使用引用X访问存储区域 从中看不到X 这将在将来用于以冲突的方式访问存储,那么遵循这一意图的编译器应该能够毫不费力地处理像您这样的代码。

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

    例如:

    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 已访问, p1 显然是从union类型的左值派生的,因此应该能够访问这样的对象,并且将用于访问存储的唯一其他现有引用是对该union类型的引用。同样,当 p2->x p3->x 被访问。不幸的是,gcc和clang都将N1570 6.5p7解释为一种指示,表明他们应该忽略联合和指向其成员的指针之间的关系。如果gcc和clang不能有效地允许上述代码访问相同结构的公共初始序列,我也不相信它们能可靠地处理像您这样的结构。

    除非或直到该标准被更正为说明在何种情况下可以使用派生左值访问结构或联合的成员,否则不清楚任何对结构或联合执行任何异常操作的代码是否应该在 -fstrict-aliasing gcc和clang的方言。另一方面,如果认识到左值派生的概念是双向的,那么编译器可能有理由假设一个结构类型的指针不会以别名引用另一个的方式使用,即使指针在使用之前被转换为第二种类型。因此,我建议使用 无效* 如果该标准修正了规则,那么就不太可能遇到麻烦。