什么是循环包含依赖项,为什么它不好,以及如何修复它



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

// 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;
|  ^~~~~~~~~~~~~

为什么?我能做些什么来解决这个问题?

为什么

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

  • #include <datastruct1.h>:Put;main.c";靠边(推到正在处理的文件堆栈)并切换到"打开";datastruct1.h">
  • #ifndef DATA_STRUCT_ONE:嗯,这还没有定义,让我们继续
  • #define DATA_STRUCT_ONE:好的,定义好了
  • CCD_ 6:;datastruct1.h";靠边并切换到"关";datastruct2.h">
  • #ifndef DATA_STRUCT_TWO:嗯,这还没有定义,让我们继续
  • #define DATA_STRUCT_TWO:好的,定义好了
  • CCD_ 9:;datastruct2.h";靠边并切换到"关";datastruct1.h">
  • #ifndef DATA_STRUCT_ONE:现在已经定义了,所以直接转到#endif
  • CCD_ 12:关闭";datastruct1.h";并从填充堆栈中弹出当前文件。我在做什么?啊,";数据结构2.h";。让我们从我们离开的地方继续
  • typedef struct DataStructTwo_t好,开始结构定义
  • DataStructOne* one;等等,什么是DataStructOne我们还没看到?(查找已处理行的列表)没有,看不到DataStructOne。惊慌失措

发生了什么?为了编译";datastruct2.h";,编译器需要";datastruct1.h";,但是CCD_ 17在其中进行防护";datastruct1.h";防止其内容实际包含在需要的地方。

这种情况是对称的,所以如果我们在"0"中切换#include指令的顺序;main.c";,当两个文件的角色颠倒时,我们得到了相同的结果。我们也不能删除保护,因为这会导致文件包含的无限链。

看来我们需要";datastruct2.h";出现在";datastruct1.h">我们需要";datastruct1.h";出现在";数据结构2.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";,在的两个位置打破循环(严格来说,这是不需要的,但减少依赖总是好的)。唉。情况并非总是如此。通常只有一种方法可以引入正向声明并打破循环。例如,如果,而不是

DataStructOne* one;

我们有

DataStructOne one; // no pointer

那么前瞻性声明在这个地方是行不通的。

如果我不能在任何码头使用远期申报怎么办

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

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

例如:-

数据结构1.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

数据结构2.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

main.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;
}

正如上面main.c中的注释中所提到的,只需要包括其中一个标头,因为另一个标头无论如何都会间接包括在内。

好吧,如果两个结构相互引用,那么很明显它们一定是相关的。你能做的最好的事情是把两者都放在一个包含文件中(因为它们是相关的),但把它们放在不同的文件中,让编译器从一个包含读取另一个包含,会让编译器读取主文件。从主文件开始读取include A,直到它到达include B的点,然后再次开始读取B到include 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),那么你将确保两个文件都将按任意顺序包含(取决于你在第一种情况下的操作方式),并且永远不会再包含(确保包含安全返回到主文件。

相关内容

  • 没有找到相关文章

最新更新