c文件与h文件及包含关系

其实要理解C文件与头文件有什么不同之处,首先需要弄明白编译器的工作过程,一般说来编译器会做以下几个过程: 
1.预处理阶段 
2.词法与语法分析阶段 
3.编译阶段,首先编译成纯汇编语句,再将之汇编成跟CPU相关的二进制码,生成各个目标文件 
4.连接阶段,将各个目标文件中的各段代码进行绝对地址定位,生成跟特定平台相关的可执行文件,当然,最后还可以用objcopy生成纯二进制码,也就是去掉了文件格式信息 

编译器在编译时是以C文件为单位进行的,也就是说如果你的项目中一个C文件都没有,那么你的项目将无法编译,连接器是以目标文件为单位,它将一个或多个目标文件进行函数与变量的重定位,生成最终的可执行文件,在PC上的程序开发,一般都有一个main函数,这是各个编译器的约定,当然,你如果自己写连接器脚本的话,可以不用main函数作为程序入口!!!! 

有了这些基础知识,再言归正传,为了生成一个最终的可执行文件,就需要一些目标文件,也就是需要C文件,而这些C文件中又需要一个main函数作为可执行程序的入口,那么我们就从一个C文件入手,假定这个C文件内容如下: 
#include   <stdio.h> 
#include   "mytest.h " 

int   main(int   argc,char   **argv) 
{ 
  test   =   25; 
  printf( "test.................%d\n ",test); 
} 

头文件内容如下(仅有一行): 
int   test; 

现在以这个例子来讲解编译器的工作: 
1.预处理阶段:编译器以C文件作为一个单元,首先读这个C文件,发现第一句与第二句是包含一个头文件,就会在所有搜索路径中寻找这两个文件,找到之后,就会将相应头文件中再去处理宏,变量,函数声明,嵌套的头文件包含等,检测依赖关系,进行宏替换,看是否有重复定义与声明的情况发生,最后将那些文件中所有的东东全部扫描进这个当前的C文件中,形成一个中间“C文件” 
2.编译阶段,在上一步中相当于将那个头文件中的test变量扫描进了一个中间C文件,那么test变量就变成了这个文件中的一个全局变量,此时就将所有这个中间C文件的所有变量,函数分配空间,将各个函数编译成二进制码,按照特定目标文件格式生成目标文件,在这种格式的目标文件中进行各个全局变量,函数的符号描述,将这些二进制码按照一定的标准组织成一个目标文件 
3.连接阶段,将上一步成生的各个目标文件,根据一些参数,连接生成最终的可执行文件,主要的工作就是重定位各个目标文件的函数,变量等,相当于将个目标文件中的二进制码按一定的规范合到一个文件中 

