深入剖析预处理

news2024/11/14 19:32:06

目录

1.预定义符号

2.#define 定义常量

3.#define定义宏

4.带有副作用的宏参数

5.宏替换的规则

6.宏函数的对比

7.#和##

7.1 #运算符

7.2 ## 运算符

8.命名约定

9.#undef

10.命令行定义

11.条件编译

12.头文件的包含

12.1 头文件被包含的方式:

12.1.1 本地文件包含

12.1.2 库文件包含

12.2 嵌套⽂件包含


1.预定义符号

C语言设置了⼀些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的。

__FILE__ *//**进⾏编译的源⽂件*
__LINE__ *//**⽂件当前的⾏号*
__DATE__ *//**⽂件被编译的⽇期*
__TIME__ *//**⽂件被编译的时间*
__STDC__ *//**如果编译器遵循**ANSI C**,其值为**1**,否则未定义

可以将系统的时间日期等等输出

#define _CRT_SECURE_NO_WARNINGS 1 
#include <stdio.h>
int main() {
    printf("%s\n", __FILE__);
    printf("%d\n", __LINE__);
    printf("%s\n", __DATE__);
    printf("%s\n", __TIME__);
    return 0;
}
//输出
/*
E:\C\Code\Cproject\practice7.8\practice7.8\ex01.c
5
Jul  8 2024
10:23:503*/

2.#define 定义常量

基本语法:

#define name--名字 stuff--内容
 

在预处理的时候,定义的变量会将下面执行的变量替换

举个例子:

#define M 100;
#define reg register //因为resister太长了,用reg替换更方便
#define DEBUG_PRINT printf("file:%s\tline:%d\t\date:% s\ttime:% s\n" \
 __FILE__,__LINE__ , \
 __DATE__,__TIME__ )      // \是续航符号 相当于一个回车
int main() {
    int a = M;  //这里的M被替换成100
    reg int num = 0; //寄存器变量 reg替换register
    printf("%d", a);
}

思考:在define定义标识符的时候,要不要在最后加上 ; ?

比如:

#define MAX 1000;
#define MAX 1000

建议不要加上 ; ,这样容易导致问题。

比如下面的场景:

if(condition)
 max = MAX;
else
 max = 0;

如果是加了分号的情况,等替换后,if和else之间就是2条语句,而没有⼤括号的时候,if后边只能有⼀条语句。这⾥会出现语法错误。

3.#define定义宏

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

下面是宏的申明方式:

#define name( parament-list ) stuff

其中的 parament-list 是⼀个由逗号隔开的符号表,它们可能出现在stuff中。

注意:

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

举例:

#define SQUARE( x ) x * x

这个宏接收⼀个参数 x .如果在上述声明之后,你把 SQUARE( 5 ); 置于程序中,预处理器就会用下面这个表达式替换上面的表达式:

#define s(n) n*n;
int main() {
    int i = 0;
    scanf("%d", &i);
    int ret = s(i);  //s(i) 就会替换成 i*i 
    printf("%d", ret);
}

如果将s(i) 中的i 变成 5+1 他会输出什么呢? 答案是 11 因为他是直接替换n 变成 5+1 * 5+1 =11

#define s(n) n*n;
int main() {
    int i = 0;
    int ret = s(5+1);  //s(i) 就会替换成 i*i 
    printf("%d", ret);
}

如果真的想算出36 那就写成 (n)*(n) 加一个括号

#define s(n) (n)*(n);
int main() {
    int i = 0;
    int ret = s(5+1);  //s(i) 就会替换成 i*i 
    printf("%d", ret);
}

写一个宏算一个数的二倍 --->写宏的时候一定不要吝啬括号,在适当的位置加上括号

#define Double(n) (2*n);
​
int main() {
    int ret = 10 * Double(5);
    printf("%d", ret);
}

4.带有副作用的宏参数

当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可能出现危险,导致不可预测的后果。副作⽤就是表达式求值的时候出现的永久性效果。

