深入剖析预处理

目录

1.预定义符号

2.#define 定义常量

3.#define定义宏

4.带有副作用的宏参数

5.宏替换的规则

6.宏函数的对比

7.#和##

7.1 #运算符

7.2 ## 运算符

8.命名约定

9.#undef

10.命令行定义

11.条件编译

12.头文件的包含

12.1 头文件被包含的方式:

12.1.1 本地文件包含

12.1.2 库文件包含

12.2 嵌套⽂件包含


1.预定义符号

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

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

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

#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 定义常量

基本语法:

#define name--名字 stuff--内容
 

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

举个例子:

#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 ); 置于程序中,预处理器就会用下面这个表达式替换上面的表达式:

#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

#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) 加一个括号

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

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

#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 ,所以不要使用带有副作用的参数 会出现预想不到的结果

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

#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. 宏可能会带来运算符优先级的问题,导致程序容易出现错。

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

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

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

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

#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个数的较⼤值的时候,不同的数据类型就得写不同的函数。

⽐如:这样太繁琐了

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

我们使用宏定义模板函数

//##会将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 的编译器提供了⼀种能力,允许在命令行中定义符号。用于启动编译过程。

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

#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就不执行

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

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

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

常见的条件编译指令:

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 文件很大,这样预处理后代码量会剧增。如果工程较大,有公共使⽤的头文件,被⼤家都能使用,⼜不做任何的处理,那么后果真的不堪设想。

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

#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容放里面....
#endif

或者用一句,即可

#pragma once

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/786589.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

React setState

老生常谈之setState 是同步的还是异步的&#xff1f; 设想setState是同步的&#xff0c;那也就是每次调用setState都要进行新旧虚拟DOM的对比&#xff0c;然后将差异化的dom更新到页面上&#xff0c;性能损耗很大 所以react把setState设置为了异步&#xff0c;当状态更新时不…

基于springboot+vue实现的厨艺交流平台(文末源码+Lw)093

93基于SpringBootVue的实现的厨艺交流平台&#xff08;源码数据库万字Lun文流程图ER图结构图演示视频软件包&#xff09; 系统功能&#xff1a; 这次开发的厨艺交流平台功能有个人中心&#xff0c;食材分类管理&#xff0c;用户管理&#xff0c;菜品分类管理&#xff0c;菜谱信…

【Axure】产品原型如何在谷歌浏览器中打开

作为一名前端开发来说&#xff0c;在拿到产品的原型图后&#xff0c;如何打开&#xff1f;直接用谷歌浏览器打开&#xff0c;是打不开的&#xff0c;需要安装对应的插件。但是谷歌插件市场在不翻墙的情况下&#xff0c;是没有办法直接打开的&#xff0c;分享一种超级简单的方法…

Softmax回归中的损失函数

目录 一、损失函数介绍&#xff1a; 因为Softmax回归时逻辑回归的推广&#xff0c;所以Softmax回归的损失函数是逻辑回归损失函数的推广。 原文链接&#xff1a;逻辑回归中的损失函数 一、损失函数介绍&#xff1a; 与回归问题成本函数不同的是&#xff0c;Softmax回归模型&a…

python-小杨的储蓄(赛氪OJ)

题目描述 小杨共有 N 个储蓄罐&#xff0c;编号从 0 到 N−1。从第 1 天开始&#xff0c;小杨每天都会往存钱罐里存钱。具体来说&#xff0c;第 i 天他会挑选一个存钱罐 ai​&#xff0c;并存入 i 元钱。过了 D 天后&#xff0c;他已经忘记每个储蓄罐里都存了多少钱了&#xff…

如何网页在线编辑微软Office Word,并导出为PDF格式。

随着互联网技术的不断发展&#xff0c;越来越多的企业开始采用在线办公模式&#xff0c;微软Office Word 是最好用的文档编辑工具&#xff0c;然而doc、docx、xls、xlsx、ppt、pptx等格式的Office文档是无法直接在浏览器中直接打开的&#xff0c;如果可以实现Web在线预览编辑Of…

Vue3 pdf.js将二进制文件流转成pdf预览

好久没写东西&#xff0c;19年之前写过一篇Vue2将pdf二进制文件流转换成pdf文件&#xff0c;如果Vue2换成Vue3了&#xff0c;顺带来一篇文章&#xff0c;pdf.js这个东西用来解决内网pdf预览&#xff0c;是个不错的选择。 首先去pdfjs官网&#xff0c;下载需要的文件 然后将下载…

通用后台管理(二)——项目搭建

目录 前言 一、安装vue-cli依赖 1、使用yarn下载vue-cli 2、使用npm下载 3、检查一下是否下载成功 二、创建项目 1、创建项目&#xff0c;my-app是项目名称 2、 这里选择vue 2&#xff0c;蓝色表示选中的。 3、启动项目 三、下载项目依赖 四、配置项目 1、修改esli…

