预处理详解

目录

一:预定义符号

二:#define定义常量

三:#define定义宏

四:带有副作用的宏定义

五:宏的替换规则

六:宏函数的对比

七:# 和 ##

7.1 #运算

7.2 ##预算符

八:命名约定

九:#undef

十:命令行定义

十一:条件编译

十二:头文件的包含 

12.1头文件被包含的方式

12.1.1本地头文件包含

12.1.2库文件的包含

12.2嵌套文件包含

十三:其他预处理指令


一:预定义符号

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

__FILE__  //进行编译的源文件
__LINE__  //文件当前的行号
__DATE__  //文件被编译的日期
__TIME__  //文件被编译的时间
__STDC__  //如果编译器遵循ASIC C,其值为1,否则未定义

//ASIC C gcc编译器是支持的,但是VS不支持

二:#define定义常量

基础:语法:

#define name stuff
#define MAX 1000      
#define reg register   //为register这个关键字,创建一个更简短的名字

//for循环判断部分什么都不写,表示恒成立,死循环
#define do_forver for(;;) //用更形象的符号来替换一种实现

#define CASE break;case//在case语句自动把break加上

//如果定义的stuff太长,可以分成几行写,除了最后一行,每行的后面都加上一个反斜杠(续行符)
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
                            date:%s\ttime:%s\t, \
                            __FILE__,__LINE__,\     // \后面不能有空格,直接按空格
                            __DATE__,__TIME__)

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

比如:

#define MAX 1000;
#define MAX 1000

建议不要加上;,这样容易出问题

比如以下场景:

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

如果定义MAX时用第一种加上分号的,等替换了以后就会变成max = MAX;;,if和else之间就是两条语句,而没有大括号的时候,if后面只能有一条语句。这里会出现语法错误。

三:#define定义宏

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

下面是宏的申明方式:

#define name(parament-list) stuff
//              参数          内容

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

注:

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

举例:

写一个宏,求平方

#define SQUARE(x) x*x

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

警告:

这个宏存在一定的问题:

看下面的代码段:

int a = 5;
printf("%d\n", SQUARE(a+1) );//宏的参数不计算,直接传到表达式中

第一眼时,也许大多数人认为就是5+1=6,算的是6的平方,结果是36啊

但是结果是11,这是为什么呢?

替换文本时,参数x被替换成a+1,所以这条语句实际上变成了

printf("%d",a + 1 * a + 1);

这样就可以清晰的看出,由替换产生的表达式并没有按照预想的次序求值。

在宏定义上加上了两个括号,这个问题便轻松解决了:

#define SQUARE(x) (x) * (x)

这样的处理之后就产生了预期的处理

printf("%d",(a+1) * (a+1) );

这里还有另一个宏定义:

算一个数的二倍:

#define DOUBLE(x) (x) + (x)

定义中我们用了括号,避免了之前出现过的问题,但是这个宏可能出现新的错误

int a = 5;
printf("%d" ,10* DOUBLE(a) );

有没有觉得结果是100的?但是实际上打印的结果是55。

替换之后的样子是:

printf("%d", 10*(5)+(5) );

乘法运算大于宏定义,所以出现了55。

这个问题的解决办法是在宏定义表达式两边加上一对括号就可以了。

#define DOUBLE(x) ( (x) + (x) )

提示:

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

四:带有副作用的宏定义

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

例如:

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

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

例如:写一个宏定义,比较两个数的值

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

//......
x = 5;
y = 8;
z = MAX(x++,y++);
printf("x = %d y = %d z = %d",x,y,z); 

这里我们要知道预处理处理之后的结果是什么:

z = ( (x++) > (y++) ? (x++) : (y++) );
//      5       8
//比较的结果是错误,不执行x++的语句,执行y++的语句,但此时x和y都已经加一变成x=6,y=9
//然后把y的值给z,z=9,y再加一,y=10
//最后的打印结果是 x = 5, y = 10,z = 9

五:宏的替换规则

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

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

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

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

注意:

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

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

六:宏函数的对比

宏通常被应用于简单的运算

比如两个数中找出比较大的一个时,写成下面的宏,更有优势一些

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

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

原因有二:

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

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

和函数相比宏的劣势:

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

2.宏是没办法调试的

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

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

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

#define MALLOC(num, type)\
         (type )malloc((num) *sizeof(type))

//.....
//使用
MALLOC(10,int);
//预处理处理之后
(int)MALLOC( (10) * sizeof(int));
宏和函数的对比

属性

