深入剖析预处理

目录

1.预定义符号

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

3.#define定义宏

4.带有副作用的宏参数

5.宏替换的规则

6.宏函数的对比

7.#和##

7.1 #运算符

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

8.命名约定

9.#undef

10.命令行定义

11.条件编译

12.头文件的包含

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

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

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

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


1.预定义符号

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

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

可以将系统的时间日期等等输出

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1 
#include <stdio.h>
int main() {
    printf("%s\n", __FILE__);
    printf("%d\n", __LINE__);
    printf("%s\n", __DATE__);
    printf("%s\n", __TIME__);
    return 0;
}
//输出
/*
E:\C\Code\Cproject\practice7.8\practice7.8\ex01.c
5
Jul  8 2024
10:23:503*/

2.#define 定义常量

基本语法:

cpp 复制代码
#define name--名字 stuff--内容
复制代码
 

在预处理的时候,定义的变量会将下面执行的变量替换

举个例子:

cpp 复制代码
#define M 100;
#define reg register //因为resister太长了,用reg替换更方便
#define DEBUG_PRINT printf("file:%s\tline:%d\t\date:% s\ttime:% s\n" \
 __FILE__,__LINE__ , \
 __DATE__,__TIME__ )      // \是续航符号 相当于一个回车
int main() {
    int a = M;  //这里的M被替换成100
    reg int num = 0; //寄存器变量 reg替换register
    printf("%d", a);
}

思考:在define定义标识符的时候,要不要在最后加上 ; ?

比如:

复制代码
#define MAX 1000;
#define MAX 1000

建议不要加上 ; ,这样容易导致问题。

比如下面的场景:

复制代码
if(condition)
 max = MAX;
else
 max = 0;

如果是加了分号的情况,等替换后,if和else之间就是2条语句,而没有⼤括号的时候,if后边只能有⼀条语句。这⾥会出现语法错误。

3.#define定义宏

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

下面是宏的申明方式:

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

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

注意:

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

举例:

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

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

cpp 复制代码
#define s(n) n*n;
int main() {
    int i = 0;
    scanf("%d", &i);
    int ret = s(i);  //s(i) 就会替换成 i*i 
    printf("%d", ret);
}

如果将s(i) 中的i 变成 5+1 他会输出什么呢? 答案是 11 因为他是直接替换n 变成 5+1 * 5+1 =11

cpp 复制代码
#define s(n) n*n;
int main() {
    int i = 0;
    int ret = s(5+1);  //s(i) 就会替换成 i*i 
    printf("%d", ret);
}

如果真的想算出36 那就写成 (n)*(n) 加一个括号

cpp 复制代码
#define s(n) (n)*(n);
int main() {
    int i = 0;
    int ret = s(5+1);  //s(i) 就会替换成 i*i 
    printf("%d", ret);
}

写一个宏算一个数的二倍 --->写宏的时候一定不要吝啬括号,在适当的位置加上括号

cpp 复制代码
#define Double(n) (2*n);
​
int main() {
    int ret = 10 * Double(5);
    printf("%d", ret);
}

4.带有副作用的宏参数

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

例如:

复制代码
x+1;\*//\*\*不带副作⽤
​
x++;\*//\*\*带有副作⽤

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

(a++)>(b++)?a++(不执行):b++执行变成14

最后a的值为11 b的值为14 ,所以不要使用带有副作用的参数 会出现预想不到的结果

cpp 复制代码
#define MAX(x,y) ((x)>(y)?(x):(y))
int main() {
    int a = 10;
    int b = 12;
    int ret = MAX(a++, b++);
    // (a++)>(b++)?a++(不执行):b++执行变成14 
    printf("%d\n", ret);
    printf("a = %d b = %d", a, b);
}

5.宏替换的规则

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

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

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

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

看如下代码,M首先被替换为100 然后再执行MAX宏

cpp 复制代码
#define M 100
#define MAX(x,y) ((x)>(y)?(x):(y))
int main() {
    int ret = MAX(M, 15);  //M首先被替换为100 然后再执行MAX宏
    printf("%d\n", ret);
    
}

注意:

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

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

下面的字符串常量不会被替换

复制代码
printf("MAX(M,15)");

6.宏函数的对比

宏通常被应⽤于执⾏简单的运算。

比如在两个数中找出较⼤的⼀个时,写成下面的宏,更有优势⼀些。

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

那为什么不用函数来完成这个任务?

原因有:

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

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

和函数相比宏的劣势:

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

  2. 宏是没法调试的。

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

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

宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。

这两句代码实现的效果一样

cpp 复制代码
int* p = (int*)malloc(10 * sizeof(int)); int* p = MALLOC(10, int);

