C语言基础:预处理指令的使用

news2024/11/25 13:32:34

本文结合工作经验,研究C语言中常见的预处理指令的用法。

文章目录

  • 1 预处理指令概念
  • 2 常见的预处理指令
    • 2.1 #include包含头文件
    • 2.2 #define定义宏
      • 2.2.1 类对象宏(object-like macro)
      • 2.2.2 类函数宏(function-like macro)
    • 2.3 条件编译
  • 3 总结

1 预处理指令概念

编译器编译C代码的第一个阶段就是预处理。预处理阶段会对预处理指令进行处理,将C代码“翻译”成另一个样子,为后续的编译、汇编、链接过程做准备。下面每个章节会研究一些常见的预处理指令。

2 常见的预处理指令

2.1 #include包含头文件

#include应该是最常见的预处理指令了,基本上每个C文件都会通过include包含若干头文件,或者头文件嵌套包含头文件。在预处理阶段,编译器会将include包含的头文件展开,写到C文件中。例如下面的C文件包含了一个头文件。

//demo.c
#include "demo_type.h"

uint8 demo(uint8 a,uint8 b)
{
	return a + b;
}
//demo_type.h
typedef unsigned char uint8;

经过预处理过程,头文件的内容被展开到了C文件include的地方,这个头文件就不再需要了。

//预处理后的demo.c
typedef unsigned char uint8;

uint8 demo(uint8 a,uint8 b)
{
	return a + b;
}

由此,这个C文件就可以使用头文件中定义的类型了。

再进一步思考,增量式编译的编译器会对包含了修改过的头文件的C文件重新编译,因此C文件不要包含多余的头文件,以免增加编译时间。

对于头文件嵌套的情况,会一层一层展开来。

2.2 #define定义宏

2.2.1 类对象宏(object-like macro)

通过#define可以定义一个宏,预处理阶段的时候,如果在代码中遇到一个宏,就会将其替换成宏所对应的内容。首先看一下不用宏定义的代码,例如如下代码:

//circle.c
float cal_area(float radius)
{
	return 3.14 * radius * radius;
}

函数输入半径,返回圆的面积。其中用到了圆周率,直接将数值3.14写道代码中。这样的数字被称为“魔法数字”。正确的做法是将其定义为一个宏,然后在函数中使用这个宏。

//circle.c
#define PI 3.14

float cal_area(float radius)
{
	return PI * radius * radius;
}

这样做有两个好处,首先,其他人阅读代码的时候,对于数字很难理解其中的含义,但是宏定义是可以从字面上知道意义的,可以增加代码的可读性。其次,如果代码中多处用到一个同样的值,又需要修改这个值(譬如将3.14改成3.1415926),就可以直接修改这个宏定义后面的数值。

2.2.2 类函数宏(function-like macro)

定义类函数宏也是使用#define定义一个看起来类似于函数的宏,使用的时候就像调用函数一样,例如如下代码:

#include <stdio.h>
#define MAX(a, b)   (((a) < (b)) ? (b) : (a))

int main()
{
    int a = 1;
    int b = 2;
    int c = MAX(a, b);
    printf("c = %d \r\n", c);
}

MAX(a, b)用来判断传入的两个参数a和b,返回较大的值。该代码经过预处理之后的i文件的片段如下:

int main()
{
    int a = 1;
    int b = 2;
    int c = (((a) < (b)) ? (b) : (a));
    printf("c = %d \r\n", c);
}

这里直接展开了类函数宏。

从工作经验来看,当实现的需求比较简单时(例如上面比较大小),可以使用类函数宏,这样可以减少系统资源使用;当需要实现比较复杂的算法,还是应该使用函数或者内联函数,这样更有利于程序的debug。

另外,类函数宏使用的时候还可能出现一些没考虑到的问题。例如下面代码参考CPrimerPlus。

#include <stdio.h>
#define SQUARE(X) X*X

int main()
{
    int x = 5;
    printf("x = %d \r\n", x);
    printf("SQUARE(x) = %d \r\n", SQUARE(x));
    printf("SQUARE(x+2) = %d \r\n", SQUARE(x+2));
    printf("100/SQUARE(x) = %d \r\n", 100/SQUARE(x));
    printf("SQUARE(++x) = %d \r\n", SQUARE(++x));
}

打印出来的结果是:
在这里插入图片描述
第一个SQUARE(x)的计算结果是正确的,但是后3个都和预期不符合。这是因为预处理器直接将字符替换的缘故。原来的表达式和预处理后的表达式如下表,就可以很容易理解了。

预处理后计算结果
SQUARE(x+2)x+2*x+25+2*5+2 = 17
100/SQUARE(x)100/x*x100/5*5 = 100
SQUARE(++x)++x*++x7*7 = 47

上面第三条的运算首先是做两次++x,将x自加为7,再进行乘法。

解决表格中的前两个问题很简单,只要把宏加上完整的括号就行,例如如下:

#include <stdio.h>
#define SQUARE(X) ((X)*(X))

int main()
{
    int x = 5;
    printf("x = %d \r\n", x);
    printf("SQUARE(x) = %d \r\n", SQUARE(x));
    printf("SQUARE(x+2) = %d \r\n", SQUARE(x+2));
    printf("100/SQUARE(x) = %d \r\n", 100/SQUARE(x));
    printf("SQUARE(++x) = %d \r\n", SQUARE(++x));
}

打印结果为:
在这里插入图片描述
但是对于第三条自加的问题还是无法解决,书中推荐不要使用这种方式。

2.3 条件编译

条件编译也是一种常用的预处理指令,预处理过程中可以通过某种条件来决定保留哪些代码块。例如,代码中需要定义一个变量,但是在仿真的过程中将其定义为全局变量,在发布的时候将其定义为局部变量。

#include <stdio.h>

#ifdef Simulation
int a = 5;
#endif

int main()
{
#ifndef Simulation
    int a = 10;
#endif
    printf("a = %d \r\n", a);
}

上述代码的意思是,当定义过Simulation这个宏的时候,将变量a定义为全局变量,赋值为5;如果没定义过Simulation这个宏,就将a定义为局部变量,并赋值为10.

这样做的好处是将同一版代码中兼容两种定义方式,通过定义一个宏来切换。条件编译还有很多灵活的用法。

3 总结

本文总结了工作中常用的一些预处理指令,以及使用的范例。

>>返回个人博客总目录

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

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

相关文章

Docker 镜像的创建

Docker 镜像的创建 创建镜像有三种方法&#xff0c;分别为基于已有镜像创建、基于本地模板创建以及基于Dockerfile创建。 1&#xff0e;基于现有镜像创建 基于现有镜像创建 先使用现有镜像创建容器 docker run 再进入容器进行内容的更新 docker exex 最后提交成新的进行 …

C++语法总结

今天给大家带来的不是某个知识点的解说&#xff0c;而是我花了几个小时的时间&#xff0c;写的一个C的思维导图&#xff0c;希望大家能够喜欢 以上是我对C语法的一些理解&#xff0c;有些可能是我遗漏了&#xff0c;有不对的地方希望大家能够指出&#xff0c;最后&#xff0c;子…

【Atcoder】 [ARC151D] Binary Representations and Queries

题目链接 Atcoder方向 Luogu方向 题目解法 首先需要得到一个性质&#xff1a; 当 X i ≠ X j Xi\ne Xj XiXj 时&#xff0c; i , j i,j i,j 操作的先后顺序可以交换 证明&#xff1a; 可以画一张图&#xff0c;只考虑 Y i Y j 0 YiYj0 YiYj0 的情况&#xff0c;其他情…

力扣256.翻转二叉树(递归/qBFS) 剑指offer 32 从上到下打印二叉树(q BFS)I II III(三道题)

