预处理器:宏、运算符与条件编译

目录

一、什么是预处理

二、预定义符号

三、define定义宏

[3.1 定义宏常量](#3.1 定义宏常量)

[3.2 定义宏函数](#3.2 定义宏函数)

[3.3 带有副作用的宏参数](#3.3 带有副作用的宏参数)

[3.4 宏与函数的对比](#3.4 宏与函数的对比)

四、#和##运算符

4.1 #运算符

4.2 ##运算符

五、undef

六、条件编译


一、什么是预处理

预处理,就是编译之前,由预处理器处理源代码的阶段,不做语法检查,只做文本替换、文件包含、条件编译。 预处理完成后才进入编译,把 .c/.cpp 变成汇编代码。

二、预定义符号

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

cpp 复制代码
__FILE__      //进⾏编译的源⽂件
__LINE__      //⽂件当前的⾏号
__DATE__      //⽂件被编译的⽇期
__TIME__      //⽂件被编译的时间
__STDC__      //如果编译器遵循ANSI C,其值为1,否则未定义
cpp 复制代码
int main()
{
	printf("file:%s line:%d\n", __FILE__, __LINE__);
	return 0;
}

三、define定义宏

#define 是预处理指令,工作在编译之前,核心行为:文本字符串直接替换,不做语法解析、不做类型检查,不分配内存。

语法格式:

cpp 复制代码
#define 宏名 替换文本

注意:末尾不能加**;** ,#必须在行首,#define整行就是宏定义,换行结束。

3.1 定义宏常量

宏常量就是不带参数的 #define,预处理做纯文本替换。

cpp 复制代码
#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__ )


int main()
{
    //程序中的所有宏都会在预处理阶段被宏替换
    MAX
    reg
    do_forever
    CASE
    DEBUG_PRINT
    return 0;    
}
bash 复制代码
# 只做预处理,不编译、不汇编、不链接
gcc -E test.c -o test.i

# 查看预处理后test.i中文件的内容
cat test.i

3.2 定义宏函数

宏函数不是真正的函数,是预处理器文本替换 ,编译前简单做字符串替换,没有函数调用栈、没有参数类型检查。

语法格式:

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

其中的 注意: parament-list 是⼀个由逗号隔开的符号表,它们可能出现在stuff中。

参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

举一个例子,比如以下的宏函数则会完成一个相乘的运算:

cpp 复制代码
#define SQUARE( x )  x * x

在代码中我们可以这样使用这个宏函数:

cpp 复制代码
//计算5*5
int ret=SQUARE( 5 );
printf("运行的结果是%d\n",ret);

代码经过预处理宏替换后就会是这样的:

cpp 复制代码
int ret=5 * 5;
printf("运行的结果是%d\n",ret);

但是这里有一个弊端,宏替换只是单纯的完成文本的替换。如果我们计算5*5的运算结果,但是直接传入4+1时对于普通的函数来讲会首先计算4+1=5然后将5传入函数计算结果。但是对于宏函数来讲其只会完成简单的文本替换不会去计算4+1=5而是将4+1整个表达式直接进行文本替换,所以替换后的结果可能是4+1*4+1计算结果为9而不是25。

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

最佳实践:所有参数加括号,整体表达式再加括号

cpp 复制代码
#define SQUARE( x )  ((x) * (x))

3.3 带有副作用的宏参数

宏是文本替换,参数会原样复制,会多次执行副作用表达式

cpp 复制代码
#define MAX(a,b) ((a)>(b)?(a):(b))

int a = 2;
int b = 3;
int m = MAX(a++, b++);
// 替换: ((a++)>(b++)?(a++):(b++))
// b++执行两次,b最终变成5,不是4!

普通函数只会计算实参一次;宏函数会把实参直接粘贴进去,有自增、函数调用就会重复执行。

解决办法:

  1. 尽量不要把带副作用(++、函数调用)传给宏;
  2. C 语言可以用({ })GNU 语句表达式把参数存临时变量;
  3. C++ 优先用 inline 内联函数、constexpr。

3.4 宏与函数的对比

属性 #define 定义宏 函数
代码长度 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 函数代码只出现于一个地方;每次调用函数时,都调用那个地方的同一份代码
执行速度 更快 存在函数的调用和返回的额外开销,速度慢一些
操作符优先级 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多写括号。 函数参数只在函数调用的时候求值一次,结果值传递给函数。表达式的求值结果可预测。
带有副作用的参数 参数可能被替换到宏体中的多个位置,如果宏的参数被多次计算,带有副作用的参数求值可能会产生不可预料的结果。 函数参数只在传参的时候求值一次,结果容易控制。
参数类型 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。 函数的参数是与类型有关的,如果类型不同,就需要不同的函数,即使执行的任务是不同的。
调试 宏是不方便调试的 函数是可以逐语句调试的
递归 宏是不能递归的 函数是可以递归的

四、#和##运算符

4.1 #运算符

#运算符将宏的⼀个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。

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

当我们有⼀个变量 int a = 10; 的时候,我们想打印出: the value of a is 10 如果写:

cpp 复制代码
#define PRINT(n) printf("the value n is %d\n",n);

经过预处理后就会被替换为:

cpp 复制代码
int a=10;
printf("the value n is %d\n",a);

但是我们使用#运算符将宏函数更改为

cpp 复制代码
#define PRINT(n) printf("the value" #n "is %d\n",n);

使用PRINT(a)进行文本替换就会在预处理阶段被处理为

cpp 复制代码
int a=10;
printf("the value " "a" " is %d\n",a);

4.2 ##运算符

## 是 C 预处理器运算符,仅在带参数宏内部使用。

作用:把两个预处理记号(token)粘贴合并成一个新记号。

这里我们想想,写一个函数求2个数的较大值的时候,不同的数据类型就得写不同的函数。

cpp 复制代码
int int_max(int x, int y)
{
    return x>y?x:y;
}

float float_max(float x, float y)
{
    return x>y?x:y;
}

但是这样写起来太繁琐了,现在我们这样写代码试试:

cpp 复制代码
//宏定义
#define GENERIC_MAX(type)         \
type type##_max(type x, type y)    \
{                                 \
    return (x>y?x:y);             \
}

使用宏,定义不同函数

cpp 复制代码
GENERIC_MAX(int)    //替换到宏体内后int##_max 生成了新的符号 int_max做函数名
GENERIC_MAX(float)  //替换到宏体内后float##_max 生成了新的符号 float_max做函数名

int main()
{
    //调⽤函数
    int m = int_max(2, 3);
    printf("%d\n", m);
    float fm = float_max(3.5f, 4.5f);
    printf("%f\n", fm);
    return 0;
}

五、undef

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

cpp 复制代码
#undef NAME
//如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除。

六、条件编译

预处理器根据条件,选择编译哪一部分代码,丢弃另一部分代码。 不是运行时 if 判断,是预处理阶段就决定代码是否保留,不满足条件的代码直接被删掉,不会进入编译。

普通if:代码全部编译进程序,运行时判断;

条件编译:不满足条件的代码直接被删除,可执行文件里根本没有这段代码

全套指令:

cpp 复制代码
#ifdef      // 如果该宏已经被定义

#ifndef     // 如果该宏没有被定义

#if         // 表达式为真(非0)
#elif       // else if
#else       // else
#endif      // 结束条件编译(必须写,不能漏)

#define     // 定义宏
#undef      // 取消宏定义

条件编译与if的区别:

条件编译 #ifdef / #if 普通 if
处理阶段 预处理阶段 程序运行时
不满足条件的代码 直接被删除,不在可执行文件中 代码编译进程序,运行才跳过
判断内容 宏、编译期常量 变量、运行时数据
结尾 必须写#endif 普通}
相关推荐
小小龙学IT3 小时前
C++ std::vector 底层实现深度解析:内存布局、扩容策略、移动语义与迭代器失效
开发语言·c++·算法
码匠许师傅3 小时前
【C++ 面试真题】聊聊 volatile 和 mutable
开发语言·c++·面试
(Charon)3 小时前
【C++】线程安全队列(二):生产者消费者模型与 condition_variable 阻塞等待
c语言·开发语言·c++
liwulin05063 小时前
【PYTHON】使用Selenium + ChromeDriver以及XPATH语法
开发语言·python·selenium
JavaPub-rodert3 小时前
我又把自己的 Go 后台管理系统升级了一遍:文件管理、2GB 上传、私有文件预览、Docker 镜像全安排上了
开发语言·docker·golang·shiyuadmin
动力 continue4 小时前
Python 元类与异常类:类的“制造工厂”与“报错定制师”
开发语言·python·制造
quantdash_cc4 小时前
基于 QuantDash 5 分钟 K 线的网格交易策略参数网格搜索寻优实战
android·开发语言·pandas·量化·quantdash
147API4 小时前
Python批量生成蒸馏数据,并发、重试、幂等和断点续跑
开发语言·jvm·python
青 春 记 忆4 小时前
零基础入门python04:用注册校验理解字典、集合和条件判断
开发语言·vscode·python·python3.11