预处理详解

前言

本人的技术笔记会优先发布在个人站点longyxblg.cn

预定义符号

C语言设置了一些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的

c 复制代码
__FILE__  //进行编译的源文件
__LINE__  //文件的行号
__DATE__  //文件被编译的日期
__TIME__  //文件被编译的时间
C 复制代码
printf("%d", __LINE__);

#define 定义常量

基本语法

c 复制代码
#define name stuff

一般来说宏定义的名称统一为全大写

例如

c 复制代码
// 宏定义是直接替换
printf("%d\n", NUM);

编译后实际执行的是

c 复制代码
printf("%d\n", 10);

需要注意的是,用 #define 定义标识符的时候不要加 ; , 因为 #define 在编译时会直接整个替换,这个 ; 也会被替换进去,就会出现语法上的错误,例如

c 复制代码
#define NUM_1 20;
printf("%d\n", NUM_1);

编译时实际执行的是

c 复制代码
printf("%d\n", 20;);

如果定义的 stuff 过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠 ,这叫续行符

C 复制代码
#define NUMS 10 + \
			 20 + \
			 30

printf("%d\n", NUMS);

#define 定义宏

#define 机制包含一个规定,允许把参数替换到文本中,这种实现通常称为宏

c 复制代码
#define name( parament-list ) stuff

其中 parament-list 是一个由 , 隔开的符号表,可能出现在 stuff 中参数列表的左括号必须与 name 紧邻,如果两者之间有任何空白存在,参数列表就会被解释为 stuff 的一部分。

c 复制代码
#define INT_MUL(x,y) x * y


int main()
{
	int a = 10;
	int b = 20;
	printf("%d\n", INT_MUL(a, b));
	return 0;
}

实际执行是

c 复制代码
printf("%d\n", a * b);

但是这个宏有个问题

c 复制代码
int a = 10;
int b = 20;
printf("%d\n", INT_MUL(a + 1, b));

实际执行的是

c 复制代码
printf("%d\n", a + 1 * b);

* 优先级比 + 高,就变成了 a + b,得到了 30 ,解决方法就是在给参数加上 () 保证计算的顺序

c 复制代码
int a = 10;
int b = 20;
printf("%d\n", INT_MUL(a + 1, b));

但是这样的话还是会有问题,这里我们重新定义一个宏

c 复制代码
#define INT_ADD(x,y) (x) + (y)

int main()
{
	int a = 10;
	int b = 20;
	printf("%d\n", 10 * INT_ADD(a, b));
	return 0; 
}

替换后执行的是

c 复制代码
printf("%d\n", 10 * (10) + (20));

这里的优先级还是不对,解决方法是给整个替换后的表达式也加上括号

c 复制代码
#define INT_ADD(x,y) ((x) + (y))

int main()
{
	int a = 10;
	int b = 20;
	printf("%d\n", 10 * INT_ADD(a, b));
	return 0;
}

总结:用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

带有副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果

例如

c 复制代码
#define MAXNUM(x,y) ((x) > (y) ? (x) : (y))

int main()
{
	int a = 10;
	int b = 20;
	// 带有副作用的宏参数
	printf("%d\n", MAXNUM(a++, b++));
	printf("a = %d\n", a);
	printf("b = %d\n", b);
	return 0;
}

替换为

c 复制代码
printf("%d\n", ((a++) > (b++) ? (a++) : (b++)));

先比较 a++ 和 b++ , 比较后 a 的值自增为 11 , b 的值自增为 21 ,然后执行 b++ , 此时这个表达式的值为 21 ,执行后 b 的值自增为22

宏替换的规则

在程序中执行使用宏替换的时候要注意以下规则:

  • 在调用宏时,首先对参数进行检查,看看是否包含任何由 #define 定义的符号。如果是,它们首先被替换

  • 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。

  • 最后,再次对结果文件进行扫描,看看它是否包含任何由 #define 定义的符号。如果是,就重复上述处理过程

注意:

宏参数和 #define 中可以出现其他 #define 定义的符号

c 复制代码
#define NUM 10
#define NUM_1 20
#define MAX_NUM1(x) ((x) > (NUM_1) ? (x) : (NUM_1))

int main()
{
	int a = 10;
    printf("%d\n", INT_MUL(NUM, NUM_1));
    printf("%d\n", MAX_NUM1(10));
	return 0;
}

但是对于宏,不能出现递归

c 复制代码
#define FACTORIAL(n)  (n == 1 ? 1 : n * FACTORIAL(n - 1))
int main()
{
	printf("%d\n", FACTORIAL(5));
	return 0;
}

当预处理搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索

这个意思就是宏替换不会把字符串里的单词换掉

c 复制代码
#define PI 3.14
int main()
{
	printf("Π的值是 PI\n");
	return 0;
}

宏和函数的对比

c 复制代码
#define MAXNUM(x,y) ((x) > (y) ? (x) : (y))

int MaxNum(int x, int y)
{
	return x > y ? x : y;
}

对比一下这个宏和函数

c 复制代码
int a = 10;
int b = 20;
int num = 0;
num = MAXNUM(a, b);
num = MaxNum(a, b);

转到反汇编,这是宏定义的汇编

这是函数的汇编

可以看到函数的汇编要执行的步骤比宏定义多,并且,宏定义的调用并不强制限制类型

c 复制代码
float a = 3.5f;
float b = 5.5f;
float c = MAXNUM(a, b);
printf("%f\n", c);

总结:

在进行小型工作时,用于调用函数和从函数返回的代码可能比实际执行小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹

函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。而宏的参数是类型无关的


宏的缺点:

使用宏时,一份宏定义的代码插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。

c 复制代码
#define PROCESS_SCORE(x, y, weight) \
    do { \
        float avg = (x) * (weight) + (y) * (1 - (weight)); \
        printf("计算中...\n"); \
        if (avg >= 60) { \
            printf("及格,平均分: %.2f\n", avg); \
        } else { \
            printf("不及格,平均分: %.2f\n", avg); \
            printf("建议补考!\n"); \
        } \
    } while(0)

int main()
{
    int math = 80;
    int English = 40;
    int phys = 90;
    int chem = 30;
    int bio = 70;
    int geo = 50;
    PROCESS_SCORE(math, English, 0.6);
    PROCESS_SCORE(phys, chem, 0.5);
    PROCESS_SCORE(bio, geo, 0.7);
    return 0;
}

展开后

c 复制代码
do {
    float avg = (math) * (0.6) + (english) * (1 - (0.6));
    printf("计算中...\n");
    if (avg >= 60) {
        printf("及格,平均分: %.2f\n", avg);
    }
    else {
        printf("不及格,平均分: %.2f\n", avg);
        printf("建议补考!\n");
    }
} while (0);

// ---------- 第二次调用展开 ----------
do {
    float avg = (phys) * (0.5) + (chem) * (1 - (0.5));
    printf("计算中...\n");
    if (avg >= 60) {
        printf("及格,平均分: %.2f\n", avg);
    }
    else {
        printf("不及格,平均分: %.2f\n", avg);
        printf("建议补考!\n");
    }
} while (0);

// ---------- 第三次调用展开 ----------
do {
    float avg = (bio) * (0.7) + (geo) * (1 - (0.7));
    printf("计算中...\n");
    if (avg >= 60) {
        printf("及格,平均分: %.2f\n", avg);
    }
    else {
        printf("不及格,平均分: %.2f\n", avg);
        printf("建议补考!\n");
    }
} while (0);

只写了 3 行调用,预处理后变成了将近 30 行代码

而如果采用函数

c 复制代码
void process_score(int x, int y, float weight) {
    float avg = x * weight + y * (1 - weight);
    printf("计算中...\n");
    if (avg >= 60) {
        printf("及格,平均分: %.2f\n", avg);
    }
    else {
        printf("不及格,平均分: %.2f\n", avg);
        printf("建议补考!\n");
    }
}

int main() {
    // 无论调用多少次,函数体在内存中只有一份
    process_score(80, 40, 0.6);
    process_score(90, 30, 0.5);
    process_score(70, 50, 0.7);
    return 0;
}

无论调用多少次,函数体只存在于内存中的一个地方,程序长度几乎不会因为调用次数的增加而增加


宏是无法调试的

c 复制代码
#define INT_ADD(x,y) ((x) + (y))

int int_add(int x, int y)
{
	return x + y;
}
int main()
{
	int c = 0;
	c = INT_ADD(10, 20);
	c = int_add(10, 20);
	return 0;
}

宏由于类型无关,也就不够严谨

宏可能会带来运算符优先级的问题


宏有时候可以做函数做不到的事,比如宏的参数可以出现类型,函数就不行

c 复制代码
#define MALLOC(num,type) \
(type*)malloc((num) * sizeof(type))

int main()
{
	// 宏的参数可以出现类型
	int* p = MALLOC(10,int);
	free(p);
	p = NULL;
	return 0;
}

# 和

#运算符所执行的操作可以理解为字符串化

