【问题标题】:Is it possible to cast pointers from a structure type to another structure type extending the first in C?是否可以将指针从结构类型转换为扩展 C 中第一个结构类型的另一个结构类型?
【发布时间】:2011-04-20 06:43:38
【问题描述】:

如果我有结构定义,例如,像这样:

struct Base {
  int foo;
};

struct Derived {
  int foo; // int foo is common for both definitions
  char *bar;
};

我可以这样做吗?

void foobar(void *ptr) {
  ((struct Base *)ptr)->foo = 1;
}

struct Derived s;

foobar(&s);

换句话说,当它的类型实际上是Derived * 时,我可以将void 指针转换为Base * 以访问其foo 成员吗?

【问题讨论】:

  • 你试过了吗?发生了什么?
  • 它在没有编译器抱怨的情况下工作,但我想知道这是一种常用的做法,还是一些讨厌的 hack(它现在对我有很大帮助)。
  • @PaulR 如果您的问题是“尝试自己而不是询问”,您应该了解未定义行为以及它如何使您的实验今天看起来正确但明天失败 - 反之亦然。

标签: c pointers casting struct


【解决方案1】:

你应该这样做

struct Base {
  int foo;
};

struct Derived {
  struct Base base;
  char *bar;
};

避免破坏严格的别名;一个常见的误解是 C 允许指针类型的任意转换:尽管它在大多数实现中都能按预期工作,但它是非标准的。

这也避免了由于使用 pragma 指令而导致的任何对齐不兼容。

