为什么 GCC make 仅在使用结构名称而不是 typedef 时才会抛出错误和不必要的警告?

2 c struct pointers gnu-make gcc-warning

我有一个由两个源文件(farm.c,init.c)和两个相应的头文件(farm.h,init.h)组成的程序两个源文件都包含头保护和彼此,因为它们都需要来自的函数/变量彼此。

\n

初始化.h:

\n
#ifndef INIT_H\n#define INIT_H\n\n#include<stdio.h>\n#include<stdlib.h>\n#include"farm.h"\n\n#define PIG_SOUND "oink"\n#define CALF_SOUND "baa"\n\nenum types {PIG, CALF};\n\ntypedef struct resources {\n    size_t pork;\n    size_t veal;\n    size_t lamb;\n    size_t milk;\n    size_t eggs;\n} resources;\n\ntypedef struct animal {\n    size_t legs;\n    char* sound;\n    int efficiency;\n    void (*exclaim)(struct animal*);\n    void (*work)(struct animal*, struct resources*);\n} animal;\n\n/* I have tried various ways of declaring structs in addition to\n   the typedef such as this */\n\n//animal stock;\n//animal farm;\n\nvoid make_pig(struct animal* a, int perf);\nvoid make_calf(struct animal* a, int perf);\n\n#endif\n
Run Code Online (Sandbox Code Playgroud)\n

农场.h:

\n
#ifndef FARM_H\n#define FARM_H\n\n#include<stdio.h>\n#include<stdlib.h>\n#include<time.h>\n#include<string.h>\n#include"init.h"\n\n/* GCC does not recognise the typedef or struct identifier \n   until these forward declarations have been made in \n   addition to the included init.h header file */\n\n//typedef struct animal animal;\n//typedef struct resources resources;\n\nvoid exclaim(animal* b);\nvoid work(struct animal* b, struct resources* s);\n\n#endif\n
Run Code Online (Sandbox Code Playgroud)\n

初始化.c:

\n
#include"init.h"\n\nvoid make_pig(animal* a, int perf) {\n    a->legs = 4;\n    a->sound = PIG_SOUND;\n    a->efficiency = perf;\n    a->exclaim = exclaim;\n    a->work = work;\n}\n\nvoid make_calf(animal* a, int perf) {\n    a->legs = 4;\n    a->sound = CALF_SOUND;\n    a->efficiency = perf;\n    a->exclaim = exclaim;\n    a->work = work;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

农场.c:

\n
#include"farm.h"\n\nint main() {\n    return 0;\n}\n\nvoid exclaim(animal* a) {\n    for (int i = 0; i < 3; i++) {\n        printf("%s ", a->sound);\n    }\n    printf("\\n");\n}\n\nvoid work(animal* a, struct resources* r) {\n    if (!strcmp(a->sound, PIG_SOUND)) {\n        r->pork += a->efficiency;\n    }\n\n    if (!strcmp(a->sound, CALF_SOUND)) {\n        r->veal += a->efficiency;\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

使用这两种类型的名称(即struct anianimal)通常在我的采用 C99 标准的 Linux 系统上工作得很好。但是,当我使用struct ani而不是animal此处时,对于struct aniand的类型使用的每个实例,我都会收到以下警告struct resources

\n
lib/farm.h:10:21: warning: \xe2\x80\x98struct ani\xe2\x80\x99 declared inside parameter list will not be visible outside of this definition or declaration\n   10 | void exclaim(struct ani* a);\n      |                     ^~~\nlib/farm.h:11:33: warning: \xe2\x80\x98struct resources\xe2\x80\x99 declared inside parameter list will not be visible outside of this definition or declaration\n   11 | void work(struct ani* a, struct resources* r);\n
Run Code Online (Sandbox Code Playgroud)\n

每次使用以下形式的函数指针时,总共会出现 10 条警告:

\n
src/init.c:17:16: warning: assignment to \xe2\x80\x98void (*)(struct ani *)\xe2\x80\x99 from incompatible pointer type \xe2\x80\x98void (*)(struct ani *)\xe2\x80\x99 [-Wincompatible-pointer-types]\n   17 |     a->exclaim = exclaim;\n      |                ^\nsrc/init.c:18:13: warning: assignment to \xe2\x80\x98void (*)(struct ani *, struct resources *)\xe2\x80\x99 from incompatible pointer type \xe2\x80\x98void (*)(struct ani *, struct resources *)\xe2\x80\x99 [-Wincompatible-pointer-types]\n   18 |     a->work = work;\n
Run Code Online (Sandbox Code Playgroud)\n

有人可以解释为什么会发生这种行为以及我如何避免出现问题吗?我通常需要花费大量的时间来解决这些错误,而且我一开始仍然没有真正理解我的错误。

\n

ric*_*ici 5

您遇到了 C 作用域规则的奇怪极端情况之一。

