C语言 预处理详解

目录

前言

一、预定义符号

二、#define定义常量

三、#define定义宏

四、带有副作用的宏参数

五、宏替换的规则

六、宏与函数的对比

七、#和##

7.1#运算符

7.2 ##运算符

八、命名约定

九、#undef

十、命令行定义

十一、条件编译

十二、头文件的包含

[12.1 头文件被包含的方式:](#12.1 头文件被包含的方式:)

[12.1.1 本地文件包含](#12.1.1 本地文件包含)

[12.1.2 库文件包含](#12.1.2 库文件包含)

[12.2 嵌套文件包含](#12.2 嵌套文件包含)

13、其他预处理指令

13.1#pragma

总结


前言

C语言中程序需要实现依赖翻译环境和运行环境,而翻译环境可以分为编译和链接两个步骤,其中编译又可以细分为预处理,编译,汇编三个阶段,今天我们就来讲一下预处理阶段。


一、 预定义符号

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

比如:

cpp 复制代码
__FILE__ //进⾏编译的源⽂件
__LINE__ //⽂件当前的⾏号
__DATE__ //⽂件被编译的⽇期
__TIME__ //⽂件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
cpp 复制代码
#include<stdio.h>

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

二、#define定义常量

基本语法:

cpp 复制代码
#define name stuff

常见用法:

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

那我们在#define定义标识符的时候,要不要在最后加上 ;

答案是不需要加上; 请看下面一段代码

cpp 复制代码
#define MAX 1000;
#define MAX 1000
cpp 复制代码
if(condition)
 max = MAX;
else
 max = 0;

如果加上;则MAX就为1000;等替换后,if和else之间就是2条语句,而没有大括号的时候,if后边只能有⼀条语句。这⾥会出现语法错误。

三、#define定义宏

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

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

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

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

比如:

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

这个宏接收⼀个参数 x .如果在上述声明之后,你把 SQUARE( 5 ); 置于程序中,预处理器就会用
下面这个表达式 替换 上面 的表达式: 5 * 5
这个宏存在⼀个问题:
观察下面的代码段:

cpp 复制代码
#include<stdio.h>

#define SQUARE(x) x*x

int main() {
	int a = 5;
	printf("%d", SQUARE(a + 1));
	return 0;
}

输出结果:

我们发现与我们希望的结果不一样,我们想要求a+1及6的平方36,结果输出结果为11。
替换文本时,参数x被替换成a + 1,所以这条语句实际上变成了:

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

由此可知替换产生的表达式并没有按照预想的次序进行求值。
在宏定义上加上两个括号,这个问题便轻松的解决了:

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

这样预处理时替换就变成了

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

让我们再来看一个:

cpp 复制代码
#define DOUBLE(x) (x) + (x)
cpp 复制代码
int a = 5;
printf("%d\n" ,10 * DOUBLE(a));

定义中我们使用了括号,想避免之前的问题,但是这个宏却出现新的错误。这将打印什么值呢?看上去,好像打印100,但事实上打印的是55。
我们发现替换后代码变为:

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

所以结果还是没有按照预想的次序进行求值。
这个问题,的解决办法是在宏定义表达式两边加上⼀对括号就可以了。

cpp 复制代码
#define DOUBLE( x) ( ( x ) + ( x ) )

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

四、带有副作用的宏参数

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

cpp 复制代码
x+1;//不带副作⽤
x++;//带有副作⽤

MAX宏可以证明具有副作用的参数所引起的问题。

cpp 复制代码
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?

输出结果:

cpp 复制代码
//代码被替换为
z=((x++)>(y++))?(x++):(y++))

//都为后置++,所以先判断5>8 然后x=6,y=9
//5>8为假,所以为y++,使用先返回y,及z=9,然后y=10

所以当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作用,结果就有可能不符号我们预期。

五、宏替换的规则

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

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们⾸ 先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复 上述处理过程。
    注意:
  4. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现 递归 。
  5. 当预处理器搜索#define定义的符号的时候, 字符串常量 的内容并不被搜索。

六、宏与函数的对比

宏通常被应应用于执行简单的运算。
比如在两个数中找出较⼤的⼀个时,写成下面的宏,更有优势⼀些。

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

为什么不用函数呢?

  1. 用 于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏
    比函数在程序的规模和速度方面更胜⼀筹
  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反
    之这个宏怎可以适用于整形、长整型、浮点型等可以用于 > 来比较的类型。宏的参数是类型无 关的
    和函数相比宏的劣势:
  3. 每次使用宏的时候,⼀份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加 序的长度。
  4. 宏是 没法调试 的。
  5. 宏由于类型无关,也就 不够严谨 。
  6. 宏可能会带来运算符优先级的问题,导致程 容易出现错 。
    宏有时候可以做函数做不到的事情。比如:宏的参数可以出现 类型 ,但是函数做不到。
    比如说我们先用malloc函数来开辟一个空间,我们可以用宏来重新定义,此时宏的参数就为类型。
cpp 复制代码
#define MALLOC(num, type)\
 (type )malloc(num sizeof(type))
 ...
//使⽤
 MALLOC(10, int);//类型作为参数

//预处理器替换之后:
 (int *)malloc(10 sizeof(int));

宏和函数的对比

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

递归 宏是不能递归的 函数是可以递归的。

七、#和##

7.1#运算符

#运算符将宏的⼀个参数转换为字符串字字面量。它仅允许出现在带参数的宏的替换列表中。
#运算符所执行的操作可以理解为" 字符串化 "。
当我们有⼀个变量 int a = 10; 的时候,我们想打印出: the value of a is 10 .
就可以写:

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

当我们按照下⾯的方式调⽤的时候:
PRINT(a);//当我们把a替换到宏的体内时,就出现了#a,而#a就是转换为"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;
}

但是我们有不同的数据这样写就过于繁琐,我们可以用宏来解决:

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;
}

输出:

八、命名约定

⼀般来讲函数的宏的使用语法很相似。所以语⾔本⾝没法帮我们区分⼆者。
那我们平时的⼀个习惯是:
1.把 宏名全部⼤写
2. 函数名不要全部⼤写

九、#undef

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

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

比如:

cpp 复制代码
#include<stdio.h>

#define SQORT(x) ((x)*(x))

int main() {
	int a = 3;
	int b = 4;
	printf("%d\n", SQORT(a));
#undef SQORT
	printf("%d\n", SQORT(b));//运行出错,SQORT未定义
}

十、命令行定义

许多C 的编译器提供了⼀种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同⼀个源文件要编译出⼀个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了⼀个某个长度的数组,如果机器内存有限,我们需要⼀个很小的数组,但是另外⼀个机器内存大些,我们需要⼀个数组能够大些。)

