百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

C语言预处理指令轻松学(5)include指令用法与奇思妙想

liebian365 2024-10-25 15:37 37 浏览 0 评论


对于一个程序员来说,只要写过一个“hello world!”的C程序,就不得不用到#include指令,因为没有#include “stdio.h”,就不可能输出”hello world!”字符串,它除了可以在程序头部包含库函数头文件的用法,其实还有很多强大的用法。今天我们就重新认识一下这个貌不惊人的预处理指令。(如果有经验的程序员,可以直接跳过基础部分,为了照顾不同程度的读者,基础用法也做详细介绍。)

文件搜索机制:双引号模式

include本意就是包含的意思,把另一个文件包含进当前文件中,有两种模式:

  • #include “file1.h”
  • #include <file2.h>

双引号模式分为绝对路径和相对路径两种模式。绝对路径是指从根目录开始按指定路径搜索头文件,“盘符:\”、“盘符:\\”、“盘符:/”就是表示windows下的根目录,形如:

  • #include “盘符:\路径\文件名.扩展名”
  • #include “盘符:\\路径\文件名.扩展名”
  • #include “盘符:/路径/文件名.扩展名”

在linux环境,只能是正斜杠/并且没有盘符的概念,在windows环境里,这三种是等价的用法。

相对路径则是指从源文件所在的当前路径开始搜索头文件,“.”就是当前目录,“..”就是当前目录的父目录,最好只用正斜杠“/”用来连接目录和目录,或目录和文件,虽然也可以用反斜杠,但最好养成windows和linux风格的一致性,因为在实际开发中,几乎都是用相相对路径的方法来搜索头文件,很好用绝对路径的。好处显而易见,就是灵活性强,源文件目录可以随意移动,而不改变头文件的搜索机制。举例如下:

  1. #include “./file.ext”
  2. #include “file.ext”
  3. #include “./sub_path/file.ext”
  4. #include “sub_path/file.ext”
  5. #include “../file.ext”
  6. #include “../brother_path/file.ext”
  7. #include “../../uncle_path/file.ext”

.”表示源文件所在的当前目录,“file.ext”表示文件,两者用正斜杠连接,表示当前目录下的文件,注意,很多教程,甚至资深程序员说“./”是当前目录,是严重错误的!正斜杠只是连接符号。

第一种和第二种是等价的用法,第二种只是第一种的简化写法。第三种和第四种用法是等价的,都表示当前目录的“子目录”下的文件,第四种也省略了当前目录的写法。第五种表示要使用的头文件在父目录下,第六种表示在兄弟目录(父目录的其它子目录),第七种表示文件在“叔叔目录”下。

文件搜索机制:尖括号模式

一般情况下,如果包含的是程序员自定义的头文件,要用到双引号模式的搜索机制,而且最好要用相对目录模式。但是如果是C语言标准的库函数头文件,就用尖括号模式,会更加方便。在尖括号内直接放入要包含的文件名即可。比如:

  • #include <stdio.h>
  • #include <stdlib.h>
  • #include<stdarg.h>

尖括号本身就表示一个特定的目录,所以不用像双引号模式那样显式地指定头文件路径。在windows系统中,安装不同的c语言IDE开发环境,标准库函数的头文件路径都会不同,在同一个开发环境中,不同的库函数所在的目录也可能不一样,当然也可以一样。

比如在visual studio 2022中,以我个人的电脑为例,stdlio.h和stdlib.h都被安装在:

C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\ucrt

而stdarg.h头文件就被安装在:

C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.37.32822\include

而在我个人电脑安装的CLION2023环境里,stdlio.h和stdlib.h被安装在:

C:\Program Files\JetBrains\CLion 2023.3.2\bin\mingw\x86_64-w64-mingw32\include

而stdarg.h头文件被安装在:

C:\Program Files\JetBrains\CLion 2023.3.2\bin\mingw\lib\gcc\x86_64-w64-mingw32\13.1.0\include

