C语言预处理指令完全指南 目录0. 简介1. #define2. 带参数的宏2.1 基本用法2.2 #运算符##运算符2.3 不定参数的宏3. #undef4. #include5. #if...#endif6. #ifdef...#endif7. defined 运算符8. #ifndef...#endif9. 预定义宏10. #line11. #error12. #pragma0. 简介C 语言编译器在编译程序之前会先使用预处理器preprocessor处理代码。预处理器首先会清理代码进行删除注释、多行语句合成一个逻辑行等工作。然后执行#开头的预处理指令。本章介绍 C 语言的预处理指令。预处理指令可以出现在程序的任何地方但是习惯上往往放在代码的开头部分。每个预处理指令都以#开头放在一行的行首指令前面可以有空白字符比如空格或制表符。#和指令的其余部分之间也可以有空格但是为了兼容老的编译器一般不留空格。所有预处理指令都是一行的除非在行尾使用反斜杠将其折行。指令结尾处不需要分号。1. #define#define是最常见的预处理指令用来将指定的词替换成另一个词。它的参数分成两个部分第一个参数就是要被替换的部分其余参数是替换后的内容。每条替换规则称为一个宏macro。#define MAX 100上面示例中#define指定将源码里面的MAX全部替换成100。MAX就称为一个宏。宏的名称不允许有空格而且必须遵守 C 语言的变量命名规则只能使用字母、数字与下划线_且首字符不能是数字。宏是原样替换指定什么内容就一模一样替换成什么内容。#define HELLO Hello, world // 相当于 printf(%s, Hello, world); printf(%s, HELLO);上面示例中宏HELLO会被原样替换成Hello, world。#define指令可以出现在源码文件的任何地方从指令出现的地方到文件末尾都有效。习惯上会将#define放在源码文件的头部。它的主要好处是会使得程序的可读性更好也更容易修改。#define指令从#开始一直到换行符为止。如果整条指令过长可以在折行处使用反斜杠延续到下一行。#define OW C programming language is invented \ in 1970s.上面示例中第一行结尾的反斜杠将#define指令拆成两行。#define允许多重替换即一个宏可以包含另一个宏。#define TWO 2 #define FOUR TWO*TWO上面示例中FOUR会被替换成2*2。注意如果宏出现在字符串里面即出现在双引号中或者是其他标识符的一部分就会失效并不会发生替换。#define TWO 2 // 输出 TWO printf(TWO\n); // 输出 22 const TWOs 22; printf(%d\n, TWOs);上面示例中双引号里面的TWO以及标识符TWOs都不会被替换。同名的宏可以重复定义只要定义是相同的就没有问题。如果定义不同就会报错。// 正确 #define FOO hello #define FOO hello // 报错 #define BAR hello #define BAR world上面示例中宏FOO没有变化所以可以重复定义宏BAR发生了变化就报错了。2. 带参数的宏2.1 基本用法宏的强大之处在于它的名称后面可以使用括号指定接受一个或多个参数。#define SQUARE(X) X*X上面示例中宏SQUARE可以接受一个参数X替换成X*X。注意宏的名称与左边圆括号之间不能有空格。这个宏的用法如下。// 替换成 z 2*2; z SQUARE(2);这种写法很像函数但又不是函数而是完全原样的替换会跟函数有不一样的行为。#define SQUARE(X) X*X // 输出19 printf(%d\n, SQUARE(3 4));上面示例中SQUARE(3 4)如果是函数输出的应该是497*7宏是原样替换所以替换成3 4*3 4最后输出19。可以看到原样替换可能导致意料之外的行为。解决办法就是在定义宏的时候尽量多使用圆括号这样可以避免很多意外。#define SQUARE(X) ((X) * (X))上面示例中SQUARE(X)替换后的形式有两层圆括号就可以避免很多错误的发生。宏的参数也可以是空的。#define getchar() getc(stdin)上面示例中宏getchar()的参数就是空的。这种情况其实可以省略圆括号但是加上了会让它看上去更像函数。一般来说带参数的宏都是一行的。下面是两个例子。#define MAX(x, y) ((x)(y)?(x):(y)) #define IS_EVEN(n) ((n)%20)如果宏的长度过长可以使用反斜杠\折行将宏写成多行。#define PRINT_NUMS_TO_PRODUCT(a, b) { \ int product (a) * (b); \ for (int i 0; i product; i) { \ printf(%d\n, i); \ } \ }上面示例中替换文本放在大括号里面这是为了创造一个块作用域避免宏内部的变量污染外部。带参数的宏也可以嵌套一个宏里面包含另一个宏。#define QUADP(a, b, c) ((-(b) sqrt((b) * (b) - 4 * (a) * (c))) / (2 * (a))) #define QUADM(a, b, c) ((-(b) - sqrt((b) * (b) - 4 * (a) * (c))) / (2 * (a))) #define QUAD(a, b, c) QUADP(a, b, c), QUADM(a, b, c)上面示例是一元二次方程组求解的宏由于存在正负两个解所以宏QUAD先替换成另外两个宏QUADP和QUADM后者再各自替换成一个解。那么什么时候使用带参数的宏什么时候使用函数呢一般来说应该首先使用函数它的功能更强、更容易理解。宏有时候会产生意想不到的替换结果而且往往只能写成一行除非对换行符进行转义但是可读性就变得很差。宏的优点是相对简单本质上是字符串替换不涉及数据类型不像函数必须定义数据类型。而且宏将每一处都替换成实际的代码省掉了函数调用的开销所以性能会好一些。另外以前的代码大量使用宏尤其是简单的数学运算为了读懂前人的代码需要对它有所了解。2.2 #运算符##运算符由于宏不涉及数据类型所以替换以后可能为各种类型的值。如果希望替换后的值为字符串可以在替换文本的参数前面加上#。#define STR(x) #x // 等同于 printf(%s\n, 3.14159); printf(%s\n, STR(3.14159));上面示例中STR(3.14159)会被替换成3.14159。如果x前面没有#这会被解释成一个浮点数有了#以后就会被转换成字符串。下面是另一个例子。#define XNAME(n) x#n // 输出 x4 printf(%s\n, XNAME(4));上面示例中#n指定参数输出为字符串再跟前面的字符串结合最终输出为x4。如果不加#这里实现起来就很麻烦了。如果替换后的文本里面参数需要跟其他标识符连在一起组成一个新的标识符可以使用##运算符。它起到粘合作用将参数“嵌入”一个标识符之中。#define MK_ID(n) i##n上面示例中n是宏MK_ID的参数这个参数需要跟标识符i粘合在一起这时i和n之间就要使用##运算符。下面是这个宏的用法示例。int MK_ID(1), MK_ID(2), MK_ID(3); // 替换成 int i1, i2, i3;上面示例中替换后的文本i1、i2、i3是三个标识符参数n是标识符的一部分。从这个例子可以看到##运算符的一个主要用途是批量生成变量名和标识符。2.3 不定参数的宏宏的参数还可以是不定数量的即不确定有多少个参数...表示剩余的参数。#define X(a, b, ...) (10*(a) 20*(b)), __VA_ARGS__上面示例中X(a, b, ...)表示X()至少有两个参数多余的参数使用...表示。在替换文本中__VA_ARGS__代表多余的参数每个参数之间使用逗号分隔。下面是用法示例。X(5, 4, 3.14, Hi!, 12) // 替换成 (10*(5) 20*(4)), 3.14, Hi!, 12注意...只能替代宏的尾部参数不能写成下面这样。// 报错 #define WRONG(X, ..., Y) #X #__VA_ARGS__ #Y上面示例中...替代中间部分的参数这是不允许的会报错。__VA_ARGS__前面加上一个#号可以让输出变成一个字符串。#define X(...) #__VA_ARGS__ printf(%s\n, X(1,2,3)); // Prints 1, 2, 33. #undef#undef指令用来取消已经使用#define定义的宏。#define LIMIT 400 #undef LIMIT上面示例的undef指令取消已经定义的宏LIMIT后面就可以重新用LIMIT定义一个宏。有时候想重新定义一个宏但不确定是否以前定义过就可以先用#undef取消然后再定义。因为同名的宏如果两次定义不一样会报错而#undef的参数如果是不存在的宏并不会报错。GCC的-U选项可以在命令行取消宏的定义相当于#undef。$ gcc -ULIMIT foo.c上面示例中的-U参数取消了宏LIMIT相当于源文件里面的#undef LIMIT。4. #include#include指令用于编译时将其他源码文件加载进入当前文件。它有两种形式。// 形式一 #include foo.h // 加载系统提供的文件 // 形式二 #include foo.h // 加载用户提供的文件形式一文件名写在尖括号里面表示该文件是系统提供的通常是标准库的库文件不需要写路径。因为编译器会到系统指定的安装目录里面去寻找这些文件。形式二文件名写在双引号里面表示该文件由用户提供具体的路径取决于编译器的设置可能是当前目录也可能是项目的工作目录。如果所要包含的文件在其他位置就需要指定路径下面是一个例子。#include /usr/local/lib/foo.hGCC编译器的-I参数也可以用来指定include命令中用户文件的加载路径。$ gcc -Iinclude/ -o code code.c上面命令中-Iinclude/指定从当前目录的include子目录里面加载用户自己的文件。#include最常见的用途就是用来加载包含函数原型的头文件后缀名为.h参见《多文件编译》一章。多个#include指令的顺序无关紧要多次包含同一个头文件也是合法的。5. #if...#endif#if...#endif指令用于预处理器的条件判断满足条件时内部的行会被编译否则就被编译器忽略。#if 0 const double pi 3.1415; // 不会执行 #endif上面示例中#if后面的0表示判断条件不成立。所以内部的变量定义语句会被编译器忽略。#if 0这种写法常用来当作注释使用不需要的代码就放在#if 0里面。#if后面的判断条件通常是一个表达式。如果表达式的值不等于0就表示判断条件为真编译内部的语句如果表达式的值等于0表示判断条件为伪则忽略内部的语句。#if...#endif之间还可以加入#else指令用于指定判断条件不成立时需要编译的语句。#define FOO 1 #if FOO printf(defined\n); #else printf(not defined\n); #endif上面示例中宏FOO如果定义过会被替换成1从而输出defined否则输出not defined。如果有多个判断条件还可以加入#elif命令。#if HAPPY_FACTOR 0 printf(Im not happy!\n); #elif HAPPY_FACTOR 1 printf(Im just regular\n); #else printf(Im extra happy!\n); #endif上面示例中通过#elif指定了第二重判断。注意#elif的位置必须在#else之前。如果多个判断条件皆不满足则执行#else的部分。没有定义过的宏等同于0。因此如果UNDEFINED是一个没有定义过的宏那么#if UNDEFINED为伪而#if !UNDEFINED为真。#if的常见应用就是打开或关闭调试模式。#define DEBUG 1 #if DEBUG printf(value of i : %d\n, i); printf(value of j : %d\n, j); #endif上面示例中通过将DEBUG设为1就打开了调试模式可以输出调试信息。GCC的-D参数可以在编译时指定宏的值因此可以很方便地打开调试开关。$ gcc -DDEBUG1 foo.c上面示例中-D参数指定宏DEBUG为1相当于在代码中指定#define DEBUG 1。6. #ifdef...#endif#ifdef...#endif指令用于判断某个宏是否定义过。有时源码文件可能会重复加载某个库为了避免这种情况可以在库文件里使用#define定义一个空的宏。通过这个宏判断库文件是否被加载了。#define EXTRA_HAPPY上面示例中EXTRA_HAPPY就是一个空的宏。然后源码文件使用#ifdef...#endif检查这个宏是否定义过。#ifdef EXTRA_HAPPY printf(Im extra happy!\n); #endif上面示例中#ifdef检查宏EXTRA_HAPPY是否定义过。如果已经存在表示加载过库文件就会打印一行提示。#ifdef可以与#else指令配合使用。#ifdef EXTRA_HAPPY printf(Im extra happy!\n); #else printf(Im just regular\n); #endif上面示例中如果宏EXTRA_HAPPY没有定义过就会执行#else的部分。#ifdef...#else...#endif可以用来实现条件加载。#ifdef MAVIS #include foo.h #define STABLES 1 #else #include bar.h #define STABLES 2 #endif上面示例中通过判断宏MAVIS是否定义过实现加载不同的头文件。7. defined 运算符上一节的#ifdef指令等同于#if defined。#ifdef FOO // 等同于 #if defined FOO上面示例中defined是一个预处理运算符如果它的参数是一个定义过的宏就会返回1否则返回0。使用这种语法可以完成多重判断。#if defined FOO x 2; #elif defined BAR x 3; #endif这个运算符的一个应用就是对于不同架构的系统加载不同的头文件。#if defined IBMPC #include ibmpc.h #elif defined MAC #include mac.h #else #include general.h #endif上面示例中不同架构的系统需要定义对应的宏。代码根据不同的宏加载对应的头文件。8. #ifndef...#endif#ifndef...#endif指令跟#ifdef...#endif正好相反。它用来判断如果某个宏没有被定义过则执行指定的操作。#ifdef EXTRA_HAPPY printf(Im extra happy!\n); #endif #ifndef EXTRA_HAPPY printf(Im just regular\n); #endif上面示例中针对宏EXTRA_HAPPY是否被定义过#ifdef和#ifndef分别指定了两种情况各自需要编译的代码。#ifndef常用于防止重复加载。举例来说为了防止头文件myheader.h被重复加载可以把它放在#ifndef...#endif里面加载。#ifndef MYHEADER_H #define MYHEADER_H #include myheader.h #endif上面示例中宏MYHEADER_H对应文件名myheader.h的大写。只要#ifndef发现这个宏没有被定义过就说明该头文件没有加载过从而加载内部的代码并会定义宏MYHEADER_H防止被再次加载。#ifndef等同于#if !defined。#ifndef FOO // 等同于 #if !defined FOO9. 预定义宏C 语言提供一些预定义的宏可以直接使用。__DATE__编译日期格式为“Mmm dd yyyy”的字符串比如 Nov 23 2021。__TIME__编译时间格式为“hh:mm:ss”。__FILE__当前文件名。__LINE__当前行号。__func__当前正在执行的函数名。该预定义宏必须在函数作用域使用。__STDC__如果被设为1表示当前编译器遵循 C 标准。__STDC_HOSTED__如果被设为1表示当前编译器可以提供完整的标准库否则被设为0嵌入式系统的标准库常常是不完整的。__STDC_VERSION__编译所使用的 C 语言版本是一个格式为yyyymmL的长整数C99 版本为“199901L”C11 版本为“201112L”C17 版本为“201710L”。下面示例打印这些预定义宏的值。#include stdio.h int main() { printf(This function: %s\n, __func__); printf(This file: %s\n, __FILE__); printf(This line: %d\n, __LINE__); printf(Compiled on: %s %s\n, __DATE__, __TIME__); printf(C Version: %ld\n, __STDC_VERSION__); }输出如下10. #line#line指令用于覆盖预定义宏__LINE__将其改为自定义的行号。后面的行将从__LINE__的新值开始计数。// 将下一行的行号重置为 300 #line 300上面示例中紧跟在#line 300后面一行的行号将被改成300其后的行会在300的基础上递增编号。#line还可以改掉预定义宏__FILE__将其改为自定义的文件名。#line 300 newfilename上面示例中下一行的行号重置为300文件名重置为newfilename。11. #error#error指令用于让预处理器抛出一个错误终止编译。#if __STDC_VERSION__ ! 201112L #error Not C11 #endif上面示例指定如果编译器不使用 C11 标准就中止编译。GCC编译器会像下面这样报错。$ gcc -stdc99 newish.c newish.c:14:2: error: #error Not C11上面示例中GCC使用C99 标准编译就报错了。#if INT_MAX 100000 #error int type is too small #endif上面示例中编译器一旦发现INT类型的最大值小于100,000就会停止编译。#error指令也可以用在#if...#elif...#else的部分。#if defined WIN32 // ... #elif defined MAC_OS // ... #elif defined LINUX // ... #else #error NOT support the operating system #endif12. #pragma#pragma指令用来修改编译器属性。// 使用 C99 标准 #pragma c9x on上面示例让编译器以C99标准进行编译。

