《C和指针》读书笔记(第十四章 预处理器)

news2025/1/18 21:16:15

目录

  • 0 简介
  • 1 预定义符号
  • 2 #define
    • 2.1 宏
    • 2.2 #define替换
    • 2.3 宏与函数
    • 2.4 带副作用的宏参数
    • 2.5 命名约定
    • 2.6 #undef
    • 2.7 命令行定义
  • 3 条件编译
    • 3.1 是否被定义
    • 3.2 嵌套指令
  • 4 文件包含
    • 4.1 函数库文件包含
    • 4.2 本地文件包含
    • 4.3 嵌套文件包含
  • 5 其他指令
  • 6 总结

0 简介

编译一个C程序涉及到很多步骤,其中第一个步骤就是预处理阶段(preprocessing)阶段。C预处理器在源代码编译之前对其进行一些文本性质的操作。它的主要任务包括删除注释、插入被#including指令包含的文件的内容、定义和替换由#define指令定义的符号以及确定代码的部分内容是否应该根据一些条件编译指令进行编译。

本期内容框架图:
在这里插入图片描述

1 预定义符号

预处理器定义了一些符号,给我i们的程序调试和版本生成等提供了很大的便利。举个例子:

#include <stdio.h>

int main()
{

	printf("This obj file name is:%s\n",__FILE__);
	printf("Current line is:%d\n", __LINE__);
	printf("-------------------------\n");
	printf("Today is:%s\n", __DATE__);
	printf("The time now is:%s\n", __TIME__);


	system("pause");
	return 0;
}

打印输出:
在这里插入图片描述
可以看到我们当前运行文件的名称,打印语句的行编号,还有编译的日期和时间都被打印了出来。

注意:此处的日期和时间是文件被编译的日期和时间,而不是运行时候的日期和时间。

2 #define

第二章是我们的重中之重,其中包含了宏定义的方法,意义和需要注意的地方。

2.1 宏

#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或者定义宏(defined macro)。
举个例子,我们采用宏定义了一个平方运算:

#include <stdio.h>
#define SQUARE(x) x*x
int main()
{
	int a = 5;
	printf("%d\n",SQUARE(a));

	system("pause");
	return 0;
}

打印输出:
在这里插入图片描述
但这样的方法有时候会出现问题,比方说:

#include <stdio.h>
#define SQUARE(x) x*x
int main()
{
	int a = 5;
	printf("%d\n",SQUARE(a + 1));

	system("pause");
	return 0;
}

打印输出:
在这里插入图片描述
按照我们的预想,应该输出6的平方,也就是36的,这是为什么呢?

与函数的实现方式不同,宏这里仅仅做了简单的替换,而没有做参数传递等工作。
可以做个对比:

#include <stdio.h>
#define SQUARE(x) x*x
int square(const int x)
{
	return x * x;
}
int main()
{
	int a = 5;
	printf("%d\n", SQUARE(a + 1));
	printf("%d\n", square(a + 1));

	system("pause");
	return 0;
}

打印输出:
在这里插入图片描述
可以发现,如果是函数的形式,执行结果就与我们的想法一致,这是因为,宏仅仅做了简单的替换工作,也就是说,执行了5+1*5+1,自然运行结果就是11

但我们也可以使用宏定义达到和预想一致的结果:

#include <stdio.h>
#define SQUARE(x) (x)*(x)
int square(const int x)
{
	return x * x;
}
int main()
{
	int a = 5;
	printf("%d\n", SQUARE(a + 1));
	printf("%d\n", square(a + 1));

	system("pause");
	return 0;
}

打印输出:
在这里插入图片描述
问题解决!

2.2 #define替换

这部分主要讲了如何将宏参数插入到字符串常量中,比方说,我们要用宏重新定义一个打印函数:

#include <stdio.h>
#define PRINT(FORMAT, VALUE)  \
		printf("The value of "VALUE" is "FORMAT"\n",VALUE)
int main()
{
	int a = 5;
	PRINT("%d", a);
	PRINT("%d", a + 3);

	system("pause");
	return 0;
}

这个时候就程序就会报错,因为无法做到准确的传值,这时候就需要我们进行简单的转换,将传入的表达式转换为字符串。