我们不用关心系统提供的标准头文件的具体路径,直接尖括号访问即可。当然,这些头文件的目录可以被修改成新的位置,但是必须要在对应的IDE里设置,而不是简单的移走,我要反复强调的是,千万不要轻易这样干。

头文件查找机制的进阶思考

1)

双引号模式:预处理器会先在指定的路径寻找头文件,如果寻找不到会再到编译器自身的路径中去寻找(存放在IDE或操作系统的环境变量中)。

尖括号:预处理只会到编译器默认的目录中去寻找。

2)

程序员自定义的头文件也可以使用尖括号模式访问,比如#include <myheader.h>。有两种方法,比如可以直接放在编译器的默认目录里,比如include目录,或者其他目录,比如stdio.h所在的目录里都可以。也可以在IDE中设置一个新的路径的环境变量也可以。

类似的上面情况,也可以反过来把标准库函数用双引号模式访问,比如#include “stdio.h”,这两种颠倒或混用的做法,我强烈不建议。

如果你需要把源代码编译成静态链接库lib或者动态链接库dll,提供给其他项目使用,就需要放到标准库的目录里,用尖括号访问是合适的,如果想了解如何用C语言生成静态或动态库,可以看我专栏的相关文章。

否则,尖括号和双引号可以让代码阅读者,能够一眼分辨出哪些是自定义头文件,哪些是标准头文件,混用的情况下,会让维护者迷惑,不是一种良好的编程实践。

包含防护机制

头文件的包含防护机制,非常重要,我在介绍预处理指令#pragma once、条件编译指令的用法时,都提到过。

include指令就是将头文件的全部内容原样复制到当前文件里,如果一个头文件被间接或直接的多次被“包含”到当前源文件,编译的时候必然报错。比如:

//code.c
#include “myheader.h”
#include “other.h”
//other.h
#include “nyheader.h”

这是最简单的例子,在other.h里间接的重复包含了myheader.h文件的内容。为了避免一个头文件被多次重复包含进同一个源文件的问题,要么使用在头文件的顶部插入一条#pragma once指令,表示这个头文件在同一个源文件里只能包含一次,关于#pragma指令的俄详细用法,可以参见《段誉和语言》的相关文章,要么使用条件编译指令,条件编译指令的用法就成为头文件包含防护机制,使用起来也很简单,就是在头文件的顶部插入如下一段代码:

#ifndef _MY_HEADER_H
#define _MY_HEADER_H
......
#endif/*my_header.h*/

_MY_HEADER_H要替换为头文件的文件名的大写字母,当这个头文件第一次被包含时,标识符 _MY_HEADER_H是没有被定义的,所以#ifndef _MY_HEADER_H语句为真,里面内容会被包含进去,省略号指代的是头文件内容。当这个头文件第二次或多次被包含时,因为#ifndef _MY_HEADER_H已经被定义了,所以#define _MY_HEADER_H为假,就不被执行,就不会被重复包含。关于条件编译指令的详细用法,可以看《段誉和语言》的相关文章。

include的本质:到底什么是“文件包含”

include本意就是包含的意思,把另一个文件包含进当前文件中,实际上只要是文件内容是文本格式,都可以被包含进来,比如.c、.h、.txt都是文本格式,就可以被包含进来。打个比方,让下面这段代码:

#include <stdio.h>
int main(){
int x,y;
x=3;
y=2;
printf(“%d\n”,x+y);
return0;
}

我们完全可以把其中部分代码放进另一个文件,比如file.txt,然后包含进来:

#include <stdio.h>
intmain(){
int x,y;
#include “file.txt”
return 0;
}
//file.txt
x = 3;
y = 2;
printf(“%d\n”,x+y);

现实中,没人会这样写,这个例子就是为了演示include的本质局势简单的包含功能。而且照样可以编译通过。但是,我们一般约定俗成的吧源代码保存为后缀为c的文件,函数的声明保存后缀为h的文件。

巧用这个特性,我们可以有很多使用有趣的用法,举两个例子分享一下。

include化简数组

比如我们定义了一个数组,元素非常多,放在源程序里影响看代码,可以放在另外的文本文件里保存,既让代码清爽,又易于修改和维护。比如:

//src.c
char *namelst[] = {
#include “names.txt”
};
//names.txt
“Tom”,
“Jack”,
......
“vicky”

通过#include “names.txt”,会让src.c文件清爽易读,而又不用去关心数组元素的具体内容。这里有个细节要说到,那就是:

#include指令必须要独占一行

用#include包含文件时,一定要单独占一行,也就是#include语句当前行的前后必须都是空白。因为预处理器会将被包含的文件的全部内容从当前行的下一行开始插入,前后不为空白,就会出错。

之前我在另一篇讲#include用法时,提到将大量数组元素包进文本文件,有个粉丝测试发现int a[]={#include “items.txt”};报错,编译不通过,就是这个原因。

include调试程序

有时候我们需要对几个代码块进行调试,一般都是通过条件编译指令来控制哪一个代码块生效,但是如果用include就会非常方便和省事,比如:

int main(){
....
//功能1:
//#include “func1.txt”
//功能2:
//#include “func2.txt”
....

当我们需要调试代码块1的时候,只要去掉#include “func1.txt”前面的注释就可以了,不挑食的就注释掉,调试的就去掉注释,非常的方便和间接,否则就必须要提哦啊见编译指令,或者用注释成块掉。我这个例子非常简单,反应不出来include的作用,在这种简单场景下,注释更方便,在复杂的情况下,就会显出include的省事。

还有更重要的一个用法,有时候我们会需要再一个项目里包含多个源文件,每个源文件都有一个main函数,因为main函数都对应某个功能的程序,编译肯定不给通过。

我们常规且简单的做法,就是一个main函数的源文件都单独为一个项目,要么通过cmake来控制在同一时间只能有一个main函数来启用。这个用法在我的另一篇文章里也有详细的介绍过。

今天我介绍另一个更加巧妙的办法,因为cmake是微软visual studio IDE的弱项,功能支持比较差,用起来还不如CLION里用的方便,但是有时候又需要使用VS,因为编译器调试能力非常优秀,所以我经常这样干:建立一个源文件:

#include “main01.h”
#include “main02.h”
#include “main03.h”
......

每个main文件里都有一个main函数,需要调试的去掉注释,不调试的就注释掉。非常简单好用,我放一个我的截图如下:

这个截图是我的项目文件夹的组织结构,一个源.c的文件作为开关控制要编译的头文件,每个.h文件都对应一个要调试的小功能。下面的图2就是源文件的内容:

下面的图就是其中一个要调试的功能,这里举的是B.h 这个文件,其实后缀改为.c也没问题,主要是为了不和源文件混淆。B.h的内容如下,注意做上角的红箭头,指明了是B.H文件:

本篇include介绍就写到这里,其他有趣的玩法,请继续关注我的技术专栏:段誉和语言。

段誉,写于合肥。

相关推荐

go语言也可以做gui,go-fltk让你做出c++级别的桌面应用

大家都知道go语言生态并没有什么好的gui开发框架,“能用”的一个手就能数的清,好用的就更是少之又少。今天为大家推荐一个go的gui库go-fltk。它是通过cgo调用了c++的fltk库,性能非常高...

旧电脑的首选系统:TinyCore!体积小+精简+速度极快,你敢安装吗

这几天老毛桃整理了几个微型Linux发行版,准备分享给大家。要知道可供我们日常使用的Linux发行版有很多,但其中的一些发行版经常会被大家忽视。其实这些微型Linux发行版是一种非常强大的创新:在一台...

codeblocks和VS2019下的fltk使用中文

在fltk中用中文有点问题。英文是这样。中文就成这个样子了。我查了查资料,说用UTF-8编码就行了。edit->Fileencoding->UTF-8然后保存文件。看下下边的编码指示确...

FLTK(Fast Light Toolkit)一个轻量级的跨平台Python GUI库

FLTK(FastLightToolkit)是一个轻量级的跨平台GUI库,特别适用于开发需要快速、高效且简单界面的应用程序。本文将介绍Python中的FLTK库,包括其特性、应用场景以及如何通过代...

中科院开源 RISC-V 处理器“香山”流片,已成功运行 Linux

IT之家1月29日消息,去年6月份,中科院大学教授、中科院计算所研究员包云岗,发布了开源高性能RISC-V处理器核心——香山。近日,包云岗在社交平台晒出图片,香山芯片已流片,回片后...

Linux 5.13内核有望合并对苹果M1处理器支持的初步代码

预计Linux5.13将初步支持苹果SiliconM1处理器,不过完整的支持工作可能还需要几年时间才能完全完成。虽然Linux已经可以在苹果SiliconM1上运行,但这需要通过一系列的补丁才能...

Ubuntu系统下COM口测试教程(ubuntu port)

1、在待测试的板上下载minicom,下载minicom有两种方法:方法一:在Ubuntu软件中心里面搜索下载方法二:按“Ctrl+Alt+T”打开终端,打开终端后输入“sudosu”回车;在下...

湖北嵌入式软件工程师培训怎么选,让自己脱颖而出

很多年轻人毕业即失业、面试总是不如意、薪酬不满意、在家躺平。“就业难”该如何应对,参加培训是否能改变自己的职业走向,在湖北,有哪些嵌入式软件工程师培训怎么选值得推荐?粤嵌科技在嵌入式培训领域有十几年经...

新阁上位机开发---10年工程师的Modbus总结

前言我算了一下,今年是我跟Modbus相识的第10年,从最开始的简单应用到协议了解,从协议开发到协议讲解,这个陪伴了10年的协议,它一直没变,变的只是我对它的理解和认识。我一直认为Modbus协议的存...

创建你的第一个可运行的嵌入式Linux系统-5

@ZHangZMo在MicrochipBuildroot中配置QT5选择Graphic配置文件增加QT5的配置修改根文件系统支持QT5修改output/target/etc/profile配置文件...

如何在Linux下给zigbee CC2530实现上位机

0、前言网友提问如下:粉丝提问项目框架汇总下这个网友的问题,其实就是实现一个网关程序,内容分为几块:下位机,通过串口与上位机相连;下位机要能够接收上位机下发的命令,并解析这些命令;下位机能够根据这些命...

Python实现串口助手 - 03串口功能实现

 串口调试助手是最核心的当然是串口数据收发与显示的功能,pzh-py-com借助的是pySerial库实现串口收发功能,今天痞子衡为大家介绍pySerial是如何在pzh-py-com发挥功能的。一、...

为什么选择UART(串口)作为调试接口,而不是I2C、SPI等其他接口

UART(通用异步收发传输器)通常被选作调试接口有以下几个原因:简单性:协议简单:UART的协议非常简单,只需设置波特率、数据位、停止位和校验位就可以进行通信。相比之下,I2C和SPI需要处理更多的通...

同一个类,不同代码,Qt 串口类QSerialPort 与各种外设通讯处理

串口通讯在各种外设通讯中是常见接口,因为各种嵌入式CPU中串口标配,工业控制中如果不够还通过各种串口芯片进行扩展。比如spi接口的W25Q128FV.对于软件而言,因为驱动接口固定,软件也相对好写,因...

嵌入式linux为什么可以通过PC上的串口去执行命令?

1、uboot(负责初始化基本硬bai件,如串口,网卡,usb口等,然du后引导系统zhi运行)2、linux系统(真正的操作系统)3、你的应用程序(基于操作系统的软件应用)当你开发板上电时,u...

取消回复欢迎 发表评论: