c语言基础(一)——预处理详解

2023-11-18

目录

1.预处理是什么?

2.预定义符号

3.define标识符

3.1 define 定义标识符

3.2 define定义宏

命名约定

3.3 宏的题目

4.# 和 ##

4.1 #的作用

4.2 ##的作用

5.带副作用的宏参数

6.宏与函数的对比

7.#undef

8.条件编译

9.头文件被包含的方式


1.预处理是什么?

预编译又称为预处理 , 是做些代码文本的替换工作。处理 # 开头的指令 , 比如拷贝 #include 包含的文件代码, #define 宏定义的替换 , 条件编译等,就是为编译做的预备工作的阶段,主要处理#开始的预编译指令,预编译指令指示了在程序正式编译前就由编译器进行的操作,可以放在程序中的任何位置。

预处理主要工作:

1.头文件的包含           2.define宏定义的替换       3.条件编译的处理      
 

2.预定义符号

  • __FINE__    //进行编译的源文件
  • __Line__     //文件当前的行号
  • __Date__     //文件被编译的日期
  • __TIME__    //文件被编译的时间
  • __STDC__  //如果编译器遵循ANSI,其值为1,否则则未定;

上面这些是预定义符号都是语言内置的

注意:符号中的左右是两个横杠,是__FINE__,而不是_FINE_,

例如:

	printf("文件:%s\n", __FILE__);
	printf("文件当前的行号:%d\n", __LINE__);
	printf("文件编译的日期:%s\n", __DATE__);
	printf("文件编译的时间:%s\n", __TIME__);

3.define标识符

3.1 define 定义标识符

# define宏定义是个演技非常高超的替身演员,在编译器编译时,真身则会替换替身,在我们代码中我们会经常使用这个替身。

#define MAX 1000

int max = MAX;//在预处理阶段,MAX会替换为1000,相当于int max=1000;

3.2 define定义宏

#defifine 机制包括了一个规定, 允许把参数替换到文本中,这种实现通常称为宏或定义宏
例如我们写一个数字的平方:
#define SQUARE(x) x*x

int main()
{
	printf("%d", SQUARE(5));//输出25
	return 0;
}

当我们传5进去后,在预编译阶段,则SQUARE(5)会被替换为5*5,所以最终输出为25.

	printf("%d", SQUARE(2 + 3));//输出11
    

为什么上面代码会输出11呢?

当我们传2+3进去后,在预编译阶段,则SQUARE(2+3)会被替换为2+3*2+3,在根据优先级计算,即可得出11,我们想要的是(2+3)*(2+3),但输出并没有符号我们预期,我们需要把宏修改一下。

#define SQUARE(x) (x)*(x)
printf("%d", SQUARE(2 + 3));//输出25

那么我们在来定义一个宏,求整数的两倍。

	#define DOUBLE(x) (x)+(x)
    printf("%d", DOUBLE(5));//输出10
	printf("%d", 2*DOUBLE(5));//输出15

 DOUBLE(5)输出10我们还是能够理解,但是2*DOUBLE(5)输出15是怎么回事呢,在预编译阶段,2*DOUBLE(5)则会被替换成2*(5)+(5),我们想要的是输出20的结果,那怎么办呢?那么我们在外边直接加个大括号即可。

#define DOUBLE(x) ((x)+(x))

总结:所以我再写宏定义时尽量给每个变量加个括号,最外层的括号也别省。例如之前#define SQUARE(x) (x)*(x),在外层的括号也加上去,变成

#define SQUARE (x)   ((x)*(x))

命名约定

一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。 那我们平时的一个习惯是:
把宏名全部大写 函数名不要全部大写。

3.3 宏的题目

写一个宏,可以将一个整数的二进制位的奇数位和偶数位交换。

代码:

#define SWAP_BIT(n)  n=(((n)&0x55555555)<<1)|(((n)&0xaaaaaaaa)>>1)

思路:先取出整数二进制的奇数位上的数,&上一个01010101010101010101010101010101提取出奇数位,对应的十六进制为0x55555555,在取出整数的二进制的偶数位上的数,&上一个10101010101010101010101010101010提取出偶数位,对应的十六进制为0xaaaaaaaa,然后对奇数位上的数左移一位(<<)就变成偶数位,对偶数位上的数右移一位(>>)就变成奇数位了,在对这两个数进行 | 就可以完成交换。

4.# 和 ##

4.1 #的作用

#VALUE会被预处理为“VALUE”

	int a = 0;
	double d = 1.11;
	printf("the value of a is %d\n", a);//输出:the value of a is 0
	printf("the value of d is %f\n", d);//输出:the value of d is 1.110000

