详解预处理

全文目录

  • 前言
  • 预定义符号
  • `#define` 定义标识符常量
  • `#define` 定义宏
    • `#define` 替换规则
    • `#` 宏参数转换字符串
    • `##` 宏参数拼接
    • 带有副作用的宏参数
  • 宏与函数的对比
  • `#undef` 移出宏定义
  • 命令行定义
  • 条件编译
  • `#include` 文件包含
    • 头文件的包含方式
    • 头文件的重复包含

前言

前面我们学习了程序的编译和链接的大致流程,其中编译、汇编、链接太过深奥,只需要了解流程即可。但是预编译中的文本操作需要深入了解一下。

预定义符号

C语言中有内置的预定义符号包括main、关键字、库函数等。除了这些还有一些日志信息:

__func__ //当前调用的函数
__FILE__ //进行编译的源文件(绝对路径)
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义

这些符号可以很好的帮我们输出当前程序的日志信息,便于日后维护代码。

// demo
printf("file:%s\tline:%d \tdate:%s\ttime:%s\n", __FILE__, __LINE__, __DATE__, __TIME__);

#define 定义标识符常量

语法:

#define name stuff
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外
// 每行的后面都加一个反斜杠(续行符)。

在预编译阶段就会将文件中的全部name 替换成 stuff

#define结尾可以加上;,加上语句就是多了一条空语句,但是一般是不加上;,容易引发语法错误。

比如上面日志信息的输出就可以使用 #define 定义一个表示符,节省代码量(而且看起来很牛的样子)。

// demo#define DEBUG_PRINT printf("file:%s\tline:%d\t \date:%s\ttime:%s\n" ,\__FILE__,__LINE__ , \__DATE__,__TIME__ )
DEBUG_PRINT;

在VS2019中,可以通过以下设置将生成预处理的文件,但是设置后就不能运行程序了,在看完预处理文件后需要重置该设置

在这里插入图片描述

打开预处理的文件就是以下内容:

printf("file:%s\tline:%d\t date:%s\ttime:%s\n" , "D:\\code\\daily_code\\FlexibleArray\\FlexibleArray\\test.c",13 , "Apr 14 2023","09:13:00" );

#define 定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或 定义宏(define macro)。

定义语法:

#define name( parament-list ) stuff
//其中的parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。

注意:

参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

宏的语法和函数的语法十分相似,所以在定义宏和函数的时候尽量做到命名约定:

把宏名全部大写
函数名不要全部大写

//demo
#define MUL(a, b) a * b
int a = 5;
int b = 15;
int c = MUL(a, b);  // 75

这样看来宏是很简单的,那么再来看一组实例

//demo 
#define MUL(a, b) a * b
int a = 5;
int b = 15;
int c = MUL(a + 5, b + 5);  // 200 ?   85!

这是因为宏是不加任何处理,直接将参数替换成对应的值,运算符的优先级就会导致结果与预期的结果不一致。所以需要为宏的每一位参数加上括号

// 修正
#define MUL(a, b) (a) * (b)

再来看一组示例:

#define DOUBLE(x) (x) + (x)
int a = 5;
printf("%d\n" ,10 * DOUBLE(a));

看上去,好像打印100,但事实上打印的是55.

解决方法:将宏整体加上括号

// 修正
#define MUL(a, b) ((a) * (b))

总结:

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

#define 替换规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

注意:

  1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

# 宏参数转换字符串

# 的作用:

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

如果想在宏中让参数不被对应的值替换,直接使用参数名时,就可以使用 #

//demo
#define PRINT(n) printf(#n"'s value is %d\n", n)
int a = 10;
int b = 20;
PRINT(a); 	// a's value is 10
PRINT(b); 	// b's value is 20

如果想让宏能够打印不同类型的值,可以将宏的参数类型作为字符串进行打印

#define PRINT(FORMAT, VALUE) printf("the value is "FORMAT"\n", VALUE);PRINT("%d", 10);
PRINT("%lf", 3.100);

## 宏参数拼接

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

简单来说就是将两边的宏参数作为字符串拼接起来:

//demo
#define CAT(girl, friend) girl##friend
int grilfriend = 18;
printf("%d\n", CAT(girl, friend)); // 18

带有副作用的宏参数

