C++ 编译链接杂谈——前向声明
在我之前的文章《C++ 编译链接杂谈》中介绍了为什么对同一个函数/变量的声明可以在多个文件中多次出现,让人觉得 #include 头文件是一件比较随意的事情,不容易出问题。事实上,在 .cpp 中 #include 头文件确实比较随意,但是在 .hpp 中 #include 头文件还是需要考虑一些问题,本篇就讨论这些问题和解决这些问题的办法。
头文件循环引用
我要说的问题是头文件循环引用。我在最近编程的过程中,在针对一个类编写头文件时,如果用到了其他的类,我就不经思考地把那个类的头文件 #include 进来。一开始这么做没什么问题,后来我设计了两个类,他们的声明中互相用到了对方。在我依然 #include 对方的头文件后,就出了头文件循环引用的问题。
// coroutine.h#include "connection.h"// connection.h#include "coroutine.h"
无限递归的可能性
由于编译器会在预处理过程中将 #include 替换为头文件文本,如果你没有用 #ifndef 的方式避免重复 #include 同一个头文件,那么上面的头文件循环引用情形就会导致编译器永远替换不完 #include。
类型未定义的错误
如果你用 #ifndef 避免了重复 #include 的错误,依然有问题。比如但看 coroutine.h 头文件,当预处理时吧 connection.h 头文件替换进来后,conn.h 内部用到了 coroutine 类型,而该类型的定义在替换进来的头文件下面。这就引起了类型未定义的错误。
前向声明
// coroutine.hclass connection; // 这条语句就是前向声明
如果我们不 #include 其他头文件,而只是用前向声明的方式声明那个被依赖的类,就解开了互相依赖的循环。但前向声明也有局限,它不是完整定义,使用前向声明的头文件只能使用这个类的引用、指针,因为它(编译它的编译器)不知道这个类的大小、具体方法、属性等。
编写头文件时的讲究
上述问题提醒我们,在编写头文件时,首先最好不要有循环依赖的场景。如果非要有最好也是以指针的形式依赖,也不要随意 #include 其他头文件。换个角度,如果当你在 .cpp 文件中引入一个 .h 文件后,你发现你不知道有哪些 .h 文件被包含在了这个 .h 文件中,是不是也不太爽。是不是对这个程序都引用了哪些功能失去了掌控?所以一个头文件还是专注于自己的声明,不要引入其他乱七八糟的声明为好。