[C语言中,头文件和源文件的关系]看完解决头文件包含问题


简单的说其实要理解C文件与头文件(即.h)有什么不同之处,首先需要弄明白编译器的工作过程,一般说来编译器会做以下几个过程:

一、编译器工作工程

  1. 预处理阶段
  2. 词法与语法分析阶段
  3. 编译阶段,首先编译成纯汇编语句,再将之汇编成跟CPU相关的二进制码,生成各个目标文件 (.obj文件)
  4. 连接阶段,将各个目标文件中的各段代码进行绝对地址定位,生成跟特定平台相关的可执行文件,当然,最后还可以用objcopy生成纯二进制码,也就是去掉了文件格式信息。(生成.exe文件)

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

(main .c文件 目标文件 可执行文件 )

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

#include 
#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和.h文件的困惑

本质上没有任何区别。 只不过一般:

.h文件是头文件,内含函数声明、宏定义、结构体定义等内容**.c文件**是程序文件,内含函数实现,变量定义等内容。而且是什么后缀也没有关系,只不过编译器会默认对某些后缀的文件采取某些动作。你可以强制编译器把任何后缀的文件都当作c文件来编。

这样分开写成两个文件是一个良好的编程风格。

而且,比方说 我在aaa.h里定义了一个函数的声明,然后我在aaa.h的同一个目录下建立aaa.c , aaa.c里定义了这个函数的实现,然后是在main函数所在.c文件里#include这个aaa.h 然后我就可以使用这个函数了。 main在运行时就会找到这个定义了这个函数的aaa.c文件。这是因为:main函数为标准C/C++的程序入口,编译器会先找到该函数所在的文件。假定编译程序编译myproj.c(其中含main())时,发现它include了mylib.h(其中声明了函数void test()),那么此时编译器将按照事先设定的路径(Include路径列表及代码文件所在的路径)查找与之同名的实现文件(扩展名为.cpp或.c,此例中为mylib.c),如果找到该文件,并在其中找到该函数(此例中为void test())的实现代码,则继续编译;如果在指定目录找不到实现文件,或者在该文件及后续的各include文件中未找到实现代码,则返回一个编译错误.其实include的过程完全可以“看成”是一个文件拼接的过程,将声明和实现分别写在头文件及C文件中,或者将二者同时写在头文件中,理论上没有本质的区别。以上是所谓动态方式。对于静态方式,基本所有的C/C++编译器都支持一种链接方式被称为Static Link,即所谓静态链接。在这种方式下,我们所要做的,就是写出包含函数,类等等声明的头文件(a.h,b.h,…),以及他们对应的实现文件(a.cpp,b.cpp,…),编译程序会将其编译为静态的库文件(a.lib,b.lib,…)。在随后的代码重用过程中,我们只需要提供相应的头文件(.h)和相应的库文件(.lib),就可以使用过去的代码了。相对动态方式而言,静态方式的好处是实现代码的隐蔽性,即C++中提倡的“接口对外,实现代码不可见”。有利于库文件的转发.c文件和.h文件的概念与联系

如果说难题最难的部分是基本概念,可能很多人都会持反对意见,但实际上也确实如此。我高中的时候学物理,老师抓的重点就是概念——概念一定要搞清,于是难题也成了容易题。如果你能分析清楚一道物理难题存在着几个物理过程,每一个过程都遵守那一条物理定律(比如动量守恒、牛II定律、能量守恒),那么就很轻松的根据定律列出这个过程的方程,N个过程必定是N个N元方程,难题也就迎刃而解。即便是高中的物理竞赛难题,最难之处也不过在于:

  • (1)、混淆你的概念,让你无法分析出几个物理过程,或某个物理过程遵循的那条物理定律;
  • (2)、存在高次方程,列出方程也解不出。而后者已经是数学的范畴了,所以说,最难之处还在于掌握清晰的概念;

