整理C语言预处理过程语法的实用方法与技巧

news/2024/11/17 17:42:45/文章来源:https://www.cnblogs.com/DSCL-ing/p/18197916

预处理

目录
  • 预处理
    • 一、宏定义
      • 数值宏常量
      • 字符串宏常量
      • 用define宏定义注释符号?
        • 程序的编译过程
        • 预处理中宏替换和去注释谁先谁后?
      • 如何写一个不会出现问题的宏函数
        • do-while-zero结构
        • do-while-zero的评价
      • 宏定义中的空格
      • 宏只能在main函数上面定义吗?
      • 宏的作用范围
      • #undef
        • 宏替换是在函数调用之前进行.
        • 块中进行#define和#undef需要谨慎
    • 二、条件编译 - 代码裁剪的工具
      • 为何要有条件编译
      • 条件编译都在那些地方用?
      • 见一见条件编译的代码
      • 宏是否被定义 vs 宏是否为真or假
      • 编译器也能够自动帮你加上宏
        • GCC
        • VS2023-VS2019
      • #ifdef/#ifndef
      • #if
        • 条件编译容易疏忽的地方
      • 让#if和#ifdef/#ifndef完全一样
      • 条件编译也支持嵌套
      • 一个使用#if defined能起到很好优化的用法
    • 三、举例一些的宏和预处理指令
      • #line
      • #error
      • #pragma
        • Message参数
        • #warning
    • 四、#和##
      • 前置:相邻字符串具有自动连接特性
      • #运算符
        • #运算符的功能:在宏定义中,将宏参数转化成字符串
        • 用法举例:
        • 使用场景:
      • ##运算符
        • 功能:
        • 用法举例:
    • 五、留言


一、宏定义


数值宏常量

#define 宏定义是个演技非常高超的替身演员,但也会经常耍大牌的,所以我们用它要慎之又慎。它可以出现在代码的任何地方,从本行宏定义开始,以后的代码就都认识这个宏了;也可以把任何东西定义成宏。因为编译器会在预编译的时候用真身替换替身,而在我们的代码里面却又用常常用替身来帮忙。看例子:

#define PI 3.141592654

在此后的代码中尽可以使用 PI 来代替 3.141592654,而且你最好就这么做。不然的话,如果我要把PI的精度再提高一些,你是否愿意一个一个的去修改这串数呢?你能保证不漏不出错?而使用 PI 的话,我们却只需要修改一次。这种情况还不是最要命的,我们再看一个例子:

#define ERROR_POWEROFF -1

如果你在代码里不用 ERROR_POWEROFF 这个宏而将-1硬编码进代码里,尤其在函数返回错误代码的时候(往往一个开发一个系统需要定义很多错误代码)。肯怕上帝都无法知道-1 表示的是什么意思吧。这个-1,我们一般称为“魔鬼数”,上帝遇到它也会发狂的。所以,我奉劝代码里一定不要出现“魔鬼数”。

关键字篇我们讨论了 const 这个关键字,我们知道const 修饰的数据是有类型的,而 define 宏定义的数据没有类型。为了安全,我建议以后在定义一些宏常数的时候用 const 代替,编译器会给 const 修饰的只读变量做类型校验,减少错误的可能。但一定要注意const修饰的不是常量而是readonly的变量,const 修饰的只读变量不能用来作为定义数组的维数,也不能放在 case 关键字后面。


字符串宏常量

举例:

#define ENG_PATH_4 "E:\\English\\listen_to_this\\listen_to_this_3"


用define宏定义注释符号?

能否使用宏定义的注释来注释代码?

#define COMMENT //
int main()
{COMMENT puts("hello");
}

第一眼看这个代码可能会搞不清程序是否执行打印,这个问题的解决我们需要知道预处理过程各步骤的执行顺序,

先看看程序预处理过程做了什么.


程序的编译过程

预处理: 预处理指令,头文件展开,去掉注释,宏替换,条件编译 (顺序是怎样的?)

编译: C语言翻译成汇编语言

汇编: 将汇编代码转化成可重定向目标文件(可被链接)

链接: 自身程序+库文件进行关联,形成可执行程序


预处理中宏替换和去注释谁先谁后?

生成的预处理结果如图:

image-20240506124541340

观察结果,如果宏替换先于去注释,则puts代码一定是被去掉的,显然puts还在,说明先去注释,再宏替换;

既然是先去注释再宏替换,那为什么预处理后却没有发现puts前面带双斜杠呢? 这就很尴尬了,其实在#define COMMENT //处的双斜杠在编译前就被识别成注释了,去掉注释后代码就变成了#define COMMET这样子,是一个仅仅用于标识的宏.

预处理指令和宏谁先处理是不可预期的.

总之,通过这点我们知道了预处理过程去注释是先于宏替换的.

上面说的是C++风格的注释,那C风格的注释呢

#define BSC //
#define BMC /*
#define EMC */BSC: Begin Single-line Comment
BMC: Begin Multi-line Comment
EMC: End   Multi-line Comment

image-20240506131827190

这就很明显了,如果有语法提示则很容易看出来,和上面所说的C++风格注释的情况是一样的原理.



如何写一个不会出现问题的宏函数

我们知道,一般的宏函数是很容易出现问题的,比如说少加了括号,因为结合性问题导致代码逻辑没有按照预期来执行...,那怎样写出一个健壮性很高的宏函数呢? 先看一个例子:

image-20240506212559805

如果我定义这样一个宏函数,并且按照一般函数的方式运用,显然不是能通过语法检查的.看一下预处理后的代码

image-20240506213807477

可以发现a = 0;已经算一条语句了,后面b = 0;;多出来,不符合语法,因此报错.


if在不带花括号的条件下只能且必须带一条语句.如果想用这条宏函数,只能将它写进if的花括号中.但是,这样的代码是不友好的,它变相的强迫用户必须带上花括号,显然不是一种很好的方式.

既然要求if分支有多条语句需要执行时必须加上花括号,那能不能直接在宏函数中加上花括号? 看一下效果

image-20240506214720101

再看一下预处理后的代码

image-20240506214859943

可以发现if花括号后面还带上了分号,这显然也不够好.

上面各种方式都是有大大小小的缺陷. 那还有没有更好的方案? 有的,最终解决方案:使用do-while-zero结构


do-while-zero结构

image-20240506222258461

看预处理后的代码:

image-20240506224304956

可以发现,在do-while-zero结构中,do后面有花括号,可以封装任意多条语句.while(0)后可以接上分号,并且while(0)是条件判定为假,结束执行循环,整体上只执行一次且必须执行一次.用法上和普通函数有类似的效果,因此具有普适性.


do-while-zero的评价

do-while-zero结构是一个编码技巧,作为一个宏函数技巧,我们可以了解一下,虽然不一定会使用它.在早些年的项目中也有很多使用的,掌握它后至少我们在看源码时可以在遇到这样子的宏函数时可以知道写的是什么...


宏定义中的空格

#include <stdio.h>#define INC(a) ((a)++) //定义宏函数不能带空格int main()
{   int i = 0;   INC (i); //使用可以带空格,但是严重不推荐   printf("%d\n", i);
}

宏只能在main函数上面定义吗?

先说结论:宏可以在源文件的任何地方定义.

验证,在main函数中定义:

image-20240507152543768

在普通函数中定义:

image-20240507152828624

在普通函数中定义,在main函数中使用:

image-20240507153009444

说明:宏定义与是否在函数体外内没有任何关系

结论:源文件的任何地方,宏都可以定义,与是否在函数内外无关.


宏的作用范围

注意:宏只在从它定义的位置开始生效.从定义开始,往后都是有效的.

不正确例子:

image-20240507153403677


#undef

#undef的作用是取消宏,限定宏的范围.

image-20240507155829310

宏替换是在函数调用之前进行.

看下面一段代码:

image-20240507162514979

#undef在函数调用的上边,这样的代码看着会有些绕.看一下运行结果:

image-20240507162655612

这是可以通过的,因为宏替换是在函数调用之前进行.这样的代码需要熟悉预编译指令的执行顺序才容易阅读.


块中进行#define和#undef需要谨慎

C语言中,尽管在代码文件中的任何位置放置#define或者#undef是合法的,但把它们放在块中会使人误解为它们只存在于块作用域,给人一种只在函数内有效的错觉.

也不排除我们只想让它在局部范围内有效,因此使用时需要慎重考虑.


二、条件编译 - 代码裁剪的工具

为何要有条件编译

条件编译主要是用于代码裁剪,通过代码裁剪,能够快速实现某种目的,如版本维护(free版本,pro版本等,功能裁剪,跨平台性等.


条件编译都在那些地方用?

举个例子

我们经常听说过,某某版程序是完全版/精简版,某某版应用是商用版/校园版,某某软件是基础版/扩展版等。

其实这些软件在公司内部都是同一个项目,是多个源文件构成的。所以,所谓的不同版本,其实就是那些功能的有无;在技术层面上,公司为了好维护,可以维护多种版本;如果是使用条件编译,想使用哪个版本,就使用哪种条件进行裁剪就行。

如著名的Linux内核,功能上也是使用条件编译进行功能裁剪的,来满足不同平台的软件。

见一见条件编译的代码

int main()
{
#ifndef DEBUG   printf("hello debug\n");
#elif RELEASE   printf("hello release\n");
#else printf("hello unknow\n");
#endif   return 0;
}

宏是否被定义 vs 宏是否为真or假

#define DEBUG    // 宏被定义#define DEBUG 1  // 宏被定义,且值为真#define DEBUG 0  // 宏被定义,且值为假

宏为真假是在宏被定义之上的.


编译器也能够自动帮你加上宏

GCC
语法:gcc 源文件  -D 宏=值
#   gcc test.c -D MACRO=1 
VS2023-VS2019

image-20240508153946278

image-20240508154007078

在vs平台上用的不多.



#ifdef/#ifndef

#ifdef/#ifndef用于检测宏是否被定义,有没有值,是真是假不重要

#ifdef 检测宏是否已经定义,是则保留,否则裁剪;#ifndef则相反

用法举例:

image-20240507171846307

#ifdef/#ifndef一般只在头文件中使用


#if

#if的默认用法和#ifdef有一定区别,其他用法差不多,#if使用更频繁.

区别是#if如果定义了宏则要求必须要有值,没定义则当作假或者else.


条件编译容易疏忽的地方

使用#if或#ifdef时,很容易会忘记写#endif.因为我们平常写if-else没有这个end,很容易会类比忘记掉#endif.所以在使用条件编译时,先把#if - #endif写上,后面就不再容易遗漏了.



让#if和#ifdef/#ifndef完全一样

#if模拟#ifdef:

#define MACRO
int main()
{
#if defined(MACRO)puts("MACRO defined!");
#elseputs("MACRO undefined!");
#endifreturn 0;
}

程序运行结果:

image-20240508160951829

如果是未定义呢? 没有别的名词,加个逻辑反就好啦

image-20240508170933763


条件编译也支持嵌套

#include<stdio.h>
#include<math.h>#define C    
#define CPP    int main()     
{             
#if defined(C)    #if defined(CPP)    puts("hello CPP");    #endif                  puts("hello C");    
#else                 puts("hello other");    
#endif                    return 0;    
}  

image-20240508173148156

注释掉#define C

image-20240508173245666

可以证明,条件编译是支持嵌套的.

不过,使用嵌套的代码阅读体验是比较差的,一般不建议使用嵌套,下面还有其他更好的代码写法推荐.



一个使用#if defined能起到很好优化的用法

[引用](C语言#if defined高级用法-CSDN博客)

在一个需要完成“多个宏定义来共同控制同一代码分支”的情况下,例如

  • TEST_1 或 TEST_2被定义,则选择执行1,2
#ifndef TEST_1
#define TEST_1
#endif#ifdef TEST_1puts("1");
#else#ifdef TEST_2puts("1");#elseputs("2");#endif
#endif
  • 或者, TEST_1和TEST_2均未定义,则选择执行1,否则执行2
#ifndef TEST_1puts("1");
#else#ifndef TEST_2puts("1");#elseputs("2");#endif
#endif

这样的代码看起来是比较冗余的,不好阅读,因为#ifdef是没有对应的"else if",我们只能采用这样的方式写.对比到一般使用的if-else,if()内可以是一个表达式,那#ifdef能否也能将宏定义组织成表达式呢?

看一下代码

#ifdef TEST_1 || TEST_2puts("1");
#elseputs("2");
#endif

这样的代码看起来是更简洁,更优雅.但它是错误的.

image-20240508163628082

因为ifdef和ifndef仅能跟一个宏定义参数,而不能使用表达式


虽然在vs下可以运行

image-20240508163859587

但是我们不推荐这样不能跨平台的代码.


因为#if需要判断真假而具有计算表达式的功能,

因此,使用#if defined#if !defined更好的选择.

  • TEST_1 或 TEST_2被定义,则选择执行1,否则执行2
#if defined TEST_1 || defined TEST_2puts("1");
#elseputs("2");
#endif
  • TEST_1 或 TEST_2未被定义,则选择执行1,否则执行2
#if !defined TEST_1 || !defined TEST_2puts("1");
#elseputs("2");
#endif


三、举例一些的宏和预处理指令

ANSI标准的5个预定义宏

__FILE__:当前文件名

__LINE__:所在行号

__STDC__:当编译器遵循ANSI C标准时该宏被定义为1。

__TIME__:表示当前源代码被编译的时间字符串。

__DATE__:表示当前源代码被编译的日期字符串。


#line

可以定制化你的文件名称和代码行号,很少使用

image-20240508220935484


#error

#error命令是C/C++语言的预处理命令之一,当预处理器预处理到#error命令时将停止编译并输出用户自定义的错误消息。

image-20240508221706882

用于人为阻止编译,在某些情况下得知编译条件不满足时,可以使用#error让编译停止.


#pragma

#pragma 指令很复杂,需要使用的时候再查一下#pragma_百度百科 (baidu.com).

Message参数

Message 参数能够在编译信息输出窗口中输出相应的信息,这对于源代码信息的控制是非常重要的。其使用方法为:

#pragma message("消息文本")

当编译器遇到这条指令时就在编译输出窗口中将消息文本打印出来。

image-20240508222436848

也可以用于在编译器检测某个宏是否被定义:

image-20240508223051592

#warning
#pragma warning(disable:4996;once:4385;error:164)

等价于:

1#pragma warning(disable:4996)//不显示4996警告信息 
2#pragma warning(once:4385)//4385号警告信息仅报告一次 
3#pragma warning(error:164)//把164号警告信息作为一个错误。

还有很多用法..,可以查下文档

#pragma_百度百科 (baidu.com)

四、#和##

前置:相邻字符串具有自动连接特性

C语言两个相邻的字符串能够自动拼接成一个字符串.

int main()
{puts("hello"" world");const char *str = "hello"" world\n";printf(str);return 0;
}

image-20240509133454682


#运算符

#运算符的功能:在宏定义中,将宏参数转化成字符串

常搭配字符串连接特性一起使用

用法举例:
#define STR(s) #sint main()
{printf("PI: "STR(3.1415926)"\n");return 0;
}

image-20240509141118633

使用场景:
#define SQR(x) printf("The square of x is %d.\n", ((x)*(x)));int main()
{SQR(8);return 0;
}

image-20240509152413609

注意到没有,引号中的字符 x 被当作普通文本来处理,而不是被当作一个可以被替换的语言
符号。
假如你确实希望在字符串中包含宏参数,那我们就可以使用“#”,它可以把语言符号转
化为字符串。上面的例子改一改:

#define SQR(x) printf("The square of "#x" is %d.\n", ((x)*(x)));
再使用

SQR(8);
则输出的是:
image-20240509152653034


##运算符

功能:

​ 将宏参数与特定的符号组合成一个全新的符号

用法举例:

image-20240509151828264


五、留言

  有不足的地方欢迎大家评论区留言指正。

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

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

相关文章

如何把多个文件(夹)平均复制到多个文件夹中

首先,需要用到的这个工具:度娘网盘 提取码:qwu2 蓝奏云 提取码:2r1z 假定的情况是,共有20个兔兔的图片,想要平均的复制4个文件夹里,那么每个文件夹里面就有5个图片(如果是5个,那每个自然是4个,具体除数是多少,根据实际情况即可)打开工具,切换到 文件批量复制 版块…

任意文件上传漏洞详解

当文件上传接口可以上传任意文件,但是不解析,文件上传后的路径可控。这种情况下有两种方法1、上传.htaccess和.user.ini配置文件。2、当知道网站根路径的情况下,可以上传到其他目录下。3、当不知道网站根路径的情况下,可以通过上传计划任务的方式实现命令执行。 文件上传漏…

ray tracing in one weekend - 5

dielectric 水、玻璃、钻石等透明材料都是电介质。当光线照射到它们身上时,它会分裂成反射光线和折射(透射)光线。我们将通过在反射和折射之间随机选择来处理这个问题,每次相互作用只产生一个散射射线。 折射程度 : 是根据两个介质折射率的差值决定的。 Refraction Snells L…

如何把多个文件(夹)随机复制到多个文件夹中

首先,需要用到的这个工具:度娘网盘 提取码:qwu2 蓝奏云 提取码:2r1z 先看文件的情况一共20个兔兔的图片,4个文件夹,把全部的图片随机的复制这些地方去打开工具,切换到 文件批量复制 版块找到右下角的 设置,点击打开勾选“随机复制”,把文件进行随机的复制选中全部的兔…

php 异步并行后续--兼容FPM使用的组件

上次给人推荐了这篇文章,关于PHP异步并行的文章,之后有人评论问这个组件能不能给fpm用,我测试了一下发现不行,于是又找到一个可以给fpm用的http请求组件. 安装很简单,就这样 composer require guzzlehttp/guzzle 进行安装一下. 然后代码示例如下: 我们先建一个文件作为一个长时…

DeepSparse: 通过剪枝和稀疏预训练,在不损失精度的情况下减少70%的模型大小,提升三倍速度

这篇论文提出了一种高稀疏性基础大型语言模型(LLMs)的新方法,通过有效的预训练和部署,实现了模型在保持高准确度的同时,显著提升了处理速度。https://avoid.overfit.cn/post/06961c02a12b48a6a3e1436b527fd2b7

从开发到部署,搭建离线私有大模型知识库

背景介绍 最近一段时间搭建了一套完整的私有大模型知识库,目前完整的服务已经完成测试部署上线。基本之前的实践过程,从工程角度整理技术方案以及中间碰到的一些问题,方便后续对这个方向有需求的研发同学们。 为什么做离线私有化部署 在大模型火热起来之后,很多企业都有尝试…

自媒体基础

自媒体:个人或者个人组织进行专业化,持续化的内容创作,并以此为盈利的方式 做自媒体的原因:盈利,个人品牌打造,进行企业品牌宣传 自媒体盈利: 1.平台分成

AJ-Report开源数据大屏存在远程命令执行漏洞

漏洞描述: 该平台可以通过post方式在validationRules参数对应值中进行命令执行,可以获得服务器权限,登陆管理后台接管大屏。如果被不法分子利用,书写反动标语,危害后果十分严重 Fofa: title="AJ-Report"POC: POST /dataSetParam/verification;swagger-ui/ HTTP/…

pde复习 第一章波动方程 第四节 高维波动方程的Cauchy问题

2024-05-18 16:14:50 星期六 知识点梳理 本节讨论的是高维波动方程,主要是计算\(\star\)公式为\(\star\)公式一定要记清,下面给出一些例题,动手计算。 例题 阅读顺序从左到右再下一行。评注:上面的两个例题的所有解法都值得认真看,还有里面的技巧(三角函数的周期性和正交…

[ABC354D]

https://www.luogu.com.cn/problem/AT_abc354_d https://atcoder.jp/contests/abc354/tasks/abc354_d 由图片可知,很显然每个 \(4\times 2\)​ 网格(称为单位网格)都是全等的。 为了方便,将 \(A,B,C,D\) 都增加 \(10^9\),因为 \(10^9\bmod 4=10^9\bmod 2=0\),所以图形没有…

实验二 电子传输系统安全-进展1

上周任务完成情况(代码链接,所写文档等将电子公文传输系统重新调试通过,并运行 小组成员每个人读完《Core.Software.Security.Security.at.the.Source.CN.软件安全.从源头开始》和《The.Security.Development.Lifecycle.CN.软件安全开发生命周期》,并写好相应的读书笔记 熟…