欢迎光临
我们一直在努力

C语言:预处理

文章目录

  • 前言
    • 本文旨在系统性地介绍C语言中的预处理。
  • 一、预定义符号
  • 二、#define定义宏
    • 2.1、#define定义标识符(无参宏)
    • 2.2、宏定义中括号的使用
    • 2.3、带有副作用的宏参数
    • 2.4、宏替换的规则
    • 2.5、宏和函数的对比
  • 三、命名约定
  • 四、#undef
  • 五、条件编译
  • 六、头文件包含
    • 6.1、头文件被包含的方式
    • 6.2、嵌套文件包含

前言

本文旨在系统性地介绍C语言中的预处理。

一、预定义符号

预定义符号是编译器预先定义的宏,可直接使用,无需 #define。它们在预处理阶段被替换为对应的值。

__FILE__ //进⾏编译的源⽂件名
__LINE__ //⽂件当前的⾏号
__DATE__ //⽂件被编译的⽇期
__TIME__ //⽂件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义

代码示例:

#include <stdio.h>

int main()
{
printf("文件:%s\\n", __FILE__);
printf("行号:%d\\n", __LINE__);
printf("编译日期:%s\\n", __DATE__);
printf("编译时间:%s\\n", __TIME__);
return 0;
}

二、#define定义宏

宏定义允许将参数替换到文本中,其语法形式如下:

#define name(paramentlist) stuff

name:宏名 (parament-list):参数列表,由逗号分隔的符号表 stuff:替换文本,参数可以出现在其中

注意事项:参数列表的左括号必须紧挨宏名,不能有空格。如果有空格,参数列表被当成替换文本。

2.1、#define定义标识符(无参宏)

基本语法

#define name stuff

代码示例:

#define MAX 1000
#define reg register // 为 register 关键字起简短别名
#define do_forever for(;;) // 用形象符号替换无限循环
#define CASE break;case // case 语句自动加 break

当 stuff 过长时,可以用反斜杠\\(续写符)分行编写(最后一行不加)告诉预处理器:下一行是当前行的延续,在预处理阶段会合并成一行。如果没有续行符,预处理后,宏定义只到第一行结束,后面的内容被当作普通代码,导致编译错误。 代码示例:

#define DEBUG_PRINT printf("file:%s\\tline:%d\\t" \\
"date:%s\\ttime:%s\\n", \\
__FILE__, __LINE__, \\
__DATE__, __TIME__)

末尾不建议加分号,否则可能引发语法错误。 代码示例:

#define MAX 1000;

if (condition)
max = MAX; // 替换后 → max = 1000;;
// 替换后if 和 else 之间变成两条语句,导致编译错误。
else
max = 0;

2.2、宏定义中括号的使用

宏定义中,给每个参数加上括号,再给整个表达式加上括号,可以避免因操作符优先级导致的意外结果。

①参数未加括号(宏是纯文本替换,参数中的操作符优先级影响计算顺序)

#define SQUARE(x) x * x

SQUARE(a + 1)
// 替换后 → a + 1 * a + 1
// 实际计算:a + (1 * a) + 1
// 结果与预期不符

解决办法:给宏体的每个参数加上括号。

#define SQUARE(x) (x) * (x)

SQUARE(a + 1)
// 替换后 → (a + 1) * (a + 1)

②整个宏体未加括号(宏体本身没有括号,导致外部运算符影响计算顺序)

#define DOUBLE(x) (x) + (x)

10 * DOUBLE(a)
// 替换后 → 10 * (5) + (5)
// 实际计算:10 * 5 + 5 = 55

解决办法:给整个宏体加上括号。

#define DOUBLE(x) ((x) + (x))

10 * DOUBLE(a)
// 替换后 → 10 * ((5) + (5)) = 100

2.3、带有副作用的宏参数

宏参数在宏体中多次出现时,带副作用的参数会被多次求值,导致不可预测的结果。应避免在宏参数中使用 ++、– 等操作,或用函数替代宏。

副作用是指表达式求值时永久改变变量值的效果。

x + 1; // 无副作用(x 不变)
x++; // 有副作用(x 被修改)

代码示例:

#include <stdio.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int main()
{
int x = 5;
int y = 8;

// 宏调用:MAX(x++, y++)
// 预处理后展开为:((x++) > (y++) ? (x++) : (y++))
// 执行过程:
// ① 比较 (x++) > (y++),即 5 > 8(假),此时 x=6,y=9
// ② 条件为假,执行 (y++),取 y 的当前值 9 赋给 z,然后 y 变为 10
// ③ 最终:x=6,y=10,z=9
int z = MAX(x++, y++);
printf("x=%d y=%d z=%d\\n", x, y, z);

return 0;
}

