【C语言】C 预处理器(C Preprocessor)

发布于:2024-04-16 ⋅ 阅读:(180) ⋅ 点赞:(0)

C provides certain language facilities by means of a preprocessor,which is conceptually a separate first step in compilation.

The two most frequently used features are #include, to include the contents of a file during compilation, and #define, to replace a token by an arbitrary sequence of characters.

Other features described in this section include conditional compilation and macros with arguments.

【来自:《The C Programming Language(Second Edition)》  (美) Brian W.Kernighan   Dennis M.Ritchie】

【C语言】C 预处理器:

  • 编译器在编译程序时,预处理器在实际编译之前进行预处理:先清理代码(删除注释、多行语句合并成一个逻辑行)、执行预处理指令。
  • 预处理器不是编译器的组成部分,而是编译过程中独立的第一步,是文本替代工具。
  • 预处理指令常用的有#include和#define。其它的还有条件编译、带参数的宏等。
  • 预处理指令必须是#开头,一般在代码开头,不需要分号结尾。
  • 预处理指令一般是一行,可在行尾用"\"折返成多行。

预处理指令
指令 描述
#include 包含一个源代码文件
#define 定义宏
#undef 取消已定义的宏
#ifdef 如果宏已经定义,则返回真
#ifndef 如果宏没有定义,则返回真
#if 如果给定条件为真,则编译下面代码
#else #if 的替代方案
#elif 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码
#endif 结束一个 #if……#else 条件编译块
#error 当遇到标准错误时,输出错误消息
#pragma 使用标准化方法,向编译器发布特殊的命令到编译器中

1、#include   引入头文件

头文件:

① 扩展名为.h的文件。

② 包含系统全局变量、宏定义、函数声明。被多个源文件引用共享。

③ 引入的头文件中,也可能引入其它头文件。

④ 有两种类型的头文件:

  • 系统头文件:编译器自带的头文件。在系统目录中。
  • 用户头文件:程序员编写的头文件。在当前目录中,也可能在其它目录中。若在其它目录下,需要指定路径。
#include <stdio.h>       // 引入系统头文件,在系统目录下查找
#include "example.h"     // 引入用户头文件,在当前目录下查找,若没有,再在系统目录下查找
#include "D:/hello.h"    // 引入用户头文件,且该文件不在当前目录,而在D盘

注意:

  • 一个#include指令只能引入一个头文件。引入多个头文件,需多个#include指令。
  • 引入头文件的地方将替换为该头文件的内容。类似于在引入头文件的地方将该头文件内容复制粘贴,与当前源文件连接成一个源文件。

Any source line of the form  #include "filename" or #include <filename>  is replaced by the contents of the file filename.

C语言的常用标准库
引入系统头文件 概述 说明
#include <assert.h> 程序诊断 设定插入点(assert),用于程序诊断
#include <ctype.h> 字符 判断和映射字符
#include <errno.h> 错误 全局整型变量errno,发生错误时记录错误码
#include <float.h> 浮点数 浮点数的限制值(基数最大值/最小值、指数最大值/最小值等)
#include <limits.h> 限制值 基本数据类型的限制值(整型最大值/最小值、无符号整型最大值/最小值等)
#include <locale.h> 本地化 设置特定地域(日期格式/货币符号等)
#include <math.h> 数学 用于数学计算的函数
#include <setjmp.h> 跳转 绕过正常的调用/返回规则。保存和恢复当前环境
#include <signal.h> 信号 处理程序执行期间报告的不同信号(中断、异常等的处理)
#include <stdarg.h> 参数 参数个数可变时获取函数参数
#include <stddef.h> 常用定义 定义常用类型和宏,
其中宏offsetof(结构体成员相对于结构体开头的偏移量)
#include <stdio.h> 输入/输出 输入/输出(标准/文件等)
#include <stdlib.h> 通用函数 内存分配、字符串转为整数等、程序终止、二分查找等其它通用工具函数
#include <string.h> 字符串 操作字符串
#include <time.h> 日期时间 操作日期和时间

