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

news2025/3/16 19:10:40

文章目录

      • 一、宏和函数的对比
      • 二、#和##运算符
        • 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 MAX

printf("%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 DEBUG
		printf("%d ", arr[i]);  //为了观察数组是否赋值成功。
#endif // DEBUG
	}
	return 0;
}

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

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

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

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

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

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

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

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

#if !defined(MAX)// 没定义执行,定义了不执行
	printf("YES");
#endif
	return 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.coloradmin.cn/o/1598234.html

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

相关文章

系统学c#:1、基础准备(软件下载与安装)

一、Vs软件下载与安装 访问Visual Studio官方网站&#xff1a; https://visualstudio.microsoft.com/zh-hans/downloads 下载Visual Studio 运行exe文件&#xff0c;点击“继续” 初始文件安装完成后选择我们需要安装的项&#xff0c;并勾选好必要的单个组件&#xff0c;设…

VisualStudio2010编译GDAL2.2.4

一、源码目录组织结构 makefile.vc文件是windows下nmake编译工具的makefile文件&#xff0c;nmake.opt是makefile文件的编译选项配置选择文件。 二、打开Visual Studio2010的64位命令行&#xff0c;切换到GDAL224的解压根目录下 输入如下命令回车开始编译源码 nmake -f makef…

Bitwarden 一款开源的密码管理器

Bitwarden Bitwarden 是一款开源的密码管理器&#xff0c;提供强大的安全性和便捷的密码管理功能。 适用于运维、系统较多、密码保密程度较高的场景&#xff01; 主要功能&#xff1a; 密码保存与自动填充&#xff1a;Bitwarden 可以安全地存储您的用户名和密码&#xff0c…

阿里云迁移到AWS云,九河云保姆级教程

随着云计算技术的不断发展,越来越多的企业开始将传统的IT基础设施迁移到云平台上,以获得更高的灵活性、可扩展性和成本效益。在众多云服务提供商中,阿里云和AWS都是备受青睐的选择。本文将探讨如何将阿里云上的资源顺利迁移到AWS云平台,并针对性地进行优化。我们九河云&#xf…

【第9章】@Autowired

文章目录 前言一、Autowired注入1. 属性注入2. 构造方法注入3. set方法注入4. 形参注入5. byName注入5.1 新增实现类5.2 注入 二、Resource1.引入库2.使用2.1 根据注解的name值注入2.2 根据对象变量名注入2.3 根据对象类型注入 总结 前言 【第8章】全注解开发介绍了全注解开发…

电竞陪玩系统开发平台搭建(小程序,公众号,app)线上线下皆有,线下计算距离。

六大核心功能 1.游戏陪练:可以选择当下火爆的游戏内容&#xff0c;选择游戏大神、职业玩家进行陪练&#xff0c;也可约附近路人玩家或是身边的小伙伴语音组队开黑&#xff0c;一起享受边玩游戏边吐槽的无限乐趣。 2.约玩交友:除了游戏陪玩功能&#xff0c;系统还设置了单独的语…

将自己的项目上传至Git

一、安装Git 官网:Git (git-scm.com) 二、注册gitee 官网:工作台 - Gitee.com 进入“我的”出现以下界面 三、创建仓库 点击加号&#xff0c;新建仓库 根据自己的需求取名&#xff0c;描述仓库&#xff0c;开源还是私有&#xff0c;点击创建即可&#xff0c;点击我的即可…

Linux学习-数据库

数据库软件: 关系型数据库: Mysql Oracle SqlServer Sqlite 非关系型数据库&#xff1a; Redis NoSQL 1.数组、链表、文件、数据库 数组、链表: 内存存放数据的方式(代码运行结束、关机数据丢失) 文件、…

一文掌握 React 开发中的 JavaScript 基础知识

前端开发中JavaScript是基石。在 React 开发中掌握掌握基础的 JavaScript 方法将有助于编写出更加高效、可维护的 React 应用程序。 在 React 开发中使用 ES6 语法可以带来更简洁、可读性更强、功能更丰富,以及更好性能和社区支持等诸多好处。这有助于提高开发效率,并构建出更…

MongoDB 索引全攻略

目录 一、索引介绍 1.1 单字段索引 1.2 复合索引 1.3 多键索引 1.4 主键索引 1.5 TTL 索引 1.6 地理空间索引 1.7 哈希索引 1.8 创建索引时注意事项 1.9 索引效果查看 二、索引实现原理 2.1 为什么使用 B-Tree 三、执行计划 一、索引介绍 任何数据库都有索引这一核心功能&…

Centos7.6部署minikube

1、什么是minikube ? Minikube是由Kubernetes社区维护的单机版的Kubernetes集群&#xff0c;支持macOS, Linux, and Windows等多种操作系统平台&#xff0c;使用最新的官方stable版本&#xff0c;并支持Kubernetes的大部分功能&#xff0c;从基础的容器编排管理&#xff0c;到…

基于AutoCAD的WMTS服务加载方法与应用研究

"针对在AutoCAD中加载地图存在数据定位操作复杂、数据渲染效率低、无法接入第三方地理信息服务的问题&#xff0c;提出了在AutoCAD中加载OGC标准的网络地图分块服务方法。基于ObjectARX二次开发插件&#xff0c;实现在AutoCAD中加载WMTS服务&#xff0c;兼容了第三方地理信…

基于Springboot的旅游管理系统

基于SpringbootVue的旅游管理系统的设计与实现 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringbootMybatis工具&#xff1a;IDEA、Maven、Navicat 系统展示 用户登录 首页展示 旅游方案展示 旅游资讯 后台管理员登录 后台管理页面首页 用户管理 …

【自动驾驶】贝叶斯算法在机器学习中的应用研究

目录 第一章&#xff1a;引言 1.1 贝叶斯算法在机器学习中的重要性 1.2 研究背景 1.3 研究目的 1.4 论文结构 第二章&#xff1a;贝叶斯算法概述 2.1 贝叶斯定理 2.2 贝叶斯算法分类 第三章&#xff1a;贝叶斯算法在机器学习中的应用 3.1 贝叶斯分类器 3.2 贝叶斯回…

SpringSecurity源码分析3--UserDetail部分

UserDetailsService.class DaoAuthenticationProvider.class AbstractUserDetailsAuthenticationProvider.class 一个允许子类重写和处理UserDetails对象的基AuthenticationProvider。该类旨在响应UsernamePasswordAuthenticationToken身份验证请求。 AuthenticationProvider…

Gartner 《2024安全和风险管理技术路线图》:高价值技术 DSP 进入广泛部署阶段

近期&#xff0c;Gartner 发布《2024年技术采用路线图&#xff1a;安全与风险管理》&#xff08;以下简称&#xff1a;《路线图》&#xff09;&#xff0c;该信息图表识别了全球企业正在采用的 44 种与安全相关的技术&#xff0c;并根据采用阶段、部署风险和企业价值进行了映射…

python中的列表、元组、字典、集合(字典篇)

数据类型定义符号访问元素是否可变是否重复是否有序列表 [ ]索引可变可重复有序元组&#xff08;&#xff09;索引不可变可重复有序字典{key&#xff1a;value}键可变可重复无序集合{ }可变不可重复无序 字典概念 在python语言中&#xff0c;字典属于内置容器类&#xff0c;其…

什么是云安全

云安全和网络安全有所不同&#xff0c;因为云安全一词 比网络安全更涵盖整个企业基础设施。一般来说&#xff0c;当人们提到云安全时&#xff0c;指的是第三方服务提供商提供的 IaaS 云环境。在这种情况下&#xff0c;云安全不仅包括网络安全工具&#xff0c;还包括服务器、容器…

C#基础|数据类型、变量

哈喽&#xff0c;你好啊&#xff0c;我是雷工&#xff01; 01 数据类型 数据类型是为了方便存储数据的&#xff0c;为了将数据按照不同的分类存储&#xff0c;所以引入数据类型。这个在PLC中已经很熟悉了。 数据类型的作用&#xff1a;就是为了更好地管理内存&#xff0c;为…

[C++][算法基础]求最小生成树(Prim)

给定一个 n 个点 m 条边的无向图&#xff0c;图中可能存在重边和自环&#xff0c;边权可能为负数。 求最小生成树的树边权重之和&#xff0c;如果最小生成树不存在则输出 impossible。 给定一张边带权的无向图 G(V,E)&#xff0c;其中 V 表示图中点的集合&#xff0c;E 表示图…