那么我们怎样可以写一个宏来替代    

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

printf("the value of d is %f\n", d);这两条语句呢?

首先我们必须先知道:

	printf("hello " "world\n");//输出hello world
字符串是有自动连接的特点的
所以我们可以定义这样一个宏:
#define PRINT(FORMAT,DATA) printf("the value of " #DATA " is " #FORMAT“\n",DATA)

int a=10;
double d=1.11;
PRINT(%d, a);//输出:the value of a is 0
PRINT(%f, d);//输出:the value of a is 1.110000

所以我们就很好通过一个宏就替代上面那两条语句。

注意:宏中的#DATA在预编译时会替换成”DATA"  ,#FORMAT会被替换为“FORMAT"。

4.2 ##的作用

##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符
	#define ADD_TO_SUM_(num,value) sum##num+=value;
    int sum1 = 1;
	int sum2 = 2;
	ADD_TO_SUM_(1, 10);//sum1增加10
	ADD_TO_SUM_(2, 10);//sum2增加10

sum##num为sumnum(num为传进来的宏参数)

5.带副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候, 如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险 ,导致不可预测的后果。
例如:
#define MAX(x,y)  ((x)>(y))?(x):(y)

int main()
{
	int a = 0;
	int b = 0;
	MAX(a++, b++);
	printf("%d %d", a, b);//结果:a为1,b为2
	return 0;
}

      为什么运行结果a为1,b为2呢?

    在预编译时,MAX(a++,b++)会被替还为  ((a++)>(b++))?(a++):(b++),

    在 ?判断之前 a跟b都进行了一次++,都变为1,判断a不比b大后,则在运行b++,b在变成2.
所以 a++,和b++这种会改变变量本身的宏参数为带有副作用的宏参数。

6.宏与函数的对比

宏跟函数其实有有很多不同点,那么我们来利用宏和函数来实现找出两个数中的较大值。

#define MAX(x, y) ((x)>(y)?(x):(y))//宏实现

int Max(int a,int b)//函数实现
{
   return a>b?a:b;
}
宏的优势:
1. 宏的参数不需要特定的类型,所以宏的使用可以传任意类型,宏是与类型无关的。而函数的参数必须为特定类型,所以使用的时候只能传特定的参数。

2.宏在预编译阶段中会直接将代码替换,而函数调用需要创建函数栈帧和返回值都需要消耗时间,所以宏的效率比函数要高

的 

宏的劣势:

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

2.由于宏是运行时将代码插入到程序中,所以宏是没办法调试

3.宏的参数可以是任意类型的,所以不严谨

4.宏会导致优先级的问题,容易出错。

5.宏是不能递归,函数可以递归

7.#undef

撤销一个宏定义,如果现存的名字需要重新定义,那么需要先移除它的旧名字。

#define MAX 100

int main()
{
	int MAX = 1000;//错误:MAX为宏定义,不能再重新定义
#undef MAX;//移除MAX的宏定义,下面的宏定义就不能用
	int MAX = 1000;//正确
	return 0;
}

宏的生命周期从#define 开始到#undef 结束

8.条件编译

条件编译的功能使得我们可以 按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件

条件编译有三种形式:

方式一:

#ifdef 标识符
   程序段一
#else
   程序段二
#endif 

上面的代码是,如果标识符有被#define定义过,则运行程序段一,如果没有定义过,则运行程序段二,如果没有程序段二(空),则#else可以没有    。 

#define DEBUG 1
int main()
{
#ifdef DEBUG
	printf("hello world\n");
#else
	printf("thank you\n");
#endif 
}    

      例如上面的代码,由于DEBUG被定义,所以在预编译的时候,编译器只会保留

int main()
{
    printf("hello world\n");
}

这段代码,其它代码被编译过滤掉,最终输出结果为hello world。

如果DEBUG没有定义,则编译器则会保留    
int main()
{
    printf("thank you\n");
}   

这段代码,其它代码被编译过滤掉,最终输出结果为 thank you。

方式二:

       

#ifndef 标识符
程序段 1
#else
程序段 2
#endif

    与第一种形式差不多,只是将#ifdef改为#ifndef,如果标识符没有被定义,则运行程序段1,有被定义则运行程序段 2,与方式一差不多,只是逻辑相反,所以笔者我就不再举例子了。

方式三:

#if 常量表达式1
	程序段 1
#elif 常量表达式2
	程序段 2
#else
	程序段 3
#endif

如果 常量表达式1的结果不为0,则为真,则只运行程序段 1,如果常量表达式结果为0(假),常量表达式为2不为0,则只运行程序段 2,如果常量表达式为1和常量表达式 2的结果都为0,则运行程序段 3。如果没有程序段 2和程序段 3(为空),则#elif和# else可以没有