非正式地,如果没有可见的声明,则在命名时,标记struct(或union,但我不会一遍又一遍地重复)就会出现。“突然出现”意味着它被认为是在当前作用域中声明的。此外,如果之前在作用域中命名了带标记的结构,然后您使用相同的标记声明了一个结构,则这两个结构将被视为相同。在结构体的声明完成之前,该结构体被视为不完整类型,但指向不完整类型的指针是完整类型,因此您可以在实际完成结构体的定义之前声明指向标记结构体的指针。

大多数时候,只需很少的思考就可以做到这一点。但函数原型有点特殊,因为函数原型本身就是一个作用域。(作用域仅持续到函数声明结束为止。)

当你把它们放在一起时,你最终会遇到你所面临的问题。不能在函数原型中使用指向标记结构的指针,除非标记结构在函数原型出现之前已知。如果之前已经提到过,即使在外部作用域中,标记也是可见的,因此将被视为相同的结构(即使它仍然不完整)。但是,如果该标记以前不可见,则会在原型作用域内创建一个新的结构类型,该结构类型在该作用域结束后(几乎立即)将不可见。

具体来说,如果您编写以下内容:

extern struct animal * barnyard;
void exclaim(struct animal*);
Run Code Online (Sandbox Code Playgroud)

那么 的两个用法struct animal引用相同的结构类型,这可能会在稍后完成(或在另一个翻译单元中)。

但是如果没有extern struct animal * barnyard;声明,原型struct animal中的命名exclaim以前是不可见的,因此仅在原型范围中声明,因此它 与 的某些后续使用不是struct animal同一类型。如果您以相反的顺序放置声明,您会看到编译器警告(假设您要求编译警告):

void exclaim(struct animal*);
extern struct animal * barnyard;
Run Code Online (Sandbox Code Playgroud)

在上帝螺栓上,祝福它的心

声明typedef的执行方式与extern上面的声明相同;它是类型别名这一事实并不相关。重要的是,在声明中使用struct animal会导致类型出现,并且您随后可以在原型中自由使用它。这与结构定义中的函数指针正常的原因相同;结构定义的开头足以导致声明标记,因此原型可以看到它。

事实上,任何包含标记结构 ( struct whatever) 的语法结构都将达到相同的目的,因为重要的是在没有可见声明的情况下提及标记结构的效果。上面,我使用extern全局声明作为示例,因为它们是可能出现在标头中的行,但还有许多其他可能性,甚至包括返回指向 a 的指针的函数声明struct(因为函数声明的返回类型不是在原型范围内)。

有关已编辑问题的一些其他评论,请参阅下文。


我个人的偏好是始终使用 typedef 作为标签的前向声明,并且永远不要在代码中使用除struct footypedef 和后续定义之外的任何地方:

typedef struct Animal Animal;

void exclaim(Animal*);

// ...

// Later or in a different header
struct Animal {
  Animal* next;
  void (*exclaim)(Animal *);
  // etc.
};
Run Code Online (Sandbox Code Playgroud)

请注意,我始终对标记和 typedef 使用相同的标识符。为什么不?自从 C 出现以来,没有任何混乱,标签也没有与其他标识符位于同一名称空间中。

对我来说,这种风格的一大优点是它可以让我分离实现细节;公共标头仅包含 typedef 声明(以及使用该类型的原型),并且只有实现需要包含实际定义(在首先包含公共标头之后)。


注意:由于编写了此答案,因此对该问题进行了编辑以添加更详细的代码示例。现在,我将在这里留下这些附加注释:

总的来说,当您提供更好的信息时,您会得到更好的答案。由于我看不到您的实际代码,因此我尽力解释正在发生的事情,让您将其应用到您的实际代码中。

在您现在添加到问题中的代码中,存在循环标头依赖性。这些应该避免;要让它们正确几乎是不可能的。循环依赖意味着您无法控制包含的顺序,因此一个标头中的声明可能不会出现在另一个标头中的使用之前。您不再有前向声明,因为根据包含顺序,它可能是后向声明。

要解决循环依赖关系,请抽象出共享组件并将它们放入新的头文件中。在这里,结构的前向声明(例如使用 typedef)非常有用,因为它们不依赖于结构定义中使用的任何内容。共享标头可能仅包含 typedef,也可能包含不需要额外依赖项的原型。

另外,避免在头文件中放入长长的库列表;仅包含定义标头中实际使用的类型实际必需的标头。

  • @cwonder:我在我的答案中添加了一些额外的文字。相关的不是“typedef”或“extern”;这是命名标记结构的事实。但包含顺序至关重要,如果存在循环依赖,则无法控制包含顺序。顺便说一句,尽管有一些比较点,但“enum”的工作方式实际上并不相同。在使用标签之前,您必须声明一个“enum”。但请记住,在 C 中,“enum”值是整数,而不是“enum”类型的实例。 (2认同)