采用队列 class Solution { public:TreeNode* invertTree(TreeNode* root) {queue<TreeNode*> q;if(rootNULL) return root;q.push(root);int i0;while(!q.empty()){TreeNode *curq.front();swap(cur->left,cur->right);if(cur->left) q.push(cur->left);if…

电商系统架构设计系列(六):电商的「账户系统」设计要特别考虑哪些问题?

上篇文章中&#xff0c;我给你留了一个思考题&#xff1a;电商的账户系统&#xff0c;该如何设计&#xff1f; 今天这篇文章&#xff0c;我们来说一下电商的账户系统。 引言 账户系统负责记录和管理用户账户的余额&#xff0c;这个余额就是每个用户临时存在电商的钱&#xff…

Spring整合Mybatis原理

首先介绍一下Mybatis的工作原理 先简略的放两张图&#xff0c;后面的知识结合这两张图比较好理解 Mybatis的基本工作原理 在 Mybatis 中&#xff0c;我们可以使用⼀个接口去定义要执行sql&#xff0c;简化代码如下&#xff1a; 定义⼀个接口&#xff0c;Select 表示要执行查询…

UE 材质实现让远处物体变小

CameraDepthFade: 根据距离摄像机的距离改变值 Fade Length: 从0到1的过渡距离 Fade Offset&#xff1a;小于该值的地方值为1 UV平铺&#xff1a;值越大&#xff0c;平铺的越少&#xff0c;纹理重复显示的越少&#xff0c;视觉效果纹理变大&#xff0c;值越小&#xff0c;平铺…

[Linux笔记]gcc/g++,动静态库,make/makefile/.PHONY

都是编译器&#xff0c;二者的选项是重叠的 基本上&#xff0c;gcc专门用于编译c&#xff0c;g专门用于编译c gcc/g形成的可执行程序默认是release版的。若要debug版&#xff0c;则使用-g选项。 如&#xff1a;gcc -o mytest test.c -g -stdc99 编译命令格式例&#xff1a; gc…

MYSQL-死锁大集合

为什么会死锁 数据准备 建个表 CREATE TABLE t_order (id int NOT NULL AUTO_INCREMENT,order_no int DEFAULT NULL,create_date datetime DEFAULT NULL,PRIMARY KEY (id),KEY index_order (order_no) USING BTREE ) ENGINEInnoDB ; 存个数据 然后我们分别创建两个事务 事…

【多模态】16、DetCLIP | 构建超大词汇字典来进行开放世界目标检测

论文&#xff1a;DetCLIP: Dictionary-Enriched Visual-Concept Paralleled Pre-training for Open-world Detection 代码&#xff1a;无。。。 出处&#xff1a;NIPS2022 | 华为诺亚方舟 | 中山大学 | 香港科技大学 效果&#xff1a; 在 LVIS 的 1203 个类别上超越了 GLIP…

每月进度总结 7月1日~7月22日

一个月已经过了三分之二了&#xff0c;感觉这个月是在学校学的很多。也是最充实的三个星期。其中也有发呆&#xff0c;也有过懊悔&#xff0c;今天状态为什么这么差&#xff0c;就学了这一点。但是还有学到知识的喜悦。总之是认识到了自己的很多不足&#xff0c;也找到了相对正…

conda在D盘创建虚拟环境

1.安装Anaconda 略 2.修改镜像源 清华的镜像源好像不能用了。交大的镜像源还能使用。 winr 输入&#xff1a; %HOMEPATH% 进入C盘的用户目录。找到.condarc的文件。打开它&#xff0c;把里面的镜像内容修改为以下内容 channels:- https://mirrors.sjtug.sjtu.edu.cn/anaco…

华硕ROG枪神6plus原装Windows11预装系统 工厂模式恢复安装带ASUSRecevory一键还原安装还原方法

华硕ROG枪神6plus原装Windows11预装系统 工厂模式恢复安装带ASUSRecevory一键还原安装还原方法 第一步&#xff1a;需要拥有文件格式为6个底包的文件 第二步&#xff1a;创建系统u盘 第三步&#xff1a;复制文件到u盘之后&#xff0c;启动华硕工厂模式 第四步&#xff1a;按…

python安装第三方包的两种方式

最近研究QQ空间、微博的&#xff08;爬虫&#xff09;模拟登录&#xff0c;发现都涉及RSA算法。于是需要下一个RSA包&#xff08;第三方包&#xff09;。折腾了很久&#xff0c;主要是感觉网上很多文章对具体要在哪里操作写得不清楚。这里做个总结&#xff0c;以免自己哪天又忘…

MES管理系统如何为汽配制造赋能

汽配制造是企业产业链的重要环节之一&#xff0c;其生产质量和效率直接影响到汽车的整体质量和安全性。然而&#xff0c;传统的汽配制造管理模式存在着一些问题&#xff0c;如生产过程不透明、信息传递不畅、生产效率低下等&#xff0c;这些问题成为了制约汽配制造发展的瓶颈。…

Qt 之 自定义配置文件类,QSettings应用

目录 一、前言 二、头文件代码 三、源文件代码 四、使用示例 五、使用效果 一、前言 在qt开发过程中&#xff0c;很多时候需要为软件添加配置&#xff0c;让软件在下一次打开时仍然保持上一次关闭时的设置。qt的配置类QSettings&#xff0c;本文通过继承QSettings实现自定…

[深度学习实战]基于PyTorch的深度学习实战(中)[线性回归、numpy矩阵的保存、模型的保存和导入、卷积层、池化层]

目录 一、前言二、线性回归2.1 训练代码2.2 绘图部分代码2.3 numpy 数组的保存和导入代码2.4 完整代码 三、numpy矩阵的保存四、模型的保存和导入4.1 保存模型4.2 导入模型 五、卷积层5.1 Conv2d5.1.1 函数定义5.1.2 参数说明5.1.3 测试代码5.1.4 最终结果 5.2 Conv1d5.2.1 函数…

【cs61b】学习笔记day1

1.1 java基础 Hello World java程序由一个类声明组成&#xff0c;使用关键字public class声明。在Java中&#xff0c;所有代码都位于类中。 运行的代码在一个名为main的方法中&#xff0c;该方法被声明为public static void main(String[] args)。 我们使用大括号{}表示一段…

Vue中TodoList案例_添加

与上一篇Vue中TodoList案例_初始化列表有四个文件变化了 安装nanoid库&#xff1a; npm i nanoid App.vue <template><div id"root"><div class"todo-container"><div class"todo-wrap"><MyHeader :addTodo"…