UE5 学习笔记【C++语法篇】:头文件间相互包含时的编译出错

本文介绍了在C++编程中遇到的头文件循环包含问题,导致编译错误的原因及解决策略。错误源于类A和类B的头文件互相包含,各自定义了对方的指针类型成员,造成类型定义顺序混乱。解决方法包括避免双向包含,改为单向访问,或者使用源文件包含,以及引入第三方代理类。
摘要由CSDN通过智能技术生成

错误示例

描述

当一个类A需要多次访问另一个类B时,习惯性会给类A分配一个B的指针类型的成员变量。

同样,如果类B也需要对A进行多次访问,就在类B中分配一个A的指针类型的成员变量。

代码

类A的头文件 A.h

#pragma once

#ifndef A_H
#define A_H

#include "B.h"

class A {

public:
	A();
	~A();

private:
	B* b;

};

#endif 

类B的头文件 B.h

#pragma once

#ifndef B_H
#define B_H

#include "A.h"

class B{

public:
	B();
	~B();

private:
	A* a;
	
};

#endif

结果

根据上述类型结构和代码编写,经过编译器编译后得到如下报错。

已启动生成…
1>------ 已启动生成: 项目: UE5_CODE_TEST, 配置: Debug x64 ------
1>A.cpp
1>G:\UE5_CODE_TEST\UE5_CODE_TEST\B.h(15,3): error C2143: 语法错误: 缺少“;(在“*”的前面)
1>G:\UE5_CODE_TEST\UE5_CODE_TEST\B.h(15,3): error C4430: 缺少类型说明符 - 假定为 int。注意: C++ 不支持默认 int
1>G:\UE5_CODE_TEST\UE5_CODE_TEST\B.h(15,6): error C2238: 意外的标记位于“;”之前
1>B.cpp
1>G:\UE5_CODE_TEST\UE5_CODE_TEST\A.h(15,3): error C2143: 语法错误: 缺少“;(在“*”的前面)
1>G:\UE5_CODE_TEST\UE5_CODE_TEST\A.h(15,3): error C4430: 缺少类型说明符 - 假定为 int。注意: C++ 不支持默认 int
1>G:\UE5_CODE_TEST\UE5_CODE_TEST\A.h(15,6): error C2238: 意外的标记位于“;”之前
1>Main.cpp
1>G:\UE5_CODE_TEST\UE5_CODE_TEST\B.h(15,3): error C2143: 语法错误: 缺少“;(在“*”的前面)
1>G:\UE5_CODE_TEST\UE5_CODE_TEST\B.h(15,3): error C4430: 缺少类型说明符 - 假定为 int。注意: C++ 不支持默认 int
1>G:\UE5_CODE_TEST\UE5_CODE_TEST\B.h(15,6): error C2238: 意外的标记位于“;”之前
1>正在生成代码...
1>已完成生成项目“UE5_CODE_TEST.vcxproj”的操作 - 失败。
========== 生成: 成功 0 个,失败 1 个,最新 0 个,跳过 0==========

错误原因

结果分析

编译报错中显示,当编译器编译到类B头文件的第15行时,发现A为未被定义的类型,导致指针a声明失败。那么为什么A未被定义呢?

编译过程

在我们写完代码后,当前的代码并不是编译器直接能够编译的。

c++在进行编译之前,会根据我们指定的预处理标识,对代码进行预处理操作,形成可编译的代码,进而送给编译器进行编译。

举个简单的例子: 编写代码的时候,我们通常都会给代码添加注释以便于理解,而在编译的时候机器是不看这些注释的代码的,也就是这些代码对于编译是没有意义的。那么预处理操作就会把这些注释给去掉,留下机器可以识别的代码进行编译。

常见的预处理标识有:#ifndef#define#endif#include……

由这些预处理标识定义了一个又一个的,一个宏对应一片代码段。在代码预处理的时候,这些宏会被替代成对应的代码段。所有的宏都被替代完毕后,便形成了最终用于编译的完整代码