例如:

x+1;*//**不带副作⽤
​
x++;*//**带有副作⽤

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

(a++)>(b++)?a++(不执行):b++执行变成14

最后a的值为11 b的值为14 ,所以不要使用带有副作用的参数 会出现预想不到的结果

#define MAX(x,y) ((x)>(y)?(x):(y))
int main() {
    int a = 10;
    int b = 12;
    int ret = MAX(a++, b++);
    // (a++)>(b++)?a++(不执行):b++执行变成14 
    printf("%d\n", ret);
    printf("a = %d b = %d", a, b);
}

5.宏替换的规则

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

  1. 在调⽤宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先先被替换。

  2. 替换文本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换。

  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

看如下代码,M首先被替换为100 然后再执行MAX宏

#define M 100
#define MAX(x,y) ((x)>(y)?(x):(y))
int main() {
    int ret = MAX(M, 15);  //M首先被替换为100 然后再执行MAX宏
    printf("%d\n", ret);
    
}

注意:

  1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。

  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

下面的字符串常量不会被替换

printf("MAX(M,15)");

6.宏函数的对比

宏通常被应⽤于执⾏简单的运算。

比如在两个数中找出较⼤的⼀个时,写成下面的宏,更有优势⼀些。

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

那为什么不用函数来完成这个任务?

原因有:

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

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

和函数相比宏的劣势:

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

  2. 宏是没法调试的。

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

  4. 宏可能会带来运算符优先级的问题,导致程序容易出现错。

宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。

这两句代码实现的效果一样

int* p = (int*)malloc(10 * sizeof(int)); int* p = MALLOC(10, int);

#define MALLOC(n,type) (type*)malloc(n*sizeof(type))
​
int main() {
    int* p = (int*)malloc(10 * sizeof(int));
    int* p = MALLOC(10, int); //宏的参数可以是类型
}
  • 宏和函数的对比

7.#和##

7.1 #运算符

#运算符将宏的⼀个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。

#运算符所执行的操作可以理解为”字符串化“。

当我们有⼀个变量 int a = 10; 的时候,我们想打印出: the value of a is 10 .

就可以写:

#define PRINT(n) printf("the value of "#n " is %d", n);

当我们按照下⾯的⽅式调⽤的时候:

PRINT(a);//当我们把a替换到宏的体内时,就出现了#a,⽽#a就是转换为"a",时⼀个字符串代码就会被预处理为:

 printf("the value of ""a" " is %d", a);

这个只适用于整形,我们再完善一下参数,使得该宏可以输出所有类型

#define Print(format,n) printf("the value of " #n" is " format "\n",n)
int main() {
    int a = 10;
    Print("%d ",a);
    
    float f = 5.33;
    Print("%f", f);
}

7.2 ## 运算符

## 可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符。 ## 被称为记号粘合

这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。

这⾥我们想想,写⼀个函数求2个数的较⼤值的时候,不同的数据类型就得写不同的函数。

⽐如:这样太繁琐了

int int_max(int x, int y)
{
 return x>y?x:y;
}
float float_max(float x, float y)
{
 return x>yx:y;
}

我们使用宏定义模板函数

//##会将type和_max整合成一个字符
//生成函数的模板
#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{ \
 return (x>y?x:y); \
}
//使用上面的模板定义函数
GENERIC_MAX(int); //传一个整形类型进去 宏中定义的函数的类型全被替换成int
​
int main() {
    printf("%d", int_max(3, 5));
}

8.命名约定

⼀般来讲函数的宏的使⽤语法很相似。所以语言本⾝没法帮我们区分⼆者。

那我们平时的⼀个习惯是:

  • 把宏名全部大写

  • 函数名不要全部大写

9.#undef

这条指令用于移除⼀个宏定义。

#undef NAME*//**如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除。

10.命令行定义

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

例如:当我们根据同⼀个源⽂件要编译出⼀个程序的不同版本的时候,这个特性有点⽤处。(假定某个程序中声明了⼀个某个⻓度的数组,如果机器内存有限,我们需要⼀个很⼩的数组,但是另外⼀个机器内存⼤些,我们需要⼀个数组能够大些。)

#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;
}

编译指令:

//linux* *环境演⽰*
gcc -D ARRAY_SIZE=10 programe.c

11.条件编译

在编译⼀个程序的时候我们如果要将⼀条语句(⼀组语句)编译或者放弃是很⽅便的。因为我们有条件编译指令。--->用于跨平台性代码编译

调试性的代码,删除可惜,保留⼜碍事,所以我们可以选择性的编译。

我们看如下代码,M是一个常量等于2 那就执行输出 如果不等于2就不执行

#define M 2;
int main() {
#if M==2;
    printf("执行这句代码"); 
#endif
}

注意::一定是常量,而不是一个变量,下面a是一个变量(变量创建是运行时才创建,现在的阶段是预处理),所以不行,输出语句也不执行

#define M 2;
int main() {
    int a = 2;
#if a==2;
    printf("执行这句代码");
#endif
}

常见的条件编译指令:

1.\#if 常量表达式
 *//...*
\#endif
*//**常量表达式由预处理器求值。*
如:
\#define __DEBUG__ 1
​
\#if __DEBUG__
 *//..*
\#endif
2.多个分⽀的条件编译
\#if 常量表达式
 *//...*
\#elif 常量表达式
 *//...*
\#else
 *//...*
\#endif
3.判断是否被定义
\#if defined(symbol)
    
\#ifdef symbol
    
\#if !defined(symbol)
    
\#ifndef symbol
4.嵌套指令
int main() {
     #if defined(OS_UNIX) //先判断这个宏是否存在
        
     #ifdef OPTION1  //存在就进入
         unix_version_option1();
     #endif
         #ifdef OPTION2
         unix_version_option2();
     #endif
         #elif defined(OS_MSDOS)
         #ifdef OPTION2
         msdos_version_option2();
     #endif
         #endif
}

12.头文件的包含

12.1 头文件被包含的方式:

12.1.1 本地文件包含

#include "filename"

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

Linux环境的标准头文件的路径:

/usr/include

VS环境的标准头文件的路径:

C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
//这是VS2013的默认路径

注意按照自己的安装路径去找。

12.1.2 库文件包含

 #include <filename.h>

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

我们要根据实际情况去选择哪一种方法,而不是一味的使用" ",

12.2 嵌套⽂件包含

我们已经知道, #include 指令可以使另外⼀个⽂件被编译。就像它实际出现于 #include 指令的地方⼀样。

这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。

⼀个头文件被包含10次,那就实际被编译10次,如果重复包含,对编译的压力就比较⼤。

#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
 
 return 0;
}

如果直接这样写,test.c文件中将test.h包含5次,那么test.h文件的内容将会被拷贝5份在test.c中。如果test.h 文件很大,这样预处理后代码量会剧增。如果工程较大,有公共使⽤的头文件,被⼤家都能使用,⼜不做任何的处理,那么后果真的不堪设想。

我们可以使用条件编译去解决这个问题,该执行的逻辑为,首先判断该头文件是否被引入,没有就引入执行引入,当第二次被引入时,判断为假不在执行引入;

#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容放里面....
#endif

或者用一句,即可

#pragma once

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

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

相关文章

C++ 语法习题(2)

第三讲 循环语句 1.偶数 编写一个程序&#xff0c;输出 1 到 100之间&#xff08;包括 1 和 100&#xff09;的全部偶数。 输入格式 无输入。 输出格式 输出全部偶数&#xff0c;每个偶数占一行。 输入样例 No input输出样例 2 4 6 ... 100 参考代码: #include <i…

玩具营销是如何拿捏成年人钱包?

好像现在的成年人逐渐热衷于偏向年轻化&#xff0c;问问题会好奇“尊嘟假嘟”&#xff0c;饭量上的“儿童套餐”&#xff0c;娃娃机前排长队......而最突出的莫过于各类各式的玩具不断收割当代年轻人&#xff0c;除去常给大朋友们小朋友们送去玩具福利的“麦、肯”双门&#xf…

数据分析中excel常用函数总结

1、相对引用&#xff0c;绝对引用和混合引用 相对引用&#xff1a;比如求和&#xff0c;当计算出一个求和值&#xff0c;其他的就可以向下填充&#xff0c;此时求和引用的行列会随着求和区域的行列变化而变化。 绝对引用&#xff1a; 对某一个引用区域进行锁定&#xff0c;让…

华为防火墙 拓扑搭建1

拓扑图 要求 1.DMZ区内的服务器&#xff0c;生产区仅能在办公时间内&#xff08;9&#xff1a;00-18&#xff1a;00&#xff09;可以访问&#xff0c;办公区设备全天可以访问 配置安全策略 设置办公时间 2.生产区不允许访问互联网&#xff0c;办公区和游客区允许访问互联网…

【Mongodb-03】亿级数据从mysql迁移到mongodb辛酸历程

亿级数据从mysql迁移到mongodb辛酸历程 一&#xff0c;亿级数据从mysql迁移到mongodb辛酸历程1&#xff0c;核心业务和前期实现2&#xff0c;分库分表考虑3&#xff0c;nosql的选择4&#xff0c;mongodb服务器购买or自己搭建5&#xff0c;数据从mysq迁移到mongodb5.1&#xff0…

算法力扣刷题记录 三十八【二叉树的层次遍历应用一及二叉树构建】

前言 二叉树层序遍历应用题目。 记录三十八 【二叉树的层次遍历应用一】 继续。 一、【107.二叉树的层次遍历 II】 题目 给你二叉树的根节点 root &#xff0c;返回其节点值 自底向上的层序遍历 。 &#xff08;即按从叶子节点所在层到根节点所在的层&#xff0c;逐层从左向…

如何解决英国Facebook直播网络延时问题?

许多商家在英国进行Facebook直播&#xff0c;但网络延时和卡顿问题常常困扰着用户。这不仅影响观众的观看体验&#xff0c;也会给商家带来巨大损失。本文将探讨解决英国Facebook直播网络延时和卡顿问题的方案&#xff0c;以促进业务发展并提升用户满意度。 海外直播的挑战 海外…

3GPP R18 Multi-USIM 是怎么回事?(三)

这篇内容相对来说都是一些死规定,比较枯燥。主要是与MUSIM feature相关的mobility and periodic registration和service request触发过程的一些规定,两部分的内容是有部分重叠的,为保证完整性,重复部分也从24.501中摘了出来。 24.501 4.25 网络和MUSIM UE可以支持MUSIM fe…

优劣分析:重启路由器 vs 使用IP代理

目前更换IP主要有两种常见方法&#xff0c;一种是重启路由器&#xff0c;另一种是使用代理IP&#xff0c;那么&#xff0c;这两种方法有什么优缺点呢&#xff1f;下面我们一起来探讨一下。 方法一&#xff1a;重启路由器变换IP 优点 1. 操作简单&#xff1a;只需断开路由器电…

头歌资源库(22)求组合数

一、 问题描述 二、算法思想 动态规划是一种将问题分解成子问题并保存子问题解以避免重复计算的算法思想。 对于组合数C(n, r)&#xff0c;我们可以将问题分解成子问题C(i, j)&#xff0c;其中i表示从1到n的选择数&#xff0c;j表示选择的元素个数。则C(n, r)可以通过求解C(i…

多标签问题

一、多标签问题与单标签问题的区别&#xff1a; 多标签问题是单标签问题的推广。 举个例子&#xff0c;同时识别图片中的小汽车&#xff0c;公交车&#xff0c;行人时&#xff0c;标签值有三个&#xff1a;小汽车&#xff0c;公交车&#xff0c;行人。 单标签问题仅对一个标签…

ASP.NET MVC Lock锁的测试

思路&#xff1a;我们让后台Thread.Sleep一段时间&#xff0c;来模拟一个耗时操作&#xff0c;而这个时间可以由前台提供。 我们开启两个或以上的页面&#xff0c;第一个耗时5秒(提交5000)&#xff0c;第二个耗时1秒(提交1000)。 期望的测试结果&#xff1a; 不加Lock锁&…

喝酒骰子夜店手灯轮盘扫雷鳄鱼拆弹你演我猜小游戏流量主小程序开源版开发

喝酒骰子夜店手灯轮盘扫雷鳄鱼拆弹你演我猜小游戏流量主小程序开源版开发 喝酒摇骰子、轮盘、扫雷大战、夜店手灯、鳄鱼拔牙、喝酒大叔、指尖光环、拆弹英雄、幸运转转转、你演我猜、眼疾手快、占领方块、你演我猜。 喝酒骰子类小程序通常包含多种互动游戏和娱乐功能&#xf…

音视频质量评判标准

一、实时通信延时指标 通过图中表格可以看到&#xff0c;如果端到端延迟在200ms以内&#xff0c;说明整个通话是优质的&#xff0c;通话效果就像大家在同一个房间里聊天一样&#xff1b;300ms以内&#xff0c;大多数人很满意&#xff0c;400ms以内&#xff0c;有小部分人可以感…

Qt常用基础控件总结—容器部件(QGroupBox类)

五、容器部件 按钮框控件QDialogButtonBox 类(很少用) 按钮组控件QButtonGroup 类(很少用) 组框控件QGroupBox 类 QGroupBox 类介绍 QGroupBox(组框),直接继承自 QWidget 类,因此使用该类创建的对象,可作为窗口使用,组框在外观上是可见的。 QGroupBox 类(组框),…

Odoo14使用hiPrint实现打印功能

使用hiPrint代替odoo原生的打印功能 可以实现快速自定义修改打印模板&#xff0c;无需每次都调整打印模板 无论是表单分页还是各种需求&#xff0c;都能满足 目录 1 使用命令创建新的模块&#xff0c;无用的demo文件可以删除掉 2 新建“打印模板”&#xff0c;用于保存打印…

AFT:Attention Free Transformer论文笔记

原文链接 2105.14103 (arxiv.org) 原文翻译 Abstract 我们介绍了 Attention Free Transformer (AFT)&#xff0c;这是 Transformer [1] 的有效变体&#xff0c;它消除了点积自注意力的需要。在 AFT 层&#xff0c;键key和值value首先与一组学习的位置偏差position biases相结…

海南云亿商务咨询有限公司助力品牌快速崛起

在数字化浪潮的推动下&#xff0c;电商行业日新月异&#xff0c;短视频平台更是成为品牌宣传和销售的新宠。海南云亿商务咨询有限公司&#xff0c;作为抖音电商服务的领军者&#xff0c;凭借其专业的团队和丰富的经验&#xff0c;助力众多品牌在抖音平台上实现了快速增长。 一…

通过rpmbuild构建Elasticsearch-7.14.2-search-guard的RPM包

系列文章目录 rpmbuild从入门到放弃 search-guard插件使用入门手册 文章目录 系列文章目录前言一、资源准备二、spec文件1.基础信息2.%prep3.%Install4.%file5.%post6.%postun 三、成果演示1.执行构建过程图示例2.执行安装RPM包示例3.进程检查4.访问esApi 总结 前言 不管是源…

Google AlphaGo 繁忙训练的一天

早晨&#xff1a;启动与准备 7:00 AM - 起床与准备 AlphaGo的研发团队成员早早起床&#xff0c;进行晨练活动&#xff0c;如跑步或瑜伽&#xff0c;保持身体健康和精力充沛。 8:30 AM - 到达工作场所 研发团队到达Google的办公室或远程工作站&#xff0c;启动设备&#xff…