【C语言基础】:预处理详解(二)

文章目录

      • 一、宏和函数的对比
      • 二、#和##运算符
        • 2.1 #运算符
        • 2.2 ##运算符
      • 三、#undef
      • 四、命令行定义
      • 五、条件编译
      • 六、头文件的包含
        • 1. 头文件包含的方式
        • 2. 嵌套文件包含

上期回顾: 【C语言基础】:预处理详解(一)

一、宏和函数的对比

宏通常被应有于执行简单的运算。
比如在两个数中找出较大的⼀个时,写成下面的宏,更有优势⼀些。

#define MAX(x, y) ((x) > (y) ? (x) : (y))

用函数来完成

  1. 调用函数
  2. 执行运算
  3. 函数返回

使用函数来完成任务就要经历这三个步骤,而这三个步骤都需要一定的时间开销,对于一些简单的运算,这无疑是不太好的。
在这里插入图片描述
用宏来完成
对于简单的运算,宏只有执行运算的时间开销,这个效率明显比函数要高得多。
在这里插入图片描述
小结

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹
  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于 > 来比较的类型。宏的参数是与类型无关的

利用宏的执行速度短,那是不是以后就只用宏了呢?这明显是不明智的,函数也有着宏所没有的优点:
3. 每次使用宏的时候,⼀份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
4. 宏是没法调试的。
5. 宏由于类型无关,也就不够严谨。(双刃剑)
6. 宏可能会带来运算符优先级的问题,导致程容易出现错。

宏也有函数做不到的功能,例如:宏的参数可以出现类型,函数就不可以
【示例】:利用宏来实现malloc函数

#include<stdio.h>
#define Malloc(n, type) (type*)malloc(n*sizeof(type))
int main()
{// int* p = (int*)malloc(5 * sizeof(int));int* prt = Malloc(5, int);return 0;
}

当我们将5和int传入到Malloc是,那么n就是5,type就是int,也就是有一个参数是类型,宏是可以实现的,但函数可以实现,预处理之后替换的结果就是(int*)malloc(5 * sizeof(int))。

宏和函数的对比
在这里插入图片描述

二、#和##运算符

2.1 #运算符

#运算符是一个预处理器运算符,用于字符串化(Stringification)。当你在宏定义中使用 # 运算符时,它会将宏的参数转换为一个字符串字面量。这意味着,当宏被展开时,参数的值会被放在双引号中,成为字符串的一部分。

【示例铺垫】

#include<stdio.h>
int main()
{printf("hello" "world\n");printf("helloworld\n");return 0;
}

在这里插入图片描述
C语言会将两个字符串看成一个字符串。

#include<stdio.h>
int main()
{int a = 1;printf("the value of a is %d\n", a);int b = 20;printf("the value of b is %d\n", b);float f = 5.6f;printf("the value of f is %f\n", f);return 0;
}

在这里插入图片描述
在这里插入图片描述
可以看到,这几个打印的只有这两个地方有所差异,那我们可以利用宏来实现这个功能。

【示例】

#define Print(n, format) printf("the value of " #n " is " format "\n", n)
int main()
{int a = 1;Print(a, "%d");int b = 20;Print(b, "%d");float f = 5.6f;Print(f, "%f");return 0;
}

在这里插入图片描述
可以发现,结果其实是一样的,这里的#运算符的作用就是将n转化成"n",例如:#a就是将a转换成"a"。
利用前面的那个铺垫,两个字符串可以看成一个字符串。

注意:使用 # 运算符时,应确保宏参数两侧有空格或其他非字母数字字符,否则可能会导致字符串化不正确。例如,#define NUM 42 和 #define NUM_ 42 会产生不同的结果,因为第一个定义会将 NUM 字符串化,而第二个定义会将 NUM_ 字符串化,并且由于 42 紧跟在 NUM_ 后面,它可能会成为字符串的一部分,导致预处理错误。

2.2 ##运算符

在C语言中,## 是预处理器的标记粘贴运算符。这个运算符可以将两个标识符拼接成一个更长的标识符。当预处理器遇到使用 ## 的宏定义时,它会将 ## 符号左边和右边的任何合法标识符或宏名称拼接在一起,创建一个新的标识符。

【示例铺垫】:求较大值

// 求整数较大值
int int_max(int x, int y)
{return x > y ? x : y;
}
// 求浮点数较大值
float float_max(float a, float b)
{return a > b ? a : b;
}

这样写显得有点繁琐,因为求不同的数据类型就要写不同的函数,这时候就可以动态创建宏名称

#include<stdio.h>
// \为续航符
#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{\
return (x>y?x:y);\
}
// 使用宏定义不同的函数
GENERIC_MAX(int)
GENERIC_MAX(float)
int main()
{int m1 = int_max(5, 6);printf("%d\n", m1);float m2 = float_max(5.6f, 3.4f);printf("%f\n", m2);return 0;
}

在这里插入图片描述
预处理之后可以更加明显的看到这之间的变化:
在这里插入图片描述
注意

  1. 由于 ## 运算符是在预处理阶段进行的,因此它不能用于运行时的代码拼接。
  2. 确保在使用 ## 运算符时,左右两边的标识符是明确的,否则可能会导致编译错误或者不可预期的行为。
  3. ##运算符可以与 # 字符串化运算符结合使用,创建更加复杂的宏定义。

命名约定
一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分⼆者。
那我们平时的⼀个习惯是:

  • 把宏名全部大写
  • 函数名不要全部大写

三、#undef

#undef是一个预处理器指令,用于取消已经定义的宏。当预处理器遇到 #undef指令时,它会移除指定宏的定义,使得宏名不再代表之前定义的文本。

#undef 指令通常用于以下情况:

  1. 防止宏名冲突:如果在不同的头文件中定义了相同的宏名,或者在修改代码时需要改变宏的定义,可以使用 #undef 来确保宏的最新定义是有效的。
  2. 条件编译:在条件编译块中,可能需要根据某些条件取消宏的定义,这时可以使用 #undef。
  3. 清理宏定义:在某些复杂的宏定义中,可能需要在宏展开后清理宏定义,以防止宏名被错误地使用。

使用方法

// 只需要提供要取消定义的宏名即可
#undef macro_name

【示例】

#define MAX 100#undef MAXprintf("%d\n", MAX); // 这里会引发错误,因为MAX已不再定义

四、命令行定义

在C语言编程中,命令行定义指的是通过编译器的命令行参数来定义宏或者设置编译时的选项。这种方法允许开发者在不修改源代码的情况下,动态地改变编译过程和生成的程序的行为。

定义宏
大多数C语言编译器允许使用命令行参数来定义宏。在GCC和Clang等编译器中,可以使用 -D 选项来定义宏。

【示例】:命令行定义

#include<stdio.h>
int main()
{int arr[SZ];// SZ未定义for (int i = 0; i < SZ; i++){arr[i] = i + 1;}for (int i = 0; i < SZ; i++){printf("%d ", arr[i]);}return 0;
}

在这里插入图片描述

五、条件编译

条件编译是C语言预处理器提供的一项功能,它允许根据预处理器指令的特定条件来包含或排除代码块。这意味着在编译时,只有满足特定条件的代码才会被编译器处理,其他不满足条件的代码将被忽略。这对于根据不同的平台、操作系统或编译时的配置来编译不同的代码非常有用。

条件编译主要使用以下预处理器指令:

  1. #ifdef:如果定义了某个宏,则编译#ifdef和#endif之间的代码块。
  2. #ifndef:如果未定义某个宏,则编译#ifndef和#endif之间的代码块。
  3. #if:如果给定的表达式为真(非零),则编译#if和#endif之间的代码块。
  4. #elif:如果前面的#if或#elif条件不满足,并且当前#elif表达式为真,则编译#elif和#endif之间的代码块。
  5. #else:如果前面的所有#if和#elif条件都不满足,则编译#else和#endif之间的代码块。
  6. #endif:结束条件编译块。

【示例1】
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。

#include<stdio.h>
#define __DEBUG__
int main()
{int arr[10] = { 0 };for (int i = 0; i < 10; i++){arr[i] = i + 1;
#ifdef DEBUGprintf("%d ", arr[i]);  //为了观察数组是否赋值成功。
#endif // DEBUG}return 0;
}

在这里插入图片描述
【示例2】:#if 常量表达式

#include<stdio.h>
int main()
{
#if 0printf("hello world");
#endifreturn 0;
}

在这里插入图片描述
在这里插入图片描述
预处理后可以发现,当不满足条件时,这里是不参与编译的

【示例3】:多分支的条件编译

#include<stdio.h>
#define M 1
int main()
{
#if M == 0printf("hehe\n");
#elif M == 1printf("haha\n");
#elif M == 2printf("heihei\n");
#endifreturn 0;
}

在这里插入图片描述
注意:最后都要以 #endif 结束。

【示例4】:判断是否被定义

#include<stdio.h>
int main()
{
#if defined(MAX)// 定义了执行,没定义不执行printf("NO");
#endif#if !defined(MAX)// 没定义执行,定义了不执行printf("YES");
#endifreturn 0;
}

在这里插入图片描述
在这里插入图片描述
其实条件编译是非常常见的,比如在头文件里面就会经常使用条件编译,以下是头文件stdio.h的部分条件编译:
在这里插入图片描述

六、头文件的包含

1. 头文件包含的方式

在C语言中,头文件的包含方式主要有两种:直接包含和间接包含。这两种方式都是为了在当前文件中引入其他文件中定义的函数、变量、类型声明等,以便在当前文件中使用它们。

  1. 直接包含
    直接包含是指在源文件或头文件中使用预处理器指令 #include 直接引入另一个文件。这是最常见的包含方式,可以确保所需的声明和定义在当前编译单元中可用。
#include <stdio.h>

编译器会在标准库的路径中搜索这些文件。这些路径通常是编译器安装时预设的,包括了所有标准库文件的位置。尖括号通常用于包含C标准库的头文件。

查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件⼀样在标准位置查找头文件。如果找不到就提示编译错误。

2. 嵌套文件包含

我们已经知道, #include 指令可以使另外⼀个文件被编译。就像它实际出现于 #include 指令的地方⼀样。
这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。
⼀个头文件被包含10次,那就实际被编译10次,如果重复包含,对编译的压力就比较大。

test.h头文件

void test();struct Stu
{int id;char name[20];
};

在这里插入图片描述
如果直接这样写,test.c文件中将test.h包含5次,那么test.h文件的内容将会被拷贝5份在test.c中。
如果test.h 文件比较大,这样预处理后代码量会剧增。如果工程比较大,有公共使用的头文件,被大家都能使用,又不做任何的处理,那么后果真的不堪设想。

解决办法
每个头文件的开头写:
test.h头文件

#ifndef __TEST_H__
#define __TEST_H__
void test();struct Stu
{int id;char name[20];
};
#endif

或者#pragma once

#pragma once
void test();struct Stu
{int id;char name[20];
};

在这里插入图片描述
就可以避免头文件的重复引入。

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

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

相关文章

数图智慧零售解决方案,赋能零售行业空间资源价值最大化

数图智慧零售解决方案 赋能零售行业空间资源价值最大 在激烈的市场竞争中&#xff0c;如何更好地提升空间资源价值&#xff0c;提高销售额&#xff0c;成为行业关注的焦点。近日&#xff0c;NIQ发布的《2024年中国饮料行业趋势与展望》称&#xff0c;“在传统零售业态店内&…

单片机STM32中断与事件的区别

【转】1-单片机STM32---中断与事件的区别 - Engraver - 博客园 (cnblogs.com) 路径不同&#xff0c;处理方式不同&#xff0c;是否有程序不同&#xff0c;是否有cpu参与不同。 事件是比中断更新的升级产物。

3_2Linux中内核级加强型火墙的管理

### 一.Selinux的功能 ### 观察现象 ①当Selinux未开启时 在/mnt中建立文件被移动到/var/ftp下可以被vsftpd服务访问 匿名用户可以通过设置后上传文件 当使用ls -Z /var/ftp查看文件时显示"?" ps auxZ | grep vsftpd 时显示&#xff1a; - root 8546 0.0 0.0 26952 …

【QT+QGIS跨平台编译】181:【QGIS+Qt跨平台编译】—【错误处理:找不到_DEBUGA】

点击查看专栏目录 文章目录 一、找不到_DEBUGA二、原因分析三、错误处理 一、找不到_DEBUGA 报错信息&#xff1a; 二、原因分析 采用了非UNICODE&#xff1a; DEFINES - UNICODE没法识别 _DEBUGA 但可以识别 _DEBUG 三、错误处理 修改 _DEBUGA 为 _DEBUG

简单的车牌号识别

目录 处理流程与界面各接口编写时遇到的一些问题上传图片识别结果标签显示中文 处理流程与界面 首先点击“上传图片”按钮&#xff0c;可以选择文件夹中含有汽车车牌的图片&#xff0c;并显示在“图片框”中。 点击“检测车牌”按钮&#xff0c;会先对“图片框”中即含有汽车车…

【漏洞复现】通天星CMSV6车载视频监控平台inspect_file文件上传漏洞

Nx01 产品简介 通天星车载视频监控平台软件拥有多种语言版本&#xff0c;应用于公交车车载视频监控、校车车载视频监控、大巴车车载视频监控、物流车载监控、油品运输车载监控等公共交通上。 Nx02 漏洞描述 通天星CMSV6车载视频监控平台/inspect_file/upload存在文件上传漏洞&…

阿姨吐槽年轻人卧铺挂帘子不让坐 评论区吵翻天了

近日&#xff0c;网络流传的一段短视频激起了公众的广泛热议。 这段视频展现了一位阿姨与在下铺挂帘子的年轻人之间的冲突。 视频中&#xff0c;阿姨情绪激动&#xff0c;她用镜头对准了那位年轻人&#xff0c;指责他在下铺挂帘子&#xff0c;使得一位70岁的老人无法坐下。 阿姨…

【C++】explicit关键字详解(explicit关键字是什么? 为什么需要explicit关键字? 如何使用explicit 关键字)

目录 一、前言 二、explicit关键字是什么&#xff1f; 三、构造函数还具有类型转换的作用 &#x1f34e;单参构造函数 ✨引出 explicit 关键字 &#x1f34d;多参构造函数 ✨为什么需要explicit关键字&#xff1f; ✨怎么使用explicit关键字&#xff1f; 四、总结 五…

Angular学习第四天--问题记录及父子组件问题

问题一、 拉取完项目&#xff0c;使用npm install命令的时候遇到的。 解决办法&#xff1a; 在查找网上五花八门的解决方案之后&#xff0c;发现都不能解决。 我的解决办法是&#xff1a; 1. 把package-lock.json给删掉&#xff1b; 2. 把package.json中公司自己库的包给删除掉…

C# Solidworks二次开发:模型中实体Entity相关操作API详解

大家好&#xff0c;今天要讲的一些API是关于实体的相关API。 在开发的过程&#xff0c;很多地方会涉及到实体的相关操作&#xff0c;比如通过实体选中节点。下面就直接开始介绍API&#xff1a; &#xff08;1&#xff09;第一个API为Select4&#xff0c;这个API的含义为选中一…

Docker 学习笔记(五):梳理 Docker 镜像知识,附带 Commit 方式提交镜像副本,安装可视化面板 portainer

一、前言 记录时间 [2024-4-10] 前置文章&#xff1a; Docker学习笔记&#xff08;一&#xff09;&#xff1a;入门篇&#xff0c;Docker概述、基本组成等&#xff0c;对Docker有一个初步的认识 Docker学习笔记&#xff08;二&#xff09;&#xff1a;在Linux中部署Docker&…

FluentUI系列 - 1 - 介绍第一个窗口

介绍一个QML的UI库&#xff0c;国人编写&#xff0c;作者也耍知乎。这个UI库确实好用&#xff0c;但是教程基本等于无&#xff0c;个人在使用中顺便记录一下学习内容。这玩意儿也有Pyside6的版本&#xff0c;有需要的可以查看PySide6-FluentUI-QML。 FluentUI库地址​github.c…

00 【哈工大_操作系统】Bochs 汇编级调试方法及指令

本文将介绍一下哈工大李治军老师《操作系统》课程在完成Lab时所使用到的 Bochs 调试工具的使用方法。这是一款汇编级调试工具&#xff0c;打开调试模式非常简单&#xff0c;只需在终端下输入如下指令&#xff1a; 1、bochs 调试基本指令大全 功能指令举例在某物理地址设置断点…

bpftime(为什么要有,介绍,原理图),如何编译运行其代码,示例代码(运行结果+解释+内核层代码,用户层代码分析)

目录 bpftime(开源用户态 eBPF 运行时) 引入 在内核态实现用户态追踪的性能损失 内核空间执行ebpf的弊端 内核态 -> 用户态 介绍 原理图 示例代码 如何编译和运行 编译 运行 运行结果 运行结果 代码分析 .c 源码 语法 #include "malloc.skel.h&…

EPSON开发新IMU产品M-G370PDS改善姿态和震动控制

爱普生IMU于2011年首次推出&#xff0c;已在一系列客户应用中使用&#xff0c;因其出色的性能和质量而享有盛誉。近年来&#xff0c;IMU的使用已经扩展到无人系统测量、航空和水下视频摄影等领域。对更准确的位置和姿态控制的需求不断增长&#xff0c;不仅如此&#xff0c;高效…

【小程序】生成短信中可点击的链接

文章目录 前言一、如何生成链接二、仔细拜读小程序开发文档文档说明1文档说明2 总结 前言 由于线上运营需求&#xff0c;需要给用户发送炮轰短信&#xff0c;用户通过短信点击链接直接跳转进入小程序 一、如何生成链接 先是找了一些三方的&#xff0c;生成的倒是快速&#xf…

DDoS攻击类型与应对措施详解

攻击与防御简介 SYN Flood攻击 原理&#xff1a; SYN Flood攻击利用的是TCP协议的三次握手机制。在正常的TCP连接建立过程中&#xff0c;客户端发送一个SYN&#xff08;同步序列编号&#xff09;报文给服务器&#xff0c;服务器回应一个SYN-ACK&#xff08;同步和确认&#xf…

微信小程序wx.getLocation 真机调试不出现隐私弹窗

在小程序的开发过程中&#xff0c;首页中包含要获取用户地理位置的功能&#xff0c;所以在这里的onLoad&#xff08;&#xff09;中调用了wx.getLocation()&#xff0c;模拟调试时一切正常&#xff0c;但到了真机环境中就隐私框就不再弹出&#xff0c;并且出现了报错&#xff0…

开源相机管理库Aravis例程学习(一)——单帧采集single-acquisition

开源相机管理库Aravis例程学习&#xff08;一&#xff09;——单帧采集single-acquisition 简介源码函数说明arv_camera_newarv_camera_acquisitionarv_camera_get_model_namearv_buffer_get_image_widtharv_buffer_get_image_height 简介 本文针对官方例程中的第一个例程&…

maven引入外部jar包

将jar包放入文件夹lib包中 pom文件 <dependency><groupId>com.jyx</groupId><artifactId>Spring-xxl</artifactId><version>1.0-SNAPSHOT</version><scope>system</scope><systemPath>${project.basedir}/lib/Spr…