而本文要分析遇到的主要问题,便是典型的 头文件包含(#include) 问题。

预处理代码分析

按照预处理操作的原理,我们首先将类A头文件A.h中的 #include “B.h” 替换成对应的代码段,结果如下:

#pragma once

#ifndef A_H
#define A_H

//#include "B_h"区域头部
#pragma once

#ifndef B_H
#define B_H

#include "A.h"

class B{

public:
	B();
	~B();

private:
	A* a;
	
};

#endif
//#include "B_h"区域尾部

class A {

public:
	A();
	~A();

private:
	B* b;

};

#endif 

然后我们再将 #include "B.h"区域 中的 #include “A.h” 替换为对应的代码段,结果如下:

#pragma once

#ifndef A_H
#define A_H

//#include "B_h"区域头部
#pragma once

#ifndef B_H
#define B_H

//#include "A.h"区域头部
#pragma once

#ifndef A_H
#define A_H

#include "B.h"

class A {

public:
	A();
	~A();

private:
	B* b;

};

#endif
//#include "A.h"区域尾部

class B{

public:
	B();
	~B();

private:
	A* a;
	
};

#endif
//#include "B_h"区域尾部

class A {

public:
	A();
	~A();

private:
	B* b;

};

#endif 

接着,我们根据 #ifndef 等宏定义,对代码需要简化的部分进行注释表示,结果如下:

#pragma once

#ifndef A_H
#define A_H

//#include "B_h"区域头部
#pragma once

#ifndef B_H
#define B_H

//#include "A.h"区域头部
/*#pragma once

#ifndef A_H

#define A_H

#include "B.h"

class A {

public:
	A();
	~A();

private:
	B* b;

};


#endif*/
//该部分由于重复定义类型A而被去除
//#include "A.h"区域尾部

class B{

public:
	B();
	~B();

private:
	A* a;
	
};

#endif
//#include "B_h"区域尾部

class A {

public:
	A();
	~A();

private:
	B* b;

};

#endif 

我们将被注释的代码段进行去除,得到最终简化后的、用于编译的完整代码 (这里为便于演示,将注释留下,实际情况下注释也将去除),如下:

#pragma once

#ifndef A_H
#define A_H

//#include "B_h"区域头部
#pragma once

#ifndef B_H
#define B_H

//#include "A.h"区域头部
//该部分由于重复定义类型A而被去除
//#include "A.h"区域尾部

class B{

public:
	B();
	~B();

private:
	A* a;
	
};

#endif
//#include "B_h"区域尾部

class A {

public:
	A();
	~A();

private:
	B* b;

};

#endif 

现在,我们便可以担当编译器,对上面的代码进行人工编译操作。
编译的方式是 从上到下顺序编译 ,和一般程序的顺序执行一样。

根据代码顺序编译的结构,我们可以看出,类A和类B的定义次序,是 类B在先,类A在后

那么当编译进行到第23行时,需要为类B分配一个A的指针类型的成员变量。而此时还未对类A进行定义,对类A的定义操作还未被执行。因此编译器就会报错,提示类A为不明确的类型。

到这里我们可以知道,报错是因为两个类头文件相互包含,且各自定义了对方类型的成员变量时,编译器编译发现了 类型定义次序的混乱

可供参考的解决方法

  1. 只在一个类中定义访问另一个类的指针。也就是只在类A中定义B的指针类型的成员变量,B中不定义,将A与B的双向访问逻辑,改善成A到B的单项访问逻辑。
  2. 在源文件中包含彼此的头文件。也就是A和B的双向访问,不再使用彼此的成员函数指针,而是通过在源文件中动态定义对方类型的指针来实现。
  3. 第三方代理。定义一个C类,将需要对A类和B类的操作定义为C类的成员函数,这样只需要再C类的头文件中引入A类和B类的头文件,算是目前的最优方法(更新于2022-6-17)。

关于作者

感谢阅读!本文是我作为UE5底层开发初学者的学习笔记,希望对你有所帮助。
当然,内容比较冗长,如有不严谨、不正确的地方,还望多多指正,非常感谢!

相关知识参考博客

1. C++(1):认识include、ifndef和ifdef
2. #pragma once用法总结

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晓晨Viro_Roamer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值