嵌入式C语言头文件的建立与使用

时间:2022-07-02 18:08:14

  如何正确编写 C 语言头文件和与之相关联的 c 源程序文件,这首先就要了解它们的各自功能。 要理解 C 文件与头文件(即.h)有什么不同之处,首先需要弄明白编译器的工作过程。
  一般说来编译器会做以下几个过程:
  1、预处理阶段
  2、编译阶段,首先编译成纯汇编语句,再将之汇编成跟 CPU 相关的二进制码,生成各个目标文件 (.obj文件)
  3、连接阶段,将各个目标文件中的各段代码进行绝对地址定位,生成跟特定平台相关的可执行文件。
  编译器在编译时是以C 文件为单位进行的,也就是说如果你的项目中一个 C 文件都没有,那么你的项目将无法编译,连接器是以目标文件为单位,它将一个或多个目标文件进行函数与变量的重定位,生成最终的可执行文件。
  为了生成一个最终的可执行文件,就需要一些目标文件,也就是首先要有 C 文件,而这些C 文件中又需要一个 main() 函数作为可执行程序的入口,那么我们就从从这一个C 文件入手, 引入头文件概念。
  假定这个C 文件内容如下:

#include <stdio.h>
#include "mytest.h"

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

  头文件"mytest.h"包含如下内容:

int test;

  现在以这个例子来讲解编译器的工作:

  1、预处理阶段:编译器以 C 文件作为一个单元,首先读这个 C 文件,发现第一句与第二句是包含一个头文件,就会在所有搜索路径中寻找这两个文件,找到之后,就会将相应头文件中的宏,变量,函数声明,嵌套的头文件包含等进行依赖关系检测,并进行宏替换,看是否有重复声明与定义的情况发生,最后将那些文件中所有的东东全部扫描进这个当前的 C 文件中,形成一个中间"C文件"。

  2、编译阶段,在上一步中相当于将第二个头文件中的 test变量扫描进了一个中间 C 文件,那么 test 变量就变成了这个文件中的一个全局变量,此时就将所有这个中间 C 文件的所有变量,函数分配空间,将各个函数编译成二进制码,按照特定目标文件格式生成目标文件,在这种格式的目标文件中进行各个全局变量,函数的符号描述,将这些二进制码按照一定的标准组织成一个目标文件。

  3、连接阶段,将上一步成生的各个目标文件,根据一些参数,连接生成最终的可执行文件,主要的工作就是重定位各个目标文件的函数,变量等,相当于将个目标文件中的二进制码按一定的规范合到一个文件中。

  再回到 C 文件与头文件各写什么内容的话题上:一般都在头文件中进行函数声明,变量声明,宏声明,结构体声明呢?而在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 文件就只需要引用一个头文件就行了, 要改某个声明的时候,只需要动一下头文件就行了这样岂不方便。

  再说头文件, 头文件是一种文本文件,使用文本编辑器将代码编写好之后,以扩展名.h 保存就行了。 如上所述头文件中一般放一些重复使用的代码,例如函数声明,变量声明,常数定义 ,宏的定义等等。
  在实际编程中,我们在需调用该 c 文件相对应的头文件用#include 语句将头文件包含进来引用时, 也就是相当于将头文件中所有内容复制到#include 处。
为了避免因为重复引用而导致的编译错误,头文件常具有

#ifndef LABEL
#define LABEL
……….. //代码部分
#endif

的格式。其中, LABEL 为一个唯一的标号,命名规则跟变量的命名规则一样。常根据它所在的头文件名来命名,例如,如果头文件的文件名叫做 hardware.h , 那么可以这样使用:

#ifndef __HARDWARE_H__
#define __HARDWARE_H__
…..... //代码部分
#endif

  这样写的意思就是,如果没有定义__HARDWARE_H__ ,则定义__HARDWARE_H__ ,并编译下面的代码部分,直到遇到#endif。这样,当重复引用时,由于__HARDWARE_H__ 已经被定义,则下面的代码部分就不会被编译了,这样就避免了重复定义。

  另外一个地方就是使用#include 时,使用引号与尖括号的意思是不一样的。 使用引号(“”)时,首先搜索工程文件所在目录,然后再搜索编译器头文件所在目录。而使用尖括号 (<>) 时,刚好是相反的搜索顺序。 假设我们有两个文件名一样的头文件 hardware.h ,但内容却是不一样的。一个保存在编译器指定的头文件目录下,我们把它叫做文件 I ;另一个则保存在当前工程的目录下,我们把它叫做文件 II 。如果我们使用的是# i nclude <hardware.h>,则我们引用到的是文件 I。如果我们使用的是#include “hardware.h” ,则我们引用的将是文件 II 。

  其实头文件对计算机而言没什么作用,她只是在预编译时#include 的地方展开一下, 其它就没别的意义了,不管是 C 还是 C++,你把你的函数,变量或者结构体,类啥的放在你的.c 文件里。然后编 lib,dll,obj等等,但对于我们程序员而言, 他们怎么知道你的 lib,dll...里面到底有什么东西? 这就要看你的头文件。你的头文件就是对用户的说明。函数,参数,各种各样的接口的说明。那既然是说明,那么头文件里面放的自然就是关于函数,变量,类的“ 声明”了。记着,是“ 声明” ,不是“ 定义” 。那么,我假设大家知道声明和定义的区别。所以,最好不要傻嘻嘻的在头文件里定义什么东西。
比如全局变量:

#ifndef _XX_头文件.H
#define _XX_头文件.H
Int A;
#endif

  那么,很糟糕的是,这里的 int A是个全局变量的定义,所以如果这个头文件被多次引用的话,你的 A会被重复定义,显然语法上错了

  只不过有了这个#ifndef 的条件编译,所以能保证你的头文件只被引用一次,不过也许还是会出岔子,但若多个 c 文件包含这个头文件时还是会出错的,因为宏名有效范围仅限于本 c 源文件,所以在这多个 c 文件编译时是不会出错的,但在链接时就会报错,说你多处定义了同一个变量
  . c 文件是程序文件,内含函数实现,变量定义等内容。这样我们将 c和 h 文件分开写成两个文件是一个良好的编程风格。 比方说我在 aaa.h 里定义了一个函数的声明,然后我在 aaa.h 的同一个目录下建立 aaa.c , aaa.c 里定义了这个函数的实现,然后是在 main 函数所在.c 文件里#include这个 aaa.h , 然后我就可以使用这个函数了。

  由上说明可知,头文件其实用来存放函数原型,函数原型是用来向编译器传递函数的一些特定信息的手段。通常情况下,如果在同一个源文件中的前面(也就是在调用者的前面)已经出现了该函数的定义,编译器就会记住这个被调用函数的参数数量和类型,以及该函数的返回值得类型。在这个源文件中,编译器会按照函数原型的声明检查后续调用的参数和返回值,确保调用者正确地按照函数原型的声明向函数传递了正确的参数数目和类型,并把返回值赋给类型匹配的变量。而函数原型则又是用函数声明来完成的,函数原型用分号结束,这是函数原型与函数定义不同的地方。考虑到代码的可读性,在书写函数原型时保留形式参数变量名,如果被调函数的定义与调用者不在同一个源文件中,那么就需要在函数原型前加上 extern 关键字通知编译器被调函数的参数情况和返回值情况。

  

  C 程序采用模块化的编程思想,需合理地将一个很大的软件划分为一系列功能独立的部分合作完成系统的需求,在模块的划分上主要依据功能。模块由头文件和实现文件组成,对头文件和实现文件的正确使用方法是:
  规则1、头文件(.h)中是对于该模块接口的声明接口包括该模块提供给其它模块调用的外部函数及外部全局变量,对这些变量和函数都需在.h 中文件中冠以 extern 关键字声明;
  规则2、模块内的函数和全局变量,需在.c文件开头冠以 static 关键字声
  规则3、永远不要在.h 文件中定义变量;许多程序员对定义变量和声明变量混淆不清, 定义变量和声明变量的区别在于定义会产生内存分配的操作,是汇编阶段的概念; 而声明则只是告诉包含该声明的模块在连接阶段从其它模块寻找外部函数和变量。如:

Int a=;
#include“module1.h”
#include“module1.h”
#include“module1.h”

  以上程序的结果是在模块 1、 2、 3中都定义了整型变量 a, a在不同的模块中对应不同的地址单元,这明显不符合编写者的本意。正确的做法是:

Extern int a;
#include“module1.h”
Int a=;
#include“module1.h”
#include“module1.h”

  这样如果模块 1、 2、 3操作 a 的话,对应的是同一片内存单元。

  规则4、如果要用其它模块定义的变量和函数,直接包含其头文件即可。某模块要访问其它模块中定义的全局变量时,只要包含该模块的头文件即可。
  共享变量声明就像在函数间共享变量的方式一样,变量可以在文件*享。为了共享函数,要把函数的定义放在一个源文件中,然后在需要调用此函数的其他文件中放置声明。共享变量的方法和此方式非常类似。在此之前,不需要区别变量的声明和它的定义。为了声明变量 i,写成如下形式: int i; 这样不仅声明 i是 int 型的变量,而且也 i 进行了定义,从而使编译器为 i留出了空间。 为了声明没有定义的变量 i,需要在变量声明的开始处放置关键字 extern:
Extern int i;
  extern 提示编译器变量 i 是在程序中的其他位置定义的(大多数可能是在不同的源文件中),因此不需要为 i分配空间。顺便说一句,extern 可以用于所有类型的变量。在数组的声明中使用 extern 时,可以忽略数组的长度: extern int a[]; 因为此刻编译器不用为数组 a 分配空间,所以也就不需要知道数组 a的长度了。
  为了在几个源文件*享变量 i,首先把变量 i 的定义放置在一个文件中:
Int i;
  如果需要对变量 i初始化,那么可以在这里放初始值。在编译这个文件时,编译器将会为变量 i分配内存空间,而其他文件将包含变量 i 的声明: extern int i; 通过在每个文件中声明变量 i,使得在这些文件中可以访问/或修改变量 i。然而,由于关键字 extern,使得编译器不会在每次编译其中某个文件时为变量 i 分配额外的内存空间。
  当在文件*享变量时,会面临和共享函数时相似的挑战:确保变量的所有声明和变量的定义一致。 为了避免矛盾,通常把共享变量的声明放置在头文件中。需要访问特殊变量的源文件可以稍后包含适当的头文件。此外,含有变量定义的源文件包含每一个含有变量声明的头文件,这样使编译器可以检查两者是否匹配。

  规则 5、作为一般规则,应该把下面所列的内容放入头(.h)文件中
a. 宏定义(预处理#define);
b. 结构、联合和枚举声明;
c. typedef声明;
d. 外部函数声明;
e. 全局变量声明;
  当声明或定义需要在多个文件*享时,把他们放入一个头文件中尤其重要。不要在两个或多个源文件的顶部重复声明或定义宏。应该把它们放入一个头文件中,然后在需要的时候用#include 包含进来。
这样做的原因并不仅仅是减少打字输入——这样可以保证在声明或定义变化的时候,只需要修改一处即可将结果一致地传播到各个源文件。(特别是,永远不要把外部函数原型放到.c 文件中)最后,不能把实际的代码(如函数体)或全局变量定义(即定义和初始化实例)放入头文件中。