再回到C文件与头文件各写什么内容的话题上: 
理论上来说C文件与头文件里的内容,只要是C语言所支持的,无论写什么都可以的,比如你在头文件中写函数体,只要在任何一个C文件包含此头文件就可以将这个函数编译成目标文件的一部分(编译是以C文件为单位的,如果不在任何C文件中包含此头文件的话,这段代码就形同虚设),你可以在C文件中进行函数声明,变量声明,结构体声明,这也不成问题!!!那为何一定要分成头文件与C文件呢?又为何一般都在头件中进行函数,变量声明,宏声明,结构体声明呢?而在C文件中去进行变量定义,函数实现呢??原因如下: 
1.如果在头文件中实现一个函数体,那么如果在多个C文件中引用它,而且又同时编译多个C文件,将其生成的目标文件连接成一个可执行文件,在每个引用此头文件的C文件所生成的目标文件中,都有一份这个函数的代码,如果这段函数又没有定义成局部函数,那么在连接时,就会发现多个相同的函数,就会报错 
2.如果在头文件中定义全局变量,并且将此全局变量赋初值,那么在多个引用此头文件的C文件中同样存在相同变量名的拷贝,关键是此变量被赋了初值,所以编译器就会将此变量放入DATA段,最终在连接阶段,会在DATA段中存在多个相同的变量,它无法将这些变量统一成一个变量(仅为此变量分配一个空间,而不是多份空间),假定这个变量在头文件没有赋初值,编译器就会将之放入BSS段,连接器会对BSS段的多个同名变量仅分配一个存储空间 
3.如果在C文件中声明宏,结构体,函数等,那么我要在另一个C文件中引用相应的宏,结构体,就必须再做一次重复的工作,如果我改了一个C文件中的一个声明,那么又忘了改其它C文件中的声明,这不就出了大问题了,程序的逻辑就变成了你不可想象的了,如果把这些公共的东西放在一个头文件中,想用它的C文件就只需要引用一个就OK了!!!这样岂不方便,要改某个声明的时候,只需要动一下头文件就行了 
4.在头文件中声明结构体,函数等,当你需要将你的代码封装成一个库,让别人来用你的代码,你又不想公布源码,那么人家如何利用你的库呢?也就是如何利用你的库中的各个函数呢??一种方法是公布源码,别人想怎么用就怎么用,另一种是提供头文件,别人从头文件中看你的函数原型,这样人家才知道如何调用你写的函数,就如同你调用printf函数一样,里面的参数是怎样的??你是怎么知道的??还不是看人家的头文件中的相关声明啊!!!当然这些东东都成了C标准,就算不看人家的头文件,你一样可以知道怎么使用
 
  包含.c文件 和编译多文件程序 是不同的。
    多文件程序: 是在源文件编译时把多个文件进行编译、连接在一起生成一个可执行文件。    包含.c文件: 按照我们上边的说法则是把多个文件合并为一个文件进行编译。
(1)包含.c文件:
   1: //file1:  main.c 
   2: #include 
   3: #include "fun.c"
   4: int main()
   5: {
   6:     int a=5,b=19;
   7:     c = a;    
   8:     sun(a,b);
   9:     printf("c=%d\n",c);
  10:     return 0;
  11: }
  12: //end of file1
   1: //file2: fun.c
   2: int c=0;
   3: void sun(int a, int b)
   4: {
   5:     printf("a+b=%d\n",a+b);
   6:     c=0;
   7:     printf("c=%d\n",c);
   8: }
   9: //end of file2   
  10:   
这个例子是采用 包含.c文件 的方法实现的。
 
在编译时,直接去编译main.c文件,预处理器会先把fun.c文件中的内容复制到main.c中来,然后再对新的main.c进行编译。
编译命令:
    gcc main.c -o main
可以看到,这里并没有对fun.c进行编译,但还是生成了最终的main可执行程序。
也可以通过命令来观察一下预处理的结果:
编译命令:
   gcc -E main.c -o main.cpp
在main.cpp文件末尾可以看来下面一段代码:
   1: //main.cpp文件中
   2: 931 # 2 "main.c" 2
   3: 932 # 1 "fun.c" 1
   4: 933 //注意这里是fun.c里边的内容
   5: 934 int c=0;
   6: 935 void sun(int a, int b)
   7: 936 {
   8: 937  printf("a+b=%d\n",a+b);
   9: 938  c=0;
  10: 939  printf("c=%d\n",c);
  11: 940 } 
  12:     //这里是main函数
  13: 941 # 3 "main.c" 2
  14: 942 int main()
  15: 943 { 
  16: 944  int a=5,b=19;
  17: 945  c = a;
  18: 946  printf("c=%d\n",c);
  19: 947  sun(a,b);
  20: 948  printf("c=%d\n",c);
  21: 949  return 0;
  22: 950 }
可见,其实就是将fun.c文件中的内容添加到了main函数之前,然后对新的文件进行编译,生成最终的可执行程序。
 
(2)编译多文件程序:
同样是上边的例子,把main.c中“ #include "fun.c" ”注释掉,加上一句:“extern int c;”因为 c 变量在另外一个文件(fun.c)中定义。
   1: //file1:  main.c 
   2: #include 
   3: //#include "fun.c"  //注释掉
   4: extern int c;        //添加这一句
   5: int main()
   6: {
   7:     int a=5,b=19;
   8:     c = a;    
   9:     sun(a,b);
  10:     printf("c=%d\n",c);
  11:     return 0;
  12: }
  13: //end of file1
  14:  
  15:  
  16: //file2: fun.c
  17: int c=0;
  18: void sun(int a, int b)
  19: {
  20:     printf("a+b=%d\n",a+b);
  21:     c=0;
  22:     printf("c=%d\n",c);
  23: }
  24: //end of file2  
 