所谓的带有副作用的参数:表达式求值的时候出现的永久性效果。

x+1;//不带副作用
x++;//带有副作用

MAX宏可以证明具有副作用的参数所引起的问题。

//demo 
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?

对于预处理之后的表达式:

z = ( (x++) > (y++) ? (x++) : (y++));

可以很容易得到输出结果:

x=6 y=10 z=9

所以我们在使用宏时,对于参数的选择需要小心谨慎

宏与函数的对比

宏因为是文本替换,所以适合用来做一些小型计算,一个MAX 就可以看出宏的优点:

#define MAX(a, b) ((a)>(b)?(a):(b))

宏的优点:

1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。

2. 更为重要的是函数的参数必须声明为特定的类型。 所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。

宏是类型无关的。 并且宏的参数可以出现类型

//demo
#define MALLOC(num, type) (type*)malloc(num * sizeof(type))

既然有优点就一定会有缺点,宏的缺点:

1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。

2. 宏是没法调试的。

3. 宏由于类型无关,也就不够严谨。

4. 宏可能会带来运算符优先级的问题和参数的副作用,导致程容易出现错。

总结对比:

在这里插入图片描述

#undef 移出宏定义

如果需要重定义一个宏,则需要用到#undef

#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除,否则会报警告

命令行定义

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

但是在一般的集成开发环境中这个好像不能不能使用,在Linux的gcc中可以使用,只需要在编译的时候加上 -D 选项即可。

#include <stdio.h>
int main()
{int array [ARRAY_SIZE];int i = 0;for(i = 0; i< ARRAY_SIZE; i ++){array[i] = i;}for(i = 0; i< ARRAY_SIZE; i ++){printf("%d " ,array[i]);}printf("\n" );
return 0;
}

编译指令:

gcc -D ARRAY_SIZE=10 test.c

条件编译

条件编译的使用方法跟if else语句相似,不同的是条件编译时由预处理器求值的,所以只能使用一些常量。

常见的条件编译指令:

1.
#if 常量表达式
//...
#endif//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif3.判断是否被定义
#if defined(symbol) // 是
#if !defined(symbol) // 否#ifdef symbol // 是
#ifndef symbol // 否4.嵌套指令
#if defined(OS_UNIX)#ifdef OPTION1unix_version_option1();#endif#ifdef OPTION2unix_version_option2();#endif
#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2();#endif
#endif

这一点在库文件中十分常见,对于跨平台的文件有着很强的实用性。

#include 文件包含

在编码过程的第一步通常是#include <stdio.h> 这样的文件包含,使得stdio.h 参与编译,在之前的预编译已经知道了,其实就是头文件的内容替换,预处理器先删除这条指令,并用包含文件的内容替换。如果一个文件被包含10次,就会替换10次。

头文件的包含方式

在编码中有两种包含头文件的方式:

一种是本地文件包含:

#include "test.h"

该方式的查找策略:

先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标 准位置查找头文件。
如果找不到就提示编译错误。

一种是库文件的包含:

查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。

库文件的包含当然也可以使用本地文件被包含方式,但是效率会更低一些,所以还是遵守规则好些。

头文件的重复包含

在正常的编码中肯定是很少出现头文件的重复包含,但是很多情况是在我们不经意间发生的, 如:

在这里插入图片描述

如果不加处理test.c 中就包含了两份comm.h。虽然没什么大问题,但是每一次重复包含都会造成代码冗余。

处理方法:

每个头文件的开头写:
1. 
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif //__TEST_H__2. 
#pragma once

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

尝试自主打造一个有限状态机(二)

前言 上一篇文章我们从理论角度去探索了状态机的定义、组成、作用以及设计&#xff0c;对状态机有了一个基本的认识&#xff0c;这么做有利于我们更好地去分析基于实际应用的状态机&#xff0c;以及在自主设计状态机时也能更加地有条不紊。本篇文章将从状态机的实际应用出发&am…

C# 使用NPOI操作EXCEL

1.添加NOPI 引用->管理NuGet程序包->添加NOPI 2.相关程序集 3.添加命名空间 using NPOI.HSSF; using NPOI.XSSF; using System.IO; using NPOI.XSSF.UserModel; using NPOI.HSSF.UserModel; 4.样例 //NPOI读入dgv private void button1_Click(object sender, EventArgs…