#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 .

就可以写:

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

当我们按照下⾯的⽅式调⽤的时候:

PRINT(a);//当我们把a替换到宏的体内时,就出现了#a,⽽#a就是转换为"a",时⼀个字符串代码就会被预处理为:

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

这个只适用于整形,我们再完善一下参数,使得该宏可以输出所有类型

cpp 复制代码
#define Print(format,n) printf("the value of " #n" is " format "\n",n)
int main() {
    int a = 10;
    Print("%d ",a);
    
    float f = 5.33;
    Print("%f", f);
}

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 复制代码
//##会将type和_max整合成一个字符
//生成函数的模板
#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{ \
 return (x>y?x:y); \
}
//使用上面的模板定义函数
GENERIC_MAX(int); //传一个整形类型进去 宏中定义的函数的类型全被替换成int
​
int main() {
    printf("%d", int_max(3, 5));
}

8.命名约定

⼀般来讲函数的宏的使⽤语法很相似。所以语言本⾝没法帮我们区分⼆者。

那我们平时的⼀个习惯是:

  • 把宏名全部大写
  • 函数名不要全部大写

9.#undef

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

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

10.命令行定义

许多C 的编译器提供了⼀种能力,允许在命令行中定义符号。用于启动编译过程。

例如:当我们根据同⼀个源⽂件要编译出⼀个程序的不同版本的时候,这个特性有点⽤处。(假定某个程序中声明了⼀个某个⻓度的数组,如果机器内存有限,我们需要⼀个很⼩的数组,但是另外⼀个机器内存⼤些,我们需要⼀个数组能够大些。)

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

编译指令:

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

11.条件编译

在编译⼀个程序的时候我们如果要将⼀条语句(⼀组语句)编译或者放弃是很⽅便的。因为我们有条件编译指令。--->用于跨平台性代码编译

调试性的代码,删除可惜,保留⼜碍事,所以我们可以选择性的编译。

我们看如下代码,M是一个常量等于2 那就执行输出 如果不等于2就不执行

cpp 复制代码
#define M 2;
int main() {
#if M==2;
    printf("执行这句代码"); 
#endif
}

注意::一定是常量,而不是一个变量,下面a是一个变量**(变量创建是运行时才创建,现在的阶段是预处理)**,所以不行,输出语句也不执行

cpp 复制代码
#define M 2;
int main() {
    int a = 2;
#if a==2;
    printf("执行这句代码");
#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.嵌套指令
int main() {
     #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
}

12.头文件的包含

12.1 头文件被包含的方式:

12.1.1 本地文件包含

复制代码
#include "filename"

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

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

复制代码
/usr/include

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

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

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

12.1.2 库文件包含

复制代码
 #include <filename.h>

查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。

我们要根据实际情况去选择哪一种方法,而不是一味的使用" ",

12.2 嵌套⽂件包含

我们已经知道, #include 指令可以使另外⼀个⽂件被编译。就像它实际出现于 #include 指令的地方⼀样。

这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。

⼀个头文件被包含10次,那就实际被编译10次,如果重复包含,对编译的压力就比较⼤。

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

如果直接这样写,test.c文件中将test.h包含5次,那么test.h文件的内容将会被拷贝5份在test.c中。如果test.h 文件很大,这样预处理后代码量会剧增。如果工程较大,有公共使⽤的头文件,被⼤家都能使用,⼜不做任何的处理,那么后果真的不堪设想。

我们可以使用条件编译去解决这个问题,该执行的逻辑为,首先判断该头文件是否被引入,没有就引入执行引入,当第二次被引入时,判断为假不在执行引入;

cpp 复制代码
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容放里面....
#endif

或者用一句,即可

cpp 复制代码
#pragma once
相关推荐
励志成为嵌入式工程师2 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
Peter_chq3 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
aloha_7894 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
hikktn5 小时前
如何在 Rust 中实现内存安全:与 C/C++ 的对比分析
c语言·安全·rust
观音山保我别报错5 小时前
C语言扫雷小游戏
c语言·开发语言·算法
dsywws5 小时前
Linux学习笔记之vim入门
linux·笔记·学习
小林熬夜学编程7 小时前
【Linux系统编程】第四十一弹---线程深度解析:从地址空间到多线程实践
linux·c语言·开发语言·c++·算法
A-超7 小时前
vue3展示pag格式动态图
笔记
墨墨祺7 小时前
嵌入式之C语言(基础篇)
c语言·开发语言
躺不平的理查德8 小时前
数据结构-链表【chapter1】【c语言版】
c语言·开发语言·数据结构·链表·visual studio