/* ~main.cpp */
double f1();
double f2(double);
main()
int number1 = f1();
int number2 = f2(number1);
/* end of ~main.cpp */
不多不少,刚刚好。同理可知,如果我们除了 main.cpp
以外,还有其他的很多 .cpp
文件也用到了 f1 和 f2 函数的话,那么它们也通通只需要在使用这两个函数前写上一句 #include "math.h"
就行了。
头文件中应该写什么
通过上面的讨论,我们可以了解到,头文件的作用就是被其他的 .cpp
包含进去的。它们本身并不参与编译,但实际上,它们的内容却在多个 .cpp
文件中得到了编译。通过”定义只能有一次“的规则,我们很容易可以得出,头文件中应该只放变量和函数的声明,而不能放它们的定义。因为一个头文件的内容实际上是会被引入到多个不同的 .cpp 文件中的,并且它们都会被编译。放声明当然没事,如果放了定义,那么也就相当于在多个文件中出现了对于一个符号(变量或函数)的定义,纵然这些定义都是相同的,但对于编译器来说,这样做不合法。
所以,应该记住的一点就是,.h
头文件中,只能存在变量或者函数的声明,而不要放定义。即,只能在头文件中写形如:extern int a
; 和 void f()
; 的句子。这些才是声明。如果写上 int a
;或者 void f() {}
这样的句子,那么一旦这个头文件被两个或两个以上的 .cpp 文件包含的话,编译器会立马报错。
当然, 还有几个特殊的情况
const
与static
形式的变量
头文件中可以写 const
对象的定义。因为全局的 const
对象默认是没有 extern
的声明的,所以它只在当前文件中有效。把这样的对象写进头文件中,即使它被包含到其他多个 .cpp
文件中,这个对象也都只在包含它的那个文件中有效,对其他文件来说是不可见的,所以便不会导致多重定义。
同时,因为这些 .cpp
文件中的该对象都是从一个头文件中包含进去的,这样也就保证了这些 .cpp
文件中的这个 const
对象的值是相同的,可谓一举两得。
同理,static
对象的定义也可以放进头文件。
内联函数(inline)
头文件中可以写内联函数(inline)的定义。
因为inline函数是需要编译器在遇到它的地方根据它的定义把它内联展开的,而并非是普通函数那样可以先声明再链接的(内联函数不会链接),所以编译器就需要在编译时看到内联函数的完整定义才行。
如果内联函数像普通函数一样只能定义一次的话,这事儿就难办了。因为在一个文件中还好,我可以把内联函数的定义写在最开始,这样可以保证后面使用的时候都可以见到定义;但是,如果我在其他的文件中还使用到了这个函数那怎么办呢?这几乎没什么太好的解决办法,因此 C++ 规定,内联函数可以在程序中定义多次,只要内联函数在一个 .cpp 文件中只出现一次,并且在所有的 .cpp 文件中,这个内联函数的定义是一样的,就能通过编译。那么显然,把内联函数的定义放进一个头文件中是非常明智的做法。
头文件中可以写类(class)的定义。
因为在程序中创建一个类的对象时,编译器只有在这个类的定义完全可见的情况下,才能知道这个类的对象应该如何布局,所以,关于类的定义的要求,跟内联函数是基本一样的。所以把类的定义放进头文件,在使用到这个类的 .cpp
文件中去包含这个头文件,是一个很好的做法。
在这里,值得一提的是,类的定义中包含着数据成员和函数成员。数据成员是要等到具体的对象被创建时才会被定义(分配空间),但函数成员却是需要在一开始就被定义的,这也就是我们通常所说的类的实现。
一般,我们的做法是,把类的定义放在头文件中,而把函数成员与数据成员的实现代码放在一个 .cpp
文件中。这是可以的,也是很好的办法。
不过,还有另一种办法。那就是直接把函数成员的实现代码也写进类定义里面。在 C++ 的类中,如果函数成员在类的定义体中被定义,那么编译器会视这个函数为内联的。因此,把函数成员的定义写进类定义体,一起放进头文件中,是合法的。
注意一下,如果把函数成员的定义写在类定义的头文件中,而没有写进类定义中,这是不合法的,因为这个函数成员此时就不是内联的了。一旦头文件被两个或两个以上的 .cpp 文件包含,这个函数成员就被重定义了。
模板的声明和定义都放置在同一个.h文件中。
标准要求编译器在实例化模板时必须在上下文中可以查看到其定义实体;而反过来,在看到实例化模板之前,编译器对模板的定义体是不处理的——原因很简单,编译器怎么会预先知道 typename
实参是什么呢?因此模板的实例化与定义体必须放到同一翻译单元中。 —— [ 任何时候都适用的20个C++技巧 ]
模板定义很特殊。由 template<…>
处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉指定模板的多重定义。所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。 —— [ 《C++编程思想》第15章(第300页)]
对C++编译器而言,当调用函数的时候,编译器只需要看到函数的声明。当定义类类型的对象时,编译器只需要知道类的定义,而不需要知道类的实现代码。因此,因该将类的定义和函数声明放在头文件中,而普通函数和类成员函数的定义放在源文件中。
但在处理模板函数和类模板时,问题发生了变化。要进行实例化模板函数和类模板,要求编译器在实例化模板时必须在上下文中可以查看到其定义实体;而反过来,在看到实例化模板之前,编译器对模板的定义体是不处理的——原因很简单,编译器怎么会预先知道 typename 实参是什么呢?因此模板的实例化与定义体必须放到同一翻译单元中。 —— [类模板和模板函数连接出错处理]
头文件中的保护措施
考虑一下,如果头文件中只包含声明语句的话,它被同一个 .cpp
文件包含再多次都没问题——因为声明语句的出现是不受限制的。然而,上面讨论到的头文件中的三个例外也是头文件很常用的一个用处。那么,一旦一个头文件中出现了上面三个例外中的任何一个,它再被一个 .cpp
包含多次的话,问题就大了。因为这三个例外中的语法元素虽然”可以定义在多个源文件中”,但是”在一个源文件中只能出现一次”。设想一下,如果 a.h 中含有类 A 的定义,b.h 中含有类 B 的定义,由于类B的定义依赖了类 A,所以 b.h 中也 #include了a.h。现在有一个源文件,它同时用到了类A和类B,于是程序员在这个源文件中既把 a.h 包含进来了,也把 b.h 包含进来了。这时,问题就来了:类A的定义在这个源文件中出现了两次!于是整个程序就不能通过编译了。你也许会认为这是程序员的失误——他应该知道 b.h 包含了 a.h ——但事实上他不应该知道。
使用 “#define
” 配合条件编译可以很好地解决这个问题。在一个头文件中,通过 #define 定义一个名字,并且通过条件编译 #ifndef...#endif
使得编译器可以根据这个名字是否被定义,再决定要不要继续编译该头文中后续的内容。这个方法虽然简单,但是写头文件时一定记得写进去。
#ifndef _XXXXXX_
#define _XXXXXX_
#endif
源文件如何根据 #include
来关联头文件
1、系统自带的头文件用尖括号括起来,这样编译器会在系统文件目录下查找。
2、用户自定义的文件用双引号括起来,编译器首先会在用户目录下查找,然后在到 C++ 安装目录(比如 VC 中可以指定和修改库文件查找路径,Unix 和 Linux 中可以通过环境变量来设定)中查找,最后在系统文件中查找。
头文件如何来关联源文件
这个问题实际上是说,已知头文件 a.h
声明了一系列函数,b.cpp
中实现了这些函数,那么如果我想在 c.cpp
中使用 a.h
中声明的这些在 b.cpp
中实现的函数,通常都是在 c.cpp
中使用 #include "a.h"
,那么 c.cpp
是怎样找到 b.cpp
中的实现呢?
其实 .cpp
和 .h
文件名称没有任何直接关系,很多编译器都可以接受其他扩展名。
编译的时候,并不会去找 b.cpp
文件中的函数实现,只有在 link 的时候才进行这个工作。我们在 b.cpp
或 c.cpp
中用 #include "a.h"
实际上是引入相关声明,使得编译可以通过,程序并不关心实现是在哪里,是怎么实现的。源文件编译后成生了目标文件(.o 或 .obj 文件),目标文件中,这些函数和变量就视作一个个符号。在 link 的时候,需要在 makefile 里面说明需要连接哪个 .o 或 .obj 文件(在这里是 b.cpp 生成的 .o 或 .obj 文件),此时,连接器会去这个 .o 或 .obj 文件中找在 b.cpp
中实现的函数,再把他们 build 到 makefile 中指定的那个可以执行文件中。
头文件内容总结
类成员数据的声明,但不能赋值
类静态数据成员的定义和赋值,但不建议,只是个声明就好。
类的成员函数的声明
非类成员函数的声明
常数的定义:如:constint a=5;
静态函数的定义
类的内联函数的定义
模板的声明与定义
所有非静态变量(不是类的数据成员)的声明
默认命名空间声明不要放在头文件,using namespace std
等应放在.cpp
中,在 .h
文件中使用 std::string