程序设计也是如此,如果概念很清晰,那基本上没什么难题(会难在数学上,比如算法的选择、时间空间与效率的取舍、稳定与资源的平衡上)。但是,要掌握清晰的概念也没那么容易。比如下面这个例子,看看你有没有很清晰透彻的认识。

//a.h
void foo();

//a.c
#include "a.h"   //我的问题出来了:这句话是要,还是不要?
void foo()
{
     return;
}

//main.c
#include "a.h"
int main(int argc, char *argv[])
{
    foo(); 
  return 0;
}                
针对上面的代码,请回答三个问题:
  1. a.c 中的 #include “a.h” 这句话是不是多余的?
  2. 为什么经常见 xx.c 里面 include 对应的 xx.h
  3. 如果 a.c 中不写,那么编译器是不是会自动把 .h 文件里面的东西跟同名的 .c 文件绑定在一起?
    (请针对上面3道题仔细考虑10分钟,莫要着急看下面的解释。😃 考虑的越多,下面理解的就越深。)

好了,时间到!请忘掉上面的3道题,以及对这三道题引发出的你的想法,然后再听我慢慢道来。正确的概念是:从C编译器角度看,.h和.c皆是浮云,就是改名为.txt、.doc也没有大的分别。换句话说,就是.h和.c没啥必然联系。.h中一般放的是同名.c文件中定义的变量、数组、函数的声明,需要让.c外部使用的声明。这个声明有啥用?只是让需要用这些声明的地方方便引用。因为 #include “xx.h” 这个宏其实际意思就是把当前这一行删掉,把 xx.h 中的内容原封不动的插入在当前行的位置。由于想写这些函数声明的地方非常多(每一个调用 xx.c 中函数的地方,都要在使用前声明一下子),所以用 #include “xx.h” 这个宏就简化了许多行代码——让预处理器自己替换好了。也就是说,xx.h 其实只是让需要写 xx.c 中函数声明的地方调用(可以少写几行字),至于 include 这个 .h 文件是谁,是 .h 还是 .c,还是与这个 .h 同名的 .c,都没有任何必然关系。
 这样你可能会说:啊?那我平时只想调用 xx.c 中的某个函数,却 include了 xx.h 文件,岂不是宏替换后出现了很多无用的声明?没错,确实引入了很多垃圾,但是它却省了你不少笔墨,并且整个版面也看起来清爽的多。鱼与熊掌不可得兼,就是这个道理。反正多些声明(.h一般只用来放声明,而放不定义,参见拙著“过马路,左右看”)也无害处,又不会影响编译,何乐而不为呢?
翻回头再看上面的3个问题,很好解答了吧?

  1. 答:不一定。这个例子中显然是多余的。但是如果.c中的函数也需要调用同个.c中的其它函数,那么这个.c往往会include同名的.h,这样就不需要为声明和调用顺序而发愁了(C语言要求使用之前必须声明,而include同名.h一般会放在.c的开头)。有很多工程甚至把这种写法约定为代码规范,以规范出清晰的代码来。
  2. 答:1中已经回答过了。
  3. 答:不会。问这个问题的人绝对是概念不清,要不就是想混水摸鱼。非常讨厌的是中国的很多考试出的都是这种烂题,生怕别人有个清楚的概念了,绝对要把考生搞晕。
    搞清楚语法和概念说易也易,说难也难。窍门有三点:

不要晕着头工作,要抽空多思考思考,多看看书;
看书要看好书,问人要问强人。烂书和烂人都会给你一个错误的概念,误导你;
勤能补拙是良训,一分辛苦一分才;

(1)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。
(2)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。
头文件用来存放函数原型。

三、头文件如何来关联源文件?

这个问题实际上是说,已知头文件“a.h”声明了一系列函数(仅有函数原型,没有函数实现),**“b.cpp”**中实现了这些函数,那么如果我想在“c.cpp”中使用“a.h”中声明的这些在“b.cpp”中实现的函数,通常都是在“c.cpp”中使用#include “a.h”,那么c.cpp是怎样找到b.cpp中的实现呢?
其实.cpp和.h文件名称没有任何直接关系,很多编译器都可以接受其他扩展名。
谭浩强老师的《C程序设计》一书中提到,编译器预处理时,要对#include命令进行“文件包含处理”:将headfile.h的全部内容复制到#include “headfile.h”处。这也正说明了,为什么很多编译器并不care到底这个文件的后缀名是什么----因为#include预处理就是完成了一个“复制并插入代码”的工作。

程序编译的时候,并不会去找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中指定的那个可以执行文件中。
在VC中,一帮情况下不需要自己写makefile,只需要将需要的文件都包括在project中,VC会自动帮你把makefile写好。

通常,编译器会在每个 .o.obj文件中都去找一下所需要的符号,而不是只在某个文件中找或者说找到一个就不找了。因此,如果在几个不同文件中实现了同一个函数,或者定义了同一个全局变量,链接的时候就会提示“redefined”.

四、头文件作用

C语言里,每个源文件是一个模块,头文件为使用该模块的用户提供接口。接口指一个功能模块暴露给其他模块用以访问具体功能的方法。

使用源文件实现模块的功能,使用头文件暴露单元的接口。用户只需包含相应的头文件就可使用该头文件中暴露的接口。

通过头文件包含的方法将程序中的各功能模块联系起来有利于模块化程序设计:

  1. 通过头文件调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制库即可。用户只需按照头文件中的接口声明来调用库功能,而不必关心接口如何实现。编译器会从库中提取相应的代码。

  2. 头文件能加强类型安全检查。若某个接口的实现或使用方式与头文件中的声明不一致,编译器就会指出错误。这一简单的规则能大大减轻程序员调试、改错的负担。

在预处理阶段,编译器将源文件包含的头文件内容复制到包含语句(#include)处。在源文件编译时,连同被包含进来的头文件内容一起编译,生成目标文件(.obj)。

如果所包含的头文件非常庞大,则会严重降低编译速度(使用GCC的-E选项可获得并查看最终预处理完的文件)。因此,在源文件中应仅包含必需的头文件,且尽量不要在头文件中包含其它头文件。

五、 头文件组织原则

源文件中实现变量、函数的定义,并指定链接范围。头文件中书写外部需要使用的全局变量、函数声明及数据类型和宏的定义。

建议组织头文件内容时遵循以下原则:

  1. 头文件划分原则:类型定义、宏定义尽量与函数声明相分离,分别位于不同的头文件中。内部函数声明头文件与外部函数声明头文件相分离,内部类型定义头文件与外部类型定义头文件相分离。

注意,类型和宏定义有时无法分拆为不同文件,比如结构体内数组成员的元素个数用常量宏表示时。因此仅分离类型宏定义与函数声明,且分别置于*.th和*.fh文件(并非强制要求)。

  1. 头文件的语义层次化原则:头文件需要有语义层次。不同语义层次的类型定义不要放在一个头文件中,不同层次的函数声明不要放在一个头文件中。

  2. 头文件的语义相关性原则:同一头文件中出现的类型定义、函数声明应该是语义相关的、有内部逻辑关系的,避免将无关的定义和声明放在一个头文件中。

  3. 头文件名应尽量与实现功能的源文件相同,即module.c和module.h。但源文件不一定要包含其同名的头文件。

  4. 头文件中不应包含本地数据,以降低模块间耦合度。

即只有源文件自己使用的类型、宏定义和变量、函数声明,不应出现在头文件里。作用域限于单文件的私有变量和函数应声明为static,以防止外部调用。将私有类型置于源文件中,会提高聚合度,并减少不必要的格式外漏。

  1. 头文件内不允许定义变量和函数,只能有宏、类型(typedef/struct/union/enum等)及变量和函数的声明。特殊情况下可extern基本类型的全局变量,源文件通过包含该头文件访问全局变量。但头文件内不应extern自定义类型(如结构体)的全局变量,否则将迫使本不需要访问该变量的源文件包含自定义类型所在头文件[1]。

  2. 说明性头文件不需要有对应的源文件。此类头文件内大多包含大量概念性宏定义或枚举类型定义,不包含任何其他类型定义和变量或函数声明。此类头文件也不应包含任何其他头文件。

  3. 使用#pragma once或header guard(亦称include guard或macro guard)避免头文件重复包含。#pragma once是一种非标准但已被现代编译器广泛支持的技巧,它明确告知预处理器“不要重复包含当前头文件”。而header guard则通过预处理命令模拟类似行为:

#ifndef  _PRJ_DIR_FILE_H  //必须确保header guard宏名永不重名
#define  _PRJ_DIR_FILE_H

//<头文件内容>

#endif

使用#pragma once相比header guard具有两个优点:

更快。编译器不会第二次读取标记#pragma once的文件,但却会读若干遍使用header guard 的文件(寻找#endif);
更简单。不再需要为每个文件的header guard取名,避免宏名重名引发的“找不到声明”问题。缺点则是:
#pragma once
保证物理上的同一个文件不会被包含多次,无法对头文件中的一段代码作#pragma once声明。若某个头文件具有多份拷贝(内容相同的多个文件),pragma不能保证它们不被重复包含。当然,这种重复包含很容易被发现并修正。9) C++中要引用C函数时,函数所在头文件内应包含extern “C”。

//.h文件头部
#ifdef  __cplusplus
extern "C" {
#endif

//<函数声明>

//.h文件尾部
#ifdef  __cplusplus
}
#endif

被extern "C"修饰的变量和函数将按照C语言方式编译和连接,否则编译器将无法找到C函数定义,从而导致链接失败。

  1. 头文件内要有面向用户的充足注释,从应用角度描述接口暴露的内容。在实际编程中,常常因头文件包含不当而引发编译时报告符号未定义的错误或重复定义的警告。要消除符号未定义的编译错误,只需在引用符号(变量、函数、数据类型及宏等)前确保它已被声明或定义[4]。要消除重复定义的警告,则需合理设计头文件包含顺序和层次。

六、头文件包含原则(重点)

建议包含头文件时遵循以下原则:

1)源文件内的头文件包含顺序应从最特殊到一般,如:

#include "通用头文件"  //内部可能定义本模块数据类型别名
#include "源文件同名头文件"
#include "本模块其他头文件"
#include "自定义工具头文件"
#include "第三方头文件"
#include "平台相关头文件"
#include "C++库头文件"
#include "C库头文件"

优点是每个头文件必须include需要的关联头文件,否则会报错。同时,源文件同名头文件置于包含列表前端便于检查该头文件是否自完备,以及类型或函数声明是否与标准库冲突。

2)减少头文件的嵌套和交叉引用,头文件仅包含其真正需要显式包含的头文件。

例如,头文件A中出现的类型定义在头文件B中,则头文件A应包含头文件B,除此以外的其他头文件不允许包含。

头文件的嵌套和交叉引用会使程序组织结构和文件组织变得混乱,同时造成潜在的错误。大型工程中,原有头文件可能会被多个其他(源或头)文件包含,在原有头文件中添加新的头文件往往牵一发而动全身。若头文件中类型定义需要其他头文件时,可将其提出来单独形成一个全局头文件。

3)头文件应包含哪些头文件仅取决于自身,而非包含该头文件的源文件。

例如,编译源文件时需要用到头文件B,且源文件已包含头文件A,而索性将头文件B包含在头文件A中,这是错误的做法。

4)尽量保证用户使用此头文件时,无需手动包含其他前提头文件,即此头文件内已包含前提头文件。

例如,面积相关操作的头文件Area.h内已包含关于点操作的头文件Point.h,则用户包含Area.h后无需再手动包含Point.h。这样用户就不必了解头文件的内在依赖关系。

5)头文件应是自完备的,即在任一源文件中包含任一头文件而不会产生编译错误。

6)源文件中包含的头文件尽量不要有顺序依赖。

7)尽量在源文件中包含头文件,而非在头文件中。且源文件仅包含所需的头文件。