cpp 复制代码
#include <stdio.h>
int main()
{
   int array [SZ];
   int i = 0;
   for(i = 0; i< SZ; i ++)
   {
    array[i] = i;
    }
   for(i = 0; i<SZ; i ++)
   {
   printf("%d " ,array[i]);
   }
   printf("\n" );
   return 0;
}

通过命令行命令:

cpp 复制代码
//linux 环境演⽰
gcc -D SZ=10 programe.c

十一、条件编译

在编译⼀个程序的时候我们如果要将⼀条语句(⼀组语句) 编译或者放弃 是很方便的。因为我们有 条件编译指令 。
比如说:
调试性的代码,删除可惜,保留⼜碍事,所以我们可以选择性的编译。

cpp 复制代码
#include<stdio.h>

#define __DEBUG__

int main(){
  int i = 0;
  int arr[10] = { 0 };
  for (i = 0; i < 10; i++)
  {
	  arr[i] = i;
#ifdef __DEBUG__
	printf("%d\n", arr[i]);//为了观察数组是否赋值成功。 
#endif //__DEBUG__
  }
 return 0;
}

通过判断是否定义__DEBUG__进行选择编译
常⻅的条件编译指令:

cpp 复制代码
1.
#if 常量表达式
 //...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
 //..
#endif

进行条件判断。条件满足进行编译if到endif的代码,不满足则不进行编译。

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

多个if进行分支判断,else if此时为elif。

cpp 复制代码
3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol

通过if defined判断是否被定义,如果被定义则执行,没被定义则不执行,可简写为ifdef。if !defined 用法则相反,可简写为 ifndef。

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

我们需要注意一个if对应一个endif。

十二、头文件的包含

12.1 头文件被包含的方式:

12.1.1 本地文件包含
cpp 复制代码
#include "filename"

查找策略 :先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件⼀样在标准位置查找头文件。 如果找不到就提示编译错误。
Linux环境的标准头文件的路径:

bash 复制代码
 /usr/include

VS环境的标准头文件的路径:

bash 复制代码
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
//这是VS2013的默认路径

注意按照自己的安装路径去找。

12.1.2 库文件包含
cpp 复制代码
#include <filename.h>

查找策略 : 查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用 "" 的形式包含?
答案是肯定的,可以,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

12.2 嵌套文件包含

我们已经知道, #include 指令可以使另外⼀个文件被编译。就像它实际出现于 #include 指令的
地方⼀样。这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。 ⼀个头文件被包含10次,那就实际被编译10次,如果重复包含,对编译的压力就比较大。
test.c

cpp 复制代码
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
 
   return 0;
}

test.h

cpp 复制代码
void test();
struct Stu
{
   int id;
   char name[20];
};

如果直接这样写,test.c⽂件中将test.h包含5次,那么test.h文件的内容将会被 拷贝 5份在test.c中。
如果test.h 文件比较大,这样预处理后代码量会剧增。如果工程比较大,有公共使用的头文件,被⼤家都能使用,又不做任何的处理,那么后果真的不堪设想。
如何解决头文件被重复引⼊的问题?答案:条件编译
每个头⽂件的开头写:

cpp 复制代码
#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif //__TEST_H__

或者:

cpp 复制代码
#pragma once

就可以避免头文件的重复引入。
注:
推荐《⾼质量C/C++编程指南》中附录的考试试卷。

13、其他预处理指令

C语言中还要其他的其他预处理指令 ,比如:

cpp 复制代码
#error
#pragma
#line
...
...

13.1#pragma

#pragma 指令控制实现定义行为。

cpp 复制代码
#pragma once 

当某个头文件中包含它时,指示编译器只对其分析一次,即使它在同一源文件中(直接或间接)被包含了多次也是如此。

cpp 复制代码
#pragma pack()

用于修改结构体的对齐字数,可以指定。


总结

上述文章讲了预处理阶段做的事,详细讲了#define的使用,和条件编译,头文件的包含等方面的知识,希望对你有所步帮助。

相关推荐
名字还没想好☜3 分钟前
Go 的 select 实战:超时、非阻塞收发与优雅退出的三个套路
开发语言·数据库·golang·go·并发
geovindu4 分钟前
java: Backtracking Algorithm
java·开发语言·windows·后端·算法·回溯算法
Scott9999HH9 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社10 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海10 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖10 小时前
JDK 26 新特性详解
java·开发语言
马优晨11 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区13 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大13 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai14 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang