【C语言】预处理详解

目录

1、预定义符号

[2、 #define 定义常量](#define 定义常量)

[3、#define 定义宏](#define 定义宏)

4、带有副作用的宏参数

5、宏替换的规则

6、宏定义和函数的对比

[7、# 、 ## 运算符](# 、 ## 运算符)

[7.1 # 运算符](# 运算符)

7.2 ##运算符

8、取消宏定义

9、条件编译


1、预定义符号

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

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

2、 #define 定义常量

cpp 复制代码
#define name stuff

用 #define 进行定义,其中name可以自己起,内容可以是 数字、字符串、代码等.......

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__ )

3、#define 定义宏

#define 机制包括了⼀个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。

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

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

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

例:这个宏接收⼀个参数 x ,如果在上述声明之后,你把 SQUARE( 5 ); 置于程序中,预处理器就会用下⾯这个表达式替换上⾯的表达式: 5 * 5

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

**警告:**若想通过SQUARE(X)定义为某个数的平方,那么存在以下问题:

此时输出结果为11,因为 SQUARE(a+1) 等价于 a+1*a+1,即5+5+1,所以定义宏时应该写成

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

总结:定义宏时尽量避免在由于参数中的操作符或邻近操作符之间不可预料的相互作用。

4、带有副作用的宏参数

当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。宏在被使用时是简单的文本替换,可能会导致不希望发生的操作被重复执行,从而产生"副作用"。

带有副作用的宏的示例:

考虑以下定义一个简单的宏用于计算一个数的平方:

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

如果你在代码中使用这个宏时传入一个具有副作用的表达式,如递增操作:

cpp 复制代码
int a = 3;
int result = SQUARE(a++);

这段代码的预期行为可能是计算a自增后的平方,但实际执行时,由于宏展开的特性,代码将被替换为:

cpp 复制代码
int result = ((a++) * (a++));

这意味着a++操作将执行两次,而不是一次。这导致a增加了两次,并且结果可能不是预期的4 * 4 = 16,而是一个未定义的结果(通常是12)。

5、宏替换的规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

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

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

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

注意:

  1. 宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。

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

6、宏定义和函数的对比

宏通常被应⽤于执⾏简单的运算。⽐如在两个数中找出较大的⼀个时,写成下⾯的宏,更有优势⼀些。定义如下:

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

宏定义相比于函数的优势:

  1. 用于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜⼀筹。

  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、⻓整型、浮点型等可以⽤于 > 来⽐较的类型。宏是类型无关的。

宏定义相比于函数的劣势:

  1. 每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加 程序的长度。

  2. 宏是没法调试的。

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

  4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

宏有时候可以做函数做不到的事情:

⽐如:宏的参数可以出现类型,但是函数做不到。

cpp 复制代码
#define MALLOC(n,type) (type*)malloc(n*sizeof(type))

int main()
{
	int *p = (int*)malloc(10 * sizeof(int));
	int *p = MALLOC(10, int);
}

宏和函数的一个对比:

7、# 、 ## 运算符

7.1 # 运算符

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

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

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

就可以写:

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

当我们按照下⾯的方式调用的时候,就出现了 #a,而 #a 就是转换为 " a " 的⼀个字符串,代码就会被预处理为:

cpp 复制代码
PRINT(a);     
cpp 复制代码
printf("the value of ""a" " is %d", a);

实例:

7.2 ##运算符

可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离文本片段创建标识符。 ## 被称为记号粘合。这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。

**例:**写⼀个函数求2个数的较大值的时候,不同的数据类型就得写不同的函数

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

但是这样写起来太繁琐了,可以通过宏定义的形式处理:

注:其中 \ 为续行符

当定义了一个 GENERIC_MAX(type) 模板的时候,以 int 为例,GENERIC_MAX(int) 等价于

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

8、取消宏定义

#undef

当代码中定义了一个宏(macro)之后,如果该宏的定义在后续代码中不再需要,或者为了避免名称冲突,你可以使用 #undef 指令来取消它的定义。

cpp 复制代码
#include <stdio.h>  
  
#define PI 3.14159  
  
int main() {  
    printf("PI is %f\n", PI);  
  
    #undef PI  
  
    // 下面这行会引发编译错误,因为 PI 已经被 undef 了  
    // printf("PI again is %f\n", PI);  
  
    return 0;  
}

9、条件编译

条件编译是预处理阶段的一个特性,它允许程序根据特定的条件包含或排除代码段。这些条件通常是在编译时确定的,而不是在程序运行时。条件编译对于跨平台开发、调试代码、或者根据不同配置编译不同版本的程序非常有用。

在C和C++(以及许多其他支持预处理器的编程语言)中,条件编译通常通过预处理指令来实现,这些指令以#字符开头。主要的条件编译指令包括#ifdef#ifndef#if#elif#else#endif

主要的条件编译指令:

  1. #ifdef :如果已定义指定的宏,则编译随后的代码直到遇到#endif
  2. #ifndef :如果未定义指定的宏,则编译随后的代码直到遇到#endif。这个指令经常用于防止头文件被重复包含。
  3. #if :后面跟着一个条件表达式(通常是宏的定义),如果条件为真(即非零),则编译随后的代码直到遇到#endif。条件表达式中还可以使用defined操作符来检查宏是否已定义。
  4. #elif :是"else if"的缩写,用于在#if#elif之后提供一个额外的条件测试。如果前面的#if#elif条件为假,则编译器会检查#elif后面的条件。
  5. #else :如果前面的#if#elif条件都为假,则编译#else后面的代码直到遇到#endif
  6. #endif:标记条件编译块的结束。
cpp 复制代码
1.
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1

#if __DEBUG__
//..
#endif

2.多个分⽀的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif

3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol

4.嵌套指令
#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

示例:

假设你正在开发一个跨平台的程序,并且需要根据不同的操作系统包含不同的代码。你可以使用条件编译来实现这一点:

cpp 复制代码
#include <stdio.h>  
  
#define WINDOWS 1  
// #define LINUX 1  // 取消注释这行以模拟在Linux上编译  
  
int main() {  
    #ifdef WINDOWS  
    printf("This is Windows.\n");  
    #elif defined(LINUX)  
    printf("This is Linux.\n");  
    #else  
    printf("Unknown operating system.\n");  
    #endif  
  
    return 0;  
}

如果定义了这个宏,就编译下面代码。 在这个例子中定义了一个宏WINDOWS来表示正在Windows上编译。如果取消注释#define LINUX 1,则程序将输出"This is Linux."。如果没有定义任何操作系统宏,它将输出"Unknown operating system."。

相关推荐
Uu_05kkq1 小时前
【C语言1】C语言常见概念(总结复习篇)——库函数、ASCII码、转义字符
c语言·数据结构·算法
嵌入式科普4 小时前
十一、从0开始卷出一个新项目之瑞萨RA6M5串口DTC接收不定长
c语言·stm32·cubeide·e2studio·ra6m5·dma接收不定长
A懿轩A4 小时前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列
1 9 J5 小时前
数据结构 C/C++(实验五:图)
c语言·数据结构·c++·学习·算法
仍然探索未知中6 小时前
C语言经典100例
c语言
爱吃西瓜的小菜鸡6 小时前
【C语言】矩阵乘法
c语言·学习·算法
Stark、8 小时前
【Linux】文件IO--fcntl/lseek/阻塞与非阻塞/文件偏移
linux·运维·服务器·c语言·后端
deja vu水中芭蕾8 小时前
嵌入式C面试
c语言·开发语言
stm 学习ing10 小时前
HDLBits训练3
c语言·经验分享·笔记·算法·fpga·eda·verilog hdl
CSND74014 小时前
Ubuntu vi(vim)编辑器配置一键补全main函数
linux·c语言·ubuntu·编辑器·vim