C语言预处理详解(上)(30)

文章目录

  • 前言
  • 一、预定义符号
  • 二、#define定义标识符
  • 三、#define定义宏
  • 四、#define的替换规则
  • 五、带有副作用的宏
  • 六、宏和函数的对比
  • 七、#undef的作用
  • 八、# 和
    • #的作用
    • ##的作用
  • 总结


前言

  C语言的入门学习差不多要到尾声了,感觉如何呢~
  前文说编译的第一步就是预编译(即预处理),那具体这个阶段会做哪些工作呢?

正文开始!


一、预定义符号

  在C语言中,有一些有意思的预定义符号,这些预定义符号都是语言内置的,即以及定义好的,我们可以直接使用。预定义符号主要有以下几个:

// 实际项目可能会有用
__FILE__        //进行编译的源文件
__LINE__        //文件当前的行号
__DATE__        //文件被编译的日期
__TIME__        //文件被编译的时间
__FUNCTION__    //进行编译的函数
__STDC__        //如果编译器遵循ANSI C,其值为1,否则未定义

这些预定义符号是已经用#define定义好的,在代码运行后的预处理阶段会被替换为相应的内容

其使用就是直接打印即可,只要注意占位符是 %s 还是 %d 即可:

#include <stdio.h>
int main()
{
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
	printf("%s\n", __FUNCTION__);
	//printf("%d\n", __STDC__); // VS2022不支持
	
	return 0;
}

运行结果如下:
在这里插入图片描述

二、#define定义标识符

用#define定义标识符的格式如下:

#define MAX 100
#define reg register //懒人觉得register太长了

这些被#define定义的标识符都将在预处理阶段被编译器替换成对应的内容

我产生了一些联想,事实上每个初学者经常会犯以下错误:
main->mian;,->,;(->( ;true->ture

于是我们将错就错,有了以下解决方案

#define mian main
#define ,
#define (
#define )
#define ture true
#define ;

开玩笑的,我们还是要从源头上解决,多敲多练,尽可能避免这种一般性的低级错误

三、#define定义宏

#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)
其声明方式为:#define name( parament-list ) stuff,其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分

#include <stdio.h>
#define SQUARE(x) x*x //求x的平方

int main()
{
	int ret = SQUARE(5);
	//相当于int ret = 5*5,在预处理阶段就被展开了;
	printf("%d\n", ret); //结果为25
	
	return 0;
}

意思是这么个意思,但是我们要注意这个声明是不太正确的,我们不能在宏的使用里吝啬括号,不然可能会发生以下错误:

printf(“%d\n” ,SQUARE( 5 + 1) );
展开为:printf(“%d\n” ,5 + 1 * 5 + 1);
结果是打印11,与我们想要的25相悖
正确声明是:#define SQUARE(x) ((x)*(x))

也就是说用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用

四、#define的替换规则

  以下面代码为例,现在我们来讲解下#define的替换规则

#include <stdio.h>
#define MAX 100
#define SQUARE(x) ((x)*(x)*MAX)

int main()
{
	int ret = SQUARE(5);
	printf("%d\n", ret);
	return 0;
}
  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换

#define定义的宏中含有#define定义的符号MAX,则调用该宏时,首先将MAX替换

#include <stdio.h>
#define SQUARE(x) ((x)*(x)*100)
int main()
{
	int ret = SQUARE(5);
	printf("%d\n", ret);
	return 0;
}
  1. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换

此时代码等价于:

#include <stdio.h>
int main()
{
	int ret = ((5)*(5)*100);
	printf("%d\n", ret);
	return 0;
}
  1. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程

上例不再包含任何由#define定义的符号

我们也要注意以下几点:

  1. 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归
#define FAC(x) (x)*FAC(x-1) //error
  1. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
#include <stdio.h>
#define MAX 100
int main()
{
	// 以下代码字符串中的MAX不会被替换为100,而字符串外的MAX会被替换
	printf("MAX = %d\n", MAX); // 结果为MAX = 100
	return 0;
}

五、带有副作用的宏

  代码执行后,除了达到我们想要的结果之外,还导致了其他问题的发生,我们就说该条语句带有副作用

例如,我们现在想比较a和b的大小,并将其较大值赋值给c,之后再将a和b同时加1:

#include <stdio.h>
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
	int a = 10;
	int b = 20;
	int c = MAX(a++, b++);
	printf("%d\n", c);
	return 0;
}

这段代码看似没有问题,但是结果却是不正确的,因为该宏经过替换后,等价于以下代码:

#include <stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	int c = ((a++)>(b++)?(a++):(b++));
	printf("%d\n", c);
	
	return 0;
}

所以,当我们使用宏的时候,应该避免传入带有副作用的宏参数

六、宏和函数的对比

  可能通过前面的学习,你也发现了一个问题,就是宏和函数的感觉特别像,那两者可以等同吗?
  事实上是不行的,两者有以下区别:

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹(因为函数要开栈帧,宏不用,这个我们下篇会介绍
  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。但是宏可以适用于整形、长整型、浮点型等可以用于来比较的类型。宏是类型无关的
  3. 宏有时候可以做到函数做不到的事情。例如,宏的参数可以出现类型,但是函数却不可以
#include <stdio.h>
#include <stdlib.h>
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))

int main()
{
	int* p2 = MALLOC(10, int);
	if (p2 == NULL)
	{
		printf("p2开辟失败\n");
		return 1;
	}
	
	free(p2);
	p2 = NULL;
	
	return 0;
}

可是,宏也有自己的劣势,在于:

  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度
  2. 宏是没法调试的(我认为这点非常不友好!)
  3. 宏由于类型无关,也就不够严谨
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错

倘若用一张图来表明宏和函数的区别,那么可以用下图:
在这里插入图片描述

七、#undef的作用

  #undef可以移除一个#define定义的标识符或宏

#include <stdio.h>
#define MAX 100
int main()
{
	printf("%d\n", MAX);//正常使用
#undef MAX // 此时MAX失效
	printf("%d\n", MAX); //报错,MAX未定义
}

八、# 和

这个我真感觉没什么用,但是还是讲一下吧:

#的作用

把一个宏参数变成对应的字符串

在介绍#的作用的之前,我先向大家说明一下:字符串是有自动连接的特点的:

	char arr[] = "hello ""world!";
	//等价于char arr[] = "hello world!";
	printf("helll ""world!\n");
	//等价于printf("helll world!\n");

认识到这点后,我们来看代码:

#include <stdio.h>
int main()
{
	int age = 10;
	printf("The value of age is %d\n", age);
	double pi = 3.14;
	printf("The value of pi is %f\n", pi);
	int* p = &age;
	printf("The value of p is %p\n", p);
	
	return 0;
}

我们发现,printf要打印的内容大部分是一样的,那么,为了避免代码冗余,我们可不可以将其封装成一个函数或是宏呢?
答案是不行,不信大家可以自行尝试一下,这时候就可以考虑用这个#了:

#include <stdio.h>
#define print(data,format) printf("The value of "#data" is "format"\n",data)
int main()
{
	int age = 10;
	print(age, "%d");
	double pi = 3.14;
	print(pi, "%f");
	int* p = &age;
	print(p, "%p");
	
	return 0;
}

这时我们只需将要打印的变量的变量名和打印格式传入即可。该代码经过预处理后等价于以下代码

#include <stdio.h>
int main()
{
	int age = 10;
	printf("The value of ""age"" is ""%d""\n", age);
	double pi = 3.14;
	printf("The value of ""pi"" is ""%f""\n", pi);
	int* p = &age;
	printf("The value of ""p"" is ""%p""\n", p);
	return 0;
}

又因为字符串有自动连接的特点,所以可以打印出期望的结果

##的作用

##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符

#include <stdio.h>
#define CAT(x,y) x##y
int main()
{
	int workhard = 100;
	printf("%d\n", CAT(work, hard));//打印100
	return 0;
}

总结

  其实预处理的过程还是蛮复杂的,关于宏和函数的对比那块大家要自行好好掌握

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

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

相关文章

Java入门:10.Java中的包

1 包 类似于OS中的文件夹。 用来存放一组含义类似或相同的java类&#xff08;接口&#xff09;&#xff0c;方便分类和管理。 对应关系&#xff1a; java程序中的类 --- os中的.java文件 java程序中的包 --- os中的文件夹 如何指定包&#xff1a; 在os中创建对应的文件夹…

Java 方法的重载

1.重载&#xff1a;在一个类中&#xff0c;方法的函数名相同&#xff0c;但形参不同。 结果&#xff1a; 2&#xff0e;方法重载的规则&#xff1a; &#xff08;1&#xff09;方法名必须相同。&#xff08;例如&#xff1a;重名的人有很多&#xff09; &#xff08;2&#x…

Aegisub字幕自动化及函数篇(图文教程附有gif动图展示)(二)

目录 template行 template pre-line template line template syl template syl noblank template char template notext template pre-line notext template syl noblank notext template keeptags ​编辑 template loop number 内联变量 ​编辑 remeber函数 re…

平台数据分类与聚类实验报告

参考书籍&#xff1a;《数据流挖掘与在线学习算法》 李志杰 1.6.1 实验目的 本书内容以及课程实验主要涉及Java程序设计语言、数据挖掘工具Weka和数据流机器学习平台MOA&#xff0c;因此&#xff0c;需要安装、配置并熟悉实验环境。Java、Weka和MOA都是开源小软件&#xff0…

SpringBoot在线教育平台:设计与实现的深度解析

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常…

SQL进阶技巧:Order by 中 NULLS LAST特性使用?

目录 1 需求描述 2 数据准备 3 问题分析 4 小结 如果觉得本文对你有帮助&#xff0c;想进一步学习SQL语言这门艺术的&#xff0c;那么不妨也可以选择去看看我的博客专栏 &#xff0c;部分内容如下&#xff1a; 数字化建设通关指南 专栏 原价99&#xff0c;现在活动价59…

ElasticSearch学习笔记(三)Ubuntu 2204 server elasticsearch集群配置

如果你只是学习elasticsearch的增、删、改、查等相关操作&#xff0c;那么在windows上安装一个ES就可以了。但是你如果想在你的生产环境中使用Elasticsearch提供的强大的功能&#xff0c;那么还是建议你使用Linux操作系统。 本文以在Ubuntu 2204 server中安装elasticsearch 8.…

MATLAB智能优化算法-学习笔记(4)——灰狼优化算法求解旅行商问题【过程+代码】

灰狼优化算法(Grey Wolf Optimizer, GWO)是一种基于灰狼社会行为的元启发式算法,主要模拟灰狼群体的捕猎行为(包括围攻、追捕、搜寻猎物等过程)。多旅行商问题(Multi-Traveling Salesman Problem, mTSP)是旅行商问题(TSP)的扩展,它涉及多个旅行商(车辆)从一个起点城…

使用AI编码,这些安全风险你真的了解吗?

前言 随着AI技术的飞速发展与普及&#xff0c;企业开发人员对AI编码助手工具如Copilot的依赖度日益增强&#xff0c;使用AI编码助手工具虽然能显著提升编程效率与质量&#xff0c;但同时也存在一系列的潜在风险。 许多开发人员可能未意识到&#xff0c;如果他们的现有代码库中…

CMSIS-RTOS V2封装层专题视频,一期视频将常用配置和用法梳理清楚,适用于RTX5和FreeRTOS(2024-09-28)

【前言】 本期视频就一个任务&#xff0c;通过ARM官方的CMSIS RTOS文档&#xff0c;将常用配置和用法给大家梳理清楚。 对于初次使用CMSIS-RTOS的用户来说&#xff0c;通过梳理官方文档&#xff0c;可以系统的了解各种用法&#xff0c;方便大家再进一步的自学或者应用&#x…

数据结构——七种排序(java)实现

文章目录 直接插入排序希尔排序选择排序冒泡排序快速排序归并排序计数排序 直接插入排序 思想&#xff1a; /*** 直接插入排序* 具有稳定性* 时间复杂度为&#xff1a;&#xff08;计算时间复杂度的时候应计算执行次数最多的语句类&#xff0c;在直接插入排序中次数最多的语句…

Ajax ( 是什么、URL、axios、HTTP、快速收集表单 )Day01

AJAX 一、Ajax是什么1.1名词解释1.1.1 服务器1.1.2 同步与异步1. 同步&#xff08;Synchronous&#xff09;2. 异步&#xff08;Asynchronous&#xff09;3. 异步 vs 同步 场景4. 异步在 Web 开发中的常见应用&#xff1a; 1.2 URL 统一资源定位符1.2.1 URL - 查询参数1.2.2 ax…

maven打包常用命令

跳过tset打包 mvn package -Dmaven.test.skiptrue

什么是 ARP 欺骗和缓存中毒攻击?

如果您熟悉蒙面歌王&#xff0c;您就会明白蒙面歌王的概念&#xff1a;有人伪装成别人。然后&#xff0c;当面具掉下来时&#xff0c;您会大吃一惊&#xff0c;知道了这位名人是谁。类似的事情也发生在 ARP 欺骗攻击中&#xff0c;只是令人惊讶的是&#xff0c;威胁行为者利用他…

获取期货股票历史数据以及均线策略分析

【数据获取】银河金融数据库&#xff08;yinhedata.com&#xff09;能够获取国内外金融股票、期货历史行情数据&#xff0c;包含各分钟级别。 【搭建策略】均线策略作为一种广泛应用于股票、期货等市场的技术分析方法&#xff0c;凭借其简单易懂、操作性强等特点&#xff0c;深…

AI绘画Stable Diffusion WebUI 2个超好用的办法-实现图片光照调节,快速生成你想要的光感大片!

大家好&#xff0c;我是画画的小强 在摄影艺术中&#xff0c;灯光的运用对于照片的质量和情感表达至关重要。它不仅能够彰显主题&#xff0c;还能为画面增添深度与立体感&#xff0c;帮助传递感情&#xff0c;以及凸显细节之美。 下面&#xff0c;我将向大家展示如何用AI绘画…

【动态规划-最长公共子序列(LCS)】【hard】【科大讯飞笔试最后一题】力扣115. 不同的子序列

给你两个字符串 s 和 t &#xff0c;统计并返回在 s 的 子序列 中 t 出现的个数&#xff0c;结果需要对 10^9 7 取模。 示例 1&#xff1a; 输入&#xff1a;s “rabbbit”, t “rabbit” 输出&#xff1a;3 解释&#xff1a; 如下所示, 有 3 种可以从 s 中得到 “rabbit”…

kafka创建多个分区时,分区会自动分配到多个不同的broker

1.分区只有一个时所有的消息生产和消费都集中在单个Broker上&#xff0c;多个broker只是提高了抗风险能力&#xff08;因为副本存在不同的broker上&#xff0c;主节点挂掉&#xff0c;可以重新选取副本为主节点&#xff09;。 2.没有消息顺序性要求可以多个分区&#xff0c;注意…

SpringBoot使用esayExcel根据模板导出excel

1、依赖 <dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.1.3</version></dependency> 2、模板 3、实体类 package com.skybird.iot.addons.productionManagement.qualityTesting…

获取期货股票分钟级别数据以及均线策略

【数据获取】 银河金融数据库&#xff08;yinhedata.com&#xff09; 能够获取国内外金融股票、期货历史行情数据&#xff0c;包含各分钟级别。 【搭建策略】 均线策略作为一种广泛应用于股票、期货等市场的技术分析方法&#xff0c;凭借其简单易懂、操作性强等特点&#xf…