【问题标题】:What is a circular include dependency, why is it bad and how do I fix it?什么是循环包含依赖项,为什么它不好以及如何修复它?
【发布时间】:2022-01-17 20:06:39
【问题描述】:

假设我有两个相互引用的数据结构。我想将它们放入单独的头文件中,如下所示:

 // datastruct1.h
 #ifndef DATA_STRUCT_ONE
 #define DATA_STRUCT_ONE

 #include <datastruct2.h>
 typedef struct DataStructOne_t
 {
   DataStructTwo* two;
 } DataStructOne;
 #endif

 // datastruct2.h
 #ifndef DATA_STRUCT_TWO
 #define DATA_STRUCT_TWO

 #include <datastruct1.h>
 typedef struct DataStructTwo_t
 {
   DataStructOne* one;
 } DataStructTwo;

 #endif

我有一个main 函数:

 #include <datastruct1.h>
 #include <datastruct2.h>

 int main() 
 {
    DataStructOne* one;
    DataStructTwo* two;
 }

但是我的编译器抱怨:

$ gcc -I. -c main.c
In file included from ./datastruct1.h:4,
                 from main.c:1:
./datastruct2.h:8:2: error: unknown type name ‘DataStructOne’
    8 |  DataStructOne* one;
      |  ^~~~~~~~~~~~~

这是为什么呢?我该怎么做才能解决这个问题?

【问题讨论】:

  • 在我的一生中,我找不到一个很好的问答对,从零开始解释这个常见问题,所以我自己写了。如果已经有一个,请随意关闭作为骗子。
  • 不是骗子,但有点相关stackoverflow.com/questions/4757565/…
  • @463035818_is_not_a_number 有许多相关的问题和答案可以部分解释问题。我一直在寻找整个墨西哥卷饼,

标签: c++ c include


【解决方案1】:

为什么?

为了理解为什么,我们需要像编译器一样思考。让我们在逐行分析main.c 时这样做。编译器会做什么?

  • #include &lt;datastruct1.h&gt;:将“main.c”放在一边(推入正在处理的文件堆栈)并切换到“datastruct1.h”
  • #ifndef DATA_STRUCT_ONE:嗯,这个没定义,继续吧。
  • #define DATA_STRUCT_ONE:好的,定义好了!
  • #include &lt;datastruct2.h&gt;:把“datastruct1.h”放到一边,切换到“datastruct2.h”
  • #ifndef DATA_STRUCT_TWO:嗯,这个没定义,继续吧。
  • #define DATA_STRUCT_TWO:好的,定义好了!
  • #include &lt;datastruct1.h&gt;:把“datastruct2.h”放到一边,切换到“datastruct1.h”
  • #ifndef DATA_STRUCT_ONE:现在已定义,所以直接转到#endif
  • (end of "datastruct1.h"):关闭“datastruct1.h”并从填充堆栈中弹出当前文件。我在做什么?啊,“datastruct2.h”。让我们从离开的地方继续。
  • typedef struct DataStructTwo_t好的,开始定义结构体
  • DataStructOne* one; 等等,DataStructOne 是什么? 我们没看到? (查找已处理行的列表)不,看不到DataStructOne。恐慌!

发生了什么?为了编译“datastruct2.h”,编译器需要“datastruct1.h”,但“datastruct1.h”中的#include 守卫阻止其内容实际包含在需要的地方。

情况是对称的,所以如果我们在“main.c”中切换#include指令的顺序,我们会得到相同的结果,两个文件的角色互换。我们也不能移除保护,因为这会导致文件包含无限链。

看来我们需要“datastruct2.h”出现在“datastruct1.h”之前并且我们需要“datastruct1.h”出现在“datastruct2.h”之前。这似乎不可能。

什么?

文件 A #includes 文件 B 反过来 #includes 文件 A 的情况显然是不可接受的。我们需要打破恶性循环。

幸运的是,C 和 C++ 有 前向声明。我们可以使用这个语言特性来重写我们的头文件:

 #ifndef DATA_STRUCT_ONE
 #define DATA_STRUCT_ONE

 // No, do not #include <datastruct2.h>
 struct DataStructTwo_t; // this is forward declaration

 typedef struct DataStructOne_t
 {
   struct DataStructTwo_t* two;
 } DataStructOne;
 #endif

