器→工具, 编程语言

C语言学习之头文件

钱魏Way · · 0 次浏览

什么是头文件?

在C语言中,头文件是包含一系列声明和宏定义的文件,它们用于共享函数声明、宏定义和其他的公共对象。在C语言中,头文件通常用 .h 作为文件扩展名。

头文件可以被划分为两类:

  • 标准头文件:这些头文件由语言标准定义,并且由编译器提供。例如,h(包含输入/输出函数,例如 printf 和 scanf),stdlib.h(包含各种常用的函数,例如内存分配 malloc, free,数学函数等),string.h (包含字符串处理的函数,例如 strcpy, strcat 等)。
  • 用户定义的头文件:这些头文件由程序员自己创建,以便在多个源文件之间共享代码。例如,如果你正在编写一个大型项目并且你决定将所有的函数声明放在一个文件中,你就可以创建一个自定义的头文件。

头文件在C语言中的使用非常重要,因为它们可以提高代码复用性,改善源代码的结构,同时也可以减少一些可能的错误。例如,如果函数声明在多个地方定义,那么任何对函数声明的更改都需要在每个位置进行更改,这可能会导致错误。但是,如果函数声明在头文件中定义,那么所有的更改只需要在一个位置进行,所有包含该头文件的源文件会自动接收到更新。

头文件的示意图:

为什么需要头文件?

C语言的设计早在几十年前,当时的计算机硬件资源有限,编译器技术相对原始。在这样的背景下,C语言使用了头文件这种方式来组织代码和提高编译效率。

C语言中的头文件主要有以下几个用途:

  • 代码重用:头文件可以被多个源文件包含,让这些源文件可以共享在头文件中定义的函数声明、宏定义和全局变量等。这避免了在多个源文件中重复这些代码,提高了代码的重用性。
  • 提供接口:头文件一般只包含函数的声明,而不包含函数的定义(即函数的具体实现)。这样,头文件就像一个接口,告诉使用者有哪些函数可以使用,每个函数的参数和返回值是什么,但不需要告诉使用者这些函数是如何实现的。这使得函数的实现可以独立于其接口进行修改,增强了代码的可维护性。
  • 提高编译速度:头文件可以被预编译,而不需要在每次编译时都重新处理。例如,标准库的头文件就被编译器预编译成预编译头文件,大大提高了编译速度。
  • 模块化编程:头文件可以帮助实现模块化编程。你可以将相关的函数和变量声明放在同一个头文件中,形成一个模块。通过包含不同的头文件,就可以使用不同的模块,提高了代码的可读性和可维护性。
  • 需要注意的是,虽然头文件在C语言中非常重要,但是也要避免一些不良的使用方式。例如,应避免头文件中包含其他头文件,避免全局变量的滥用,避免头文件的循环依赖等。

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

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

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

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

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

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

然而,现代的编程语言,如Python、Java、C#、JavaScript等,由于是在计算机硬件资源丰富,编译器和解释器技术更先进的环境中诞生的,因此它们的设计理念和组织方式与C语言有所不同。

