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

在结构中引用未定义的类型是如何合法的?

  •  8
  • paxdiablo  · 技术社区  · 15 年前

    作为回答另一个问题的一部分,我遇到了这样一段代码,GCC编译它时没有抱怨。

    typedef struct {
        struct xyz *z;
    } xyz;
    int main (void) {
        return 0;
    }
    

    这是我一直用来构造指向自己的类型(例如链接列表)的方法,但我一直认为您必须 名称 结构,以便可以使用自引用。换句话说,你不能使用 xyz *z 在结构中,因为typedef在该点尚未完成。

    但是这个样品是 命名结构,它仍在编译。我原以为编译器中会有一些黑色的魔力,自动翻译上面的代码,因为结构和typedef名称是相同的。

    但这个小美人也很管用:

    typedef struct {
        struct NOTHING_LIKE_xyz *z;
    } xyz;
    

    我这里缺什么?这似乎是明显的违反,因为没有 struct NOTHING_LIKE_xyz 任何地方定义的类型。

    当我将它从指针更改为实际类型时,我得到预期的错误:

    typedef struct {
        struct NOTHING_LIKE_xyz z;
    } xyz;
    
    qqq.c:2: error: field `z' has incomplete type
    

    另外,当我移除 struct ,我得到一个错误( parse error before "NOTHING ... )

    ISO C中允许这样做吗?


    更新:A struct NOSUCHTYPE *variable; 也可以编译,因此 里面 它似乎有效的结构。我在C99标准中找不到任何允许对结构指针如此宽泛的内容。

    7 回复  |  直到 13 年前
        1
  •  6
  •   caf    15 年前

    您所采用的C99标准的部分是6.7.2.3,第7段:

    如果窗体的类型说明符 struct-or-union identifier 发生 除上述其中一项之外 表格,没有其他声明 作为标记的标识符是可见的,然后 它声明一个不完整的结构或 联合类型,并声明 标识符作为该类型的标记。

    …和6.2.5第22段:

    未知的结构或联合类型 内容(如6.7.2.3所述)为 类型不完整。完成了, 对于该类型的所有声明, 声明相同的结构或联合 稍后使用其定义内容标记 同样的范围。

        2
  •  7
  •   Pascal Cuoq    15 年前

    正如第二个案例中的警告所说, struct NOTHING_LIKE_xyz 是一个 不完全型 ,像 void 或未知大小的数组。不完整的类型只能作为指向的类型出现,但允许作为结构最后一个成员的未知大小数组除外,在这种情况下,使结构本身成为不完整的类型。下面的代码不能取消对不完整类型的任何指针的引用(有充分的理由)。

    不完整的类型可以在C语言中提供一些类型封装。 中的相应段落 http://www.ibm.com/developerworks/library/pa-ctypes1/ 似乎是个很好的解释。

        3
  •  2
  •   kennytm    15 年前

    第1和第2种情况定义得很好,因为指针的大小和对齐方式是已知的。C编译器只需要大小和对齐信息来定义结构。

    第三个大小写无效,因为该实际结构的大小未知。

    但是要注意,要使第一个案例符合逻辑,您需要为结构命名:

    //             vvv
    typedef struct xyz {
        struct xyz *z;
    } xyz;
    

    否则,外部结构和 *z 将被视为两种不同的结构。


    第二个案例有一个流行的用例,叫做 "opaque pointer" (pimpl) . 例如,可以将包装结构定义为

     typedef struct {
        struct X_impl* impl;
     } X;
     // usually just: typedef struct X_impl* X;
     int baz(X x);
    

    在标题中,然后在其中一个 .c ,

     #include "header.h"
     struct X_impl {
        int foo;
        int bar[123];
        ...
     };
     int baz(X x) {
        return x.impl->foo;
     }
    

    优势就在于此 C 您不能弄乱对象的内部。它是一种封装。

        4
  •  1
  •   R Samuel Klatchko    15 年前

    你必须说出它的名字。在这方面:

    typedef struct {
        struct xyz *z;
    } xyz;
    

    将无法指向自身 z 引用一些完整的其他类型,而不是刚刚定义的未命名结构。试试这个:

    int main()
    {
        xyz me1;
        xyz me2;
        me1.z = &me2;   // this will not compile
    }
    

    您将得到一个关于不兼容类型的错误。

        5
  •  1
  •   AnT stands with Russia    15 年前

    好。。。我只能说你先前的假设是错误的。每次使用 struct X 构造(本身或作为更大声明的一部分),它被解释为具有结构标记的结构类型的声明 X . 它可以是以前声明的结构类型的重新声明。或者,它可以是 新的 结构类型。新标记在其出现的范围内声明。在您的具体示例中,它恰好是一个文件范围(因为C语言没有“类范围”,如C++中那样)。

    这种行为的更有趣的例子是当声明出现在函数原型中时:

    void foo(struct X *p); // assuming `struct X` has not been declared before
    

    在这种情况下,新的 结构X 声明已 功能原型范围 在原型的末尾。如果您声明一个文件范围 结构X 后来

    struct X;
    

    尝试传递一个指针 结构X 键入以上函数,编译器将对不匹配的指针类型进行诊断。

    struct X *p = 0;
    foo(p); // different pointer types for argument and parameter
    

    这也立即意味着在以下声明中

    void foo(struct X *p);
    void bar(struct X *p);
    void baz(struct X *p);
    

    每个 结构X 声明是对 不同的 类型,每个本地的函数原型范围。

    但是如果你预先申报 结构X 如在

    struct X;
    void foo(struct X *p);
    void bar(struct X *p);
    void baz(struct X *p);
    

    全部的 结构X 所有函数原型中的引用将引用 相同的 预先声明的 结构X 类型。

        6
  •  0
  •   Scott Wales    15 年前

    我也在想这个。结果发现 struct NOTHING_LIKE_xyz * z 正在转发声明 struct NOTHING_LIKE_xyz . 作为一个复杂的例子,

    typedef struct {
        struct foo * bar;
        int j;
    } foo;
    
    struct foo {
        int i;
    };
    
    void foobar(foo * f)
    {
        f->bar->i;
        f->bar->j;
    }
    

    在这里 f->bar 指的是类型 struct foo 不是 typedef struct { ... } foo . 第一行编译得很好,但第二行会出错。对于链表的实现没有太大的用处。

        7
  •  0
  •   supercat    13 年前

    当声明结构类型的变量或字段时,编译器必须分配足够的字节来保存该结构。由于结构可能需要一个字节,或者可能需要数千个字节,所以编译器无法知道需要分配多少空间。有些语言使用多遍编译器,它可以在一次传递中找出结构的大小,并在以后的传递中为其分配空间;但是,由于C被设计为允许单遍编译,所以这是不可能的。因此,C禁止声明不完整结构类型的变量或字段。

    另一方面,当声明指向结构类型的指针的变量或字段时,编译器必须分配足够的字节来保存指向结构的指针。 不管结构是占用一个字节还是一百万个字节,指针总是需要相同的空间量。 实际上,编译器可以将指向不完整类型的指针作为void*进行遍历,直到获得有关其类型的更多信息,然后在发现有关类型的更多信息后将其视为指向适当类型的指针。不完整的类型指针与void*不太相似,因为可以用void*做一些不完整类型不能做的事情(例如,如果p1是指向结构s1的指针,而p2是指向结构s2的指针,则不能将p1赋给p2),但不能用指向不完整类型的指针做任何事情,而不能用指向void*的指针。基本上,从编译器的角度来看,指向不完整类型的指针是指针大小的字节块。它可以复制到或从其他类似的指针大小的字节块复制,但就是这样。编译器可以生成这样做的代码,而不必知道对于指针大小的字节块还有什么其他用途。