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

初始化.h:

#ifndef INIT_H
#define INIT_H

#include<stdio.h>
#include<stdlib.h>
#include"farm.h"

#define PIG_SOUND "oink"
#define CALF_SOUND "baa"

enum types {PIG, CALF};

typedef struct resources {
    size_t pork;
    size_t veal;
    size_t lamb;
    size_t milk;
    size_t eggs;
} resources;

typedef struct animal {
    size_t legs;
    char* sound;
    int efficiency;
    void (*exclaim)(struct animal*);
    void (*work)(struct animal*, struct resources*);
} animal;

/* I have tried various ways of declaring structs in addition to
   the typedef such as this */

//animal stock;
//animal farm;

void make_pig(struct animal* a, int perf);
void make_calf(struct animal* a, int perf);

#endif

农场h:

#ifndef FARM_H
#define FARM_H

#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<string.h>
#include"init.h"

/* GCC does not recognise the typedef or struct identifier 
   until these forward declarations have been made in 
   addition to the included init.h header file */

//typedef struct animal animal;
//typedef struct resources resources;

void exclaim(animal* b);
void work(struct animal* b, struct resources* s);

#endif

初始化.c:

#include"init.h"

void make_pig(animal* a, int perf) {
    a->legs = 4;
    a->sound = PIG_SOUND;
    a->efficiency = perf;
    a->exclaim = exclaim;
    a->work = work;
}

void make_calf(animal* a, int perf) {
    a->legs = 4;
    a->sound = CALF_SOUND;
    a->efficiency = perf;
    a->exclaim = exclaim;
    a->work = work;
}

农场c:

#include"farm.h"

int main() {
    return 0;
}

void exclaim(animal* a) {
    for (int i = 0; i < 3; i++) {
        printf("%s ", a->sound);
    }
    printf("\n");
}

void work(animal* a, struct resources* r) {
    if (!strcmp(a->sound, PIG_SOUND)) {
        r->pork += a->efficiency;
    }

    if (!strcmp(a->sound, CALF_SOUND)) {
        r->veal += a->efficiency;
    }
}

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

lib/农场h:10:21: warning: ‘struct ani’ declared inside parameter list will not be visible outside of this definition or declaration
   10 | void exclaim(struct ani* a);
      |                     ^~~
lib/农场h:11:33: warning: ‘struct resources’ declared inside parameter list will not be visible outside of this definition or declaration
   11 | void work(struct ani* a, struct resources* r);

每次使用该窗体的函数指针时,总共有10个警告:

src/初始化.c:17:16: warning: assignment to ‘void (*)(struct ani *)’ from incompatible pointer type ‘void (*)(struct ani *)’ [-Wincompatible-pointer-types]
   17 |     a->exclaim = exclaim;
      |                ^
src/初始化.c:18:13: warning: assignment to ‘void (*)(struct ani *, struct resources *)’ from incompatible pointer type ‘void (*)(struct ani *, struct resources *)’ [-Wincompatible-pointer-types]
   18 |     a->work = work;

有人能解释一下为什么会发生这种行为,以及我如何避免出现问题吗?解决这些错误通常会花费我一段不可行的时间,但我仍然没有真正理解我的错误.

推荐答案

您遇到了C范围规则的一个奇怪的角落.

非正式地说,如果没有可见的声明,当它被命名时,一个标记为struct(或union,但我不会反复重复这一点)就会出现."弹簧开始存在"意味着它被视为已宣布in the current scope.此外,如果一个标记的 struct 以前在一个范围中命名,然后您用相同的标记声明一个 struct ,那么这两个 struct 被认为是相同的.在完成 struct 的声明之前, struct 被视为不完整类型,但指向不完整类型的指针是完整类型,因此可以在实际完成 struct 定义之前声明指向标记 struct 的指针.

大多数时候,这只需要很少的思考就行了.但函数原型有点特殊,因为函数原型本身就是一个范围.(作用域仅持续到函数声明结束.)

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

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

extern struct animal * barnyard;
void exclaim(struct animal*);

然后,struct animal的两个用法指的是相同的 struct 类型,这可能会在以后完成(或在另一个翻译单元中完成).

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

void exclaim(struct animal*);
extern struct animal * barnyard;

(On godbolt, bless it's heart)

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

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

有关已编辑问题的其他 comments ,请参见下文.


我个人的偏好是always使用typedef作为标记的转发声明,never在我的代码中除typedef和后续定义之外的任何地方使用struct foo:

typedef struct Animal Animal;

void exclaim(Animal*);

// ...

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

注意,我总是对标记和typedef使用相同的标识符.为什么不呢?没有混淆,自从C是premordial以来,标记就没有和其他标识符位于同一名称空间中.

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


Note: since this answer was written, the question was edited to add a more detailed code sample. For now, I'll just leave these additional notes here:

总的来说,当你提供更好的信息时,你会得到更好的答案.因为我看不到您的实际代码,所以我尽了最大努力,试图解释发生了什么,让您将其应用到实际代码中.

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

要解决循环依赖关系,请抽象出共享组件并将其放入新的头文件中.这里, struct 的前向声明(例如使用typedef)非常有用,因为它们不依赖于 struct 定义中使用的任何东西.共享头可能只包括typedef,也可能包括不需要其他依赖项的原型.

此外,避免在头文件中放置长的库包含列表;仅包括定义标头中实际使用的类型实际需要的标头.

C++相关问答推荐

为什么PLT表中没有push指令?

在C语言中使用scanf()时我无法理解的警告

为什么可以通过指向常量int的指针间接地改变整数的值?

ZED for SDL上的C语言服务器

难以理解Makefile隐含规则

为什么I2C会发送错误的数据?

C是否用0填充多维数组的其余部分?

C中的指针增量和减量(*--*++p)

关于";*(++p)->;t";、&++p->;t";和&q;++*p->;t";的问题

如何在提取的索引中分配空值?

cairo 剪辑区域是否存在多个矩形?

正数之和是负数

用C++高效解析HTTP请求的方法

具有正确标头的C struct 定义问题

c程序,让用户输入两类数字,并给出输出用户输入多少个数字

哪个首选包含第三个库S头文件?#INCLUDE;文件名或#INCLUDE<;文件名&>?

C中的数组下标和指针算法给出了不同的结果

C 语言中 CORDIC 对数的问题

C/C++编译器可以在编译过程中通过按引用传递来优化按值传递吗?

为什么需要struct in_addr