以下是其他一些语言不需要头文件的原因:

  • 动态类型系统:像Python和JavaScript这样的语言具有动态类型系统,这意味着变量的类型在运行时被确定,而非在编译时。因此,这些语言无需在编译阶段知道所有函数和变量的具体定义。
  • 晚期绑定:一些语言(比如Python和Java)将函数和变量的查找推迟到运行时,也就是所谓的”晚期绑定”。这使得在编译阶段无需知道所有的函数和变量定义。
  • 编译器优化:现代编译器(如Java和C#的编译器)已经足够智能,可以在编译阶段自动处理很多C语言需要手动处理的问题,例如寻找和链接函数和变量的定义。
  • 模块系统:许多现代语言提供了模块或者包的系统,使得代码的组织和共享更加容易。例如,Python中的import,Java中的import,JavaScript中的import/export等。
  • 面向对象:一些语言,如Java和C#,采用了面向对象的方式来组织代码。在这些语言中,函数(在这些语言中被称为方法)和变量(被称为属性)被封装在对象中,这使得跟踪函数和变量的定义变得更加容易。

总的来说,不同的编程语言具有不同的设计理念和实现机制,这也导致了它们在组织和管理代码方面的差异。

以GO语言为例:Go语言的设计者们明显受到了C语言头文件带来的一些问题的启示,因此在Go语言中,他们采用了一种完全不同的方式来组织代码。Go语言使用包(packages)来组织和重用代码。一个包就是一个代码的集合,它可以包含函数、变量、常量以及任何你想要定义的类型。

在Go语言中,如果你想使用其他包中的代码,只需要导入(import)这个包就行了。编译器会自动查找和链接这个包的源代码。而且,Go编译器在编译时会检查导入的包中每个公开的函数和变量的定义,因此你不需要像在C语言中那样创建和维护头文件。

下面是一个简单的例子,展示了在Go语言中如何导入和使用包:

package main

import (
    "fmt"
    "math"
)

func main() {
    fmt.Println("Hello, World!")
    fmt.Println("The square root of 2 is", math.Sqrt(2))
}

在这个例子中,我们导入了 fmt 和 math 两个标准库包。然后在 main 函数中,我们使用了 fmt.Println 函数来打印消息,使用了 math.Sqrt 函数来计算2的平方根。所有的这些都不需要任何头文件。

头文件组织原则

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

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

  • 头文件划分原则:类型定义、宏定义尽量与函数声明相分离,分别位于不同的头文件中。内部函数声明头文件与外部函数声明头文件相分离,内部类型定义头文件与外部类型定义头文件相分离。注意,类型和宏定义有时无法分拆为不同文件,比如结构体内数组成员的元素个数用常量宏表示时。因此仅分离类型宏定义与函数声明,且分别置于*.th和*.fh文件(并非强制要求)。
  • 头文件的语义层次化原则:头文件需要有语义层次。不同语义层次的类型定义不要放在一个头文件中,不同层次的函数声明不要放在一个头文件中。
  • 头文件的语义相关性原则:同一头文件中出现的类型定义、函数声明应该是语义相关的、有内部逻辑关系的,避免将无关的定义和声明放在一个头文件中。头文件名应尽量与实现功能的源文件相同,即c和module.h。但源文件不一定要包含其同名的头文件。
  • 头文件中不应包含本地数据,以降低模块间耦合度。即只有源文件自己使用的类型、宏定义和变量、函数声明,不应出现在头文件里。作用域限于单文件的私有变量和函数应声明为static,以防止外部调用。将私有类型置于源文件中,会提高聚合度,并减少不必要的格式外漏。
  • 头文件内不允许定义变量和函数,只能有宏、类型(typedef/struct/union/enum等)及变量和函数的声明。特殊情况下可extern基本类型的全局变量,源文件通过包含该头文件访问全局变量。但头文件内不应extern自定义类型(如结构体)的全局变量,否则将迫使本不需要访问该变量的源文件包含自定义类型所在头文件。
  • 说明性头文件不需要有对应的源文件。此类头文件内大多包含大量概念性宏定义或枚举类型定义,不包含任何其他类型定义和变量或函数声明。此类头文件也不应包含任何其他头文件。
  • 使用#pragma once或header guard(亦称include guard或macro guard)避免头文件重复包含。#pragma once是一种非标准但已被现代编译器广泛支持的技巧,它明确告知预处理器“不要重复包含当前头文件”。使用#pragma once相比header guard具有两个优点:
    • 更快。编译器不会第二次读取标记#pragma once的文件,但却会读若干遍使用header guard 的文件(寻找#endif);
    • 更简单。不再需要为每个文件的header guard取名,避免宏名重名引发的“找不到声明”问题。 缺点则是:
    • #pragma once保证物理上的同一个文件不会被包含多次,无法对头文件中的一段代码作#pragma once声明。若某个头文件具有多份拷贝(内容相同的多个文件),pragma不能保证它们不被重复包含。当然,这种重复包含很容易被发现并修正。
#ifndef  _PRJ_DIR_FILE_H  //必须确保header guard宏名永不重名
#define  _PRJ_DIR_FILE_H

//<头文件内容>

#endif
  • C++中要引用C函数时,函数所在头文件内应包含extern “C”。被extern “C”修饰的变量和函数将按照C语言方式编译和连接,否则编译器将无法找到C函数定义,从而导致链接失败。
//.h文件头部
#ifdef  __cplusplus
extern "C" {
#endif

//<函数声明>

//.h文件尾部
#ifdef  __cplusplus
}
#endif
  • 头文件内要有面向用户的充足注释,从应用角度描述接口暴露的内容。

头文件使用详解

头文件是扩展名为 .h 的文件,包含了函数声明和宏定义,能够被其他源文件(***.c)引用,一般使用 #include ***.h 来引用头文件。

引用头文件相当于复制头文件的内容,但是我们不会直接在源文件中复制头文件的内容,因为这么做很容易出错,特别在程序是由多个源文件组成的时候。

优秀程序员的习惯:

  • 将全局变量、宏定义、函数声明 放在.h文件中
  • 将函数定义 和 算法逻辑 放在.c文件中

头文件有两种类型:

  • 用户的头文件:#include “file”   // 导入用户头文件
  • 编译器头文件:#include <file>   // 导入系统头文件

头文件包含

在C语言中,#include <> 和 #include “” 是预处理器用来包含头文件的两种方式,它们的主要区别在于查找头文件的方式和位置:

  • #include <>:这种方式主要用来包含标准库的头文件。编译器会在预设的系统目录中查找头文件。例如,#include <stdio.h>。
  • #include “”:这种方式主要用来包含用户自定义的头文件。编译器首先会在当前的源文件所在的目录查找头文件,如果没有找到,然后再在预设的系统目录中查找。例如,#include “myheader.h”。

如果你引用的头文件是标准库的头文件或官方路径下的头文件,一般使用尖括号<>包含;如果你使用的头文件是自定义的或项目中的头文件,一般使用双引号””包含。头文件路径一般分为绝对路径和相对路径:绝对路径以根目录/或者Windows下的每个盘符为路径起点;相对路径则是以程序文件当前的目录为起点。

#include ”/home/wit/code/xx.h”  //Linux下的绝对路径
#include “F:/litao/code/xx.h"   //Windows下的绝对路径
#include ”../lcd/lcd.h”         //相对路径,..表示当前目录的上一层目录
#include ”./lcd.h”             //相对路径,.表示当前目录
#include ”lcd.h”               //相对路径,当前文件所在的目录

编译器在编译过程中会按照这些路径信息到指定的位置去查找头文件,然后通过预处理器作展开处理。在查找头文件的过程中,编译器会按照默认的搜索顺序到不同的路径下面去搜索,以#include 为例,当我们使用尖括号<>包含一个头文件时,头文件的搜索顺序为:

  • 通过GCC参数gcc-I指定的目录(注:大写的i)
  • 通过环境变量CINCLUDEPATH指定的目录
  • GCC的内定目录
  • 搜索规则:当不同目录下存在相同的头文件时,先搜到那个使用哪个,搜索停止

当我们使用双引号“”来包含头文件路径时,编译器会首先到项目当前目录去搜索需要的头文件,在当前项目目录下面搜不到,再到其他指定的路径下面去搜索:

  • 项目当前目录
  • 通过GCC参数gcc-I指定的目录
  • 通过环境变量CINCLUDEPATH指定的目录
  • GCC的内定目录
  • 搜索规则:当不同目录下存在相同的头文件时,先搜到那个使用哪个

在编译程序时,如果我们的头文件没有放到官方路径下面,我们可以通过gcc -I来指定头文件路径,编译器在编译程序时,就会到用户指定的路径目录下面去搜索该头文件。如果你不想通过这种方式,也可以通过设置环境变量来添加头文件的搜索路径。在Linux环境下我们经常使用的环境变量有:

  • PATH:可执行程序的搜索路径
  • C_INCLUDE_PATH: C语言头文件搜索路径
  • CPLUS_INCLUDE_PATH: C++头文件搜索路径
  • LIBRARY_PATH:库搜索路径

我们可以在一个环境变量内设置多个头文件搜索路径,各个路径之间使用冒号:隔开。如果你想每次系统开机,这个环境变量设置的路径信息都生效,可以将下面的export命令添加到系统的启动脚本::~/.bashrc文件中。

export C_INCLUDE_PATH=$C_INCLUDE_PATH:/path1:/path2

除此之外,我们也可以将头文件添加到GCC内定的官方目录下面。编译器在上面指定的各种路径下面找不到对应的头文件时,最后会到GCC的内定目录下面去寻找。这些目录是GCC在安装时,通过—prefex参数指定安装路径时指定的,常见的内定目录有:

/usr/include
/usr/local/include
/usr/include/i386-linux-gnu
/usr/lib/gcc/i686-linux-gnu/5/include
/usr/lib/gcc/i686-linux-gnu/5/include-fixed
/usr/lib/gcc-cross/arm-linux-gnueabi/5/include

所以,总结来说,#include <> 是用来引入系统的头文件,而 #include “” 是用来引入用户自定义的头文件。但是,如果系统路径中也有用户自定义的头文件的话,也是可以用 #include <> 来引入的。

头文件包含原则

在实际编程中,常常因头文件包含不当而引发编译时报告符号未定义的错误或重复定义的警告。要消除符号未定义的编译错误,只需在引用符号(变量、函数、数据类型及宏等)前确保它已被声明或定义。要消除重复定义的警告,则需合理设计头文件包含顺序和层次。

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

规则一、每个由.c和.h文件组成的模块都应符合清晰的功能

从概念上讲,一个模块就是一些可以被一起开发和维护的声明和函数,并假定会在不同的工程中被重用。不要强行合并一些需要被分开来维护的内容,也不要强行分离一些总是要被一起维护的内容。标准库中的math.h和string.h就个很明显的例子。

规则二、总是在头文件中使用『包含文件防护』

这是与#ifdef最紧密的例子。选择一个基于头文件名的防护符,这个简单的防护符会确保在一个工程中头文件名总是相互独立的。按照惯例这个防护符是全部大写的。比如Geometry_base.h文件中就应当以如下内容开头:

#ifndef GEOMETRY_BASE_H
#define GEOMETRY_BASE_H
并且以如下内容结束
 #endif

规则三、把使用本模块需要的声明都放在头文件中,这个文件也常被用来访问此模块。

因此#include进来的头文件提供了编译此模块所有必要的信息,并且让链接器能正确地链接它们。此外,如果模块 A 需要使用模块 X 的功能,那么它总是要#include “X.h”文件,永远不要出现 X 模块中的声明的硬编码。为什么呢?如果 X 模块被修改了,但是你却忘记修改那些 A 模块中那些被硬编码了的声明,那么模块 A 就会出现一些不容易被编译器和链接器发现的run-time错误。这个种行为违反了One Definition Rule但是编译器和连接器却又难以发现。总是通过一个模块的头文件来引用模块可以确保只有一处的声明需要被维护,这也对遵守One Definition Rule有帮助。

规则四、头文件只包含声明,并且他要在其.c文件中被导入

把模块中的结构体、函数原型和使用extern修饰的全局变量的声明放在.h文件中;把函数的定义和全局变量的定义以及初始化的过程放到.c文件中。.c文件必须导入对应的.h文件;编译器会检查两个文件之间的差异,并确保两个之间的一致性。

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

声明:

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)的编译警告。

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

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