相关新闻

最新新闻

程序员的光荣与梦想——论侠客梦的延续与幻灭

程序员的光荣与梦想——论侠客梦的延续与幻灭

这不是很奇怪么?所有行业的初学者都可以被称作“菜鸟”,但是只有电脑高手(特别是程序牛人)被称作“大侠”。这到底是巧合呢,还是另有原因?今天1-2-3吃饱了撑着没事干,跟大家一起YY下这个问题。 …

2026/7/6 3:44:34
真实项目中的四重奏式特征筛选:数据质量、统计相关、多变量稳定与业务终审

真实项目中的四重奏式特征筛选:数据质量、统计相关、多变量稳定与业务终审

1. 这不是又一篇“调个sklearn就完事”的 Feature Selection 教程你点开这篇,大概率刚学完 Pandas 和 Scikit-learn 的基础 API,正对着一个真实数据集发愁:列有 47 个,其中 3 个是 ID 字段、5 个是时间戳拆出来的冗余特征、2 个明…

2026/7/6 3:44:34
现代化智能终端AShell,是否能够替代你的古法终端?让服务器运维更加高效智能化,快来试试看!

现代化智能终端AShell,是否能够替代你的古法终端?让服务器运维更加高效智能化,快来试试看!

现代化智能终端AShell 最近Vibe Coding了一款现代化智能终端软件,把本地终端、远程 SSH、SFTP、主机监控、端口转发与 AI 助手整合到了一个桌面应用里。 技术栈主要使用Tauri Vue3 Xterm.js Claude Code。 AI使用演示 Ashell使用演示消息广播使用演示 Ashell 消…

