自从学校里学过c++这门课之后就再也没有用过这门语言,最近因为项目需要才重拾c++,现在回头来看,发现有许多原来觉得很难理解的地方如果从编译的角度去理解会很容易,接下来我们就来看一下c++里的几个概念。
1、模板
大家都知道c++里的模板其实就是一个占位符,告诉编译器这里是在编译时必须用一个具体的类型来代替。而虚函数则不同,它是要在运行期绑定的,因此虚函数是无法使用模板的。
2、完整类型和非完整类型
什么叫做完整类型,什么又叫做非完整类型呢?看下面的例子:
A.hpp
class B;
class A{
public:
void print(B& b);
}
B.hpp
#include "A.hpp"
class B{
public:
void print(A& a);
}
上面这个例子里,A.hpp文件里的B就是非完整类型,而B.hpp里的A就是完整类型。为啥有这种需求呢?如果写过java或者php等其他语言的人知道,如果A类引用了B类,而B类引用了A类,那程序员根本无需担心,编译器会自己完成这种依赖检查,但是编译器是如何完成这种依赖检查的呢?很简单,其实也就是利用了上述这种办法。在解析A时如果遇到B无法解析则先占位,等到B被加载了以后再解析。而c++里这一点必须靠程序员来人肉完成。其实java和php那种结构即使靠程序人肉也无法完成这种需求,如果编译器不支持的话,原因就是java和php不像c++这种支持header和source分离的策略。
我们知道c或者c++里有可以将定义和实现分开,每个编译单元是单独编译,最后再通过链接器来加载的。那么如果要实现A类里引用了B类,并且B类里引用了A类这种需求的话,那么我们就必须使用非完整类型的,即给个占位符。这一点可以从source文件开始理解,原因是编译器其实只认source文件的。
还是上面的例子,我们补完source文件
A.cpp
#include "B.hpp"
#include "A.hpp"
void A::print(B& b)
{
b.print(*this);
}
编译器的起点其实是这个文件,它首先把B.hpp加载进来,那么B.hpp里又要加载A.hpp,而A.hpp里定义了A的实现,因此接下来的编译是成功的。
假如说我们的A.cpp里没有b.print一行,那么我们是否需要include B.hpp呢?理论上是不需要的,因为我们根据不需要B的任何方法,我们只是需要它的一个描述而已,这就是所谓非完整类型,而只有当我们需要调用它里面的方法时,我们才会遇到一个完整类型的需求。