规则五、将外部全局变量的声明使用extern修饰后放在头文件里,并且把声明定义放在.c文件中

全局变量的使用原则:

  • 若全局变量仅在单个源文件中访问,则可将该变量改为该文件内的静态全局变量;
  • 若全局变量仅由单个函数访问,则可将该变量改为该函数内的静态局部变量;
  • 尽量不要使用extern声明全局变量,最好提供函数访问这些变量。直接暴露全局变量是不安全的,外部用户未必完全理解这些变量的含义。
  • 设计和调用访问动态全局变量、静态全局变量、静态局部变量的函数时,需要考虑重入问题。

针对有些需要在整个程序中访问的全局变量,把它用extern修饰后放到头文件中,就像下面这样:

extern int g_number_of_entires;

其他模块在使用此模块时只需要导入对应的.h文件。在模块自身的.c文件中也要导入相应的.h文件,并且在文件开头的地方应当出现对应全局变量的声明定义,就像下面这样:

int g_number_of_entities = 0;

当然,某些默认为 0 的变量可以被直接当作初始值来使用,静态变量或者全局变量会被自动地初始化成 0。但是显式地初始化成 0 并不是必要的,因为这等于标记这个声明是定义声明,意思就是这个变量的值是唯一的可确定的。注意不同的 C 编译器和链接器可能会允许其他设置全局变量的方式,但是这是被 C++ 的标准所接受的,并且对 C 语言里也是有效的,这也确保了全局变量的One Definiton Rule。