哪些独立站外链策略最有效?

在当前的SEO领域中&#xff0c;独立站外链策略的效果差异很大&#xff0c;但GPB外链无疑是其中最为有效的一种。GPB外链&#xff0c;指的是通过高质量、包收录且dofollow的顶级域名独立站来获得外链&#xff0c;这种外链策略能够显著提升目标网站的整体排名数据。 关键词排名的…

最全 Steam 流操作!!!Java Stream 流操作常用 API

文章目录 Java Stream 流操作常用 API一、准备工作二、Stream 常用 API1、sorted 排序2、list 转为 map(并解决重复key问题)3、filter 方法过滤指定查询条件4、根据指定列分组5、通过 map 获取指定列集合6、根据 List 中 Object 某个属性去重7、list 统计&#xff08;求和、最大…

Nignx配置

Nginx配置之nginx.conf文件解析及配置 1、nginx.conf文件解析 user www-data; worker_processes auto; pid /run/nginx.pid; include /etc/nginx/modules-enabled/*.conf;events {worker_connections 768;# multi_accept on; }http {### Basic Settings###开启文件的高效传输…

RK3568------Openharmony 4.0-Release WIFI/BT模组适配

RK3568------Openharmony 4.0-Release WIFI/BT模组(ap6236)适配 文章目录 RK3568------Openharmony 4.0-Release WIFI/BT模组(ap6236)适配前言一、驱动移植二、设备树配置三 、内核配置四、遇到的问题五、效果展示总结 前言 随着RK3568适配工作的推进&#xff0c;整体适配工作…

差分+前缀和习题集

&#xff08;luogu题号&#xff09; P6568 [NOI Online #3 提高组] 水壶 思路分析 前缀和优化问题。 其实题意就是让你求有k1个数的区间和最大值&#xff0c;那么直接前缀和优化&#xff0c;就可以通过本题。 代码 #include<bits/stdc.h> using namespace std;const in…

spring的bean注册

bean注册 第三方jar包的类想添加到ioc中&#xff0c;加不了Component该怎么办呢。 可以使用Bean和Import引入jar包&#xff0c;可以使用maven安装到本地仓库。 修改bean的名字&#xff1a;Bean("aaa")使用ioc的已经存在的bean对象&#xff0c;如Country&#xff1a;p…

【数据分享】1981—2023年中国逐日归一化植被指数(NDVI)栅格数据

NDVI&#xff0c;全名为Normalized Difference Vegetation Index&#xff0c;中文名称为归一化植被指数。这个指数可以用来定性和定量评价植被覆盖及其生长活力&#xff0c;我们也可以简单地将它理解为体现植被密度和健康状况的一个指标。 本次我们给大家分享的是1981年6月24日…

VSCode用ssh连接ubuntu虚拟机实现远程访问文件夹

1. ubuntu安装ssh服务 1.1 安装 sudo apt-get install ssh sudo apt-get install openssh-server1.2 启动ssh服务 sudo service ssh start sudo service ssh status # 查看状态 ## 或者用下面方式重启ssh服务 ## /etc/init.d/ssh restart1.3 ssh服务加入开机启动 sudo syst…

从天空到地面:无人机航拍推流直播技术在洞庭湖决口封堵中的全方位支援

据新闻报道&#xff0c;受持续强降雨影响&#xff0c;湖南省华容县团洲垸洞庭湖一线堤防发生管涌险情&#xff0c;随后出现决口。截至7月8日20时左右&#xff0c;226米长的洞庭湖一线堤防决口已累计进占208米&#xff0c;目前剩余18米&#xff0c;有望在今晚或9日凌晨实现合龙。…

python爬虫基础入门

步骤 获取网页内容&#xff1a; http请求 python的Requests库 解析网页内容 html网页结构 python的Beautiful Soup库 储存或分析数据 储存进数据库 作为ai分析的数据 转化为图表显示出来 DDoS攻击 通过给服务器发送海量高频请求&#xff0c;大量消耗网页资源&#…

加密与安全_密钥体系的三个核心目标之完整性解决方案

文章目录 Pre机密性完整性1. 哈希函数&#xff08;Hash Function&#xff09;定义特征常见算法应用散列函数常用场景散列函数无法解决的问题 2. 消息认证码&#xff08;MAC&#xff09;概述定义常见算法工作原理如何使用 MACMAC 的问题 不可否认性数字签名&#xff08;Digital …

详细解读COB显示屏使用的共阴技术原理

倒装COB显示屏技术中采用的共阴技术是一种旨在提升能效并且减少驱动功耗的LED驱动方式&#xff0c;常规LED显示屏一般采用共阳极或者独立驱动的方式&#xff0c;而共阴技术就有所不同了&#xff0c;其基本原理如下&#xff1a; 一、基本概念&#xff1a;   共阴技术是指在LED…