将一个结构体指针转换为另一个 - C

38

请看下面的代码。

enum type {CONS, ATOM, FUNC, LAMBDA};

typedef struct{
  enum type type;
} object;

typedef struct {
  enum type type;
  object *car;
  object *cdr;
} cons_object;

object *cons (object *first, object *second) {
  cons_object *ptr = (cons_object *) malloc (sizeof (cons_object));
  ptr->type = CONS;
  ptr->car = first;
  ptr->cdr = second;
  return (object *) ptr;
}

cons函数中,变量ptr的类型为cons_object*。但在返回值中,它被转换为object*类型。

  1. 我想知道这是怎么可能的,因为cons_objectobject是不同的结构体。
  2. 这样做是否存在任何问题?

有什么想法!

3个回答

38
这是一种在C中实现“面向对象”的常用技术,非常不错。由于在C中,struct的内存布局是被定义好的,只要两个对象共享相同的布局,你就可以安全地在它们之间转换指针。也就是说,在object结构体和cons_object结构体中,type成员的偏移量是相同的。
在这种情况下,type成员告诉API,object是一个cons_object还是foo_object或其他类型的对象,所以你可能会看到类似这样的代码:
void traverse(object *obj)
{
    if (obj->type == CONS) {
        cons_object *cons = (cons_object *)obj;
        traverse(cons->car);
        traverse(cons->cdr);
    } else if (obj->type == FOO) {
        foo_object *foo = (foo_object *)obj;
        traverse_foo(foo);
    } else ... etc
}

更常见的是,我看到过将“父类”定义为“子类”的第一个成员的实现方式,就像这样:

typedef struct {
    enum type type;
} object;

typedef struct {
    object parent;

    object *car;
    object *cdr;
} cons_object;

这个方法基本上是一样的,但你可以确保子“类”的内存布局与父类相同。也就是说,如果你向“基类” object 添加一个成员,它会自动被子类捕获,你不需要手动确保所有的结构都同步。


3
请注意,这是合法的 C 代码,具有明确定义的行为,而不是“黑科技”或利用“未定义行为”。 - R.. GitHub STOP HELPING ICE
7
如果有人引用了一个标准,并解释了为什么原始示例不违反所声称的严格别名规则,那将是很棒的事情。链接是:https://dev59.com/xG865IYBdhLWcg3wivGi#3766967 这个问题还提到了嵌套结构体:https://dev59.com/VGoy5IYBdhLWcg3wo_no。 - Ciro Santilli OurBigBook.com
3
拥有相同的内存布局并不意味着根据严格别名规则可以在结构体指针之间进行强制转换是合法的。实际上,在CPython的对象实现中,有一份关于解决这个问题的PEP: https://www.python.org/dev/peps/pep-3123/。显然,这些转换似乎合理,并且几乎总是有效的,但UB就是UB。 - Praxeolitic
6
虽然来得有些晚,但我也同意这个答案是误导性的——具有相同布局的两个不相关结构体可能不能彼此别名。 - Oliver Charlesworth
2
@OliverCharlesworth:这个答案对于Dennis Ritchie发明的语言以及绝大多数编译器可以配置处理的方言都是正确的。标准将这种能力降级为“流行扩展”,实现不需要支持,但不对省略它的方言是否适合任何特定目的做出判断。 - supercat
显示剩余3条评论

22
为了补充Dean的回答,这里有关于指针转换的一般性内容。我忘记这个术语是什么了,但指针到指针的转换不执行任何转换(就像int到float一样)。它只是重新解释他们所指向的位(全部为编译器的利益)。我想这叫做“非破坏性转换”。数据不会改变,只有编译器解释所指向的内容的方式会改变。
例如, 如果ptr是一个指向对象的指针,那么编译器知道有一个名为type的特定偏移量的字段,类型为enum type。另一方面,如果将ptr强制转换为指向不同类型cons_object的指针,它也将以类似的方式知道如何访问cons_object的每个具有自己偏移量的字段。
为了说明,想象一下cons_object的内存布局:
                    +---+---+---+---+
cons_object *ptr -> | t | y | p | e | enum type
                    +---+---+---+---+
                    | c | a | r |   | object *
                    +---+---+---+---+
                    | c | d | r |   | object *
                    +---+---+---+---+

type字段偏移量为0,car为4,cdr为8。要访问car字段,编译器只需要将指向结构体的指针加上4

如果将指针强制转换为指向object的指针:

                    +---+---+---+---+
((object *)ptr)  -> | t | y | p | e | enum type
                    +---+---+---+---+
                    | c | a | r |   |
                    +---+---+---+---+
                    | c | d | r |   |
                    +---+---+---+---+

编译器只需要知道存在一个名为type的字段,偏移量为0。无论内存中存储的是什么,都在内存中。
指针甚至不必有任何关系。您可以将指向int的指针转换为指向cons_object的指针。如果要访问car字段,就像访问任何普通的内存访问一样。它与结构的开头有一定的偏移量。在这种情况下,存储在该内存位置中的内容未知,但这并不重要。要访问字段,只需要偏移量,并且该信息包含在类型的定义中。
指向int的指针指向一块内存:
                        +---+---+---+---+
int             *ptr -> | i | n | t |   | int
                        +---+---+---+---+

转换为cons_object指针:

                        +---+---+---+---+
((cons_object *)ptr) -> | i | n | t |   | enum type
                        +---+---+---+---+
                        | X | X | X | X | object *
                        +---+---+---+---+
                        | X | X | X | X | object *
                        +---+---+---+---+

2
但是指向指针的转换不执行任何转换(就像int到float一样)。它只是对它们所指向的位的重新解释(全部为编译器的利益)。这是错误的。不同类型的指针可以具有不同的表示形式,甚至它们的大小也可以不相同。http://c-faq.com/null/machexamp.html - Karol S

15

3
据我所知,这个答案是正确的,而当前被接受的答案是错误的。另请参见https://dev59.com/oafja4cB1Zd3GeqPul7g - Oliver Charlesworth

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