规则六、把模块内部的声明移出头文件

有时候模块需要严格使用内部的不被外部所支持的零件来。如果某些结构体声明、全局变量或者函数仅仅在模块内部要被使用到,那就把他们只放在.c文件的顶部,并且不要在.h文件中提及它们。此外,使用static来修饰这些全局变量和函数来关联它们。

这么做的话,其他模块就不会知道(也无法知道)这些内部的声明、全局变量或者函数。使用static修饰过的声明会让链接器来强制实施它们内部的关联性。

规则七、每个头文件的都应仅仅导入此头文件所必须的一些头文件,来让这个头文件能够被正确地编译

这个头文件到底需要什么呢?如果一个结构体 X 备用来作为这个头文件的中一个结构体中的成员,那么你就必须导入X.h进来,那么编译器就知道这个 X 结构体到底是谁。不要导入一些仅仅在.c文件里需要的头文件。

举个例子的话,<math.h>就常常被使用在函数的定义中,将他在.c文件中导入,而不是.h文件。

头文件应包含哪些头文件仅取决于自身,而非包含该头文件的源文件。例如,编译源文件时需要用到头文件B,且源文件已包含头文件A,而索性将头文件B包含在头文件A中,这是错误的做法。

尽量保证用户使用此头文件时,无需手动包含其他前提头文件,即此头文件内已包含前提头文件。例如,面积相关操作的头文件Area.h内已包含关于点操作的头文件Point.h,则用户包含Area.h后无需再手动包含Point.h。这样用户就不必了解头文件的内在依赖关系。

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

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

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