这次如果还是按照上面的方法只编译main.c的话就会出错,因为变量c和函数sun并没有在main.c中定义,所以编译时需要将fun.c一起编译:
编译命令:   
    gcc -c main.c -o main.o                 #编译main.c
    gcc -c fun.c -o fun.o                       #编译fun.c
    gcc main.o fun.o -o main              #用main.o fun.o生成main

1. 包含.c文件的方法: 容易产生"重定义",大家想想如果一个工程中有多个文件都同时包含了某一个件,那么这个被包含文件的内容就会被复制到多个文件中去,也就相当于每个包含该文件的文件中都定义被包含文件中的变量和函数,这样在链接时就会产生"重定义"错误。
2. 多文件分开编译的方法: 这个比较好,不容易出现"重定义"之类的问题,这也是我们最常用的一种方法,但是并不是像上面这个例子中这样直接去用,而是使用"头文件"将各个.c文件联系起来。
     上边这个例子大家会发现,在main.c中需要加上“extern int c;”这样一句声明,如果包含的文件较多?如果全局变量较多?...这个我们可以省掉吗?回答是肯定的!方法就是给它写上一个头文件。
 
       接下来看一下使用头文件的来实现这个例子的方法:
   1: //file1:  main.c 
   2: #include 
   3: #include "fun.h"       //fun.c修改为fun.h
   4: //extern int c;        //这行也不要了
   5: int main()
   6: {
   7:     int a=5,b=19;
   8:     c = a;    
   9:     sun(a,b);
  10:     printf("c=%d\n",c);
  11:     return 0;
  12: }
  13: //end of file1
 
   1:  
   2: //file2: fun.c
   3: #include "fun.h"
   4: int c=0;                      //变量c的定义
   5: void sun(int a, int b)        //函数sun()的定义
   6: {
   7:     printf("a+b=%d\n",a+b);
   8:     c=0;
   9:     printf("c=%d\n",c);
  10: }
  11: //end of file2  
 
   1: //file3: fun.h 
   2: extern int c;             //把c声明为外部可用的
   3: void sun(int a, int b);   //sun()函数的声明
   4: //end of file3
这样再看一下,在要用到fun.c中定义的函数或变量的文件中只要包含fun.h文件就可以了
预处理时会把fun.h中的内容复制到包含它的文件中去,而复制的这些内容只是声名,不是定义,所以它被复制再多份也不会出现"重定义"的错误。。。
但是一般会用如下方法定义adc.h头文件:
#ifndef __ADC_H 
#define __ADC_H
内容
#endif
        把同一类功能写到一个.c文件中,这样可以把他们划为一个模块,另外再对应的写上一个.h文件做它的声明。这样以后再使用这个模块时只需要把这两个文件添加进工程,同时在要使用模块内函数或变量的文件中包含.h文件就可以了。
         
        举个很实际的例子,在单片机、ARM或其他嵌入式开发中,每一个平台可能本身都有多种不同的硬件模块,使用时需要去写相应的驱动程序,这样就可以把各个硬件模块的驱动程序作为一个模块(比如adc驱动对对应adc.c和adc.h,IIC驱动对应I2C.c和I2C.h等),当具体使用到某个模块时,只需要在将对应的.c和.h文件添加进工程,并在文件中包含对就的.h文件即可。
 
所以关于头文件的写法个人 总结以下几点:
(1) 对应的.c文件中写变量、函数的定义
(2) 对应的.h文件中写变量、函数的声明
(3) 如果有数据类型的定义 和 宏定义 ,请写的头文件(.h)
(4) 头文件中一定加上#ifndef...#define....#endif之类的防止重包含的语句
(5) 模块的.c文件中别忘包含自己的.h文件(原因:adc.h文件声明函数以及一些变量,adc.c文件定义函数,这些函数可能会用到adc.h文件声明的变量,若没用到adc.h中的内容,也可以不包含adc.h文件(已验证))
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值