一、C++ 编译模式
通常,在一个 C++ 程序中,只包含两类文件―― .cpp 文件和 .h 文件。其中,.cpp 文件被称作 C++ 源文件,里面放的都是 C++ 的源代码;而 .h 文件则被称作 C++ 头文件,里面放的也是 C++ 的源代码。
C++ 语言支持" 分别编译 "(separatecompilation)。也就是说,一个程序所有的内容,可以分成不同的部分分别放在不同的 .cpp 文件里。.cpp 文件里的东西都是相对独立的,在编译(compile)时不需要与其他文件互通,只需要在编译成目标文件后再与其他的目标文件做一次链接(link)就行了。比如,在文件 a.cpp 中定义了一个全局函数 "void a(){}",而在文件 b.cpp 中需要调用这个函数。即使这样,文件 a.cpp 和文件 b.cpp 并不需要相互知道对方的存在,而是可以分别地对它们进行编译,编译成目标文件之后再链接,整个程序就可以运行了。
这是怎么实现的呢?从写程序的角度来讲,很简单。在文件 b.cpp 中,在调用 "void a()" 函数之前,先声明一下这个函数 "void a();",就可以了。这是因为编译器在编译 b.cpp 的时候会生成一个 符号表 (symbol table),像 "void a()" 这样的看不到定义的符号,就会被存放在这个表中。再进行链接的时候,编译器就会在别的目标文件中去寻找这个符号的定义。一旦找到了,程序也就可以顺利地生成了。
注意这里提到了两个概念,一个是"定义",一个是"声明"。简单地说,"定义"就是把一个符号完完整整地描述出来:它是变量还是函数,返回什么类型,需要什么参数等等。而"声明"则只是声明这个符号的存在,即告诉编译器,这个符号是在其他文件中定义的,我这里先用着,你链接的时候再到别的地方去找找看它到底是什么吧。定义的时候要按 C++ 语法完整地定义一个符号(变量或者函数),而声明的时候就只需要写出这个符号的原型了。需要注意的是, 一个符号,在整个程序中可以被声明多次,但却要且仅要被定义一次 。试想,如果一个符号出现了两种不同的定义,编译器该听谁的?
这种机制给 C++ 程序员们带来了很多好处,同时也引出了一种编写程序的方法。考虑一下,如果有一个很常用的函数 "void f() {}",在整个程序中的许多 .cpp 文件中都会被调用,那么,我们就只需要在一个文件中定义这个函数,而在其他的文件中声明这个函数就可以了。一个函数还好对付,声明起来也就一句话。但是,如果函数多了,比如是一大堆的数学函数,有好几百个,那怎么办?能保证每个程序员都可以完完全全地把所有函数的形式都准确地记下来并写出来吗?
二、什么是头文件
很显然,答案是不可能。但是有一个很简单地办法,可以帮助程序员们省去记住那么多函数原型的麻烦:我们可以把那几百个函数的声明语句全都先写好,放在一个文件里,等到程序员需要它们的时候,就把这些东西全部 copy 进他的源代码中。
这个方法固然可行,但还是太麻烦,而且还显得很笨拙。于是,头文件便可以发挥它的作用了。所谓的头文件,其实它的内容跟 .cpp 文件中的内容是一样的,都是 C++ 的源代码。但 头文件不用被编译 。我们把所有的函数声明全部放进一个头文件中,当某一个 .cpp 源文件需要它们时,它们就可以通过一个宏命令 "#include" 包含进这个 .cpp 文件中,从而把它们的内容合并到 .cpp 文件中去。当 .cpp 文件被编译时,这些被包含进去的 .h 文件的作用便发挥了。
举一个例子吧,假设所有的数学函数只有两个:f1 和 f2,那么我们把它们的定义放在 math.cpp 里: