c语言-预处理详解【求个关注!】

预处理详解

  • 一 预处理阶段
    • 1 知识背景:
    • 2 预定义符号
    • 3 #define 定义常量
      • 当定义的标识符的值过长时:
      • 注意,如果#define定义的标识符,其值的末尾有; 则说明; 是该标识符值的一部分
    • 4 #define 定义宏
      • 宏的声明方式:
      • 当传入的参数是一个符号时:
      • 当传入的参数是一个表达式时:
      • 带有副作用的宏参数
      • 宏替换的规则
    • 5 宏与函数的对比
      • (1)在执行小型运算时,宏更有优势:
      • (2)宏对于函数的劣势
      • (3)总的对比
    • 6 #与##操作符
      • 6.1 #操作符
      • 6.2 ## 操作符
    • 7 #undef
    • 8 条件编译
      • 举例:
      • 常见的条件编译指令
    • 9 头文件的包含
      • 9.1 头文件包含的形式:
        • 9.1.1 本地文件的包含:
        • 9.1.2 库文件的包含
        • 两种包含方式查找策略的不同:
      • 9.2 嵌套文件包含


一 预处理阶段

1 知识背景:

一个c语言项目可能由多个.c文件与.h文件组成,那么它是如何转换成可执行程序的呢?
每一个.c文件单独通过编译器转换成目标文件
在windows系统中,目标文件的后缀为.obj,在linux系统中,目标文件的后缀为.o
通过链接器,将多个目标文件与链接库链接起来,生成可执行程序
链接库是指运行时的库(支持程序运行的基本函数的集合)或者第三方库(比如c语言标准库)
用高级编程语言所写的代码,要执行,需要在转换成操作系统可执行的二进制
代码
大体的转换流程是:编译-链接 转换成可执行程序
如果再细分,则是预处理-编译-汇编-链接 转换成可执行程序

如图:

在这里插入图片描述
本篇所写仅涉及与预处理阶段相关的内容【处理的代码方面】

2 预定义符号

c语言中定义了一些预定义符号,这些预定义符号可以直接使用,当然这些预定义符号也在预处理阶段处理!

//_ _FLIE_ _  //用来编译的源文件  
//__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("%d\n", __STDC__);//在VS编译器中并没有STDC的规定!return 0;
}

在这里插入图片描述

在预处理之后,生成的文件test.i 为:
在这里插入图片描述

3 #define 定义常量

在程序中#define 定义的符号,在经过预处理之后,直接被其值替换
#include<stdio.h>
#define MAX 100           // 定义符号 值为数值常量
#define Str  "hello World"// 定义符号 值为字符串常量
#define MA  'a'           // 定义符号 值为字符
int main() {
// #define 可以定义符号,其值为常量printf("%d\n", MAX);printf("%s\n", Str);printf("%c\n", MA);return 0;
}

在这里插入图片描述

当定义的标识符的值过长时:

#define Print printf("file:%s\tline:%d\tDATE:%s\tTIME:%s\t",__FILE__,__LINE__,__DATE__,__TIME__);
#include<stdio.h>
//在默认情况下#define定义标识符只能在一行中定义,但如果想换行的话,则需在每一行末尾加上 \,意为这一行的扩展
#define Print printf("file:%s\t \
line:%d\t                   \
DATE:%s\t                    \
TIME:%s\t",                   \
__FILE__,__LINE__,__DATE__,__TIME__);
int main() {return 0;
}

注意,如果#define定义的标识符,其值的末尾有; 则说明; 是该标识符值的一部分

在这里插入图片描述

4 #define 定义宏

宏是带有参数的标识符,参数与标识符对应的值有关

宏的声明方式:

#define name(parament-Iist ) stuff 
其中stuff是宏体,parament-list代表用,隔开的参数集
注意:name与()的左括号,中间不能有间隔,否则编译器会认为(parament-Iist)是宏体的一部分

当传入的参数是一个符号时:

#include<stdio.h>
#define SQUARE(x) x*x
int main() {int c = SQUARE(5);printf("%d\n", c);return 0;
}

在这里插入图片描述

当传入的参数是一个表达式时:

#include<stdio.h>
#define SQUARE(x) x*x
int main() {int c = SQUARE(5+1);printf("%d\n", c);//预期值是36 ,但结果是;return 0;
}

在这里插入图片描述
这是因为,在经历预处理时,先将宏的形参变为实参变为:

// 5+1*5+1

再将转换后的文本替换到原来程序所在的文本的位置即:

#include<stdio.h>
#define SQUARE(x) x*x
int main() {int c = 5+1*5+1;//此时的结果为1*5 + 6 ==11printf("%d\n", c);return 0;
}

如何解决这个问题呢?只需要在宏的定义中,将每一个参数用()括起来,再将整个宏的体括起来
这是为了防止外在的程序直接与宏的部分体进行计算,例如:

#include<stdio.h>
#define ADD(x) x+x
int main() {int a = 5;//目标打印出50,但是结果为:printf("%d\n", ADD(5) * a);return 0;
}

在这里插入图片描述
解决方法:

#include<stdio.h>
#define ADD(x) ((x)+(x)) // 将参数括起来,再将整个宏体括起来
int main() {int a = 5;printf("%d\n", ADD(5) * a);return 0;
}

在这里插入图片描述

带有副作用的宏参数

当带副作用的参数(所谓副作用即在计算结束后,自身的值发生变化)在宏体中,超过一次,
就会可能导致错误的结果

例:

#include<stdio.h>
#define MAX(x,y) ((x)>(y)?x:y)int main() {int a = 3;int b = 5;//如果是按照函数的思维,c 返回值应该是5,但是结果是:int c = MAX(a++,b++);printf("%d\n", a);printf("%d\n", b);printf("%d\n", c);return 0;
}

在这里插入图片描述

所以尽量不要用这种带有副作用的形参
x++//有副作用
x+1//挺好

宏替换的规则

有以下几个步骤:
1   首先扫描宏的参数,如果参数中有#define定义的符号或宏,则替换成对应的值
2   随后将替换文本插入到程序中原文本处
3   最后重新扫描结果文本,将#define定义的宏或符号转换成宏体或值

举例:

#include<stdio.h>
#define MAX(x,y) ((x)>(y)?(x):(y))
#define M 10 
int main() {int a = 3;int b = 5;int c = MAX(M, a);// 预处理时,1 首先扫描宏的参数,发现#define定义的M,将其转换成其值 10:// 2   int c = MAX(10,a);这条语句代替int c = MAX(M,a);插入到程序中// 3  最后再扫描结果文本 int c = MAX(10,a);发现MAX也是#define定义,//转换为其对应的宏体//    ((10)>(a)?(10):(a))
// 最后变为://int c = ((10)>(a)?(10):(a));return 0;
}
注意:1  宏参数或#define定义的符号中可以出现其他#define定义的符号或宏,但是对于宏不能出现递归2   当预处理器搜索#define定义的符号与宏时,字符串常量的内容并不被搜索!

5 宏与函数的对比

(1)在执行小型运算时,宏更有优势:

举例:

#include<stdio.h>
//#define MAX(x,y) ((x)>(y)?(x):(y))
int Max(int x, int y) {return x > y ? x : y;
}
int main() {int a = 3;int b = 5;//int c = MAX(a, b);int c = Max(a, b);return 0;
}

用函数所需的指令:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上面是调用函数执行的指令,共用了11条

这是执行函数中的语句所用的指令:
在这里插入图片描述
这是返回函数值所执行的指令:
在这里插入图片描述
在这里插入图片描述
用宏需要的指令:
在这里插入图片描述

结果一目了然,说明当功能运算语句占比较小时,宏的执行更能够节省时间

(2)宏对于函数的劣势

1        除非宏比较小,否则宏替换到程序中的代码量极大,预处理之后会产生极大的冗余代码而函数则只有固定的一份代码
2       宏是无法调试的,因为我们看到的代码与预处理替换后的代码不同,我们不能查看到问题出现在哪里
3       宏的参数没有类型,不够严谨(当然这也是宏对于函数的优势,只能说是一把双刃剑)

(3)总的对比

在这里插入图片描述

6 #与##操作符

6.1 #操作符

此操作符的作用将宏的参数转换成**字符串字面量**(就是转换成参数的标识符的字符串形式) 
它仅允许出现在带参数的宏体中(或者说是宏的替换列表中)

举例:

// 在举例之前,先补充一个背景知识点
#include<stdio.h>
int main() {printf("helloworld\n");// 如果用两对双引号打印呢?printf("hello"   "world\n"); return 0;
}

在这里插入图片描述

#include<stdio.h>
//用#操作符将参数n变为字符串字面量,即参数的标识符的字符串状态
#define DIGIT(n) printf("the value " #n " is %d\n",n); 
int main() {int a = 3;//在传入实参后,替换的文本实际为://printf("the value " "a" "is %d\n", a);DIGIT(a);return 0;
}

在这里插入图片描述

6.2 ## 操作符

##操作符可以把它两边的符号合成一个符号,它允许宏定义从分离的文本片段
中创建标识符,这样的连接必须产生一个合法的标识符

在这里插入图片描述

7 #undef