2、#define, #undef     宏定义, 取消宏定义

(2-1)不带参数的宏定义

#define 宏名 替换文本

// 例如: example.h
#define NUMBER 10

#define 告诉预处理器,将代码中所有出现的宏名替换成指定的替换文本。

但字符串双引号中的以及与其它标识符(例如:局部变量名)冲突的不会被替换。

#include <stdio.h>
#include "example.h"                       // 已定义宏NUMBER

int main(void)
{
    printf("NUMBER is %d\n", NUMBER);      // 字符串双引号中的不会被替换
    return 0;
}

// 结果:
NUMBER is 10

同名的宏可以重复定义,但定义必须相同,否则报错。

#include "example.h"          // 已定义宏NUMBER 
#define NUMBER 5              // warning: "NUMBER" redefined

#include "example.h"          // 已定义宏NUMBER
#define NUMBER 10             // 没有报错

若要重新定义宏,需先取消之前的定义,再定义。

#include <stdio.h>
#include "example.h"       // 已定义宏NUMBER
#undef NUMBER              // 取消已定义的宏
#define NUMBER 5           // 重新定义宏

int main(void)
{
    printf("NUMBER is %d\n", NUMBER);
    return 0;
}

// 结果:
NUMBER is 5

(2-2)带参数的宏定义

宏定义可以带参数,称为带参数的宏。 #define 宏名(形参列表)  替换文本

注意:

  • 替换文本中所有参数都用括号括起来,整个替换文本也用括号括起来,避免错误。
  • 使用时必须保持参数正确,否则导致编译错误或运行错误。
#include <stdio.h>
#define MAX(a,b) ((a > b) ? (a) : (b))                  // 带参数的宏

int main(void)
{
    int m = 2, n = 3;
    printf("%d and %d, max is %d\n", m, n, MAX(m,n));   // 预处理器将MAX(m,n)文本替换为((m>n)?(m):(n))
    return 0;
}

// 结果:
2 and 3, max is 3
#include <stdio.h>
#define SQUARE(x) x*x            // 没有正确使用括号的错误案例
int main(void)
{
    int m = 2, n = 3;
    printf("square(%d+%d) = %d\n", m, n, SQUARE(m+n));  // 预处理器将SQUARE(m,n)文本替换为m+n*m+n
    return 0;
}

// 结果:
square(2+3) = 11                 // 2+3*2+3=11
   


#include <stdio.h>
#define SQUARE(x) ((x)*(x))      // 正确使用括号

int main(void)
{
    int m = 2, n = 3;
    printf("square(%d+%d) = %d\n", m, n, SQUARE(m+n));  // 预处理器将SQUARE(m,n)文本替换为((m+n)*(m+n))
    return 0;
}

// 结果:
square(2+3) = 25                // ((2+3)*(2+3))=25

宏定义中的参数也可以是不定参数(即不确定参数数量),称为不定参数宏

形参列表中使用"..."表示不定参数(可变参数),替换文本中使用"__VA_ARGS__"表示"..."匹配的所有传递给宏的参数。

注意:

  • 不定参数宏一般用于日志记录、调试信息输出等。
  • "..."在形参列表最后。
  • 若有固定参数,替换文本最好使用"##__VA_ARGS__"。例如:#define  PRINT(str, ...)  printf(str, ##__VA_ARGS__)。
  • 若有固定参数,使用宏时固定参数必须提供。若使用时除固定参数外没有其它参数,"##__VA_ARGS__"可移除逗号(固定参数后有逗号","),否则报错error: expected expression before ')' token。
  • "__VA_ARGS__"只能在支持C99标准的编译器上使用。
  • 因不定参数宏的复杂性,需谨慎使用。
#include <stdio.h>
#define PRINT(...) printf(__VA_ARGS__)    // 不定参数宏