视频云存储/安防监控EasyCVR视频汇聚平台接入GB国标设备时,无法显示通道信息该如何解决?

安防视频监控/视频集中存储/云存储/磁盘阵列EasyCVR平台可拓展性强、视频能力灵活、部署轻快&#xff0c;可支持的主流标准协议有国标GB28181、RTSP/Onvif、RTMP等&#xff0c;以及支持厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等。平台既具备传统安…

Vue2-快速搭建pc端后台管理系统

一.推荐二次开发框架 vue-element-admin Star(84k)vue-antd-admin Star(3.5k) 二.vue-element-admin 官网链接:https://panjiachen.github.io/vue-element-admin-site/zh/ 我这里搭建的是基础模版vue-admin-template(推荐) # 克隆项目 git clone https://github.com/PanJi…

wazhu配置以及漏洞复现

目录 1.wazhu配置 进入官网下载 部署wazhu 修改网络适配器 重启 本地开启apache wazhu案例复现 前端页面 执行 1.wazhu配置 进入官网下载 Virtual Machine (OVA) - Installation alternatives (wazuh.com) 部署wazhu 修改网络适配器 重启 service network restart 本地…

C语言练习4(巩固提升)

C语言练习4 选择题 前言 面对复杂变化的世界&#xff0c;人类社会向何处去&#xff1f;亚洲前途在哪里&#xff1f;我认为&#xff0c;回答这些时代之问&#xff0c;我们要不畏浮云遮望眼&#xff0c;善于拨云见日&#xff0c;把握历史规律&#xff0c;认清世界大势。 选择题 …

程序的编译链接【编译链接大概步骤】

全文目录 &#x1f600; 前言&#x1f642; 翻译环境和执行环境&#x1f636; 编译和链接&#x1f635;‍&#x1f4ab; 预编译&#xff08;预处理&#xff09;&#x1f635;‍&#x1f4ab; 编译&#x1f635;‍&#x1f4ab; 汇编&#x1f635;‍&#x1f4ab; 链接 &#x1…

35、下载、安装 jdk11 记录,Idea中把项目从 jdk8 换 jdk 11

之前一直用jdk8&#xff0c;现在改成 11的试试看 登录官网下载这个11 https://www.oracle.com/cn/java/technologies/downloads/#java11-windows 下载jdk的oracle官网 需要自己注册oracle账户 修改环境变量的 JAVA_HOME Path 路径这里原本添加8的时候有了&#xff0c;不…

npm常用命令 + 前端常用的包管理工具 以及 npm淘宝镜像配置等

npm常用命令 前端常用的包管理工具 以及 npm淘宝镜像配置等 1. 前言1.1 NodeJs的下载安装1.2 windows上1.3 常用包管理工具 2. npm2.1 npm 的安装2.2 npm初始化包2.3 npm 安装、卸载包2.3.1 非全局安装2.3.1.1 单个包的安装2.3.1.1.1 默认版本安装2.3.1.1.2 指定版本安装 2.3.…

suricata初体验+wireshark流量分析

目录 一、suricata介绍 1.下载安装 2.如何使用-攻击模拟 二、wireshark流量分析 1.wireshark过滤器使用 2.wireshark其他使用 一、suricata介绍 1.下载安装 通过官网下载suricata&#xff0c;根据官网步骤进行安装。 官网地址&#xff1a; https://documentation.wazuh.…

WOFOST模型与PCSE模型应用

实现作物产量的准确估算对于农田生态系统响应全球变化、可持续发展、科学粮食政策制定、粮食安全维护都至关重要。传统的经验模型、光能利用率模型等估产模型原理简单&#xff0c;数据容易获取&#xff0c;但是作物生长发育非常复杂&#xff0c;中间涉及众多生理生化过程&#…

Python在电路课程中的应用

1 需求 课程中有大量的计算&#xff0c;电路方程、复数计算&#xff0c;之前都是用的MATLAB online&#xff0c;可现在要过期了&#xff0c;只能更换平台。 2 工具 https://www.online-python.com/ Python3 在线工具 | 菜鸟工具 (runoob.com) 3 Sinusoid 章节 涉及到复数计…