预处理详解

简介: 预处理详解

一、预处理符号

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

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

写代码输出来看一下:

#include <stdio.h>
int main()
{
  printf("%s\n", __FILE__);
  printf("%d\n", __LINE__);
  printf("%s\n", __DATE__);
  printf("%s\n", __TIME__);
  printf("%s\n", __STDC__);
  return 0;
}

这里如果使用VS编译器会报错

这样就说明VS编译器不遵循 ANSI  C

注释掉这行代码再来看一下:

可以看到输出了文件的路径名称,行号,编译日期和编译时间。

二、 #define 定义常量

基本语法(使用方法)

#define name stuff

看几个例子:

1、

#define MAX 100
 
int main()
{
  printf("%d", MAX);
  return 0;
}

这里会输出100,在预处理的过程中,会将代码中的MAX 替换成 100

       这里如果是这样

#define N 2+3
int main()
{
  printf("%d", 2*N);
  return 0;
}

这里输出结果是7,而不是10

这是因为预处理将N替换成了2+3,而不是替换成5;

当然#define 还可以定义关键字或者符号等

2、

#define CASE break;case

我们知道switch 语句中,每一个case分支都需要在末尾加上break,才能真正实现分支,而我们有时候就很容易以往break,这时我们可以这样来写:

#define CASE break;case
 
int main()
{
  int n = 0;
  scanf("%d", &n);
  switch(n)
  {
    case 0:
      ;
    CASE 1:
      ;
    CASE 2:
      ;
    default:
      break;
  }
  return 0;
}

这里需要注意:第一个分支应该写case,而不是CASE。

3、

看以下代码

#define DEBUG_PRINT printf("file:%s\tline:%d\t \
              date:%s\ttime:%s\n" ,\
              __FILE__,__LINE__ , \
              __DATE__,__TIME__ )

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

在define定义标识符时,要不要加上 ;  ?

#define MAX 1000;
#define MAX 1000

建议不要加上 ;  容易导致问题,比如:

在我们写代码的时候,我们会在语句最后写分号,这时如果定义标识符时,结尾加了;(分号)就会导致问题

if(condition)
    max = MAX;
else
    max = 0;

       这里如果加了分号,替换以后,if与else之间就是两条语句,而不加大括号时,if之后只能有一条语句,就会出现语法错误

三、 #define 定义宏

       宏

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

声明方式

#define name( parament-list ) stuff

注意:

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

举例:

使用宏,求n * n

#define SQUARE(n) n*n
int main()
{
  int n;
  scanf("%d", &n);
  printf("%d\n", SQUARE(n));
  return 0;
}

这里确实实现了宏定义求n的平方,但是还存在问题,

#define SQUARE(n) n*n
int main()
{
  int n;
  scanf("%d", &n);
  printf("%d\n", SQUARE(n+1));
  return 0;
}

当我们把n+1传给define定义的宏时,所求的5+1的平方结果是11,而不是36?

       这是因为,宏参数是直接替换的,就是我们将n+1传宏,这是计算的就是 n+1*n+1 这样的结果自然就是11。

那我们该怎么解决这个问题呢?

       其实很简单,就是加括号,(在宏定义时不要吝啬括号)

#define SQUARE(n) ((n)*(n))
int main()
{
  int n;
  scanf("%d", &n);
  //printf("%d\n", SQUARE(n));
  printf("%d\n", SQUARE(n + 1));
  return 0;
}

这样写,SQUARE求n+1时就替换成 ((n+1)*(n+1))这样计算结果就不会出错了

注意:

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

       带有副作用的宏

当我们写宏时,宏参数在宏的定义中出现超过一次时,如果参数带有副作用,那我们在使用这个宏时就可能出现危险,导致不可预测的后果。

       这里副作用就是指:表达式求值的时候出现永久性效果(就比如,自加,自减)

这里写一个比较大小的宏来看一个这种错误:

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
int main()
{
  int x, y, z;
  x = 5;
  y = 8;
  z = MAX(x++, y++);
  printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
  return 0;
}

这里x++ ,y++ 预处理完结果

z = ((x++)>(y++)?(x++):(y++));

这样输出的结果:

       宏替换的规则

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

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
  2. 替换文本随后被插入程序中原来文本的位置。对于宏,参数名被它们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述过程

注意

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

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

       宏函数的对比

我们可以发现宏和函数很相似,但是宏又不同于函数

四、 # 和 ##

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

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

就比如,我们有一个变量 int a = 10 ;我想要输出 the value of a is 10

就这样来写:

 

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

在经过预处理之后代码就被预处理为

printf("the value of" "a" "is %d" , a);

就可以输出我们想要的结果

#define PRINT(n) printf("the value of "#n " is %d", n);
int main()
{
  int a = 10;
  PRINT(a);
  return 0;
}

       ##运算符

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

## 被称为记号粘合

这里我们写一个求两个数的较大值的代码

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

用函数写,我们对于不同的数据类型就要写不同的函数,太麻烦了,我们现在写宏