2.4、宏替换的规则

① 参数检查:检查宏参数是否包含 #define 定义的符号,如果有则先替换 ② 文本插入:将替换文本插入到宏调用的位置,参数名被对应的值替换 ③ 再次扫描:对结果文件再次扫描,检查是否还有 #define 定义的符号,如果有则重复上述过程

代码示例:

#define PI 3.14
#define AREA(r) (PI * (r) * (r))

double s = AREA(5);

替换过程: ①检查参数 5,没有 #define 定义的符号; ②插入替换文本:(PI * (5) * (5)); ③再次扫描,发现 PI 是 #define 定义的符号; ④替换 PI:(3.14 * (5) * (5))。

注意事项: ①宏不能递归:宏不能调用自身; ②字符串内容不搜索:字符串字面量中的内容不会被宏替换。

代码示例:

#define PI 3.14

printf("PI = %f", PI); // 宏被替换
printf("PI"); // 字符串中的 PI 不会被替换

2.5、宏和函数的对比

对比项宏(Macro)函数(Function)
执行时机 预处理阶段展开 运行时调用
执行效率 高(无调用开销) 低(有调用和返回开销)
代码长度 大型宏多次展开使程序膨胀 代码只存一份
类型检查
调试 无法调试 可逐语句调试
类型通用 类型无关 参数类型固定
参数为类型 可以(如 MALLOC(10, int)) 不可以
副作用风险 参数多次求值 参数只求值一次
内存开销 不占栈空间 每次调用占用栈空间
递归 不可递归 可递归
操作符优先级 易受邻近操作符影响 安全(参数先求值)

三、命名约定

宏名全部大写,函数名不要全部大写,C语言本身没有区分,目的是让代码可读性更强,便于区分宏和函数。

四、#undef

#undef 指令用于移除一个已存在的宏定义。如果需要重新定义一个宏名,或取消某个宏的作用,可以先用 #undef 移除旧定义。

语法形式

#undef NAME

代码示例:

#include <stdio.h>

#define MAX 100

// 此时 MAX 已定义

#undef MAX // 移除 MAX 的宏定义

// 此时 MAX 未定义,不能再使用

#ifdef MAX
printf("MAX 已定义\\n");
#else
printf("MAX 未定义\\n");
#endif

五、条件编译

条件编译用于在预处理阶段根据条件决定哪些代码被保留、哪些被丢弃,常用于调试信息控制、头文件保护和跨平台适配。

常见条件编译指令: ①单分支条件编译

#if 常量表达式
// 表达式为真时编译
#endif

#define __DEBUG__ 1

#if __DEBUG__
printf("调试模式\\n"); // 被编译
#endif

②多分支条件编译

#define VERSION 2

#if VERSION == 1
printf("版本1\\n");
#elif VERSION == 2
printf("版本2\\n");
#else
printf("其他版本\\n");
#endif

③判断是否被定义

写法含义
#if defined(symbol) symbol 已定义时为真
#ifdef symbol 同上(简写)
#if !defined(symbol) symbol 未定义时为真
#ifndef symbol 同上(简写)

#define DEBUG

#ifdef DEBUG
printf("调试模式\\n"); // 被编译
#endif

#ifndef RELEASE
printf("非发布模式\\n"); // 不被编译
#endif

④嵌套条件编译

#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif

六、头文件包含

6.1、头文件被包含的方式

包含方式查找路径适用场景
#include <xxx.h> 直接去标准系统路径查找 库文件(标准库、第三方库)
#include “xxx.h” 先在当前源文件所在目录查找,找不到再去标准路径查找 本地头文件(项目自定义)

库文件也可以用 “” 包含,但是查找效率低,不易区分是库还是本地文件。

6.2、嵌套文件包含

头文件被重复包含

// test.c
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"

预处理后,test.h 的内容会被拷贝 5 次,导致代码膨胀和编译压力增加。

解决方案1:头文件守卫(条件编译)

#ifndef __TEST_H__
#define __TEST_H__

// 头文件的内容
void test();
struct Stu
{
int id;
char name[20];
};

#endif // __TEST_H__

解决方案2:#pragma once

#pragma once

// 头文件的内容
void test();
struct Stu
{
int id;
char name[20];
};

赞(0)
未经允许不得转载:171主机测评 » C语言:预处理
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址