出处:http://blog.csdn.net/xiaojiegege123456/article/details/25317061
一 .h .c 文件
先来看看这些文件的编译连接过程,然后引出一些具体的问题,如.h文件的相关作用、头文件编写规则、
(1)编译、链接
先来分析下下面这个小例子:
//a.h | //a.c | //main.c |
1 预处理阶段:预处理器看到#include "文件名"就把这个文件读进来,比如它编译main.c,看到#include"a.h",它就把a.h的内容读进来,它知道了,有一类A,包含一个成员函数f,这个函数接受一个int型的参数,返回一个int型的值。
2 编译/汇编阶段:再往下编译很容易就把A a这行读懂了,它知道是要拿A这个类在栈上生成一个对象。再往下,它知道了下面要调用A的成员函数f了,参数是3,由于它知道这个函数要一个整形数用参数,这个3正好匹配,那就正好把它放到栈上,生成一条调用f(int)函数的指令(一般可能是一句call),至于这个f(int)函数到底在哪里,它不知道,它留着空,链接时再解决。该阶段生成a.o和main.o文件
3 链接阶段:链接a.o和main.o文件,即明确了f(int)函数的实现所在的地址,把main.o中空着的这个地址位置填上正确的地址。最终生成了可执行文件main.out。
大致过程如下:
源码 | 预处理 | 编译、汇编 | 链接 |
Main.c a.h a.c | Main.i a.i | Main.sàmain.o a.sàa.o | Main.out |
main.c文件中引用了#include”a.h”,根据C语言的特性,任何变量、函数在使用前都必须先声明,(方法一)包含相应的声明头文件是一种方法,当然若不怕麻烦,(方法二)可以将main中用到的每一个变量、函数都重新声明一遍。
小记:
1 在编译main.c,其包含了头文件#include“a.h“,但是此时根本不需要知道a.c中的实现内容。也就是说各个.c 文件的编译是相互独立的(C语言中一个.c文件对应一个模块)
2 根据方法二,说明.h文件并不是必须的,也就是说并不是每个.c文件都需要一个对应的.h文件,
二 头文件的由来
刚开始并没有.h文件,编译器也不认识头文件,大家的代码都是.c .cpp,但是人们渐渐的发现了这样组织的缺点:1、很多.c(.cpp)文件中的声明语句就是相同的,但他们却不得不一个字一个字地重复地将这些内容敲入每个.c(.cpp)文件。如上面所说的方法二。2、当其中一个声明有变更时,就需要检查所有的.c(.cpp)文件,并修改其中的声明,啊~简直是世界末日降临!
形成.h文件:将重复的部分提取出来,放在一个新文件里,然后在需要的.c(.cpp)文件中敲入#includeXXXX这样的语句。具体叙述见【参考1】、具体的例子演变说明头文件的必要性【参考二】
三 头文件的构成
(1)一般的写法
//mymath.h #ifndef _mymath_H #define _mymath_H extern int Global_A; //声明必要的全局变量 ...... extern void fun(); //声明必要的外部函数 //根据下面的准则5,这里的extern最好不要,因为在顶层作用域中 //函数、变量的默认存储类说明符为extern ..... #endif |
在头文件中声明了全局变量和外部函数都添加了必要这两个字,说明这是提供给别的模块使用的函数即接口,对于那些只在自己本模块中用的函数不必放在头文件中进行声明,只需在实现文件中进行声明即可,见下面的实现代码。
#ifndef、#define、#endif的作用见【文章】。
//mymath.c #include "mymath.h " #include <一些需要使用的C库文件> … ----------------------------------------------------------------- int Global_A ; //定义必要的全局变量和函数 void fun(); … Static int a,b,c; //声明一些内部使用的全局变量及函数 //这里的内部指的是本编译模块如第一小节说的main.c 或者 a.c //既然是本模块的变量和函数,应该声明为static Static void somefun(); ----------------------------------------------------------------- //函数实现体 void fun() { … } Static void somefun() { … } |
在【参考3】中,作者提出对全局变量的定义进行了重新的布局。一个模块与其他模块的数据传递最好通过专有的模块进行,而不要直接通过数据单元直接传递(这是VC++的思想),因此不建议在模块的头文件中声明全局变量;全局变量最好统一定义在一个固定的文件中,所以可以采用下面的方法:
//Globel_Var.c /*******定义本工程中所用到的全局变量*******/ int speed ; int torque ; … … … |
//Globel_Var.H /*******声明本工程中所用到的全局变量*******/ extern int speed; extern int torque; … … |
定义一个Global_Var.C文件来放全局变量,然后在与之相对应的Globel_Var.H文件中来声明全局变量。
这样哪个文件用到这两个变量就可以在该文件的开头处写上文件包含命令;例如aa.C文件要用到speed,toque两个变量,可以在aa.H文件中包含Globel_Var.H文件。(这里不是很明白原作者为什么将Global_Var.H头文件包含在aa.H中,而不是包含在要用到它的实现模块中?求大神帮忙)
(2)头文件类型
1、头文件是为用户提供调用接口,这种头文件中声明了模块中需要给其他模块使用的函数和数据。更多规则见【参考3】
2、说明性头文件,这种头文件不需要有一个对应的代码文件,在这种文件里大多包含了大量的宏定义,没有暴露的数据变量和函数。
(3)一些准则
1头文件中不能有可执行代码,也不能有数据的定义,只能有宏、类型(typedef,struct,union,menu,class),数据和函数的声明。
(1)要么是纯纯的声明 用extern声明并无赋值或者函数体
(2)在程序执行时只有一份数据如const常量、全局静态变量
(3)唯一宗旨保持数据的一次定义规则
#define NAMESTRING “name” typedef unsigned long word; menu{ flag1,flag2};
typedef struct { int x; int y; }Piont;
extent Fun(void); extent int a;
|
2头文件中不能包本地数据(模块自己使用的数据或函数,不被其他模块使用)
这一点相当于面向对象程序设计里的私有成员,即只有模块自己使用的函数,数据,不要用extern在头文件里声明,只有模块自己使用的宏,常量,类型也不要在头文件里声明,应该在自己的*.c文件里声明。
3含一些需要使用的声明。在头文件里声明外部需要使用的数据,函数,宏,类型。
4 防止重复包含
5 在.h文件中声明的函数,如果在其对应的.c文件中有定义,那么我们在声明这个函数时,不使用extern修饰符,如果反之,则必须显示使用extern修饰符.
6 不要在.c文件中使用extern,这是在.h文件中使用的。
四 头文件的作用
(1)简化了声明的书写。
(2)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。
(3)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。
五 结论
拼拼筹筹总算把这个写完,但是大部分的内容都是参考里的博文,感谢他们的分享。
头文件刚开始的主要作用是简化各个模块之间互相引用时声明的方便性,也就是头文件包含的是非本模块要用到的函数接口或变量或类型。再着有了小节四中的其他作用。
头文件的出现于存在与C语言本身的特性也息息相关。因为C语言中不管是变量还是函数,在使用前都需要声明;变量、函数的作用域也十分的重要【文章】
要想写好头文件,需明白一些知识点:1、C语言中声明和定义的区别【文章】 2 哪些声明该写到头文件中 3 头文件在程序模块中的作用
注:
1 extern 应用感觉比较混乱,一个是人为的一些规定,另外编译器默认认为顶层作用域的存储类说明符为extern
2 在C++中class A{};这是一个定义,而classA;这是一个声明。
关于类的声明和定义。
class A; //类的声明
类的声明和普通变量声明一样,不产生目标代码,可以在同一,以及多个编译单元重复声明。
class A {
}; //类的定义
类的定义就特殊一点了,可能会有疑问,为什么不能把int x;这样的变量定义放到.h中(见4)但是可以把
类的定义放在头文件中重复引用呢?同时类的函数非inline定义(写在类定义里面的函数是inline,除外)不能写在
头文件中呢。
这是因为类的定义,只是告诉编译器,类的数据格式是如何的,实例话后对象该占多大空间。
类的定义也不产生目标代码。因此它和普通变量的声明唯一的区别是不能在同一编译单元内出现多次。
//source1.cc
class A;
class A; //类重复声明,OK
class A{
};
class A{
};
class A{
int x;
}; //同一编译单元内,类重复定义,会编译时报错,因为编译器不知道在该编译单元,A a;的话要生产怎样的a.
//如果class A{};定义在head.h ,而head.h 没有
//#ifndef #endif 就很可能在同一编译单元出现类重复定义的编译错误情况。
但是在不同编译单元内,类可以重复定义,因为类的定义未产生实际代码。
//source1.cc
class A{
}
//source2.cc
class A{
} //不同编译单元,类重复定义,OK。所以类的定义可以写在头文件中!
//source1.cc
class A{
}
//source2.cc
class A{
int x;
} //不同编译单元,OK
3 //head.h
int x;
//source1.cc
#include “head.h”
//source2.cc
#include “head.h”
头文件不被编译,.cc中的引用 include “ head.h”其实就是在预编译的时候将head.h中的内容插入到.cc中。
所以上面的例子如果
g++ –o test source1.cc source2.cc, 同样会链时发现重复定义的全局变量x。
因此变量定义,包括函数的定义不要写到头文件中,因为头文件很可能要被多个.cc引用。
那么如果我的head.h如下这么写呢,是否防止了x的链接时重定义出错呢?
//head.h
#ifndef _HEAD_H_
#define _HEAD_H_
int x;
#endif
//source1.cc
#include “head.h”
//source2.cc
#include “head.h”
现在是否g++ –o test source1.cc source2.cc就没有问题了呢,答案是否定的。
所有的头文件都是应该如上加#ifndef #endif的,但它的作用是防止头文件在同一编译单元被重复引用。
就是说防止可能的
//source1.cc
#include “head.h”
#include “head.h”
这种情况,当然我们不会主动写成上面的形式但是,下面的情况很可能发送
//source1.cc
#include “head.h”
#inlcude “a.h”
//a.h
#include “head.h”
这样就在不经意见产生了同一编译单元的头文件重复引用,于是soruc1.cc 就出现了两个int x;定义。
但是对于不同的编译单元source1.cc,source2.cc他们都是还会引用head.h的,即使#ifndef #endif的存在。【参考5】
六 参考资料:
1 http://blog.csdn.net/bm1408/article/details/606382
2 http://blog.csdn.net/janders/article/details/611081
3 http://blog.csdn.net/chenyiming_1990/article/details/9494041
4 http://www.cnblogs.com/wolf-lifeng/p/3156936.html
5 http://www.cnblogs.com/rocketfan/archive/2009/10/02/1577361.html