用于移除一个宏定义
#undef NAME  // 如果移除一个宏定义,移除其名字即可
#include<stdio.h>
#define SQUARE(x) x*x
int main() {//#undef SQUARE	printf("%d\n", SQUARE(3));return 0;
}

在这里插入图片描述

#include<stdio.h>
#define SQUARE(x) x*x
int main() {#undef SQUARE	printf("%d\n", SQUARE(3));return 0;
}

在这里插入图片描述

8 条件编译

我们可以通过一些语句来设定一些代码是否进行编译,这样即避免删掉可能有用的代码,又防止暂时不需要的代码消耗资源

举例:

#include<stdio.h>
#define DEBUG
int main() {int arr[10] = { 0 };for (int i = 0; i < 10; i++) {arr[i] = i;#ifdef DEBUG  // 如果DEBUG被定义则下面这条语句可执行,反之不可printf("%d\n", arr[i]);#endif }return 0;
}

常见的条件编译指令

1    格式: #if  常量表达式#endif#if作为开始的标志,#endif作为结束的标志,如果常量表达式的返回值>0则指令中间的代码块便可执行

举例:

#include<stdio.h>
#define M 10
int main() {
#if M>0printf("hehe\n");
#endif // M>0return 0;
}

在这里插入图片描述

   2   多分支的条件编译指令

举例:

#include<stdio.h>
#define M 3
int main() {
#if M==1printf("hehe");
#elif M==2printf("haha");
#elseprintf("wawa");
#endif // M=1return 0;
}

在这里插入图片描述

3       判断是否被定义的条件编译
#include<stdio.h>
#define DEBUG
int main() {int arr[10] = { 0 };for (int i = 0; i < 10; i++) {arr[i] = i;
//#if defined(symbol) #endif 组合 与#ifdef symbol #endif 组合的功能相同,
// 如果symbol被定义,则指令中的代码块可执行#if defined(DEBUG)
//#ifdef DEBUG printf("%d\n", arr[i]);//#endif 
#endif }return 0;
}
3.2    如果是不定义情况下可执行代码的条件编译:
#include<stdio.h>
#define M 10
int main() {
#ifndef Mprintf("haha");
#endif // !Mreturn 0;
}

在这里插入图片描述

 4  嵌套指令,前面的几个指令可以嵌套,就像是if else语句一样,大家可以尝试一下

9 头文件的包含

头文件是存放函数声明的文件,让其他函数调用使用

9.1 头文件包含的形式:

9.1.1 本地文件的包含:
 当我们引用本地文件时,即自己写的文件,格式为:#include " FileName"

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

9.1.2 库文件的包含

当我们调用开发环境为我们提供的标准库函数时,就需要引用相应的头文件

格式为:#include <FileName>

比如调用下面的printf函数,就需要包含stdio.h文件
在这里插入图片描述

两种包含方式查找策略的不同:
1   对于用" " 包含头文件的形式,在查找相应的头文件时,先在本文件的目录中查找,如果找到了则包含进来,如果没找到则去存放库函数的位置所在的路径去查找。
2   而用<>包含头文件的形式,在查找相应的头文件时,直接去存放库函数
的位置所在的路径去查找所以库文件也可以用" " 来包含,但是执行起来比<>慢

举例1:在这里插入图片描述
找到了.h文件
在电脑中找到存放stdio.h头文件的路径
在这里插入图片描述

9.2 嵌套文件包含

当我们在调用头文件时,可能不止调用一个 ,如果出现这种情况:

在这里插入图片描述
如果文件的调用结构是这样的,那对于test.c来说,是调用了两次con.h
如果结构更复杂呢?会导致代码量的极大冗余
这个问题怎么解决呢?用条件编译!
举例:

#ifndef DEBUG
#define DEBUG
int Add(int x, int y);
#endif
// 这些代码的执行规则是:如果.h文件第一次被调用,那此时DEBUG还没有被定义
// 就定义DEBUG,然后将文件包含过去,即将声明赋值过去,当.h文件又被调用时,则
// 因为DEBUG已经被定义,所以指令中间的代码不被编译,不会被包含过去,而总项目
// 只需要声明这个.h文件一次即可,这样就避免了重复声明!

在这里插入图片描述
在这里插入图片描述
或者用

#pragma once 
来避免头文件的重复引入

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

全面支持工业协议钡铼IOy系列模块无缝融入PLC及工业物联网架构

钡铼IOy系列模块作为工业控制领域的创新产品&#xff0c;以其卓越的性能和广泛的适用性&#xff0c;无缝融入PLC&#xff08;可编程逻辑控制器&#xff09;及工业物联网架构&#xff0c;为工业自动化领域带来了全新的可能性。下面我们将详细探讨钡铼IOy系列模块与PLC及工业物联…

路径规划 | 基于蜣螂优化算法的无人机三维路径规划(Matlab)

目录 效果一览基本介绍程序设计参考文献 效果一览 基本介绍 基于蜣螂优化算法的无人机三维路径规划【23年新算法应用】可直接运行 Matlab语言 1.读取地形数据&#xff0c;利用蜣螂算法DBO优化三维路径&#xff0c;目标函数为总路径最短&#xff0c;同时不能撞到障碍物&#xff…

Python100个库分享第14个—plyfile(将ply文件展示3d模型)

目录 专栏导读库的介绍库的安装ply文件格式介绍ply下载网址&#xff08;是斯坦福大学的3d模型下载网址&#xff09;报错解决完整代码参考&#xff1a;总结 专栏导读 &#x1f338; 欢迎来到Python办公自动化专栏—Python处理办公问题&#xff0c;解放您的双手 &#x1f3f3;️…

软件测试面试八股文(2024最新版)

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 准备找工作的小伙伴们&#xff0c;今天我给大家带来了一些自动化测试面试题&#xff0c;在这个公…

XTTS数据迁移方案

前置条件检查 XTTS使用限制较多&#xff0c;V3版本按照本节逐项检查 目标库操作系统不能是windows 源库&#xff1a;redhut 7.9 目标库&#xff1a;redhut 7.9 检查数据库时区&#xff08;两边都需要&#xff09; SQL> select dbtimezone from dual; 检查结果两边都一致…

linux的线程概念

目录 1.原理 2.线程的周边概念 3.创建线程的接口 1.pthread_create 2.pthread_join 3.pthread_detach 4.终止线程 5.C11封装的多线程库 4.线程库的大概结构 5.__thread&#xff08;只能修饰内置类型&#xff09; 6.线程的互斥 1.了解原理 2.加锁 1.接口 2.代码示…

医院一站式后勤管理系统 processApkUpload.upload 任意文件上传漏洞复现

0x01 产品简介 医院一站式后勤管理系统由南京博纳睿通软件科技有限公司开发的一款基于现代医院后勤管理理念的业务系统,产品结合后勤业务管理特点,通过管理平台将后勤管理业务予以系统化、规范化和流程化,从而形成一套构建于平台之上且成熟完善的后勤管理体系,并可在此体系…

Unity绘制地图

首先在项目/Assets文件夹下创建一个Tiles文件夹 在层级下点击鼠标右键选择2D对象选择瓦片地图创建Tilemap。 选择地图素材 如果素材需要裁剪&#xff0c;在检查器Sprite模式选择多个&#xff0c;点击Sprite Editor,选择切 &#xff0c;选择类型Grid By Cell Count&#xff0c;…

CTFshow-PWN-Test_your_nc(pwn0-pwn4)

1、pwn0 连上&#xff0c;等它程序执行完你可以直接来到 shell 界面 执行命令&#xff0c;获取 flag ctfshow{294ffc57-ee28-40ea-8c74-4dfeaf89d1e7} 2、pwn1 提供一个后门函数&#xff0c;连上即可得到flag 下载附件&#xff0c;拉进 ubantu &#xff0c;使用命令 checksec …

腾讯EdgeOne产品测评体验——多重攻击实战验证安全壁垒:DDoS攻击|CC压测|Web漏洞扫描|SQL注入

腾讯EdgeOne产品测评体验——实战验证安全壁垒&#xff1a;DDoS攻击|CC压测|Web漏洞扫描|SQL注入 写在最前面一、产品概述1.1 什么是边缘安全加速平台 EO&#xff1f;1.2 EdgeOne产品功能 二、准备工作2.1 选择&#xff1a;NS&#xff08;Name Server&#xff09;接入模式或 CN…

vue3 依赖-组件tablepage-vue3说明文档,列表页快速开发,使用思路及范例(Ⅳ)其他配置项

github求⭐ vue3 依赖-组件tablepage-vue3说明文档&#xff0c;列表页快速开发&#xff0c;使用思路及范例&#xff08;Ⅰ&#xff09;配置项文档 vue3 依赖-组件tablepage-vue3说明文档&#xff0c;列表页快速开发&#xff0c;使用思路及范例&#xff08;Ⅱ&#xff09;搜索…

微信小程序认证指南及注意事项

如何认证小程序&#xff1f; 一、操作步骤 登录小程序后台&#xff1a; https://mp.weixin.qq.com/ (点击前往) 找到设置&#xff0c;基本设置&#xff1b; 在【基本信息】处有备案和认证入口&#xff1b; 点击微信认证的【去认证】; 按照步骤指引一步步填写信息&#xff…