IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> UE5 学习笔记【C++语法篇】:头文件间相互包含时的编译出错 -> 正文阅读

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

错误示例

描述

当一个类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的双向访问,不再使用彼此的成员函数指针,而是通过在源文件中动态定义对方类型的指针来实现。

关于作者

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

相关知识参考博客

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

  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2021-12-13 13:11:29  更:2021-12-13 13:11:58 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/27 20:28:50-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码