int main()
{
#if 10
	printf("hello world\n");
#elif 10
	printf("you are very handsome\n");
#else
	printf("thank you\n");
#endif
	return 0;
}

对于上面的代码,由于常量表达式不为0,所以在预编译的时候,编译器只保留


int main()
{
    printf("hello world\n");

     return 0;
}
这段代码,其它代码被过滤掉。最终运行结果为 hello world ,如果常量表达式1的结果为0,则运行结果为 you are very handsome ,如果常量表达式1和常量表达2都为0,则运行结果为thank you。

假设我们有一个Add.h头文件,里面包含着几个函数的声明:

int Add_Int(int x, int y);
double Add_Double(double x, double y);
float Add_Float(float x, float y);

我们在test.c上引入Add.h的头文件,如下:

#include"Add.h"
int main()
{
	int a = 0, b = 0;
	Add_int(a, b);
	return 0;
}

在预编译阶段时,则编译器会发生替换,替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。

则test.c文件预编译后会变成:
 

int Add_Int(int x, int y);
double Add_Double(double x, double y);
float Add_Float(float x, float y);

int main()
{
	int a = 0, b = 0;
	Add_int(a, b);
	return 0;
}

但是如果我们的test.c文件多次包含头文件Add.h后,则预编译时会将Add.h的内容会包含多次,这就会导致test.c文件中的内容重复。

那么如何解决头文件的重复包含?答案是:条件编译

我们可以对Add.h头文件就行修改

#ifndef ADD_H
#define ADD_H 1
	int Add_Int(int x, int y);
	double Add_Double(double x, double y);
	float Add_Float(float x, float y);
#endif

如果test.c引入一次Add.h头文件,由于没有定义#define Add_H,则预编译时会定义一次#define Add_H,并且将头文件中的内容给包含进去,如果在引入一次Add.h头文件,因为ADD_H已经被定义,那么条件编译不会将头文件的内容包含进来,所以这就很巧妙的解决这些问题。

或者我们有一种现代写法:

#pragma once
int Add_Int(int x, int y);
double Add_Double(double x, double y);
float Add_Float(float x, float y);

上面的两种方式都能避免我们的头文件的重复引用。

9.头文件被包含的方式

c语言提供了两种头文件的包含方式,它实际是宏替换的延伸,有两种格式:

格式一:

#include<stdio.h> 

stdio为包含文件的名称,用尖括号括,预处理时会直接去库函数的头文件中查找,找不到则编译错误。

格式二:

#include"file.h"

 ”file.h为包含文件的名称,直接在当前目录下查找名为file.h的头文件,找不到在按系统指定的路径信息,搜索其他目录。找到文件后,用

文件内容替换该语句。

 点个赞呗~

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

c语言基础(一)——预处理详解 的相关文章

