【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 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];
};

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

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

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

相关文章

Vue3---基础10(路由)

写一个最基本的路由导航 下载、创建、使用路由 下载路由 npm i vue-router 创建路由 先在 src 内去创建一个 router 文件夹 在文件夹内创建一个 index 文件 index.ts 内代码 // 创建一个路由器&#xff0c;并暴露出去 // 引入createRouter import { createRouter, createWeb…

CSS使用自己的字体

在项目的根目录下的static文件夹中放置字体文件。在项目中使用这个字体&#xff0c;需要2个步骤。 一. 你需要在全局样式文件中引入它。 假设你的全局样式文件是App.vue或者App.vue中引入的App.scss文件&#xff0c;你可以像这样引入字体文件&#xff1a; font-face {font-fa…

深度学习体系结构——CNN, RNN, GAN, Transformers, Encoder-Decoder Architectures算法原理与应用

1. 卷积神经网络 卷积神经网络&#xff08;CNN&#xff09;是一种特别适用于处理具有网格结构的数据&#xff0c;如图像和视频的人工神经网络。可以将其视作一个由多层过滤器构成的系统&#xff0c;这些过滤器能够处理图像并从中提取出有助于进行预测的有意义特征。 设想你手…

MySQL中的存储过程详解(上篇)

使用语言 MySQL 使用工具 Navicat Premium 16 代码能力快速提升小方法&#xff0c;看完代码自己敲一遍&#xff0c;十分有用 拖动表名到查询文件中就可以直接把名字拉进来中括号&#xff0c;就代表可写可不写 目录 1.认识存储过程 1.1 存储过程的作用 1.2 存储过程简介…

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

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

顺序表 (头删 尾删 清空)

//头删 | 1 #include "head.h" | 1 #ifndef ww87 void head_del(p lp) | 2 int main(int argc, const char *argv[]) …

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

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

民航电子数据库:[E14024]事务内变更操作次数超过最大许可值10000,可通过系统参数max_trans_modify适当调整限制

目录 一、场景二、异常情况三、原因四、排查五、解决 一、场景 1、对接民航电子数据 2、执行delete语句时报错 二、异常情况 三、原因 通过报错信息就可以看出&#xff0c;是系统参数max_trans_modify配置导致 当删除的数据量 > max_trans_modify时&#xff0c;删除就会…

Visual studio项目默认“Header Files”、“Source Files”等过滤器消失后展开的方法。

使用Visual Studio进行项目开发创建默认工程的解决方案资源管理器里查看项目文件&#xff0c;所有的文件是按照其所属的类型自动归类&#xff0c;例如&#xff1a;.h头文件自动划归到Header Files文件夹&#xff0c;.cpp文件自动划归到Source Files文件夹下&#xff0c;如下图所…

关于AG32 MCU的一些奇思妙想

1、AG32VF103的网口是100M还是10M&#xff1f; RE: 都是100M的。 2、用FPGA能不能再仿出一个网口&#xff1f;有些产品用到两个网口。 理论上可以&#xff0c;但是要考虑&#xff0c;一个是cpld实现难度&#xff0c;一个是需要的逻辑单元。因为mac逻辑多&#xff0c;内置的2KL…

Python Flask Web 框架-API接口开发_4

一、1、安装 Falsk 当前用户安装 pip3 install --user Flask 确认安装成功&#xff1a; 进入python交互模式看下Flask的介绍和版本&#xff1a; $ python3>>> import flask >>> print(flask.__doc__)flask~~~~~A microframework based on Werkzeug. Its …

快速掌握Spring监控(Spring Boot admin)

监控 监控可视化监控平台Admin底层逻辑info 自定义端点 监控 监控的作用&#xff1a; 监控服务状态是否宕机监控服务运行指标&#xff08;内存&#xff0c;虚拟机&#xff0c;线程&#xff0c;请求等&#xff09;监控日志管理服务&#xff08;服务下线&#xff09; 监控的实…

linux进阶篇:使用Apache搭建文件服务器目录

Linux服务搭建篇&#xff1a;使用Apache搭建文件服务器目录 一、关于文件服务器 ​ 在一个项目中&#xff0c;如果想把公共软件或者资料共享给项目组成员&#xff0c;可以搭建一个简易的文件服务器来实现&#xff0c;只要是在局域网内的成员都可以通过浏览器或者wget命令来下…

书生·浦语大模型全链路开源体系-第5课

书生浦语大模型全链路开源体系-第5课 书生浦语大模型全链路开源体系-第5课相关资源LMDeploy基础配置LMDeploy运行环境下载internlm2-chat-1_8b模型使用Transformer来直接运行InternLM2-Chat-1.8B模型使用LMDeploy以命令行方式与InternLM2-Chat-1.8B模型对话设置KV Cache最大占用…

wps使用Latex编辑公式没有Latex formula

wps使用Latex编辑公式没有Latex formula 1. 下载CTEX2. 下载LaTeXEE3. 配置Miktex4. 配置latexee5. 用管理员权限运行latexeqedit.exe6. wps插入latex公式 1. 下载CTEX 下载CTEX网址&#xff0c;我下载的下图这个&#xff0c;下载完了之后运行exe文件安装ctex。 2. 下载LaTe…

视频国标学习

总体介绍 GB/T28181协议&#xff0c;全名叫《安全防范视频监控联网系统信息传输、交换、控制技术要求》&#xff0c;是由中国国家标准委员会发布的一种国家级的标准。它主要对视频监控系统的各个方面做了明确的规定&#xff0c;使得不同厂商生产的视频监控设备能够相互连通&am…

【C++】<入门>C++入门基础知识

C入门 1. 入门0. 本节知识点熟悉目的1. C关键字&#xff08;C98&#xff09; 2. 命名空间2.1 命名空间定义2.2 命名空间使用 3. C输入&输出4. 缺省参数4.1 缺省参数概念4.2 缺省参数分类 5. 函数重载5.1 函数重载概念5.2 C支持函数重载的原理--名字修饰&#xff08;name Ma…

IntelliJ IDEA 2023中文--让编程更高效、更智能

IntelliJ IDEA 2023是一款功能强大的集成开发环境&#xff08;IDE&#xff09;&#xff0c;专为Java开发者打造。它以其智能、高效和人性化的特点&#xff0c;帮助开发者更快、更好地编写代码。IntelliJ IDEA 2023支持多种语言和框架&#xff0c;包括Java、Kotlin、Spring等&am…

SpringCloud之LoadBalancer负载均衡器的简单使用

SpringCloud之LoadBalancer负载均衡器的简单使用 loadbalancer用于对提供服务的集群做一个节点的选取规则。 如图所示&#xff0c;load balancer集成在调用方 示例 创建loadbalance-base模块,并引入相关依赖 <dependencies><dependency><groupId>org.spr…

Unity笔记之下拉刷新列表

这样的效果&#xff1b; 代码&#xff1a; using System; using System.Collections; using System.Collections.Generic; using Sirenix.OdinInspector; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI;public class ScrollRectUpdateView : Mon…