错误示例
描述
当一个类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为不明确的类型。
到这里我们可以知道,报错是因为两个类头文件相互包含,且各自定义了对方类型的成员变量时,编译器编译发现了 类型定义次序的混乱 。
可供参考的解决方法
- 只在一个类中定义访问另一个类的指针。也就是只在类A中定义B的指针类型的成员变量,B中不定义,将A与B的双向访问逻辑,改善成A到B的单项访问逻辑。
- 在源文件中包含彼此的头文件。也就是A和B的双向访问,不再使用彼此的成员函数指针,而是通过在源文件中动态定义对方类型的指针来实现。
- 第三方代理。定义一个C类,将需要对A类和B类的操作定义为C类的成员函数,这样只需要再C类的头文件中引入A类和B类的头文件,算是目前的最优方法(更新于2022-6-17)。
关于作者
感谢阅读!本文是我作为UE5底层开发初学者的学习笔记,希望对你有所帮助。
当然,内容比较冗长,如有不严谨、不正确的地方,还望多多指正,非常感谢!