c 复制代码
#define PRINT_1(n) printf(""#n"的值是%d\n",n);
#define PRINT_2(n) printf("n的值是%d\n",n);

int main()
{
	int a = 10;
	PRINT_1(a);
	PRINT_2(a);
	return 0;
}

可以把位于它两边的符号合成一个符号,允许宏定义从分离的文本片段创建标识符

c 复制代码
#define GENERAL_MAX(type) \
type type##_max(type x , type y) \
{\
	return (x > y ? x : y);\
}

GENERAL_MAX(int)
GENERAL_MAX(float)

int main()
{
	printf("%d\n", int_max(1, 2));
	printf("%f\n", float_max(1.2,2.2));
	return 0;
}

#undef

这条指令用于移除一个宏定义

条件编译

单分支条件编译

c 复制代码
#if 常量表达式
    //...
#endif
c 复制代码
#define NUM 0;

int main()
{
	#if NUM
		printf("666\n");
	#endif
		return 0;
}

多分支条件编译

c 复制代码
#if 常量表达式
    //...
#elif 常量表达式
    //...
#else 
    //...
#endif
c 复制代码
#define NUM_1 0;
#define NUM_2 1;

int main()
{
	// 多分支条件编译
	#if NUM_1
		printf("666\n");
	#elif NUM_2
		printf("哈哈哈!\n");
	#else
		printf("呵呵\n");
	#endif
	return 0;
}

判断是否被定义

c 复制代码
#if defined
#ifdef
c 复制代码
#define NUM_1 0;
#if defined(NUM_1)
	printf("I LOVE C!\n");
#endif

#ifdef NUM_1
	printf("C LOVE ME!\n");
#endif 

判断是否不被定义

c 复制代码
#if !defined
#ifndef
c 复制代码
#define NUM_1 0;
#if !defined(NUM_1)
	printf("I LOVE C!\n");
#endif

#ifndef NUM_1
	printf("C LOVE ME!\n");
#endif 

也可以像分支语句一样进行嵌套

c 复制代码
#ifdef  NUM_1
	#if NUM_1
	#elif NUM_2
		printf("唱跳rap篮球!\n");
	#endif
#endif

头文件包含

头文件包含有两种方式

c 复制代码
#include <stdio.h>
#include "stdio.h"

"" 包含的头文件会优先在当前项目文件夹下寻找头文件,找不到编译器会去到标准路径下查找,如果再找不到提示编译错误

<> 包含的头文件会直接去标准路径下查找,找不到就提示编译错误

""也能查找库文件,但是效率不如<>高,所以包含库文件一般使用 <>

嵌套文件包含

#include 的指令可以使另外一个文件被编译,预处理器先删除这条指令,并用包含文件的内容替换。意味着一个头文件被包含几次就会被编译几次,如果出现重复包含,对编译压力较大。

解决方法如下:

在每个头文件的首行写

c 复制代码
#ifndef __STDIO_H
#define __STDIO_H \
#include <stdio.h> // 写想要包含的头文件
#endif

c 复制代码
#pragma once

这样就只会让同一个头文件被包含一次

其他预处理指令

只介绍了少数预处理指令,更多预处理指令可参考 《C语言深度解剖》

相关推荐
小雪崩2 小时前
嵌入式学习 day25:哈希表及排序与查找
linux·c语言·数据结构·学习·排序算法
yiqiefeimeng2 小时前
C语言指针难倒90%人?买房比喻让你瞬间开窍
c语言·学习·编程·指针·比喻
caimouse3 小时前
ReactOS 窗口系统分析(5):可见区域与窗口 DC — vis.c + windc.c
c语言·开发语言
_Narcissus_5 小时前
动态规划初探(含完整代码实现)
c语言·数据结构·c++·算法·动态规划·背包问题·最短路径
Cerman5 小时前
QEMU启动OVMF shell超时分析
c语言·windows·经验分享
_Narcissus_6 小时前
枚举和模拟算法笔记
c语言·数据结构·c++·笔记·算法·模拟·枚举
Fa_Mian_Tuan7 小时前
最小生成树之Kruskal算法(超详细原理+完整可运行C语言代码)
c语言·开发语言·数据结构·笔记·算法·图论
老当益壮梁奶奶8 小时前
Linux软件编程学习笔记(二)Linux文件IO编程入门:从fopen到fgetc
linux·c语言·c++·笔记·学习
sunoo-2298 小时前
C 语言文件 IO 全攻略:从基础函数到实战踩坑(BMP 读取 + 词典查询)
linux·c语言·前端·笔记·vscode·学习