转换结构体指针

3
假设代码是用c11编译并启用了严格别名。
我不是在寻找其他方法,我想专注于这个特定的问题,看它是否起作用或者为什么不起作用。
(如果我无意中犯了一些无关的错误,请告诉我,我会修复它)
c11标准如下:
6.2.5.28 所有指向结构类型的指针应具有相同的表示和对齐要求。
6.7.2.1.6 结构体是由一系列成员组成的类型,其存储按顺序分配。
这意味着结构体A和B中指针的大小和对齐方式是相同的。
#include <stdio.h>
#include <stdlib.h>

struct S1
{
    int i ;
} ;

struct S2
{
    float f ;
} ;

struct A
{
    struct S1* p ;
} ;


struct B
{
    struct S2* p ;
} ;


int main( void )
{

结构体A和B都有指向S1和S2结构体的指针,并且保证结构体A和B具有相同的大小和对齐方式。

我们有一个struct B,其成员指针是一个指向S2结构体的指针,但它指向某个S1结构体,这是通过void*转换实现的。

struct S1 s1 = { 0 } ;

struct B* b = malloc( sizeof( *b ) ) ;
b->p = ( void* ) &s1 ;

没问题,我们可以存储指针,只要我们不实际使用指针。 但我们想使用它。 我们可以将指针强制转换为struct S1。

( ( struct S1* )(b->p) )->i = 123 ;    //redundant brackets for emphasis

printf("%d\n" , s1.i ) ;

并且正确地使用它。

到目前为止,我没有看到任何问题,因为指针被转换为正确的类型。

但是我们可以将整个结构体B转换为结构体A吗?尽管它们在大小和对齐方面相同,但标准可能会抱怨(?), 编译器会产生未定义的行为吗?

( ( struct A* )b)->p->i = 666 ;

printf("%d\n" , s1.i ) ;

我知道解决方法是使用联合体(或使用void并在每次正确转换时进行),因为标准允许使用未使用的成员来存储值。
6.5.2.3.3(95) 如果用于读取联合体对象内容的成员与上次用于在对象中存储值的成员不同,则该值的对象表示的适当部分将被重新解释为新类型的对象表示,如6.2.6所述(有时称为“类型字谜”)。 这可能是一个陷阱表示。
但是,我想避免这种情况:
struct C
{
    union
    {
        struct S1* p1 ;
        struct S2* p2 ;
    } ;
} ;

struct C* c = malloc( sizeof( *c ) ) ;

c->p2 = ( void* )&s1 ;

c->p1->i = 444 ;

printf("%d\n" , s1.i ) ;

return 0 ;
}

Above code without text.


“结构体A和B保证具有相同的大小” - 我不认为这是有保障的...(尽管我承认在实践中这很可能是正确的...) - Oliver Charlesworth
@OliCharlesworth 我不知道它们何时会有所不同。只有在非常特殊的情况或系统下吗? - this
也许这个问题超出了范围。你有没有想过如何使用这个魔法?你只是想将浮点数的一部分修改为整数吗?如果是的话,你肯定可以更容易地做到。我真的看不出来进行这些结构转换有什么意义。 - Marian
@ Marian 你有什么想法如何使用这个魔法吗? 是的。你只是想将浮点数的一部分修改为整数吗? 我想知道如何正确访问在问题中详细说明的结构体。我真的看不出来做这些结构转换有任何意义。 这是一个 sscce。这意味着它只是我整个代码库的最小示例,目前需要进行这些转换。 - this
3个回答

6

到目前为止,您所描述的:

但是我们能否将整个结构体B转换为结构体A?

都是正确的,但是这个问题的答案很遗憾是否定的。 只有当两个结构体包含“公共初始序列”时,即它们的前几个成员具有相同的类型时,才允许通过不兼容类型的指针访问结构体。由于您的结构体没有(即,第一个成员的类型不同),因此不能合法地通过指向S2的指针访问S1类型的对象,反之亦然。特别地,这样做会违反严格别名规则