int main(void)
{
    int m = 2, n = 3;
    PRINT("Output: %d, %d", m, n);        // 预处理器将PRINT(...)文本替换为printf("Output: %d, %d", m, n)
    return 0;
}

// 结果:
Output: 2, 3
#include <stdio.h>
#define PRINT(str, ...) printf(str, ##__VA_ARGS__)    // 有固定参数str

int main(void)
{
    int m = 2, n = 3;
    PRINT("no extra argument\n");                // 预处理器将PRINT(...)文本替换为printf("no extra argument\n")
    PRINT("two extra arguments:%d %d\n", m, n);  // 预处理器将PRINT(...)文本替换为printf("two extra arguments:%d %d\n", m, n)
    return 0;
}

// 结果:
no extra argument
two extra arguments:2 3
#include <stdio.h>
#define PRINT(str, ...) printf(str, __VA_ARGS__)   // 固定参数str,可变参数...

int main(void)
{
    int m = 2, n = 3;
    PRINT("Output: no extra argument\n");     // 没有可变参数,报错:error: expected expression before ')' token
    PRINT();                                  // 没有固定参数,报错:error: expected expression before ',' token
    return 0;
}

 

(2-3)预处理器运算符

"#"运算符:将宏的某个参数转为字符串。

#include <stdio.h>
#define STR(x) #x

int main(void)
{
    int m = 2, n = 3;
    printf(STR(m+n) "=%d\n", m+n);         // 预处理器将STR(m+n)文本替换为"m+n"
    printf("%s=%d\n", STR(m+n), m+n);      // 预处理器将STR(m+n)文本替换为"m+n"
    return 0;
}

// 结果:
m+n=5
m+n=5

"##"运算符:合并宏中的两个参数,可将宏中两个独立的标记拼接成一个新的标记。

若拼接之后的标记没有声明,会报错。确保拼接后的标记已声明。

若参数也是宏,且报错,可尝试再定义一个宏,做中间转换。

#include <stdio.h>
#define CONCATE(a,b) a##b

int main(void)
{
    int m = 2, n = 3;
    printf("%d\n", CONCATE(2,3));       // 预处理器将CONCATE(2,3)文本替换为23
    printf("%d\n", CONCATE(m,n));       // 报错:error: 'mn' undeclared。预处理器将CONCATE(m,n)文本替换为mn,但mn没有声明
    return 0;
}
#include <stdio.h>
#define CONCATE(a,b) a##b

int main(void)
{
    int m = 2, n = 3;
    int mn = 18;
    printf("%d\n", CONCATE(2,3));       // 预处理器将CONCATE(2,3)文本替换为23
    printf("%d\n", CONCATE(m,n));       // 预处理器将CONCATE(m,n)文本替换为mn
    return 0;
}

// 结果:
23
18
#include <stdio.h>
#define M 2
#define N 3
#define _CONCATE(a,b) a##b
#define CONCATE(a,b) _CONCATE(a,b)

int main(void)
{
    printf("%d\n", CONCATE(M,N));         // 宏的参数也是宏,预处理器将CONCATE(M,N)文本替换为23。(_CONCATE(M,N)即M##N即2##3即23)
    return 0;
}

// 结果:
23

注意宏的正确使用和潜在风险。

尽量保持宏的简洁易用,避免过于复杂,确保代码清晰、易于维护。

过于复杂的宏会增加代码的阅读难度和理解难度,甚至导致编译时错误或运行时发生错误。

尽量使用标准库已定义的宏。

3、条件编译

#if ... #elif ... #else ... #endif              
#if defined ... #endif 
         判断宏名是否定义过

#if !defined ...  #endif         判断宏名是否没有定义过

#include <stdio.h>

#if !defined(NUMBER)    // 若条件为真,执行下面语句
    #define NUMBER 10
#endif                  // 条件语句块结束