规则九、一个头文件应当能被他自身所成功编译

一个头文件需要显示地导入或者传递一些任何他需要的东西。如果不遵守这个规则的话,在修改所导入的头文件或者被其他头文件所导入时会出现一些令人疑惑的问题。你可以建立一个test.c并在这个源文件里仅仅导入A.h来检查这个头文件是否可以被他自身所编译。这里不应该出现任何编译错误。如果发生错误的话说明有些内容需要被导入或者传递声明。测试所有的头文件从导入顺序的底部移到最前面,这会帮助你找到一些与其他头文件的意外的依赖问题。

规则十、源文件内的头文件包含顺序应从最特殊到一般,如:

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

优点是每个头文件必须include需要的关联头文件,否则会报错。同时,源文件同名头文件置于包含列表前端便于检查该头文件是否自完备,以及类型或函数声明是否与标准库冲突。A.c文件应该最先导入A.h文件,之后再导入其他所需的头文件

总是吧#include “A.h”放在第一位来避免丢失任何其他的所依赖的头文件。之后如果模块A的代码中需要使用模块X的话,显式地#include “X.h”,那么A.c文件并不是意外地导入X.h在其他位置。

并没有一致规定A.c文件需要再次导入A.h文件中已经导入的文件,但是有两个建议:

  • 如果h文件在逻辑上不可避免地需要出现在A.h中,那么在.c文件中再次被导入是多余的了。所以在A.c文件中不导入X.h文件是可以的。
  • 如果在h文件中#include “X.h”可以让读者更明确我们在使用 X 模块,并且 A 模块中需要对 X 模块中的某些修改所以来的话就需要在A.c文件中导入X.h。举个例子:可能我们已经有一个结构体Thing了,之后需要摆脱它,但是依然需要在实现的代码中需要他,所以导入这个头文件可以帮助我们摆脱编译错误,那么就可以再次导入X.h在A.c文件中。当然,如果X.h文件不再是必须的了,那么对这个文件的导入语句就应该被删去。

规则十一、永远不要因为任何原因导入一个.c文件

这是鲜有发生的,而且总是会把一切都搞乱。为什么会发生这种事呢?因为有时候我们为了维护上的方便而需要把一块代码放到同一个文件中而被其他.c文件锁共享,所以你把它单独分为一个文件。因为这些代码并不是由一些常规的声明和定义,你知道把他们放在一个头文件里会误导别人,所以你把它放在一个.c文件里,并且之后用#include “stuff.c”。

但是这么做会造成其他开发者或编译器的困惑,因为.c文件应当被分别编译,所以你必须另外告诉其他开发者不要编译这个.c文件。此外,如果它们丢失了这份难以纪录的点,它们会得到由编译器返回的大量奇怪的问题,让人们困惑它们应该怎么使用你的代码。

