C语言头文件深度剖析

本文详细解释了C语言中头文件的作用、声明与定义的区别,强调了先声明后使用的原则,以及头文件的包含策略、预处理、内联函数等内容,特别提到了Linux内核头文件的包含方式。
摘要由CSDN通过智能技术生成

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

提示:这里可以添加本文要记录的大概内容:
软件项目中,最让新手头疼的就是各种头文件。


提示:以下是本篇文章正文内容,下面案例可供参考

一、基本概念

头文件的作用就是对一个模块(c文件)封装的API函数进行声明,其他模块想要调用这个接口函数,必须要首先包含该对应的头文件,然后才可以直接使用。

为什么要先声明后使用 – C语言的历史遗留问题(有兴趣自行研究)

在c语言项目中,除了main,跳转符号(goto)不需要声明,任何标识符在使用之前都要提前声明。可以在函数内部声明,也可以在函数外声明,也可以在头文件中声明。一般情况下,函数的声明会被直接放在头文件中,作为本模块封装的API,供其他模块使用。程序员在其他模块想要引用这些API函数,直接#include这个头文件就可以了,然后就可以直接调用了。

变量的声明和定义

一个变量的定义最终会生成与具体平台相关的内存分配汇编指令,而变量的声明则告诉编译器,该变量可能在其他文件中定义,编译时先不要报错,等链接时可以到指定的文件里去看看有没有。一个变量只能定义一次,只能够分配一次内存空间,但是可以多次声明。变量的定义要放到C文件中,不放在头文件中,因为头文件会被多人使用,被多个文件包含,头文件经过预处理器多次展开之后就编程了多次定义。

// 变量的声明declaration是指在程序中声明变量的存在,但不进行初始化或分配内存空间。
// 声明变量告诉编译器变量的名称和类型,以便在后续的程序中使用。
// 变量声明包括:声明变量的类型,声明变量的标识符
int x; // 表示声明一个整形变量

// 变量的定义是在程序中声明变量的存在,并进行初始化或分配内存空间
// 定义变量是告诉编译器为该变量分配内存空间,并为变量赋予初始值
// 包括:变量类型,变量标识符,变量的值

int y = 20; // 表示定义一个整型变量并赋予初始值为10

// 函数的声明和定义(简单理解)
// 函数的声明不包含函数的实现代码
// 函数的定义包含函数的实现代码

头文件中,除了函数声明,还有数据类型的定义,宏定义等。

如果在一个项目中多次包含相同的头文件,编译器也不会报错,因为在预处理阶段预处理器已经将头文件展开了:一个变量或函数可以有多次声明。但是如果头文件中定义了宏或新的数据类型,头文件被多次包含,编译器在编译时就会出现重定义错误。(因为多次包含,多次展开,多次定义)。为了防止冲定义错误,可以在头文件中使用条件编译来预防头文件的多次包含。

// lcd.h
#ifndef __LCD_H__ 					// __文件名_H__
#define __LCD_H__

#define PI 3.14
void lcd_init(void);

struct person{
	int age;
	char name[10];
}
#endif

上面的预处理指令可以预防头文件多次展开,当一个C文件包含多个模块的头文件,通过这种间接包含,有可能多次包含一个头文件,通过上面的预处理指令,无论包含几次,预处理过程只展开一次。

隐式声明

定义:如果一个C程序引用了在其他文件中定义的函数而没有在本文件中声明,编译器也不会报错,编译器会认为这个函数可能会在其他文件中定义,等连接的时候找不到其定义才会报错。

会给出一个warning的警告提示。但是程序依然是可以运行的。

函数的隐式声明带来的冲突,不仅仅是与自己定义函数的冲突,如果我们引用库函数而没有包含对应的头文件,也有可能与库函数发生类型冲突。这可能会给程序带来很多无法预料到的生层次的bug。因此,为了编写高质量的程序,要养成先声明后使用的良好编程习惯。

变量的声明与定义

如何对外部文件的符号进行声明呢?

可以在本文件中使用extern关键字对其他文件中的符号(标识符)进行声明

extern int i;
extern int a[20];
extern int function();

从语法角度来看,extern关键字可以扩展一个全局变量或函数声明的作用域。从编译的角度看,extern关键字用来告诉编译器,这些变量可能已经在别的文件中定义,先不要报错。

定义和声明

  • 如果省略extern并且具有初始化语句,则为定义语句。 int i= 10;
  • 如果使用extern,无初始化语句,则为声明语句。extern int i;
  • 如果没有extern 无初始化语句,试探性定义。 int i;

试探性定义:该变量可能在别的文件中有定义,先暂时声明,若别的文件中没有定义,按照语法规则初始化该变量i,并将该语句定性为定义:初始值为一些默认值NULL,0,undefined values

前向引用,前向声明

C语言规范:先声明后使用

一个变量的声明就是声明其类型,声明是给编译器用于编译器的语法检查的。在C语言中,并不是所有的标识符都需要先声明后使用的。在一个标识符未声明完成之前,就对其引用,一般成为前向引用。

  • 隐式声明(c99, c11, c++标准已经禁止)
  • 语句标号:跳转向后的标号
  • 不完全类型:在被定义完整之前用于某种用途

语句标号:goto关键字可以向前跳跃也可以向后跳跃,不需要对语句标号进行事先声明。

c语言标识符的常见类型:

  • object type
  • fuction type
  • incomplete type

不完全类型incomplete type:

  • void
  • int a[]
  • a structure or union type of unkonwn content

代码如下(示例):

goto error;
int array_print(int a[], int len);

struct LIST_NODE{
    struct LIST_NODE *next;
    int data;
}; //定义完成结束符,到这里才算对LIST_NODE 标识符声明完成。

对于一个未指定长度的数组,不需要声明就可以直接使用。在链表节点LIST_NODE结构类型中,在LIST_NODE声明完成之前,就直接在结构体内使用其类型定义了一个指针成员next。

当对一个标识符前向引用时,只关注标识符类型,不关注标识符大小,值或具体的事先。当我们对一个不完全类型进行前向引用时,我们只能使用该标识符的部分属性:类型,其他一些属性,如变量值,结构成员,大小等,是不能使用是的。

原理(有兴趣可以查阅资料,读者只是略读)

定义与声明的一致性

  • 模块的封装: xxx.c/xxx.h
  • 模块的使用:#include"xx.h"

在实际的软件项目中,经常会看到一个模块的C源文件,它会包含自己模块对应的头文件。即自己封装的模块,自己又去调用它

除了可以使用头文件中定义的宏/数据类型,也可以让编译器检查定义与声明的一致性。将函数声明和定义放到一个文件中,编译器在编译时就会帮我们进行自检:检查函数的声明和定义是否一致。

头文件路径

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

标准库的头文件或者官方路径下的头文件使用<>括号;如果你使用的头文件或者项目中的头文件 “”

头文件的路径一般分为绝对路径和相对路径。绝对路径是以根目录"/"或者windows下的盘符作为起点,相对路径则以程序文件当前的鲁姆作为起点。

在查找头文件的过程中,编译器会按照默认的搜索顺序到不同的路径下去搜索。<>头文件

  • 通过GCC参数gcc -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内定的目录下寻找。

linux内核中的头文件

一个linux内核或者驱动源文件中,头文件的包含方式通常有以下几种

#include<linux/xx.h>
#include<asm/xx.h>
#include<mach/xx.h>

这些头文件通常分布在linux内核源码的不同路径之下

更多内容,可以自行查阅

头文件中的内联函数

使用inline关键字修饰的函数就是内联函数。内联函数与普通函数不同的是,编译器在编译内联函数时,会根据需要在调用处直接展开,从而省去了函数的调用开销。


总结

提示:这里对文章进行总结:
文章简要对C语言头文件进行一些分析。对于更加深的linux内核头文件和头文件中的内联函数,本人没有做过多的探究。感兴趣,请自行查阅

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值