随机推荐

  • Java StringBuilder类(为什么更高效、常用方法、构造器)

    StringBuilder类 一 引言 二 StringBuilder类特点 三 StringBuilder构造方法 四 StringBuilder常用方法 五 StringBuilder为什么高效率 六 StringBuffer类 一 引
  • android 文件删除命令大全,Android中删除sdcard里文件的命令

    Android中删除sdcard里文件的命令 有时我们需要删除android模拟器或手机上的sd卡文件 特别是模拟器上的 这时我们就需要借助于一些命令来帮助我们删除文件 复制代码 代码如下 重新挂载模拟器 adb remount 开始she
  • fasterxml ToStringSerializerBase报错

    ToStringSerializerBase报错 报错内容 整合dubbo时报错 Caused by java lang NoClassDefFoundError com fasterxml jackson databind ser std
  • 英语姓名

    英语姓名的一般结构为 教名 自取名 姓 如 William Jafferson Clinton 但在很多场合中间名往往略去不写 如 George Bush 而且许多人更喜欢用昵称取代正式教名 如 Bill Clinton 上述教名和中间名又
  • Unity中的单元测试

    我知道的单元测试有两种方法 1 从Unity的AssetStore窗口下载UnityTestTools 已下架了 2 使用TestRuner 2018 2 5以上版本有 我用的是2018 2 7 TestRuner的用法 操作窗口 Wind
  • 堵塞队列之ArrayBlockingQueue和LinkedBlockingQueue解析

    在线程池创建的时候 需要传一个堵塞队列来维护需要执行的线程任务 其中最常用的是ArrayBlockingQueue和LinkedBlockingQueue 他们都继承了BlockingQueue接口 ArrayBlockingQueue 一
  • 交叉熵损失

    什么是交叉熵损失 提起损失 我们最熟悉的可能就是MSE 最小均方误差损失了 MSE通俗理解 就是预测值与真实值之间取个差 再求平方 交叉熵损失也是一种衡量预测值与真实值之间的差异的方式 两者的定义不同 适用的范围也不同 通常来说 交叉熵损失
  • 如何从Python入门机器学习

    学习Python基本语法 首先我在Python官网找到入门教程 快速过了一遍Python的基本语法 相信对于稍微有点编程基础的人来说这都不是事儿 作为实践 接着我用Python实现了一个基于命令行翻译脚本 到此Python算入门了 这里啰嗦
  • 如何用R分析CNKI文献关键词词频?

    疑惑 如何用VOSviewer分析CNKI数据 一文发布后 有同学问我 王老师 我有个问题 我用cnki导出关键词后 想统计关键词的词频 我应该用什么样的工具 如果不利用citespace和python 做出excel那种的统计表格 该怎么
  • 操作系统笔记整理8——虚拟存储器

    点此链接可跳转到 操作系统笔记整理 目录索引页 参考书籍 计算机操作系统 第四版 汤小丹等编著 文章目录 点此链接可跳转到 操作系统笔记整理 目录索引页 虚拟存储器概述 请求分页存储管理方式 请求页表机制 地址变换 物理块的分配策略 例题
  • Sqli-labs之Less-28和Less-28a

    Less 28 基于错误的 单引号字符型 过滤了union和select等的注入 判断注入类型 1和1 正常回显 1 报错 单引号字符型 2 26 26 1 1回显为id 1 有小括号 参看源码 发现没有过滤or与and 过滤了相连的uni
  • vuforia for unity 入门教程

    一 配置vuforia环境 与 运行简单AR程序 1 安装unity2017 1 从官网上下载unity2017下载器 2 运行下载器勾选上vuforiasupport选项 其余不变 点击开始安装 2 创建AR项目与配置AR运行环境 1 打
  • KITTI数据集介绍

    KITTI数据集 7481 training images and point clouds and 7518 test images and point clouds 采集平台 1惯性导航系统 GPS IMU OXTS RT 3003 1
  • Python基础教程(入门教程),30分钟玩转Python编程!

    这是一篇针对初学者的 Python 基础教程 只要你认真阅读 花费 30 分钟即可快速了解 Python 这篇 Python 入门教程讲解的知识点包括 Python 编程环境的搭建 Python 基本操作入门 Python 数据类型 Pyt
  • bit、Byte、bps、Bps、pps、Gbps的单位详细说明及换算

    bit Byte bps Bps pps Gbps的单位详细说明及换算 bit 电脑记忆体中最小的单位 在二进位电脑系统中 每一bit 可以代表0 或 1 的数位讯号 Byte 字节单位 一般表示存储介质大小的单位 一个B 常用大写的B来表
  • 为什么uzi排到古手羽就秒_为什么uzi排到古手羽就秒退看一下直播间里的弹幕就清楚了!...

    为什么uzi排到古手羽就秒退 看一下直播间里的弹幕就清楚了 百度APP游戏年度票选活动 前言 12月24号英雄联盟电竞圈快讯 12月份的电竞圈显得有点冷清 不过龙百万仍然会带来每天的精彩快讯 和召唤师们聊聊天 谈谈心 刚出道就遭受巨大压力的
  • 【总结】MySQL1

    MySQL 1 关系型数据库 1 1 特点 理论基础 关系代数 集合论 一阶逻辑 关系运算 具体表象 用二维表装数据 表 table entity relation 列 column field attribute 行 row record
  • 认识Java虚拟机中的类加载子系统和执行引擎

    目录 一 JVM整体架构 二 JVM类装载子系统 一 类加载过程 二 类加载器 三 类加载机制 1 全盘负责委托机制 2 双亲委派机制 Parents Delegation Model
  • XMT.com超200万被区块链终端交易

    链客 专为开发者而生 有问必答 此文章来自区块链技术社区 未经允许拒绝转载 狭义来讲 区块链是一种按照时间顺序将数据区块以顺序相连的方式组合成的一种链式数据结构 并以密码学方式保证的不可篡改和不可伪造的分布式账本 当下 区块链俨然成为炙手可
  • c语言基础(一)——预处理详解

    目录 1 预处理是什么 2 预定义符号 3 define标识符 3 1 define 定义标识符 3 2 define定义宏 命名约定 3 3 宏的题目 4 和 4 1 的作用 4 2 的作用 5 带副作用的宏参数 6 宏与函数的对比 7