C/C++编程 头文件与源文件中的内容

从规模较小的程序转到比较复杂的程序,头文件与源文件中的内容组织困扰了很久,特别是头文件中该放哪些内容,到处搜索文章并进行了一次总结,如果有什么错误或者值得商榷的地方,希望大家能够不吝赐教。


引入问题:

编译模式:一个程序的源代码,可以放到不同的文件进行存放,每一个源文件都是独立的,可以分别进行编译,生成程序的时候只需要将各个目标程序进行一次连接便可以了。比如在一个文件中定义了一个函数 void a(),而另外一个文件中只有void a()的声明,如此并不影响把这个文件编译成目标文件,当一个文件中找不到void a()的定义,便把这个函数符号放到符号表里,等到连接的时候再去别的文件中寻找void A()的定义。这种编译模式让我们可以在一个文件中定义函数,那么如果希望使用这个函数,只要在使用前进行声明就可以了。问题来了,如果有很多的函数需要使用,按照前文提到的方法,需要在这个文件声明函数,如果很多文件都需要使用这些函数,总不能重复定义,于是引入了头文件。从这里可以看出,头文件是为声明而生的。

头文件:头文件和源文件的内容都是C++源代码,里面存放的是声明,但是头文件不会被编译,在xxx.cpp编译的预处理过程中,#include<...>会被替换,仅仅就是文本替换,同时有着宏替换,还有清除一些无关代码,例如注释。

/*math.h*/
int Fx(int x);
int Fy(int y);

/*math.cpp*/
int Fx(int x)
{
    return x;
}

int Fy(int y)
{
    return y;
}

/*main.cpp*/
#include "math.h"

int main()
{
    return 0;
}

math.cpp 和 main.cpp 可以分别编译得到 math.o  和 main.o,最后进行连接,便可以得到一个完整的程序。

头文件的内容:头文件里应该只放函数和变量的声明,不能放定义,因为头文件会被多个源文件包含,声明可以有多份,但函数的定义只能有一份,虽然函数定义的内容是相同的,但不符合规范。基于函数和变量的定义只能有一次的支撑,将进行详细的讨论。

1.头文件为声明提供一个集中存放的位置,一般包含类的定义,枚举的定义,extern变量的声明,函数的声明,const对象的定义,inline函数的定义,适当的其它头文件;

2.头文件中只能有声明,不能有定义,存在有几个特例;

(1)const对象:头文件中可以写const对象的定义。因为全局的const对象默 认是没有extern的声明的,所以它只在当前文件中有效。把这样的对象写进头文件中,即使它被包含到其他多个.cpp文件中,这个对象也都只在包含它的 那个文件中有效,对其他文件来说是不可见的,所以便不会导致多重定义。同时,因为这些.cpp文件中的该对象都是从一个头文件中包含进去的,这样也就保证 了这些.cpp文件中的这个const对象的值是相同的,可谓一举两得。同理,static对象的定义也可以放进头文件。在大部分的编译器实现中,编译器都会用相应的常量表达式来替换对这些const变量的使用。所以,在实践中不会有任何存储空间用于存储 用常量表达式初始化的const变量。如果const变量不是用常量表达式初始化的,那么它就不应该在头文件中定义。它应该和其他的变量一样,应该定义在一个源文件中并初始化,在头文件中为它添加extern声明,以便被多个文件共享。(2)inline函数:inline函数是需要编译器在遇到它的地方根据它的定义把它内联展开的,而并非是普通函数那样可以先声明再链接的(内联函数不会链接),所以编译器就需要在编译时看到内联函数的完整定义才行。C++规定,内联函数可以在程序中定义多次,只要内联函数在一个.cpp文件中只出现一次,并且在所有的.cpp文 件中,这个内联函数的定义是一样的,就能通过编译。所以为了避免重复编写,将inline函数写在头文件中。

(3)类的定义:在程序中创建一个类的对象时,编译器只有在这个类的定义完全可见的情况下,才能知道这个类的对象应该如何布局,所以,关于类的 定义的要求,跟内联函数是基本一样的。一般,我们的做法是,把类的定义放在头文件中,而把函数成员的实现代码放在一个.cpp文件中。这是可以的,也是很好的办法。 不过,还有另一种办法。那就是直接把函数成员的实现代码也写进类定义里面。在C++的类中,如果函数成员在类的定义体中被定义,那么编译器会视这个函数为 内联的。因此,把函数成员的定义写进类定义体,一起放进头文件中,是合法的。注意一下,如果把函数成员的定义写在类定义的头文件中,而没有写进类定义中, 这是不合法的,因为这个函数成员此时就不是内联的了。一旦头文件被两个或两个以上的.cpp文件包含,这个函数成员就被重定义了。

(4)枚举类型:枚举类型相当于const对象,可直接放在头文件中。

(5)适当的其它头文件:在这一点上,尽量减少对其它的头文件的依赖,当一个头文件被包含的同时也引入了一项新的依赖(dependency),叧要该头文件被修改,代码就要重新编译。如果你的头文件包含了其他头文件,返些头文件的任何改动也将导致那些包含了你的头文件的代码重新编译。使用前置声明可以显著减少需要包含的头文件数量。

举例说明:头文件中用到类 File,但不需要访问 File的声明,则头文件中需前置声明 class File;无需#include "file.h"。

在头文件如何做到使用类 Foo 而无需访问类的定义:

1) 将数据成员类型声明为 Foo *戒 Foo &;

2) 参数、迒回值类型为 Foo 的函数只是声明(不定义实现);

3) 静态数据成员的类型可以被声明为 Foo,因为静态数据成员的定义在类定义之外。

另一方面,如果你的类是 Foo 的子类,或者含有类型为 Foo 的非静态数据成员,则必须为之包含头文件。有时,使用指针成员(pointer members,如果是 scoped_ptr 更好)替代对象成员(object members)的确更有意义。然而,返样的做法会降低代码可读性及执行效率。如果仅仅为了少包含头文件,还是不要这样替代的好。


补充:

1.在头文件中定义这些实体,是因为编译器需要它们的定义而不只是声明来产生代码。

2.头文件保护:使用#ifnedef ... #define... #endif进行头文件保护,防止重复包含。

3.直接在使用外连接实体的文件中,直接对外连接实体进行一次extern声明(若外连接实体类型为函数,则不用extern关键字)。

4.全局变量、全局函数默认是外连接的。类定义、全局常量、const对象(变量)、typedef类型、宏定义 默认为内连接的。另外,定义时被显示加上static关键字的全局变量和全局函数,具有文件作用域,连接类型也为内连接。默认为内连接的标识符,如果显示加上extern关键字,即变成外连接。


郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。