Skip to content

关于C++前向声明

如果有两个类相互之间发生了引用,可能会产生一些问题。《C++ primer 第5版》中12.1.1节和12.1.6节给出的例子就出现了这种情况。在这两节中,作者定义了两个类StrBlobStrBlobPtr来模仿容器和迭代器的功能。类StrBlob中的begin()end()函数中用到了StrBlobPtr类;类StrBlobPtr的构造函数中用到了StrBlob类。

参考

头文件循环包含

如果两个相互引用的类的声明在不同的头文件,则最有可能出现的问题就是头文件发生了循环包含。

例如,在以下代码中有A、B两个类,他们发生了相互引用,并且声明在不同的头文件中。

//A.h
#pragma once
#include "B.h"

class A{...};

//B.h
#pragma once
#include "A"

class B{...};

这样的代码是无法通过编译的,因为头文件发生了循环包含。当编译器正在编译A.h时,发现A.h中包含了B.h,编译器就去编译B.h,然而B.h中包含了A.h,编译器会认为A.h已经编译过了,但是实际情况是A.h并没有编译完成,因此编译类B中的代码时就会发生错误。

前置声明(Forward Declaration)

前置声明是指,如果要在类A中使用另外一个类B,可以在类A之前给出类B的前置声明。例如

class B;    //前置声明
class A{...};

使用前置声明可以解决循环包含的问题。在上面的例子中,我们可以在头文件A.h中包含B.h,然后在B.h中只给出A的前置声明,而不用包含A.h。

//A.h
#pragma once
#include "B.h"

class A{...};

//B.h
#pragma once

class A;
class B{...};

前置声明的问题

前置声明的问题在于,它只是个类名。在上面的例子中,如果类B中只是出现了类A的引用、指针、友元声明等(即只用到了类B的名字),利用前置声明取代#include是可以的。

但是如果类B中用到了类A中的成员,使用前置声明代替#include就行不通了,编译器会报错(因为编译器找不到这些成员的声明)。通常情况下,只有类中成员函数的函数体内会用到另外一个类的成员,因此最好将函数声明和定义分开,即将声明放在头文件中,定义放在.cpp文件中。

对于inline函数,由于不能将inline函数的声明和定义分别放到两个文件中,因此使用了前置声明后,inline函数内部是不能使用被声明类的成员的,除非取消inline声明,把函数的定义和实现分开。

前置声明的其他问题

网上有些资料说,前置声明有时会造成程序运行时出现错误的结果。所以在google的C++ code style中建议应尽量避免使用前置声明,尽可能使用#include。

总结

编程时应该:(1)尽量避免出现两个类相互引用的情况,(2)尽可能多地使用#include,而不是前置声明,(3)尽可能将函数的声明和定义分离。

如果两个类相互引用的情况不可避免,无论这两个类是否声明在同一个头文件中,都需要使用前置声明以避免循环包含。