如果他并不像其他一个正常的头文件或者源文件的话,不要把他们命名成差不多的。如果你认为你必须这么做的话,首先确认这部分代码并不能被单独分成一个模块,之后将这个文件使用不一样的后缀名.inc或者.inl以来使用他。

规则十二、头文件中若能前置声明(亦称前向声明),就不要包含另一头文件。

结构体类型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)。将具体定义放在源文件中可部分避免该问题。

仅当前置声明不能满足或过于麻烦时才使用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指针代替非基本类型的值变量或指针,以避免再包含类型定义所在的头文件。但这将影响代码可读性并降低程序执行效率,应权衡利弊。

规则十三、避免包含重量级的平台头文件,如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);
}

引用头文件

#include 指令会指示 C 预处理器浏览指定的文件作为输入。预处理器的输出包含了已经生成的输出,被引用文件生成的输出以及 #include 指令之后的文本输出。例如,如果您有一个头文件 header.h,如下:

char *test (void);

和一个使用了头文件的主程序 program.c,如下:

int x;
#include "header.h"

int main (void)
{
   puts (test ());
}

编译器会看到如下代码信息:

int x;
char *test (void);

int main (void){
   puts (test ());
}

只引用一次头文件

如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中,如下:

#ifndef HEADER_FILE    // 如果没有初始化 HEADER_FILE
    #define HEADER_FILE
#endif

这种结构就是通常所说的包装器 #ifndef。当再次引用头文件时,条件为假,因为 HEADER_FILE 已定义。此时,预处理器会跳过文件的整个内容,编译器会忽略它。

有条件引用

有时需要从多个不同的头文件中选择一个引用到程序中。例如,需要指定在不同的操作系统上使用的配置参数。您可以通过一系列条件来实现这点,如下:

#if SYSTEM_1
   # include "system_1.h"
#elif SYSTEM_2
   # include "system_2.h"
#elif SYSTEM_3
   ...
#endif

但是如果头文件比较多的时候,这么做是很不妥当的,预处理器使用宏来定义头文件的名称,我们可以导入宏名称 来代替:

#define SYSTEM_H "system_1.h"
...
#include SYSTEM_H

SYSTEM_H 会扩展,预处理器会查找 system_1.h,就像 #include 最初编写的那样。SYSTEM_H 可通过 -D 选项被您的 Makefile 定义。

多个 .h 文件和多个 .c 文件

在有多个 .h 文件和多个 .c 文件的时候,往往我们会用一个 global.h 的头文件来包括所有的 .h 文件,然后在除 global.h 文件外的头文件中 包含 global.h 就可以实现所有头文件的包含,同时不会乱。方便在各个文件里面调用其他文件的函数或者变量。

#ifndef _GLOBAL_H
#define _GLOBAL_H
#include <fstream>
#include <iostream>
#include <math.h>
#include <Config.h>

头文件使用示例

定义myFun.h 在里面声明函数:

//声明函数 

int myCal(int n1, int n2, char oper);

void salHello();

myFun.c实现声明的函数:

#include<stdio.h>

int myCal(int n1, int n2, char oper) { 
    //定义一个变量 res ,保存运算的结果 
    double res = 0.0; 
    switch(oper) { 
        case '+' : 
            res = n1 + n2; 
            break; 
        
        case '-': 
            res = n1 - n2; 
            break; 
        
        case '*': 
            res = n1 * n2; 
            break; 
        
        case '/': 
            res = n1 / n2; 
            break; 
        
        default : 
            printf("你的运算符有误~"); 
        }
        
    printf("\n%d %c %d = %.2f\n", n1, oper, n2, res); 
    return res;	
}


void sayHello() { 
    //定义函数 
    printf("say Hello"); 
}

hello.c引用myfun.h

#include <stdio.h>
#include <stdlib.h>
#include "myfun.h" 
/* run this program using the console pauser or add your own getch, system("pause") or input loop */

void main() {
    //使用 myCal 完成计算任务 
    int n1 = 10; 
    int n2 = 50; 
    char oper = '-'; 
    double res = 0.0; 
    //调用 myfun.c 中定义的函数 myCal 
    
    res = myCal(n1, n2, oper); 
    printf("\nres=%.2f", res);
        
    sayHello();
}

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注