8)头文件中若能前置声明(亦称前向声明[5]),就不要包含另一头文件。仅当前置声明不能满足或过于麻烦时才使用include,如此可减少依赖性方面的问题。示例如下:

struct T_MeInfoMap;  //前置声明
struct T_OmciMsg;    //前置声明

typedef FUNC_STATUS (*OmciChkFunc)(struct T_MeInfoMap *ptMeInfo, struct T_OmciMsg *ptMsg, struct T_OmciMsg *ptAckMsg);


//OMCI实体信息
typedef struct{
    INT16U wMeClass;               //实体类别
    OMCI_ATTR_INFO *pMeAttrInfo;   //实体所定义的属性信息指针
    INT8U  ucAttrNum;              //实体所定义的属性数目
    INT16U wTotalAttrLen;          //实体所有属性所占的总字节数,初始化为0,动态计算
    INT8U  *pszDbName;             //实体存库时的数据表名称,建议不要超过DB_NAME_LEN(32)
    INT16U wMaxRecNum;             //实体存库时支持的最大记录数目
    OmciChkFunc fnCheck;           //Omci校验函数指针
    BOOL   bDbCreated;             //实体数据表是否已创建
}OMCI_ME_INFO_MAP;

如上,在OmciChkFunc函数的实现源文件内包含T_MeInfoMap和T_OmciMsg所在头文件即可。

另举一例如下:

typedef TBL_SET_MODE (*OperTypeFunc)(INT8U *pTblEntry);

typedef INT8U (*CmpRecFunc)(VOID *pvCmpData, VOID *pvRecData); //为避免头文件交叉引用,与CompareRecFunc异名同构

//表属性信息
typedef struct{
    INT16U wMaxEntryNum;         //表属性最大表项数目(实体记录数目wMaxRecNum * wMaxEntryNum <= MAX_RECORD_NUM)
    OperTypeFunc fnGetOperType;  //操作类型函数指针。根据表项数据或外界需求(只读表)解析当前表项操作类型
    TBL_KEY_INFO tCmpKeyInfo;    //检索表属性子表记录时的匹配关键字信息(TBL_KEY_INFO)
    CmpRecFunc   fnCmpAddKey;    //增加表项时需要检测的关键字匹配函数指针
    CmpRecFunc   fnCmpDelKey;    //删除表项时需要检测的关键字匹配函数指针
    INT16U wTblEntrySize;        //表属性表项字节数,由外部动态赋值
}TBL_ATTR_INFO;

如上,CompareRecFunc函数原型由其他头文件提供,此处为避免头文件交叉引用定义其异名同构原型CmpRecFunc。

在不会引起歧义的前提下,头文件内尽可能使用VOID指针代替非基本类型的值变量或指针,以避免再包含类型定义所在的头文件。但这将影响代码可读性并降低程序执行效率,应权衡利弊。

9)避免包含重量级的平台头文件,如windows.h或d3d9.h等。若仅使用该头文件少量函数,可extern函数到源文件内。如下:

/****************************************************************************************
                       外部函数声明 (当外部接口未提供头文件或头文件过于复杂时) 
 ****************************************************************************************/
//因声明所在头文件引用混乱,此处仅extern函数声明。
extern INT32S DBShmCliInit(VOID); //#include "db_shm_mgr.h"
extern INT32S cmLockInit(VOID);   //#include "common_cmapi.h"
若还使用该头文件某些类型和宏定义,可创建适配性源文件。在该源文件内包含平台头文件,封装新的接口并将其声明在同名头文件内,其他源文件将通过适配头文件间接访问平台接口。如下:

/*****************************************************************************************
* 文件名称:Omci_Send_Msg.c
* 内容摘要:OMCI消息转发接口
* 其它说明: 该头文件封装SEND接口,以避免其他源文件包含支撑api和pid公共头文件导致引用混乱。
 *****************************************************************************************/

#include "Omci_Common.h"
#include "Omci_Send_Msg.h"
#include "oss_api.h"