在这种情况下我们可以用同样的方式重写“datastruct2.h”,消除它对“datastruct1.h”的依赖,在两个处打破循环(严格来说,这是不需要的,但更少的依赖总是好的)。唉。这并非总是如此。通常只有一种方法可以引入前向声明并打破循环。对于 ecample,如果,而不是

DataStructOne* one;

我们有

DataStructOne one; // no pointer

那么前向声明在这个地方就不起作用了。

如果我不能使用前向声明怎么办?

那么你有一个设计问题。例如,如果 both DataStructOne* one;DataStructTwo* two; 你有 DataStructOne one;DataStructTwo two;,那么这个数据结构在 C 或 C++ 中是不可实现的。您需要将其中一个字段更改为指针(在 C++ 中:智能指针),或者完全消除它。

【讨论】:

  • 在 C++ 中,可以通过使用模板(可能带有未使用的类型参数)来规避这一点。
【解决方案2】:

解决循环依赖并在可能的情况下仍使用typedefs 的一种方法是将标头分成两部分,每个部分都有一个单独的保护宏。第一部分为不完整的struct 类型提供typedefs,第二部分完成struct 类型的声明。

例如:-

datastruct1.h

#ifndef DATA_STRUCT_ONE_PRIV
#define DATA_STRUCT_ONE_PRIV
/* First part of datastruct1.h. */

typedef struct DataStructOne_t DataStructOne;

#endif

#ifndef DATA_STRUCT_ONE
#define DATA_STRUCT_ONE
/* Second part of datastruct1.h */

#include <datastruct2.h>

struct DataStructOne_t
{
    DataStructTwo *two;
};

#endif

datastruct2.h

#ifndef DATA_STRUCT_TWO_PRIV
#define DATA_STRUCT_TWO_PRIV
/* First part of datastruct2.h. */

typedef struct DataStructTwo_t DataStructTwo;

#endif

#ifndef DATA_STRUCT_TWO
#define DATA_STRUCT_TWO
/* Second part of datastruct2.h */

#include <datastruct1.h>

struct DataStructTwo_t
{
    DataStructOne *one;
};

#endif

ma​​in.c

/*
 * You can reverse the order of these #includes or omit one of them
 * if you want.
 */
#include <datastruct1.h>
#include <datastruct2.h>

int main(void)
{
    DataStructOne *one;
    DataStructTwo *two;
}

正如上面 ma​​in.c 中的评论所述,只需要包含一个标头,因为无论如何都会间接包含另一个标头。

【讨论】:

    【解决方案3】:

    好吧,如果两个结构相互引用,很明显它们一定是相关的。您可以做的最好的事情是将两者放在一个包含文件中(因为它们是相关的)但将它们放在不同的位置并使编译器从一个包含另一个读取将使编译器读取主文件... .from主文件开始读取包含 A 直到它到达包含 B 的点,然后再次开始读取 B 到包含 A 的点(我们将以没有结束的递归方式再次开始读取 A)你将永远不会停止阅读每个文件,更糟糕的是,第二次看到相同的结构定义时会出现错误(因为它之前已经定义过)

    为了允许用户毫无问题地包含任何或两个文件,当遇到包含文件 A 时进行定义:

    文件 A.h

    #ifndef INCLUDE_FILE_A
    #define INCLUDE_FILE_A
    /* ... the whole stuff of file A with the proper includes of other files.*/
    #include "B.h"
    #endif /* INCLUDE_FILE_A */
    

    文件 B.h

    #ifndef INCLUDE_FILE_B
    #define INCLUDE_FILE_B
    /* ... the whole stuff of file B with the proper includes of other files.*/
    #include "A.h"
    #endif /* INCLUDE_FILE_B */
    

    因此,文件 A 中的定义仅在之前未包含 INCLUDE_FILE_A 时使用,如果已包含文件 A,则跳过它们(当然,对于 B.h 也是如此)。

    如果您在文件 B 上进行相同操作(但使用 INCLUDE_FILE_B),那么您将确保这两个文件将以任一顺序包含在内(取决于您在第一种情况下的操作方式)并且永远不会包含在内再次(使包含安全返回到主文件。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-11-20
      • 1970-01-01
      相关资源
      最近更新 更多