【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.mzph.cn/news/819508.shtml

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

相关文章

Web前端-JavaScript

黑马程序员JavaWeb开发教程 文章目录 一、js引入方式1、内部脚本2、外部脚本 二、js基础语法1、书写语法&#xff08;1&#xff09;基本语法&#xff08;2&#xff09;输出语句 2、变量&#xff08;1&#xff09;变量&#xff08;2&#xff09;注意事项 3、数据类型、运算符、流…

腾讯云服务器CVM标准型S8实例CPU内存、网络和存储性能测评

腾讯云第八代云服务器标准型S8实例基于全新优化虚拟化平台&#xff0c;CPU采用Intel Emerald Rapids 全新处理器&#xff0c;睿频3.0GHz&#xff0c;内存采用最新DDR5&#xff0c;默认网络优化&#xff0c;最高内网收发能力达4500万pps&#xff0c;最高内网带宽可支持120Gbps。…

java编译过程

java编译器将 java 源文件转换成 class 文件的过程。 &#xff08;1&#xff09;词法分析器 作用&#xff1a;将Java源文件的字符流转变成对应的Token流 每个词法单元&#xff08;token&#xff09;都有一个类型&#xff08;token type&#xff09;和一个值&#xff08;toke…

Ollama教程——使用langchain:ollama与langchain的强强联合

Ollama教程——使用langchain&#xff1a;ollama与langchain的强强联合 简介背景知识ollama简介langchain简介结合使用的重要性 环境搭建安装LangChain安装ollama环境设置 加载文档使用WebBaseLoader加载《奥德赛》 文档处理分割文档 向模型提问构建查询使用文档内容进行查询创…

小米SU7的防晒秘籍

在春日渐暖的日子里&#xff0c;夏天悄然而至。大家有没有从衣柜深处翻出夏衣和防晒装备&#xff0c;来迎接夏日阳光的“偏爱”呢&#xff1f; 深知防晒烦恼的小米&#xff0c;在小米SU7的设计中也充分考虑了汽车防晒这一痛点&#xff0c;采用前风挡三层镀银、天幕双层镀银、四…

漂亮,功能就差?错!优秀B端一定是颜值、体验、功能三位一体。

每次发一些漂亮的B端页面&#xff0c;都会有些人跳出来怼&#xff0c;他们都有一个固定的思维模式&#xff1a;漂亮的B端&#xff0c;一定功能差。这就好比马路上看到开豪车的美女&#xff0c;就觉得钱来路不正。 先给大家看一些过气的B端界面&#xff0c;是不是有似曾相识的感…

Java 集合【补充复习】

Java 集合【补充复习】 Java 集合概述Collection 接口继承树Map 接口继承树 Collection 接口方法使用 iterator 接口遍历集合元素使用 forearch 遍历集合元素 List 接口List 实现类之一&#xff1a;ArrayListList 实现类之二&#xff1a;LinkedList Set 接口Set 实现类之一&…

【Alphalens】使用Alphalens配合Akshare进行双均线因子分析,附源码及常见问题

Alphalens 是非常著名的一个python因子分析库。但是该库由于目前已经不怎么维护&#xff0c;问题非常多。最新的使用建议使用alphalens-reloaded&#xff0c;地址&#xff1a;stefan-jansen/alphalens-reloaded: Performance analysis of predictive (alpha) stock factors (gi…

【数据结构|C语言版】顺序表应用

前言1. 基于动态顺序表实现通讯录1.1 通讯录功能1.2 代码实现1.2.1 SeqList.h1.2.2 SeqList.c1.2.3 Contact.h1.2.4 Contact.c1.2.5 test.c 1.3 控制台测试1.3.1 添加联系人1.3.2 删除联系人1.3.3 修改联系人1.3.4 查找联系人1.3.5 清空通讯录1.3.6 通讯录读档和存档 2. 好题测…

Java SPI机制详解

Java SPI机制详解 1、什么是SPI&#xff1f; SPI 全称为 (Service Provider Interface) &#xff0c;是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制&#xff0c; 比如有个接口&#xff0c;想运行时动态的给它添加实现&#xff0c;你只需要添加一个实现。我们…

B端:导航条长得不都一样吗?错了,这里看过来就懂了。

B端导航条看似都一样&#xff0c;大差不差&#xff0c;仔细看一下&#xff0c;其实各有各的不同&#xff0c;这里方向了十多个&#xff0c;大家仔细看细节。

avicat连接异常,错误编号2059-authentication plugin…

错误原因为密码方式不对&#xff0c;具体可自行百度 首先管理员执行cmd进入 mysql安装目录 bin下边 我的是C:\Program Files\MySQL\MySQL Server 8.2\bin> 执行 mysql -u -root -p 然后输入密码 123456 进入mysql数据库 use mysql 执行 ALTER USER rootlocalhost IDE…

关于沃进科技无线模块demo软件移植问题

文章目录 一、无线模块开发测试准备二、开发板硬件三、开发板默认功能上电默认界面功能选择界面数据包发送界面数据包接收显示界面射频性能测试界面参数设置界面固件信息显示界面 四、软件开发软件SDK框图1、射频硬件驱动&#xff08;详见./radio/myRadio_gpio.c&#xff09;2、…

51单片机实验04 -数码管的动态显示实验

目录 一、实验目的 二、实验内容 三、实验原理 四、实验方法 五&#xff0c;实验效果及代码 1&#xff0c;效果 2&#xff0c;代码 六&#xff0c;课后习题 1&#xff0c;使用定时器T0的中断函数1 从999999~0计时 1&#xff09;效果 2&#xff09;代码 2&#xff0c…

配置linux的oracle 21c启停服务

一、配置启停 1、使用root用户登陆 su - root 2、修改oratab文件 修改oratab文件&#xff0c;将红框里面的N改为“Y”&#xff0c;使启停脚本能够生效 vi /etc/oratab 3、验证 配置好后就能够使用 dbshut 停止服务 和 dbstart 启动服务 了 2.1启动服务 su - oracle dbstart…

什么是线程?线程和进程谁更弔?

第一个参数是所创建进程的pid。 第二个是线程的属性。 第三个参数是返回值为void*&#xff0c;参数也为void*的函数指针。 第四个参数是给第三个参数的参数&#xff0c;也就是给给函数传参。 #include<iostream> #include<pthread.h> #include<unistd.h>…

折叠面板组件(vue)

代码 <template><div class"collapse-info"><div class"collapse-title"><div class"title-left">{{ title }}</div><div click"changeHide"> <Button size"small" v-if"sho…

生产计划和排单管理怎么做

阅读本文&#xff0c;你将了解到&#xff1a;1、企业是如何制定生产计划和进行排单管理&#xff1f; 2.企业在执行生产计划和进行排单管理过程中会遇到那些问题&#xff1f; 3.企业如何高效利用工具去解决问题&#xff1f; 一、生产计划和排单管理是什么 1.生产计划和排单管理…

【uniapp】【uview2.0】【u-sticky】Sticky 吸顶

把pages.json文件中的 “navigationStyle"设置为"custom”, 出现的问题是&#xff0c;莫名奇妙多了个 一个高度 解决方法 /* 使用CSS的sticky定位 */ .sticky {/* #ifdef H5 */ position: -webkit-sticky;position: sticky;top: 0; /* 设置距顶部的距离 */z-ind…

[Python开发问题] Selenium ERROR: Unable to find a matching set of capabilities

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…