#define定义的宏函数
代码长度每次使用,宏代码都会插入到程序中。除了非常小的宏之外,程序的长度都会大幅度的增加代码只出现在一个地方,每次使用时,都会调用那个地方的同一份代码
执行速度更快存在函数的调用和返回的额外的开销,所以相对慢一些
操作符优先级宏参数的求值是在周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写时多些括号函数参数只在函数调用时求值一次,它的结果值传递给函数。表达式求值结果更容易预测
带有副作用的参数参数可能被替换到宏体中的多个位置,如果宏的多个参数被多次计算,带有副作用的参数求值可能就会产生不可预料的后果函数参数只在传参时候求值一次,结果易控制
参数类型宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型函数的参数是与类型有关,如果参数类型不同,就需要不同的函数,即使他们执行任何是不同的
调试宏是不方便调试的函数是逐语句调试的
递归宏是不能递归的函数是可以递归的

七:# 和 ##

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);

运算代码就能在屏幕上打印:

the value of a is 10

7.2 ##预算符

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

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

这里我们想想,写一个函数求2个数的较大值的时候,不同的数据类型就得写不同的函数

比如:

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

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

但是这样写起来就太繁琐了,现在我们这样写代码试试:

#define GAMERIC_MAX(type)\
type type##_max(type x,type y)\
{                              \
    retrun (x>y?x:y);          \
}

使用宏,定义不同函数

GENERIC_MAX(int) //替换到宏体内int##max,生成了新的符号,int_max做函数名
GENRIC_MAX(float)//替换到宏体内float##max,生成了新的符号,float_max
int main()
{
    //调用函数
    int m = int_max(2,3);
    printf("%d",m);
    float fm = float_max(3.5f,4.5f);
    printf("%f",fm);
    return 0;
}

输出:

1. 3
2. 4.500000

八:命名约定

一般来讲函数和宏的使用语法很相似。所以语言本身没法帮我们区分二者

平时命名时,有一个习惯:

把宏名全部大写

函数名不要全部大写

但是也有例外:offsetof是宏,但是全部小写

九:#undef

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

#undef NAME
//如果先存在一个名字需要被重新定义,那么它的旧名字首先要被移出

十:命令行定义

命令定义在VS中无法使用,要在gcc环境下才可以使用

许多的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;
}

编译指令

//gcc环境下演示
gcc -D ARRAY_SIZE=10 programe.c
//-D表示给代码中的一些符合赋值
//pragrame.c是指文件名

十一:条件编译

在程序编译的时候,如果我们要将一条语句(一组语句)编译或者放弃很方便的。因为我们有条件编译指令。

比如说:

编译性的指令,删除可惜,保留又碍事,所以我们可以选择选性编译。

#include <stdio.h>
int main()
{
    int i = 0;
    int arr[10] = {0};
    for(i = 0; i<10;i++)
    {
        arr[i] = i;
        #ifdef __DEBUG // __DEBUG__定义了,中间就会编译,未被定义就不会被编译
        printf("%d",arr[i]);//为了观察数组是否赋值成功
        #endif //__DEBUG__
     }
    return 0;
}

常见的编译指令:(均由#endif 结束)

1.
#if 常量表达式//表达式为真就编译,为假就不编译

//....
#end if

如:
#define __DEBUG__ 1
#if __DEBUG__
//....
#end if

2.多条分支的条件赋值
//多条内只会选一个真的
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//....
#end if

如:
#define M 3
int main()
{
    #if M == 1
        printf("haha");
    #elif M == 2
        printf("heihei");
    #elif M == 3
        printf("hehe");
    #else
        printf("~~~");
    #end if
}

3.判断是个否被定义
//如果被定义
#if defined(symbol)
#ifdef symbol

//如果未被定义
#if !defined(symbol)
#ifndef symbol

4.嵌套定义
#if defined (OS_UNIX)
    #ifdef OPTION1
        unix_version_option1();
    #end if
    #ifdef OPTION2
        unix_version_option2();
    #end if
#elif defined(OS_MSDOS)
    #ifdef OPTION2
            msdos_version_option2();
    #endif
#endif

十二:头文件的包含 

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次,如果重复包含对于编译的压力就比较大。

如果直接这样写,test.c文件中将test.h包含5次,那么test.h文件的内容将会被拷贝5次在test.c中。

如果test.h文件比较大,这样预处理后代码量会剧增。如果工程比较大,有公共使用的头文件,被大家都使用,又不做任何任何处理,那么后果真是不堪设想。

如何解决头文件被重复引用的问题?答案是:条件编译 

每个头文件的开头写:

#ifndef __TEST__H
//如果__TEST__H 没有定义就参与编译,如果重复就有定义了,就不参与编译了
#define __TEST__H

//头文件内容
#endif //__TEST__H

或者

#pragma once

就可以避免头文件的重复引用。

十三:其他预处理指令

#error
#pragma
#line
//.....

#pragma pack()//结构体部分有介绍

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

mac电脑版MATLAB R2023b for Mac中文激活版

MATLAB R2023b for Mac&#xff1a;科学计算的终极工具 软件下载&#xff1a;MATLAB R2023b for Mac中文激活版下载 &#x1f52c; 探索科学&#xff0c;无限可能 MATLAB R2023b for Mac&#xff0c;助您深入挖掘科学计算的奥秘。从数据分析、算法设计到可视化展示&#xff0c;…

物联网导论

物联网起源 物联网&#xff1a;是一个基于互联网、传统电信网等信息承载体&#xff0c;让所有能够被独立寻址的普通物理对象实现互联互通的网络。它具有普通对象设备化、自治终端互联化和普适服务智能化三个重要特征。 按照规定的协议&#xff0c;将具有感知、通信、计算等功…

T2 小美的平衡矩阵(25分) - 美团编程题 题解

考试平台&#xff1a; 牛客网 题目类型&#xff1a; 30道单选题&#xff08;60分&#xff09; 2 道编程题 &#xff08;15分 25分&#xff09; 考试时间&#xff1a; 2024-03-09 &#xff08;两小时&#xff09; 题目描述 小美拿到了一个n*n的矩阵&#xff0c;其中每个元素是…

简单BFF架构设计

又到周五了有了一个小时的闲暇时间简单写点东西&#xff0c;介绍一个简单的BFF的架构。BFF:Backends For Frontends,其实现在是个比较常见的前端架构设计的方案&#xff0c;其最大的优势便在于前端可以高度自由的在Node层做一些server端才可以做的东西&#xff0c;比如SSR、登录…

【JavaEE进阶】Spring中事务的实现

文章目录 &#x1f343;前言&#x1f334;事务简介&#x1f6a9; 什么是事务?&#x1f6a9;为什么需要事务?&#x1f6a9;事务的操作 &#x1f340;Spring 中事务的实现&#x1f6a9;Spring 编程式事务&#x1f6a9;Spring声明式事务Transactional&#x1f6a9;Transactional…

Using WebView from more than one process

关于作者&#xff1a;CSDN内容合伙人、技术专家&#xff0c; 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 &#xff0c;擅长java后端、移动开发、商业变现、人工智能等&#xff0c;希望大家多多支持。 未经允许不得转载 目录 一、导读二、概览三、问题过程源码追踪…

Pinctrl子系统_04_Pinctrl子系统主要数据结构

引言 本节说明Pinctrl子系统中主要的数据结构&#xff0c;对这些数据结构有所了解&#xff0c;也就是对Pinctrl子系统有所了解了。 前面说过&#xff0c;要使用Pinctrl子系统&#xff0c;就需要去配置设备树。 以内核面向对象的思想&#xff0c;设备树可以分为两部分&#x…

ssrf漏洞

SSRF漏洞概述和演示 SSRF&#xff08;Server-Side Request Forgery&#xff0c;服务器端请求伪造&#xff09;是一种常见的Web应用程序安全漏洞。它允许攻击者诱使服务器端应用程序发起任意HTTP(S)请求到内部系统或者网络&#xff0c;而这些请求通常是正常情况下服务器自身为了…

MYSQL | 数据库到底是怎么来的?

“以史为鉴&#xff0c;可以让我们更深刻地理解现在&#xff0c;预见未来。” 要想知道一件东西是怎么发生的, 我们不妨把时间拨回关系型数据库被提出前后来探索。在信息技术飞速发展的今天&#xff0c;回望数据库管理系统的演进之路&#xff0c;我们可以深刻理解到技术进步如…

产品推荐 - 基于6U VPX的双TMS320C6678+Xilinx FPGA K7 XC7K420T的图像信号处理板

综合图像处理硬件平台包括图像信号处理板2块&#xff0c;视频处理板1块&#xff0c;主控板1块&#xff0c;电源板1块&#xff0c;VPX背板1块。 一、板卡概述 图像信号处理板包括2片TI 多核DSP处理器-TMS320C6678&#xff0c;1片Xilinx FPGA XC7K420T-1FFG1156&#xff0c;1片…

20240309-1-校招前端面试常见问题-前端框架及常用工具

校招前端面试常见问题【5】——前端框架及常用工具 React Q&#xff1a;请简述一下虚拟 DOM 的概念&#xff1f; 基于 React 进行开发时所有的 DOM 构造都是通过虚拟 DOM 进行&#xff0c;每当数据变化时&#xff0c;React 都会重新构建整个 DOM 树&#xff0c;然后 React 将…

selenium之PO设计模式

初识PO模式 PO&#xff08;PageObject&#xff09;是一种设计模式。简单来说就是把一些繁琐的定位方法、元素操作方式等封装到类中&#xff0c;通过类与类之间的调用完成特定操作。 PO被认为是自动化测试项目开发实践的最佳设计模式之一。 在学习PO模式前&#xff0c;可以先…

力扣日记3.8-【回溯算法篇】37. 解数独

力扣日记&#xff1a;【回溯算法篇】37. 解数独 日期&#xff1a;2023.3.8 参考&#xff1a;代码随想录、力扣 37. 解数独 题目描述 难度&#xff1a;困难 编写一个程序&#xff0c;通过填充空格来解决数独问题。 数独的解法需 遵循如下规则&#xff1a; 数字 1-9 在每一行只…

存货计价方式 比较-移动平均和批次计价

SAP常用的存货计价方式有 标准价格移动平均价格批次计价 标准价格常用于制造企业&#xff0c;今天的方案比较主要集中在销售型企业常用的移动平均价和批次计价 批次计价&#xff1a; 移动平均&#xff1a; 两种计价方式的Pros&Cons 比较 批次计价 移动平均优点 1…

基于单片机的水平角度仪系统设计

目 录 摘 要 I Abstract II 引 言 1 1控制系统设计 3 1.1系统方案设计 3 1.2系统工作原理 4 2硬件设计 6 2.1单片机 6 2.1.1单片机最小系统 6 2.1.2 STC89C52单片机的性能 7 2.2角度采集电路 8 2.2.1 ADXL345传感器的工作原理 9 2.2.2 ADXL345传感器倾角测量的原理 9 2.2.3 AD…

YOLOv8优化策略:特征融合篇 | GELAN(广义高效层聚合网络)结构来自YOLOv9

🚀🚀🚀本文改进:使用GELAN改进架构引入到YOLOv8 🚀🚀🚀YOLOv8改进专栏:http://t.csdnimg.cn/hGhVK 学姐带你学习YOLOv8,从入门到创新,轻轻松松搞定科研; 1.YOLOv9介绍 论文: 2402.13616.pdf (arxiv.org) 摘要: 如今的深度学习方法重点关注如何设计最合适…

用 ChatGPT 搭配 STAR 原则,准备英文面试超轻松

用 ChatGPT 搭配 STAR 原则&#xff0c;准备英文面试超轻松 ChatGPT 除了可以帮忙改简历&#xff0c;在你的求职历程中&#xff0c;ChatGPT 也可以帮忙练英文面试。在我们实测之后&#xff0c;发现 ChatGPT 在练习英文面试上&#xff0c;不仅能针对你的回答给予回馈&#xff0…

Docker下Jenkins打包java项目并部署

docker 构建Jenkins sudo docker run --namezen_haslett --userjenkins --privilegedtrue --volume/home/cyf/server/jenkins/jenkins_home:/var/jenkins_home -v /usr/lib/jvm/java-17-openjdk-amd64:/usr/lib/jvm/java-17-openjdk-amd64 -v /usr/lib/maven/apache-mav…

FFmpeg--AAC音频解码流程

文章目录 AAC 组成函数分析读aac帧写aac帧aac的head参数设置 运行结果 AAC 组成 AAC音频格式&#xff1a;是⼀种由MPEG-4标准定义的有损⾳频压缩格式 ADTS:是AAC音频的传输流格式 AAC音频文件的每一帧由ADTS Header和AAC Audio Data组成 每⼀帧的ADTS的头⽂件都包含了⾳频的采…

ArmSoM Rockchip系列产品 通用教程 之 GPIO 使用

1. GPIO简介​ GPIO&#xff0c;全称 General-Purpose Input/Output&#xff08;通用输入输出&#xff09;&#xff0c;是一种在计算机和嵌入式系统中常见的数字输入输出接口。它允许软件控制硬件的数字输入和输出&#xff0c;例如开关、传感器、LED灯等。GPIO通常由一个芯片或…