2026/7/6 3:44:34
Python量化交易系统实战:从回测到实盘的工程化落地

Python量化交易系统实战:从回测到实盘的工程化落地

1. 这不是“学Python”,而是用Python重新理解金融市场你点开这个标题,大概率不是想系统学Python语法——你手头可能已经能写个爬虫、处理过Excel表格,甚至用pandas做过基础分析。但当你真正想把代码扔进实盘环境、让程序替你盯盘、下单、风控…

2026/7/6 3:44:34
Biotin-PEG8-oxyamine HCl salt,生物素-八聚乙二醇-氧胺盐酸盐

Biotin-PEG8-oxyamine HCl salt,生物素-八聚乙二醇-氧胺盐酸盐

基础信息中文名称:生物素-八聚乙二醇-氧胺盐酸盐英文名称:Biotin-PEG8-oxyamine HCl salt类型:小分子PEG生物素氧胺肼衍生物,外观通常为白色固体粉末核心结构:由生物素(Biotin)、八聚乙二醇&…

2026/7/6 3:44:34
PCB布局3大常见误区解析:从BGA阴影效应到40mil间距的工程取舍

PCB布局3大常见误区解析:从BGA阴影效应到40mil间距的工程取舍

PCB布局3大常见误区解析:从BGA阴影效应到40mil间距的工程取舍在硬件工程师的日常工作中,PCB布局往往是最容易被低估却又最影响最终产品性能的环节。许多初学者在完成原理图设计后,常常迫不及待地将元器件"塞"进电路板,却…

2026/7/6 3:39:34

月新闻