int main(void)
{
    printf("NUMBER is %d\n", NUMBER);
    return 0;
}

// 结果:
NUMBER is 10
#include <stdio.h>
#include "example.h"

#if !defined(NUMBER)    // 若条件为真,执行下面语句
    #define NUMBER 10
#else                   // 若if条件为假,执行下面语句
    #undef NUMBER
    #define NUMBER 5
#endif                  // 条件语句块结束

int main(void)
{
    printf("NUMBER is %d\n", NUMBER);
    return 0;
}

// 结果:
NUMBER is 5

#ifdef ... #endif      判断宏名是否定义过

注意:#ifdef 宏名    等同于   #if defined(宏名)  。

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

#ifdef NUMBER           // 若条件为真,执行下面语句
    #undef NUMBER
    #define NUMBER 5
#endif                  // 条件语句块结束


int main(void)
{
    printf("NUMBER is %d\n", NUMBER);
    return 0;
}

// 结果:
NUMBER is 5

#ifndef ... #endif     判断宏名是否没有定义过

注意:#ifndef 宏名    等同于    #if !defined(宏名)  。

#include <stdio.h>

#ifndef NUMBER          // 若条件为真,执行下面语句
    #define NUMBER 10
#endif                  // 条件语句块结束

int main(void)
{
    printf("NUMBER is %d\n", NUMBER);
    return 0;
}

// 结果:
NUMBER is 10

4、预定义宏

预定义宏:编译器提前定义好的宏,可以使用不能修改。

描述
__DATE__ 当前日期,一个以 "MMM DD YYYY" 格式表示的字符常量。
__TIME__ 当前时间,一个以 "HH:MM:SS" 格式表示的字符常量。
__FILE__ 这会包含当前文件名,一个字符串常量。
__LINE__ 这会包含当前行号,一个十进制常量。
__STDC__ 当编译器以 ANSI 标准编译时,则定义为 1。
__STDC_HOSTED__ 当编译器可以提供完整的标准库时,则定义为1。否则为0(嵌入式系统的标准库一般不完整)
__STDC_VERSION__ 编译使用的C语言版本,格式为yyyymmL的长整数,C99 版本为“199901L”,C11 版本为“201112L”,C17 版本为“201710L”
#include <stdio.h>

int main(void)
{
    printf("function: %s\n", __func__);
    printf("date: %s\n", __DATE__);
    printf("time: %s\n", __TIME__);
    printf("file: %s\n", __FILE__);
    printf("line: %d\n", __LINE__);
    printf("ANSI: %d\n", __STDC__);
    printf("full standard library: %d\n", __STDC_HOSTED__);
    printf("C version: %ld\n", __STDC_VERSION__);
    return 0;
}

// 结果:
function: main
date: Apr 11 2024
time: 17:00:26
file: preprocessor.c
line: 9
ANSI: 1
full standard library: 1
C version: 201710

补充:宏与函数的区别

函数
只是文本替换 完成某个特定功能的任务。可对表达式进行计算
文本替换时,代码长度增加 函数定义在一个地方,每次调用都是同一地方的代码
不占用内存空间 占用内存空间(开辟栈空间,压栈出栈,记录返回地址)
不用遵循函数调用和返回规则,速度更快 遵循函数调用和返回规则,有额外开销
参数可以任何类型,只要参数类型合法 参数需标明类型,不同类型的参数使用不同函数解决
因括号或参数没有正确使用以及运算符优先级问题,可能导致编译或运行错误 运行时可能错误
宏在预处理阶段就进行文本替换了,无法调试 可以调试

使用#undef 取消之前的定义,可以确保是函数而不是宏。

Names may be undefined with #undef, usually to ensure that a routine is really a function, not a macro.

#undef getchar                // 取消getchar的宏定义

int getchar(void){...}        // 确保gechar是函数,而不是宏


网站公告

今日签到

点亮在社区的每一天
去签到