预处理详解

简介: 预处理详解

一、预处理符号

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

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

相关文章
|
24天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
16天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
20天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2577 22
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
|
18天前
|
人工智能 IDE 程序员
期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
在云栖大会上,阿里云云原生应用平台负责人丁宇宣布,「通义灵码」完成全面升级,并正式发布 AI 程序员。
|
3天前
|
JSON 自然语言处理 数据管理
阿里云百炼产品月刊【2024年9月】
阿里云百炼产品月刊【2024年9月】,涵盖本月产品和功能发布、活动,应用实践等内容,帮助您快速了解阿里云百炼产品的最新动态。
阿里云百炼产品月刊【2024年9月】
|
2天前
|
存储 人工智能 搜索推荐
数据治理,是时候打破刻板印象了
瓴羊智能数据建设与治理产品Datapin全面升级,可演进扩展的数据架构体系为企业数据治理预留发展空间,推出敏捷版用以解决企业数据量不大但需构建数据的场景问题,基于大模型打造的DataAgent更是为企业用好数据资产提供了便利。
163 2
|
20天前
|
机器学习/深度学习 算法 数据可视化
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
2024年中国研究生数学建模竞赛C题聚焦磁性元件磁芯损耗建模。题目背景介绍了电能变换技术的发展与应用,强调磁性元件在功率变换器中的重要性。磁芯损耗受多种因素影响,现有模型难以精确预测。题目要求通过数据分析建立高精度磁芯损耗模型。具体任务包括励磁波形分类、修正斯坦麦茨方程、分析影响因素、构建预测模型及优化设计条件。涉及数据预处理、特征提取、机器学习及优化算法等技术。适合电气、材料、计算机等多个专业学生参与。
1576 16
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
|
22天前
|
编解码 JSON 自然语言处理
通义千问重磅开源Qwen2.5,性能超越Llama
击败Meta,阿里Qwen2.5再登全球开源大模型王座
977 14
|
4天前
|
Linux 虚拟化 开发者
一键将CentOs的yum源更换为国内阿里yum源
一键将CentOs的yum源更换为国内阿里yum源
221 2
|
17天前
|
人工智能 开发框架 Java
重磅发布!AI 驱动的 Java 开发框架:Spring AI Alibaba
随着生成式 AI 的快速发展,基于 AI 开发框架构建 AI 应用的诉求迅速增长,涌现出了包括 LangChain、LlamaIndex 等开发框架,但大部分框架只提供了 Python 语言的实现。但这些开发框架对于国内习惯了 Spring 开发范式的 Java 开发者而言,并非十分友好和丝滑。因此,我们基于 Spring AI 发布并快速演进 Spring AI Alibaba,通过提供一种方便的 API 抽象,帮助 Java 开发者简化 AI 应用的开发。同时,提供了完整的开源配套,包括可观测、网关、消息队列、配置中心等。
734 9