#include <stdio.h>
#define PRINT(FORMAT, VALUE)  \
		printf("The value of "#VALUE" is "FORMAT"\n",VALUE)
int main()
{
	int a = 5;
	PRINT("%d", a);
	PRINT("%d", a + 3);

	system("pause");
	return 0;
}

打印输出
在这里插入图片描述
这个时候,宏定义的优势就体现了出来,发现要实现这样的功能,函数的封装就稍显乏力了。

2.3 宏与函数

通过以上的例子我们可以发现,在很多时候,宏与函数的作用都很类似,可以互相替换。但是二者也有不同之处,书上有个表格看着十分清楚:

属性#define宏函数
代码长度每次使用都会被插入到程序中,所以宏的内容不宜过多函数每次调用都是同一份代码,对内存相对更加友好
执行速度更快存在函数调用/返回的额外开销
操作符优先级宏参数的求值是在所有周围表达式的操作环境里,所以必须加上括号表达式的结果更加容易预测
参数求值参数每次用于宏定义时,都会被重新求值。由于多次求值,具有副作用的参数可能产生不可预料的结果参数的副作用并不会造成任何忒书的问题
参数类型宏与类型无关,只要操作是合法的,可以使用任何参数类型函数的参数与类型强相关,参数的类型不同,就需要定义不同的函数,或者需要对实参进行强转

可以看出,宏与函数各有千秋,各有胜负,所以在合适的场合选择合适的实现方法,量体裁衣才是最好的选择。

2.4 带副作用的宏参数

当参数有副作用的时候,我们的宏往往能产生意想不到的结果,在我们的实际开发中需要格外注意。比方下面这个例子:

#include <stdio.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main()
{
	int x = 2, y = 5, z = 0;
	z = MAX(x++, y++);
	printf("x = %d, y = %d, z = %d",x, y, z);
	system("pause");
	return 0;
}

z的值应该是多少呢?如果这个max用函数进行实现,肯定可以很容易地得出答案,然而在宏定义中似乎就没有这么简单了,因为宏定义需要多次用两个表达式替换ab

但是多加思考,发现其实也不难,x++被执行了两次,而y++被执行了三次,所以答案就一目了然了,打印输出:
在这里插入图片描述
那,如果是++x和++y呢?我们可以自己进行探索。。。

2.5 命名约定

很多时候为了更好地开发,从形式上对宏和函数进行区分是十分必要的,一种常见的方式是将宏名字全部大写。相信这点很多人在开发中已经体会到了。

2.6 #undef

这条预处理指令用于移除一个宏定义。
声明方式:

#undef name

如果一个现存的名字需要被重新定义,那么它的旧定义首先必须用#undef移除。

2.7 命令行定义

许多C编译器提供了一种能力,允许在命令行中定义符号,用于启动编译过程。

在windows的常见编译器中,并不会用到相关内容,故略去。

3 条件编译

3.1 是否被定义

3.2 嵌套指令

即使在大型的项目开发中,嵌套指令也很少用到。而且即使遇到了也不难理解。书上给出了基本形式,本文不再赘述。

#ifdef OS_UNIX
	#ifdef OPTION1
		unix_version_of_option1();
	#endif
	#ifdef OPTION2
		unix_version_of_option2();
	#endif
#elif defined OS_MSDOS
	#ifdef OPTION2
		msdos_version_of_option2();
	#endif	
#endif

同条件语句的嵌套类似,所以相对来时比较容易读懂。

4 文件包含

在实际开发中,稍大的项目都会采用模块化开发的方式,这样可以避免了很多麻烦。使得代码的开发,迭代,阅读都方便很多。

文件包含便很好地诠释了这一思想。

4.1 函数库文件包含

所谓的函数库文件就是我们在编程之前,已经为我们写好的一些程序(一般是函数)。我们有时候称其为接口。我们一般采用尖括号进行包含。如我们此前的程序:

#include <stdio.h>

这样在程序编译和运行的时候,电脑才能够识别printf等语句。

4.2 本地文件包含

所谓的本地文件包含一般指我们自己写的本地文件。举个例子:
我们创建一个print.c文件,并写如下函数:

#include <stdio.h>
#include "print.h"
void fun_print(char *x)
{
	if (x != NULL)
	{
		printf("%s",x);
	}
}

然后创建print.h文件,对其进行声明:

#pragma once
void fun_print(char *x);

最后,在main.c文件中进行调用:

#include <stdio.h>
#include "print.h"
int main()
{
	fun_print("Hello world!");
	system("pause");
	return 0;
}

这就是本地文件包含,为了与系统文件包含进行区分,一般会采用双引号

4.3 嵌套文件包含

在大型的项目开发中,存在大量的源文件和头文件,显然也存在着错综复杂的包含关系,嵌套的包含关系便成了一种普遍存在的现象,这本身没什么,但是如果出现了多重包含的现象,在编译的时候编译器就会报错,无法编译通过。

也就是说,多重包含会直接影响到程序的运行效率或者内存大小。具体的内容可以查相关资料。那应该如何避免这个问题呢?
一般为了防止头文件多重包含,然后编译的时候出现多重定义的情况,我们可以采如下的方法:

#ifndef __HEADERNAME__H
#define __HEADERNAME__H 1

或者

#pragma once

#pragma once是一种用于头文件保护的预处理指令。它的作用是确保同一个文件不会被多次包含,避免在编译过程中引发重定义错误。可以说,它的作用类似于#ifndef和#define的组合。相比于#ifndef的方式,#pragma once更加简洁、高效,并且可以针对整个文件进行保护。

然而,需要注意的是,#pragma once是非标准的方式,虽然被大多数编译器广泛支持,但也存在兼容性问题。一些较老的编译器可能不支持#pragma once,因此在选择使用时需要考虑到项目的兼容性。

综上所述,选择使用哪种方式取决于具体情况,并可以根据团队的开发规范进行约定。只要能合理地避开缺点,这两种方式都是可以接受的。

5 其他指令

#error是C语言预处理指令之一,在编译阶段检查源代码中是否存在错误,并在编译时输出错误信息。当编译器遇到#error指令时,会停止编译并显示#error后的错误信息。

#include <stdio.h>
#include "print.h"	
int main()
{
#ifdef PRINT
	fun_print("Hello world!");
#else
	#error Print is not defined!
#endif
	system("pause");
	return 0;
}

点击运行,发现无法执行编译,直接输出错误内容:
在这里插入图片描述
当然,不同的IDE可能会以不同的形式报错。

#progma指令是另一种机制,用于支持因编译器而异的特性。比方说我们前文提到的内容

#pragma once

就可以防止头文件重包含。

所谓的支持因编译器而异的特性就是说,同样的#pragma指令,在不同的编译器中可能会产生不同的效果,要视具体情况而定。

6 总结

编译C程序的第一步就是对它进行预处理,预处理器共支持5个符号。

#define指令可以用于”重写“C语言,使它看上去像是其他语言。

条件编译可以在不同条件下执行不同的代码段,这样做比大面积屏蔽程序方便很多,在大型项目开发中有广泛的使用。

在实际开发中,要尽量避免头文件的多重包含,尽管有时候开发环境并不会直接报错误或者警告。

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

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

相关文章

Leetcode 662. 二叉树最大宽度

文章目录 题目代码&#xff08;9.30 首刷看解析&#xff09; 题目 Leetcode 662. 二叉树最大宽度 代码&#xff08;9.30 首刷看解析&#xff09; class Solution { public:int widthOfBinaryTree(TreeNode* root) {unsigned long long res 1;using pr pair<TreeNode*, u…

数据集笔记:芝加哥共享单车OD数据

2013年到2022年3月&#xff0c;芝加哥共享单车OD数据 数据地址&#xff1a;Divvy Data (divvybikes.com)

使用python脚本的时间盲注完整步骤

文章目录 一、获取数据库名称长度二、获取数据库名称三、获取表名总长度四、获取表名五、获取指定表列名总长度六、获取指定表列名七、获取指定表指定列的表内数据总长度八、获取指定表指定列的表内数据 一、获取数据库名称长度 测试环境是bwapp靶场 SQL Injection - Blind - …

【考研数学】高等数学第七模块 —— 曲线积分与曲面积分 | 2. 对坐标的曲线积分(第二类积分)

文章目录 一、曲线积分1.2 对坐标的曲线积分&#xff08;第二类曲线积分&#xff09;1.2.1 问题产生 —— 做功问题1.2.2 对坐标的曲线积分的定义&#xff08;了解&#xff09;1.2.3 对坐标的曲线积分的性质1.2.4 二维空间对坐标的曲线积分计算法1. 定积分法2. 二重积分法&…

YOLOv7改进:结合CotNet Transformer结构

1.简介 京东AI研究院提出的一种新的注意力结构。将CoT Block代替了ResNet结构中的3x3卷积&#xff0c;在分类检测分割等任务效果都出类拔萃 论文&#xff1a;Contextual Transformer Networks for Visual Recognition 论文地址&#xff1a;https://arxiv.org/abs/2107.12292 有…

C进阶--数据的存储

⚙ 1. 数据类型介绍 1.1基本内置类型 ⭕ 整形&#xff1a; char(char又叫短整型)unsigned charsigned charshortunsigned short[int]signed short [int]intunsigned intsigned intlongunsigned long [int]signed long [int] ⭕ 浮点数&#xff1a; float&#xff08;单精度浮…

Linux下C语言操作网卡的几个代码实例?特别实用

前面写了一篇关于网络相关的文章&#xff1a;如何获取当前可用网口。 《简简单单教你如何用C语言列举当前所有网口&#xff01;》 那么如何使用C语言直接操作网口&#xff1f; 比如读写IP地址、读写MAC地址等。 一、原理 主要通过系统用socket()、ioctl()、实现 int sock…

基于arduino的土壤湿度检测

1.总体设计框图 本浇花系统总体上分为硬件和软件两大组成部分。硬件部分包括Arduino UNO开发板、温湿度传感器、通信模块、浇水执行系统和液晶显示等。软件部分包括Android客户端。系统结构如图1所示 本浇花系统总体上分为硬件和软件两大组成部分。硬件部分包括Arduino UN…

LeetCode算法题---第2天

注:大佬解答来自LetCode官方题解 80.删除有序数组的重复项Ⅱ 1.题目 2.个人解答 var removeDuplicates function (nums) {let res [];for (let index 0; index < nums.length; index) {let num 0;if (res.includes(nums[index])) {for (let i 0; i < res.length; …

Python2020年06月Python二级 -- 编程题解析

题目一 数字转汉字 用户输入一个1~9&#xff08;包含1和9&#xff09;之间的任一数字&#xff0c;程序输出对应的汉字。 如输入2&#xff0c;程序输出“二”。可重复查询。 答案: 方法一 list1[一,二,三,四,五,六,七,八,九] while True:n int(input(请输入1~9之间任意一个数字…

Windows 安装CMake

CMake 简介 CMake是一个开源的、跨平台的自动化构建系統&#xff0c;用來管理软件构建的过程。 其用途主要包括&#xff1a; 1. 跨平台编译&#xff1a;CMake支援Windows&#xff0c;Mac OS&#xff0c;Linux等多种操作系統&#xff0c;且支援多数主流编译器如GCC&#xff0…

如何在 Elasticsearch 中使用 Openai Embedding 进行语义搜索

随着强大的 GPT 模型的出现&#xff0c;文本的语义提取得到了改进。 在本文中&#xff0c;我们将使用嵌入向量在文档中进行搜索&#xff0c;而不是使用关键字进行老式搜索。 什么是嵌入 - embedding&#xff1f; 在深度学习术语中&#xff0c;嵌入是文本或图像等内容的数字表示…

centos 7.9同时安装JDK1.8和openjdk11两个版本

1.使用的原因 在服务器上&#xff0c;有些情况因为有一些系统比较老&#xff0c;所以需要使用JDK8版本&#xff0c;但随着时间的发展&#xff0c;新的软件出来&#xff0c;一般都会使用比较新的JDK版本。所以就出现了我们标题的需求&#xff0c;一个系统内同时安装两个不同的版…

Bartende:Mac菜单栏图标管理软件

Bartender 是一款可以帮助用户更好地管理和组织菜单栏图标的 macOS 软件。它允许用户隐藏和重新排列菜单栏图标&#xff0c;从而减少混乱和杂乱。 以下是 Bartender 的主要特点&#xff1a; 菜单栏图标隐藏&#xff1a;Bartender 允许用户隐藏菜单栏图标&#xff0c;只在需要时…

【Vue】数据监视输入绑定

hello&#xff0c;我是小索奇&#xff0c;精心制作的Vue系列持续发放&#xff0c;涵盖大量的经验和示例&#xff0c;如有需要&#xff0c;可以收藏哈 本章给大家讲解的是数据监视&#xff0c;前面的章节已经更新完毕&#xff0c;后面的章节持续输出&#xff0c;有任何问题都可以…

孤举者难起,众行者易趋,openGauss 5.1.0版本正式发布!

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&am…

基于SSM的教师办公管理的设计与实现(有报告)。Javaee项目。

演示视频&#xff1a; 基于SSM的教师办公管理的设计与实现&#xff08;有报告&#xff09;。Javaee项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通过Spring S…

Selenium Webdriver自动化测试框架

最近正在编写selenium webdriver自动化框架&#xff0c;经过几天的努力&#xff0c;目前基本已经实现了一套即能满足数据驱动、又能满足Web关键字驱动的自动化框架&#xff08;主要基于 antjenkinstestngselenium webdriverjxl实现&#xff09;。通过这次的自动化框架开发&…

[CISCN2019 华北赛区 Day2 Web1]Hack World 布尔注入

正确的值 错误的值 我们首先fuzz一下 发现空格被过滤了 我们首先测试 (1)(1) (1)(2) 确定了是布尔注入了 我们写一下查询语句 (select(ascii(mid(flag,1,1))>1)from(flag))(select(ascii(mid(flag,1,1))102)from(flag)) 确定了f 开头 我们开始写脚本 import string …

喜获殊荣!迅镭激光获评“2023年苏州市质量奖”!

近日&#xff0c;苏州市质量奖评定委员会公示2023年苏州市质量奖评定结果&#xff0c;经过层层严格评审&#xff0c;迅镭激光从众多企业中脱颖而出&#xff0c;成功获评“苏州市质量奖”称号! 苏州市质量奖是苏州市政府设立&#xff0c;授予在经营质量上表现优秀的苏州企业的专…