【讨论】:

    【解决方案2】:

    许多现实世界的 C 程序假定您展示的结构是安全的,并且存在对 C 标准的解释(特别是“通用初始序列”规则,C99 §6.5.2.3 p5) .不幸的是,自从我最初回答这个问题以来的五年里,我可以轻松获得的所有编译器(即 GCC 和 Clang)都对通用初始序列规则进行了不同的、更狭义的解释,在这种解释下,你展示的构造会引发未定义的行为。具体来说,试试这个程序:

    #include <stdio.h>
    #include <string.h>
    
    typedef struct A { int x; int y; }          A;
    typedef struct B { int x; int y; float z; } B;
    typedef struct C { A a;          float z; } C;
    
    int testAB(A *a, B *b)
    {
      b->x = 1;
      a->x = 2;
      return b->x;
    }
    
    int testAC(A *a, C *c)
    {
      c->a.x = 1;
      a->x = 2;
      return c->a.x;
    }
    
    int main(void)
    {
      B bee;
      C cee;
      int r;
    
      memset(&bee, 0, sizeof bee);
      memset(&cee, 0, sizeof cee);
    
      r = testAB((A *)&bee, &bee);
      printf("testAB: r=%d bee.x=%d\n", r, bee.x);
    
      r = testAC(&cee.a, &cee);
      printf("testAC: r=%d cee.x=%d\n", r, cee.a.x);
    
      return 0;
    }
    

    在启用优化(并且没有-fno-strict-aliasing)的情况下进行编译时,GCC 和 Clang 都会假设testAB 的两个指针参数不能指向同一个对象,所以我得到的输出如下

    testAB: r=1 bee.x=2
    testAC: r=2 cee.x=2
    

    他们没有对testAC 做出这样的假设,但是——之前的印象是,testAB 必须被编译为好像它的两个参数可以指向同一个对象——我不再对自己对标准的理解有足够的信心来说明 是否可以保证继续工作。

    【讨论】:

    • 评论不用于扩展讨论;这个对话是moved to chat
    • @GeorgeStocker 在这种情况下我碰巧不太在意,因为它不是 在 cmets 中进行了广泛的讨论,但这是第一次发生这种情况我的 Stack Overflow 答案之一,因此特此通知您,我不希望版主迁移或删除我的问题和答案中的 any cmets,也不迁移或删除 any在任何情况下,除了实际的垃圾邮件和滥用之外,我的 cmets 对任何人的问题或答案。当前的政策是错误的,并且会对网站造成严重损害。
    【解决方案3】:

    这将适用于这种特殊情况。两个结构和命中的第一个成员中的foo 字段具有相同的类型。但是,在结构中的字段(不是第一个成员)的一般情况下,情况并非如此。对齐和包装等项目可以以微妙的方式打破这种情况。

    【讨论】:

      【解决方案4】:

      由于您似乎是针对 C 中的面向对象编程,我建议您查看以下链接:

      http://www.planetpdf.com/codecuts/pdfs/ooc.pdf

      详细介绍了在 ANSI C 中处理 oop 原则的方法。

      【讨论】:

      • 谢谢,这似乎是我咨询的第一个真正有用的资源 :)
      【解决方案5】:

      在特定情况下,这可能有效,但通常 - 不,因为结构对齐。

      您可以使用不同的 #pragmas 来使(实际上,尝试)对齐方式相同 - 然后,是的,这会起作用。

      如果您使用的是 Microsoft Visual Studio,您可能会发现 this article 很有用。

      【讨论】:

        【解决方案6】:

        还有一件小事可能对你正在做的事情有所帮助或相关..

        #define SHARED_DATA int id;
        
        typedef union base_t {
            SHARED_DATA;
            window_t win;
            list_t   list;
            button_t button;         
        }
        
        typedef struct window_t {
            SHARED_DATA;
            int something;
            void* blah;
        }
        
        typedef struct window_t {
            SHARED_DATA;
            int size;
         }
        
        typedef struct button_t {
            SHARED_DATA;
            int clicked;
         }
        

        现在您可以将共享属性放入 SHARED_DATA 并通过打包到联合中的“超类”处理不同的类型。您可以使用 SHARED_DATA 仅存储“类标识符”或存储指针。无论哪种方式在某些时候为我对事件类型的通用处理提供了方便。希望我不会在这个话题上跑得太远

        【讨论】:

          【解决方案7】:

          我知道这是一个老问题,但在我看来还有很多话要说,其他一些答案是不正确的。

          首先,这个演员阵容:

          (struct Base *)ptr
          

          ... 是允许的,但前提是满足对齐要求。在许多编译器上,您的两个结构将具有相同的对齐要求,并且在任何情况下都很容易验证。如果你克服了这个障碍,接下来就是转换的结果大部分是未指定的——也就是说,C 标准中没有要求指针一旦转换仍然指向同一个对象(只有在 将它转换回来之后 到原来的类型,它一定会这样做)。

          然而,在实践中,通用系统的编译器通常使指针转换的结果指向同一个对象。

          (指针转换在 C99 标准和更新的 C11 标准的第 6.3.2.3 节中都有介绍。我相信两者的规则基本相同)。

          最后,您需要应对所谓的“严格别名”规则(C99/C11 6.5 第 7 段);基本上,不允许您通过另一种类型的指针访问一种类型的对象(有某些例外,在您的示例中不适用)。请参阅"What is the strict-aliasing rule?",或非常深入讨论,请阅读我的blog post关于这个主题。

          总之,您在代码中尝试的内容并不能保证有效。它可以保证始终与某些编译器(以及某些编译器选项)一起工作,并且它可能偶然与许多编译器一起工作,但它肯定会根据 C 语言标准调用未定义的行为。

          可以做的是这样的:

          *((int *)ptr) = 1;
          

          ...即因为您知道结构的第一个成员是int,所以您只需直接转换为int,这样就绕过了别名问题,因为这两种类型的结构实际上在该地址都包含int。您依赖于知道编译器将使用的结构布局,并且您仍然依赖于指针转换的非标准语义,但实际上这不太可能给您带来问题。

          【讨论】:

          • “其他答案不正确” -> 哪些?你的答案和他们到底有什么不同?
          • @hmijail (a) 所有这些,除了已接受的答案已被编辑,并且 (b) 如果您阅读我的答案和其他答案,差异应该足够明显。其中一个声称结构对齐会导致问题(实际上通常不会),但实际上指针转换具有未指定的结果(实际上这无关紧要)。其中两个声称 OP 的代码“将起作用”。我同意克里斯托夫的那个(可能第一次错过了)。其他人没有提供直接的答案。无论如何,我会编辑以删除一揽子声明。
          • @hmijail 顺便说一句,由于我自己的 cmets 已修改接受的答案,请参阅discussion。顺便说一句,我的回答与其他人的最大区别在于它实际上讨论了严格的别名规则。
          【解决方案8】:

          C 的好处/坏处在于你可以转换任何东西——问题是,它可能不起作用。 :) 但是,在你的情况下,它会*,因为你有两个结构,它们的第一个成员都是相同的类型;有关示例,请参见 this program。现在,如果 struct derived 有一个不同的类型作为它的第一个元素——例如,char *bar——那么不,你会得到奇怪的行为。

          * 我想我应该用“几乎总是”来限定它;那里有很多不同的 C 编译器,所以有些可能有不同的行为。不过,我知道它会在 GCC 中工作。

          【讨论】:

          • “几乎总是”并以程序为例在谈论 C 和 UB 相关问题时会导致灾难。
          猜你喜欢
          • 2020-12-30
          • 2016-05-05
          • 1970-01-01
          • 2019-03-18
          • 1970-01-01
          • 2017-08-31
          • 1970-01-01
          • 2023-03-16
          相关资源
          最近更新 更多