来自C99,6.5.7:

一个对象只能通过以下类型的lvalue表达式访问其存储值:76)

— 与对象的有效类型兼容的类型,

— 对象的有效类型的限定版本,

— 与对象的有效类型对应的有符号或无符号类型,

— 与对象的有效类型的限定版本对应的有符号或无符号类型,

— 包括上述类型之一在其成员中的聚合或联合类型(包括子聚合或包含联合的成员),或

— 字符类型。


如果我禁用严格别名规则,你认为我能逃脱吗? - this
我能再问一个问题吗?如果通过一个联合将结构体B指针强制转换为结构体A,为什么不能解决问题呢?不是像我在示例中展示的成员变量那样,而是指针本身。像这样:http://pastebin.com/ERc0A4eB。别告诉我那样实际上会起作用。 - this
@self。我并没有说使用联合体不能解决问题。当您使用联合体时,严格的别名规则不再被违反,但是生成的指针仍然可能是陷阱表示(结果是实现定义的)。然而,我所知道的几乎所有编译器和平台都允许这样做并产生正确的结果,因此如果您真的想这样做,请使用联合体。 - user529758
通用初始序列异常仅适用于作为联合体一部分的结构体。通过从不同的、布局兼容的结构体转换而来的指针访问仍然是一种违规行为。 - tab
@H2CO3 很抱歉打扰您。但我有另一个问题。叹气 我知道...我到处搜索,但找不到任何东西。如果我通过void*函数参数传递指针B,并在函数内部将其转换为A,那么这仍然是别名侵犯吗?我是否仍然需要通过联合进行转换? - this
显示剩余5条评论

2
在表达式((struct A *) b)->p->i中,对p的访问违反了C 2011年6.5 7的规定,该规定指出“对象的存储值只能通过具有以下类型之一的lvalue表达式访问:与对象的有效类型兼容的类型,...”。b->p是指向struct S2的指针,但((struct A *) b)->p是具有指向struct S1的指针类型的lvalue表达式。尽管这些指针的表示可能相同,但它们不是兼容的类型。

0

我认为在这种特定情况下,您的示例将起作用并符合标准。 ANSI标准如下:

A pointer to a structure object, suitably
cast, points to its initial member (or if that member is a bit-field,
then to the unit in which it resides), and vice versa.  There may
therefore be unnamed holes within a structure object, but not at its
beginning, as necessary to achieve the appropriate alignment.

在你的例子中,指针p始终是结构体的第一个(也是唯一的)字段。根据我对前一段的理解,指向struct A的指针与指向A::p(请原谅C++符号)的指针相同,这与指向B::p的指针以及指向B的指针相同。显式转换不会改变指针的值,因此你的例子应符合标准。

毫无疑问,这种编程风格并不美观,你的老板可能不会欣赏。


指针b->p和a->p是不同的,它们分别是类型S1和S2的指针。当进行强制转换时它们相同,但这就是问题所在,在C语言中有时你不能只是这样做。 - this
如果您拿到一个类型为A*的指针a和一个类型为B*的指针b,并且满足a==b,那么表达式a->p得到的值与((B*)a)->p以及b->p((A*)b)->p得到的值是相同的。这不就是您问题的核心吗? - Marian
你是不是想表达这个意思:a->p == ((B)a)->p == b->p ==((A*)b)->p*?如果是的话,那么如果a==b,那么a->pb->p的指针类型是不同的。因为它们以不同的方式解释所指向的数据,所以需要进行所有的强制转换。 - this
@self。我的意思是:如果(char*)a == (char*)b,那么(char*) a->p等于(char*) ((struct B*)a)->p,它等于b->p,它等于(char*)((struct A*)b)->p在这种特殊情况下,因为p是结构体A和B的第一个字段。 - Marian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接