/**********************************************************************************************
                                         函数实现区
**********************************************************************************************/

//向自身进程发送异步消息
INT32U OmciAsynSendSelf(INT16U wEvent, VOID *pvMsg, INT16U wMsgLen)
{
    PID dwSelfPid = 0;
    SELF(&dwSelfPid);
    return ASEND(wEvent, pvMsg, wMsgLen, dwSelfPid);
}

10)对于函数库(包括标准库和自定义的公共宏及接口)的头文件,可将其加入到一个通用头文件中。需要控制该头文件的体积(主要是该头文件所包含的所有头文件内容大小),并确保所有源文件首先包含该通用头文件。示例如下:

#ifndef  _OMCI_COMMON_H
#define  _OMCI_COMMON_H

/*******************************************************************************************
* 说明:
* 本文件仅应包含与具体通信协议无关的通用数据类型及宏定义。
* 为简化头文件包含且不失可移植性,本文件内可包含少量C库通用头文件。
* 因本文件内定义基本数据类型别名,故.c文件中应将本头文件置于包含列表顶端,
* 否则编译时可能产生类型未定义错误。
*******************************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <limits.h>

#include "Omci_Byte.h"

//<Other Contents...>

注意,示例头文件内包含C库文件虽能简化包含,但却与规则1冲突。也可另外增加包含库文件列表的通用头文件。

11)若不确定类型、宏定义或函数声明所在头文件具体路径,可在源文件中再次定义或声明,编译器会以redefined警告或conflicting错误给出类型、宏定义或函数声明所在头文件路径。

七、 注解

「【注1】全局变量的使用原则」

1)若全局变量仅在单个源文件中访问,则可将该变量改为该文件内的静态全局变量;

2)若全局变量仅由单个函数访问,则可将该变量改为该函数内的静态局部变量;

3)尽量不要使用extern声明全局变量,最好提供函数访问这些变量。直接暴露全局变量是不安全的,外部用户未必完全理解这些变量的含义。

4)设计和调用访问动态全局变量、静态全局变量、静态局部变量的函数时,需要考虑重入问题。

「【注2】#pragma once的可移植性」

#ifndef由C/C++语言标准支持,不受编译器任何限制;而#pragma once仅由编译器提供保证,存在可移植性等问题。

某些gcc编译器版本(如3.2.3)会报告“warning: #pragma once is obsolete”的警告,而其他较老版本的编译器可能会报错。但随着gcc 3.4的发布,#pragma once中的一些问题(主要与符号链接和硬链接有关)得以解决,#pragma once命令也标记为“未废弃”。

还有种写法同时使用#pragma once和header guard编写“可移植性”代码,以利用编译器可能支持的#pragma once优化。如下:

#pragma once
#ifndef _PRJ_DIR_FILE_H
#define _PRJ_DIR_FILE_H

//<头文件内容>

#endif

该法似乎兼有两者的优点。但既然使用#ifndef就有宏名重名的风险,也无法避免不支持#pragma once的编译器告警或报错,故混用两种方法似乎不能带来更多的好处,反倒让不熟悉的人感到困惑。

注意,如果使用header guard,理论上可在代码任何地方判断当前是否已经包含某个头文件。但应避免通过该判断来改变后续代码的逻辑走向!

这种做法将使程序依赖于头文件的包含顺序,极不可取。若需要实现“若当前包含HeaderA.h,才加入StructB结构”,可对StructB结构创建HeaderB.h头文件,在HeaderA.h中包含HeaderB.h。

「【注3】extern “C”」

C++语言在编译时为实现函数重载,会结合函数名、参数数目及类型信息而生成一个中间函数名。

例如,C++中函数void foo(int x, float y)编译后在符号库中生成的名字为_foo_int_float(不同编译器可能生成不同函数名,但均采用相同机制,生成的新名字称为”mangled name”);而该函数被C编译器编译后在符号库中的名字为_foo。

C语言中不支持extern "C"声明,在.c文件中包含extern "C"时会出现编译语法错误。

当然编译器也可以为其他语言提供链接说明。例如:extern “FORTRAN”、extern "Ada"等。

「【注4】声明(declaration)与定义(definition)」

全局变量或函数可(在多个编译单元中)有多处声明,但只允许定义一次。全局变量定义时分配空间并赋初始值(如果有);函数定义时提供函数体内容。

声明:
extern int iGlobal;
extern int func();int func();

定义:
int iGlobal = 0;int iGlobal;
int func ()
{
    return 1;
}

在多个源文件中共享变量或函数时,需确保定义和声明的一致性。通常在某个相关的源文件中定义,然后在头文件中进行外部声明。需要使用时包含相应的头文件即可。定义变量的源文件也应包含该头文件,以便编译器检查定义和声明的一致性。

该规则可提供高度的可移植性:它与ANSI/ISO C标准一致,同时也兼顾大多数ANSI前的编译器和链接器。(Unix编译器和链接器常使用允许多重定义的“通用模式”,只要保证最多对一处定义进行初始化即可。

该方式被ANSI C标准称为一种“通用扩展”)。某些很老的系统可能要求显式初始化以区别定义和外部声明。

通用扩展在《深入理解计算机系统》中解释为:多重定义的符号只允许最多一个强符号。函数和定义时已初始化的全局变量是强符号;未初始化的全局变量是弱符号。Unix链接器使用以下规则来处理多重定义的符号:

规则一:不允许有多个强符号。在被多个源文件包含的头文件内定义的全局变量会被定义多次(预处理阶段会将头文件内容展开在源文件中),若在定义时显式地赋值(初始化),则会违反此规则。

规则二:若存在一个强符号和多个弱符号,则选择强符号。

规则三:若存在多个弱符号,则从这些弱符号中任选一个。

当不同文件内定义同名(即便类型和含义不同)的全局变量时,该变量共享同一块内存(地址相同)。若变量定义时均初始化,则会产生重定义(multiple definition)的链接错误;若某处变量定义时未初始化,则无链接错误,仅在因类型不同而大小不同时可能产生符号大小变化(size of symbol `XXX’ changed)的编译警告。

在最坏情况下,编译链接正常,但不同文件对同名全局变量读写时相互影响,引发非常诡异的问题。这种风险在使用无法接触源码的第三方库时尤为突出。

因此,应尽量避免使用全局变量。若确有必要,应采用静态全局变量(无强弱之分,且不会和其他全局符号产生冲突),并封装访问函数供外部文件调用。

「【注5】前向声明(forward declaration)」

结构体类型S在声明之后定义之前是一个不完全类型(incomplete type),即已知S是一个类型,但不知道包含哪些成员。

不完全类型只能用于定义指向该类型的指针,或声明使用该类型作为形参指针类型或返回指针类型的函数。指针类型对编译器而言大小固定(如32位机上为四字节),不会出现编译错误。

假设先后定义两个结构A和B,且两个结构需要互相引用。在定义A时B还没有定义,则要引用B就需要前向声明结构B(struct B;)。示例如下:

typedef BOOL (*func)(const DefStruct *ptStrt);
 
typedef struct DefStruct_t
{
    int i;
    func f;
}DefStruct;
如上在DefStruct中使用回调函数func声明,这样交叉引用必然编译报错。进行前向声明即可:

typedef struct DefStruct_t DefStruct;
typedef BOOL (*func)(const DefStruct *ptStrt);

struct DefStruct_t
{
    int i;
    func f;
};

注意,在前向声明和具体定义之间涉及标识符(变量、结构、函数等)实现细节的使用都是非法的。若函数被前向声明但未被调用,则编译和运行正常;若前向声明函数被调用但未被定义,则编译正常但链接报错(undefined reference)。将具体定义放在源文件中可部分避免该问题。

转载自:https://zhuanlan.zhihu.com/p/303245383
https://www.cnblogs.com/windfall/p/5100292.html

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值