#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{ \
 return (x>y?x:y); \
}
 
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

这条指令用于一处一个宏定义

六、 命名约定

这个主要来区分宏和函数名

       一般来说,

       把宏名全部大写

       函数名不要全部大写

七、 命令行定义

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

       当我根据同一个源文件要编译出不同的版本时,这个就很有用。(假设一个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另一个机器内存大,我们需要一个大的数组)

#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

八、条件编译

在编译程序的过程中,我们如果想要将一条语句放弃是非常容易的。我们就要使用条件编译

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

常见的条件编译指令有很多

//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
    
  #endif
#ifdef OPTION2
  
#endif
#elif defined(OS_MSDOS)
  #ifdef OPTION2
 
  #endif
#endif

九、头文件包含

头文件的包含方式有两种:

       本地文件包含

查找策略:现在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件那样在标准位置查找头文件

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

/user/include

       Vs环境的便准头文件路径

C:\program Files (x86) \ Microsrft Visual Stdio 12.0 \ VC \ include

       库文件包含

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

这样,我们是不是可以对于库函数也能用 “”来包含,这样当然是可以的

但是这样查找的效率就低一些,当然这样也不容易区分是库文件还是本地文件了

学到这里,我们知道#include指令预处理器会删除这条指令,并用包含文件替换

       但是,如果一个文件被包含很多次,那实际就编译多次,如果重复包含,这对编译压力就比较大

对于这个问题,我们可以在文件的开头这样写

#ifndef __TEST_H__
#define __TEST_H__
//文件内容
#endif

这中相当于把文件的内容直接写在了条件编译中,我们也可以直接写

#pragma

这样可以避免头文件重复引入

十、其他预处理指令

还有很多预处理指令

       比如

这里就不做过多的介绍了

#error
#pragma
#line

感谢观看,希望一下内容对你有所帮助,如果内容对你有作用,可以一键三连加关注,作者也正在学习中,有错误的地方还请指出,感谢!!!

相关文章
|
存储 机器学习/深度学习 安全
PACS覆盖放射、超声、内镜、病理等医技科室业务流程
医学影像PACS系统(Picture Archiving and Communication System)是一个医院信息系统,用于存储、检索、传输和显示医学影像。它可以集成多种医疗设备,如X光机、CT、MRI、超声等,将这些设备产生的数字影像转换成标准格式,进行存储和管理,以便医生和专业技术人员进行诊断和治疗。
277 4
|
Android开发
【Android App】蓝牙的设备配对、音频传输、点对点通信的讲解及实战(附源码和演示 超详细)
【Android App】蓝牙的设备配对、音频传输、点对点通信的讲解及实战(附源码和演示 超详细)
3052 1
|
6月前
|
人工智能 Cloud Native 容灾
深圳农商银行三代核心系统全面投产 以云原生架构筑牢数字化转型基石
深圳农商银行完成第三代核心系统全面上云,日均交易超3000万笔,峰值处理效率提升2倍以上。扎根深圳70余年,与阿里云共建“两地三中心”分布式云平台,实现高可用体系及全栈护航。此次云原生转型为行业提供可复制样本,未来将深化云计算与AI合作,推动普惠金融服务升级。
483 17
|
10月前
html+js+css实现的建筑方块立体数字时钟源码
html+js+css实现的建筑方块立体数字时钟源码
500 33
|
存储 消息中间件 负载均衡
Zookeeper 简单介绍
Zookeeper 简单介绍
|
存储 JSON 物联网
查询性能提升 10 倍、存储空间节省 65%,Apache Doris 半结构化数据分析方案及典型场景
本文我们将聚焦企业最普遍使用的 JSON 数据,分别介绍业界传统方案以及 Apache Doris 半结构化数据存储分析的三种方案,并通过图表直观展示这些方案的优势与不足。同时,结合具体应用场景,分享不同需求场景下的使用方式,帮助用户快速选择最合适的 JSON 数据存储及分析方案。
566 15
查询性能提升 10 倍、存储空间节省 65%,Apache Doris 半结构化数据分析方案及典型场景
|
存储 负载均衡 NoSQL
一文让你搞懂 zookeeper
一文让你搞懂 zookeeper
18014 16
|
数据挖掘
【数据挖掘】Lasso回归原理讲解及实战应用(超详细 附源码)
【数据挖掘】Lasso回归原理讲解及实战应用(超详细 附源码)
1425 0
|
安全 KVM 虚拟化
OpenEuler 中配置 KVM 虚拟化环境指南
本文档详细介绍了如何在OpenEuler系统中配置和管理KVM虚拟化环境,包括环境准备、组件安装、虚拟机安装及管理命令等,适合初学者和有经验的用户。内容覆盖了从桥接网卡配置到虚拟机的安装与管理,以及常见问题的解决方法,帮助用户高效利用虚拟化技术。
1309 0
|
缓存 运维 NoSQL
使用 psutil 获取硬件、网络以及进程信息
使用 psutil 获取硬件、网络以及进程信息
231 0