预处理命令
现在,我们已经把 C 语言中最基础的内容讲完了,我们回过头来看看 C 语言文件中最前面的代码 - 预处理命令
,在 C 与 C++ 中,凡是以 # 号开头的均为 预处理命令
。
严格来说预编译指令并不是 C 和 C++ 语言的组成部分,不能直接对他们进行编译。必须在对写好的代码进行编译之前,先对程序中这些特殊的命令进行 预处理
。经过预处理之后,得到的就是可供计算机直接执行的目标代码。
预编译指令的结尾都没有分号,例如:
#ifndef _STDBOOL_H
#define _STDBOOL_H
#ifndef __cplusplus
#if defined __STDC_VERSION__ && __STDC_VERSION__ > 201710L
/* bool, true and false are keywords. */
#else
#define bool _Bool
#define true 1
#define false 0
#endif
#else /* __cplusplus */
/* Supporting _Bool in C++ is a GCC extension. */
#define _Bool bool
#endif /* __cplusplus */
/* Signal that all the definitions are present. */
#define __bool_true_false_are_defined 1
#endif /* stdbool.h */
以上代码是 <stdbool.h>
头文件的源代码,它就是完全使用预处理命令写的,一般预处理命令包括以下 3 个功能:
- 文件包含 -
#include
; - 宏定义 -
#define
; - 条件编译 -
#ifdef
、#ifndef
。
#define - 宏定义
#define
叫做宏定义,作用是在预处理阶段进行文本替换,语法格式为:
#define 名字 值
#define、名字与值之间需要用空格隔开,其中最后的 值
可以是数字、表达式、代码语句等。为了防止与字符串变量混淆,习惯上,我们通常把宏名全部大写
例如,圆周率为 3.14159,凡是代码中需要用到圆周率的地方,都可以使用宏定义来代替,代码如下:
#define PI 3.14159
再比如,我们在嵌入式开发 UI 界面的时候,会需要用到各种颜色,颜色一般会使用 6 位十六进制数表示,肉眼观察的话很难理解,为了避免出现这样的数字,我们就可以使用宏定义来代替:
#define WHITE 0xFFFFFF
#define BLACK 0x000000
#define PINK 0xFFC0CB
#define BLUE 0x0000FF
#define CYAN 0x00FFFF
#define GREEN 0x008000
#define GOLD 0xFFD700
宏定义对所定义的类型没有限制,也可以替换数据类型,代码语句等等:
#include <stdio.h>
#define D double // 宏定义 double 数据类型
#define HELLO printf("Hello, world!");
int main() {
D num = 11.11;
printf("num: %.2f\n", num);
HELLO
return 0;
}
#include - 文件包含
#include
叫做文件包含命令,用来引入对应的头文件(.h 文件)。#include
的处理过程很简单,就是将头文件的内容直接插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。换句话说,#include
其实就是一种纯文本的替换操作。
在学习文件包含命令之前,我们还需要了解一下 源文件
与 头文件
的概念:
源文件(Source File)
是源代码所在文件,其中定义了各种函数、变量和数据结构等,后缀名为.c
。头文件(Header File)
是包含 C 语言函数原型、宏定义、变量、结构体等声明的文件,通常以.h
为扩展名。头文件用于在源文件中引用和共享函数和变量的声明,以便在编译时能够正确地识别和使用这些函数和变量。头文件中通常不包含实际的函数实现,而只包含函数的声明和必要的宏定义。
二者的根本区别在于:
源文件
包含了实际的代码实现,而头文件
只包含了声明和定义;源文件
用于编译和链接生成可执行文件,而头文件
用于在编译时进行函数和变量的声明和共享。
源文件和头文件之间的关系是通过预处理器指令 #include
来建立的。在源文件中使用 #include
指令引用头文件,编译器在编译源文件时会将头文件的内容插入到 #include
指令所在的位置,使得源文件中的函数和变量能够正确地识别和使用。
#include
的用法有两种,如下所示:
#include <stdio.h>
#include "my_file.h"
使用尖括号 <>
和双引号 ""
的区别在于头文件的搜索路径不同:
- 使用尖括号
<>
,编译器会到系统路径下查找头文件,而不是在当前目录中查找; - 而使用双引号
""
,编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。
简单来说就是,系统定义的头文件使用尖括号,用户定义的头文件使用双引号。
前面我们一直使用尖括号来引入标准头文件,现在我们也可以使用双引号了,如下所示:
#include "stdio.h"
#include "string.h"
stdio.h
和 string.h
都是标准头文件,它们存放于系统路径下,所以使用尖括号和双引号都能够成功引入;而我们自己编写的头文件,一般存放于当前项目的路径下,所以不能使用尖括号,只能使用双引号。
当然,你也可以把当前项目所在的目录添加到系统路径,这样就可以使用尖括号了,但是一般没人这么做,纯粹多此一举,费力不讨好。
关于 #include
用法的注意事项:
- 文件包含允许嵌套,也就是说在一个被包含的文件中又可以包含另一个文件。
- 同一个头文件可以被多次引入,多次引入的效果和一次引入的效果相同,因为头文件在代码层面有防止重复引入的机制。
什么情况下,我们会需要用到自己编写头文件呢?
一个大型的项目可以分解成多个文件进行编写、调试和管理,每个文件对应于程序中的一个模块或功能,不同的文件之间可以相互调用和使用,最终组合在一起形成完整的程序,这样的编程方式也被称为 多文件编程
。
多文件编程
的好处在于:
- 结构清晰:将程序分割成多个文件可以使代码结构更加清晰。不同的功能模块可以放在不同的文件中,使得代码逻辑更加明确,易于理解和维护。
- 代码复用:多文件编程可以促进代码的复用。将可复用的代码块抽象成一个独立的文件,其他文件可以引用这个文件中的代码,避免了重复编写相同的代码。
- 编译效率提高:当程序变得越来越庞大时,使用多文件编程可以提高编译的效率。因为每个文件的修改只会导致被修改的文件重新编译,而不需要重新编译整个程序。
- 团队合作:在大型项目中,多文件编程使得团队成员可以同时工作在不同的文件中,而不会相互影响。这样可以提高团队的开发效率,并且方便代码的合并和管理。
- 便于调试:将程序分割成多个文件,可以更方便地进行模块化调试。当出现问题时,只需要关注与问题相关的文件,而不需要对整个程序进行调试。
- 在多文件编程中,通常有一个主文件(也称为主函数所在的文件),它作为整个程序的入口点。主文件调用其他文件中的函数或者包含其他文件中的代码,以实现程序的各个功能。
在多文件编程中,一个程序通常包含多个源文件(.c文件)和一个头文件(.h文件)。头文件中通常包含函数声明、宏定义和结构体定义等信息。
常见的文件组织结构如下:
├── main.c // 程序入口文件
├── module_1.c // 模块1源文件
├── module_1.h // 模块1头文件
├── module_2.c // 模块2源文件
├── module_2.h // 模块2头文件
├── test.c // test 源文件
└── test.h // test 头文件
头文件的主要作用是包含全局变量、函数声明、宏定义和结构体定义等信息。
例如,test.h
可能包含以下内容:
#include "stdio.h"
void hello();
头文件的作用在于提供了对应模块的接口,其他文件可以通过包含该头文件,以访问其中的函数和变量。源文件分别对应不同的功能模块,通常实现了头文件中声明的函数。
例如,test.c
源文件可能包含如下内容:
#include "test.h"
void hello() {
printf("hello, world!");
}
main.c
文件是程序的入口,负责调用其他模块的函数,协调各个模块的功能,代码如下:
#include "test.h"
int main() {
// 调用 test.h 的函数
hello();
return 0;
}
条件编译
预处理的最后一个功能就是条件编译,在一般情况下,源文件的所有代码都会参加编译,以生成目标代码。但在某些特殊情况下,我们可能只想对部分满足条件的代码语句进行编译,这就需要用到 条件编译
。
在编译预处理阶段,会把 test.h
文件的内容,插入到 include 的位置,如果重复加载头文件,那么代码就会重复插入。例如:
// main.c 文件
#include "test.h"
#include "test.h"
int main() {
// 调用 test.h 的函数
hello();
return 0;
}
虽然没有报错,但是,如果我们在头文件中定义了一个结构体、枚举类型,那么就会报错。代码如下:
// test.h 文件
#include "stdio.h"
typedef struct student_info {
char name[10]; // 名字
int age; // 年龄
double height; // 身高
char gender; // 性别
} student;
enum weekday
{
MON = 1, TUE, WED, THU, FRI, SAT, SUN
};
void hello();
报错如下:

为什么我们再重复调用 stdio.h
时没有出现任何报错呢?这是因为 stdio.h 中使用了条件编译。
条件编译的关键字与 if 语句类似:
#if
#elif
#else
#endif
#ifdef
#ifndef
#ifndef
与 #endif
经常用于头文件的包含保护,以防止同一个头文件被多次包含。#ifndef
是 if not defined
的缩写,也就是 是否未定义
的意思。它们会根据 某个宏是否被定义
来控制代码的编译过程。而 #endif
就是结束 结束条件编译块
,不可以省略,并需要 #if、#ifdef、#ifndef
搭配使用
以下是使用条件编译后 test.h
头文件中的代码:
#ifndef TEST_H
#define TEST_H
#include "stdio.h"
struct student_info {
char name[100];
int age;
char gender;
};
void hello();
#endif // test.h
当第一次包含 test.h
头文件时,TEST_H
宏没有定义,所以预处理器会定义这个宏,并编译头文件的内容。如果在同一个源文件中再次包含这个头文件,由于 TEST_H
已经被定义,预处理器会跳过整个头文件的内容,从而避免重复定义和编译错误。