预处理指令详解

前言

上一节我们了解了文件操作的相关内容,本节我们来了解一下预处理指令,那么废话不多说,我们正式开始今天的学习

预定义符号

在C语言中,设置了一些预定义的符号,可以供我们直接使用,预定义符号是在程序的预处理过程中直接被处理的,那么C语言中定义的符号有下面几种:

1.__FILE__

表示当前正在编译的源文件的地址

2.__LINE__

表示文件当前所在的行号

3.__DATE__

表示文件被编译的日期

4.__TIME__

表示文件被编译的时间

5.__STDC__

如果编译器遵循ANSI C,其取值为1,若不是,则它的取值未定义(gcc编译器支持)

下面我们来尝试使用一下以上预定义符号:

#define _CRT_SECURE_NO_WARNINGS 1#include <stdio.h>int main(void)
{printf("%s\n", __FILE__);printf("%s\n", __DATE__);printf("%s\n", __TIME__);printf("%d\n", __LINE__);return 0;
}

这些符号都是在编译前的预处理阶段就进行了处理

#define 定义常量

#define 有两种不同的功能:

1. #define 可以定义符号(常量)

2. #define 可以定义宏

#define 使用的基本语法如下:

#define name stuff

例如:

#define MAX 100
#define MIN 1

在程序的预处理阶段,代码中的 name 变量就会被替换为数值 stuff

stuff 定义的数值的类型不一定全要为整数,例如:

#define STR "hello world"

我们还可以使用 #define 定义相对复杂的函数,例如:

#define forever for(;;)

在这个代码当中,for 函数的初始化部分、调整部分和判断部分都被省略。由于该 for 函数判断条件没有写,这样就意味着判断条件是恒为真的,就会造成死循环

如果我们定义的 stuff 长度过长,我们可以运用续行符( \ ),除了程序的最后一行以外,程序的每一行都可以添加换行符

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

那么此时,我们肯定会存在一个疑问:在 #define 定义标识符的时候,需不需要在末尾加上一个;呢?

建议是最好不要加上分号,举个简单的例子:

#define MAX 1000;int main(void)
{int a = MAX;return 0;
}

在此代码的预处理阶段,原代码会将代码转变成如下这种形式:

#define MAX 1000;int main(void)
{int a = 1000;;return 0;
}

此时 int a = 1000后面就有两个分号,就会造成程序的错误

还有可能会出现这样的情况:

#define MAX 1000;int main(void)
{printf("%d\n", MAX);return 0;
}

我们在打印 MAX 的时候,由于其带了分号,就会造成打印不成功

#define 定义宏

#define 允许把参数替换到文本中去,这种实现通常被称作定义宏

宏的声明方式如下:

​
#define name( parament-list ) stuff​

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

其中需要注意:

参数列表的左括号必须与 name 紧紧的挨在一起,如果两者之间存在着空格,那么参数列表就会被解释成 stuff 的一部分

下面来举一个例子:

例如我们需要定义一个宏来求某个数的平方,我们则可以这么写:

#define SQUARE(x) x*xint main(void)
{int a = 4;int ret = SQUARE(a);printf("%d\n", ret);return 0;
}

局限性

但是这个程序存在一定的弊端:若 SQUARE 里面的参数是 a + 1:

#define SQUARE(x) x*xint main(void)
{int a = 4;int ret = SQUARE(a+1);printf("%d\n", ret);return 0;
}

我们发现结果并不是我们想要的 25 ,而是 9 ,这是为什么呢?

因为宏参数在进行替换的时候,是直接采取替换的,也就是说,会被替换成:a + 1 * a + 1,由于乘法的优先级是大于加法的,所以会采取 4 + 4 + 1 的运算模式,所以算出来的结果就是 9

要解决这个问题,我们需要在宏中添加括号:

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

这样算出来的结果才是 25

在这个定义中我们使⽤了括号,想避免之前的问题,但是同时这个宏可能会出现新的错误

例如:

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

我们想要打印 100 到屏幕上,但我们实际运行程序的时候,打印的却是 55 

这个问题,的解决办法是在宏定义表达式两边加上⼀对括号

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

带有副作用的宏参数

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

例如我们要写一个宏,其功能是求两个数的较大值:

#define MAX(X,Y) ((X)>(Y)?(X):(Y))int main(void)
{int a = 1;int b = 2;int m = MAX(a++, b++);//int m = (a++,b++) ((a++)>(b++)?(a++):(b++));printf("m = %d\n", m);printf("a = %d\n", a);printf("b = %d\n", b);return 0;
}

我们可以发现 ++ 的操作在程序中执行的次数不止一次,这样就导致了结果存在问题

宏的替换规则

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

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

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

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

我们需要注意:

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

例如:

int m = MAX(a, MAX(2, 3));

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

宏与函数的对比

我们来比较一下函数和宏:

#define MAX(X,Y) ((X)>(Y)?(X):(Y))int Max(int x, int y)
{return x > y ? x : y;
}

宏通常被应⽤于执⾏简单的运算,当执行较为简单的运算的时候,宏相较于函数更加具有优势,原因有两点:

1.通过 函数栈帧的创建与销毁 的知识,我们可以知道,函数在调用函数、执行运算、返回函数的时候都需要花费时间,宏在执行小型运算时所需要i的时间相较于函数使用的时间、空间会更少,所以宏⽐函数在程序的规模和速度⽅⾯更胜⼀筹

2.函数的参数必须要声明为特定的类型

和函数相⽐宏的劣势:

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

2.宏是没法调试的

3.宏由于类型⽆关,不够严谨

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

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

例如:

#define Malloc(n,type) (type*)malloc(n*sizeof(type))int main(void) 
{int* p = (int*)malloc(10 * sizeof(int));Malloc(10, int);return 0;
}

宏和函数的对比:

属性#define定义宏函数
代码长度每次使用的时候,宏代码都会被插入程序,程序长度会大幅增加函数代码只出现在一个地方,每次使用的时候去那个地方调用对程序长度影响不大
执行速度更快因为有函数的调用与返回,相对较慢
操作符优先级需要多加括号,因为操作符优先级的缘故,不加括号往往会造成无法预料的结果参数只在调用的时候求值一次,他的结果直接传递给函数,表达式的取值更容易预测
带有副作用的参数参数可以被替换到函数的多个位置,可能导致宏的参数被多次计算,产生不可预料的结果参数只在调用的时候求值一次,他的结果直接传递给函数,表达式的取值更容易预测
参数类型宏的参数与类型无关函数的参数与类型有关
调试宏不方便调试函数可以逐语句调试
递归不支持递归支持递归

拓展:在C++中,有一个函数叫做内联函数(inline),这个函数既具有函数的特点,又具有宏的特点

# 以及 ##

# 运算符

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

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

直接写出概念可能有点难以理解,下面我们来举例说明:

我们首先铺垫一下:

int main(void)
{printf("hello""world\n");printf("helloworld\n");return 0;
}

如上面的代码,这两个字符串本质上是相同的

再看一下这个代码:

int main(void)
{int a = 1;printf("the value of a is %d\n", a);int b = 20;printf("the value of b is %d\n", b);float f = 5.6f;printf("the value of f is %f\n", f);return 0;
}

这三个打印函数的格式是非常相似的,那么我们此时就考虑到另一个问题:我们能不能把它封装成一个函数呢?或者说可不可以把它写成一个宏呢?答案是可行的

#define Print(n,format)		printf("the value of n is "format"\n", n)int main(void)
{int a = 1;Print(a,"%d");int b = 20;Print(b,"%d");float f = 5.6f;Print(f,"%f");return 0;
}

我们根据前置的铺垫可以写出这样的代码,但当我们在运行程序的时候发现了这样的一个问题:

我们可以观察到:其中的 n 并没有被替换,此时我们就需要用到 # 操作符了,# 操作符可以让参数直接转换成字符串

#define Print(n,format)		printf("the value of "#n" is "format"\n", n)int main(void)
{int a = 1;Print(a,"%d");int b = 20;Print(b,"%d");float f = 5.6f;Print(f,"%f");return 0;
}

## 运算符

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

## 被称为记号粘合,这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的

我们同样的进行举例说明:

如果我们想写⼀个函数求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 GENERIC_MAX(type)								\type type##_max(type x,type y)					\{												\return x>y?x:y;								\}//定义函数
GENERIC_MAX(int);
GENERIC_MAX(float);int main(void)
{int r1 = int_max(3, 5);printf("%d\n", r1);float r2 = float_max(3.1f, 5.2f);printf("%f\n", r2);return 0;
}

此时我们用 ## 操作符把位于两端的符号合并成为了一个符号,并由此来定义一个函数

命名约定

因为宏和函数的使用语法很相似,所以在我们书写宏的时候,通常会这样的习惯:

1.将宏的名字全部大写

2.函数的名字不要全部大写

这样我们能更好的分辨宏和函数,当然,有时会存在着特殊情况;例如 offsetof 是一个宏,它是用来计算结构体成员相较于结构体起始位置的偏移量的宏,他虽然是宏但是它全是小写

#undef 的使用

#undef 指令⽤于移除⼀个宏定义,例如:

#define MAX 100int main(void)
{printf("%d\n", MAX);
#undef MAXprintf("%d\n", MAX);return 0;
}

此时我们移除了 MAX 所以会报错

命令行定义

我们在 gcc 编译器上可以在命令行里指定变量的数据,当我们根据同⼀个源⽂件要编译出⼀个程序的不同版本的时候,这个特性有点⽤处,(假定某个程序中声明了⼀个某个⻓度的数组,如果机器内存有限,我们需要⼀个很⼩的数组,但是另外⼀个机器内存⼤些,我们需要⼀个数组能够⼤些)因为我当前的环境是 VS 而且该内容并不是很重要,所以仅仅作了解就行

条件编译

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

对于某些调试性的代码,直接删除很可惜,但是保留下来又很碍事,所以我们此时可以选择性的编译,例如:

#include <stdio.h>
#define __DEBUG__ 
int main()
{int i = 0;int arr[10] = { 0 };for (i = 0; i < 10; i++){arr[i] = i;
#ifdef __DEBUG__printf("%d\n", arr[i]);//为了观察数组是否赋值成功。#endif //__DEBUG__}return 0;
}

假如我们不需要使用 printf 函数,我们可以注释掉 

#define __DEBUG__

这样当我们在运行代码的时候, printf 的指令就不会被执行

下面我们来盘点一下常见的条件编译指令:

1.单分支的条件编译语句           

                 #if 常量表达式

        //...

                #endif

例如:

int main(void)
{
#if 0printf("haha\n");
#endifreturn 0;
}

这样就不会执行打印 haha 的操作,只有满足 if 后面的条件时,里面的语句才会被执行

2.多分支的条件编译语句

使用 #elif 和 #else 来进行多分支的条件编译语句

#define M 2int main(void) 
{
#if M==0printf("haha\n");
#elif M==1printf("hehe\n");
#elif M==2printf("hello world\n");
#elseprintf("luelue");
#endifreturn 0;
}

3.判断是否被定义

#define MAX 0int main(void)
{
#if defined(MAX)printf("haha\n");
#endifreturn 0;
}

使用 #if defined( ) 可以判断是否被定义,他还有另外一这种写法:

​
#define MAX 0int main(void)
{
#ifdef MAXprintf("haha\n");
#endifreturn 0;
}​

两者的逻辑都是一模一样的,若是想写不被定义的代码,只需要在 #define 前加上 !,或者写作 #ifndef

4.嵌套指令

#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

当我们代码的长度不长时,我们很难用到条件编译指令,当我们的代码很长,且想要实现多平台的功能时,就经常会用到条件编译指令

头文件的包含

我们知道,我们可以自己创造头文件

#pragma onceint Add(int x, int y) 
{return x + y;
}

该头文件里面的内容可以在其他其他文件里被使用,我们通常认为我们自定义的头文件需要用 " " 包含 而不是使用 < >

#define _CRT_SECURE_NO_WARNINGS 1#include <stdio.h>#include "test.h"int main(void)
{int a = 1;int b = 2;int c = Add(a, b);printf("c = %d\n", c);return 0;
}

其实不然," " 和 < > 的不同仅仅在于他们的查找策略不同,二者使用过程中,并不存在绝对的错误

" " 的查找策略:先在源⽂件所在⽬录下查找,如果该头⽂件未找到,编译器就像查找库函数头⽂件⼀样在 标准位置查找头⽂件,如果都找不到就会报编译错误

也就是说,库文件可以用  " " 查找。可是,但是这样做查找的效率就低些,当然这样也不容易区分是库⽂件还是本地⽂件了

嵌套文件包含

假设头文件被多次包含会怎么样?

如果test.c⽂件中将test.h包含5次,那么test.h⽂件的内容将会被拷⻉5份在test.c中。 如果test.h⽂件⽐较⼤,这样预处理后代码量会剧增。如果⼯程⽐较⼤,有公共使⽤的头⽂件,被⼤家都能使⽤,⼜不做任何的处理,那么后果真的不堪设想。 如何解决头⽂件被重复引⼊的问题呢?

答案是使用条件编译

#ifndef __TEST_H__
#define __TEST_H__int Add(int x, int y) 
{return x + y;
}#endif

这样做就只会被包含一次,或者使用:

#pragma once

结尾

条件编译指令的内容很多,远远不止我上面归纳的这些,感兴趣的朋友可以自行去了解,我这里便不再作过多的讲解了,谢谢您的浏览

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

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

相关文章

一场人生的风险控制,商业社会识人指南

一、资料前言 本套社会识人资料&#xff0c;大小679.94M&#xff0c;共有37个文件。 二、资料目录 识人的终极目的&#xff1a;一整场人生的风险控制.pdf 信任的搭建&#xff1a;更多的时间与维度.pdf 没有搞不定的人&#xff01;角色人格与全面人格.pdf 政治不正确的正确…

程序员为什么不能一次性写好,需要一直改Bug?

程序员为什么不能一次性写好&#xff0c;需要一直改Bug&#xff1f; 我有一问&#xff1a; 你为什么不上清华呢&#xff0c;高考答满分不就行了&#xff1f; 程序员在软件开发过程中可能会遇到需要不断修改Bug的情况&#xff0c;这主要是由以下几个原因造成的&#xff1a; 复杂…

Linux简单介绍

Linux简单介绍 编译器VMware虚拟机Ubuntu——LinuxOS为什么使用LinuxOS&#xff1f; 目录结构Windows目录结构Linux操作系统home是不是家目录&#xff1f; Linux常用命令终端命令行提示符与权限切换命令tab 作用&#xff1a;自动补全上下箭头pwd命令ls命令mkdir命令touch命令rm…

智能革命:ChatGPT3.5与GPT4.0的融合,携手DALL·E 3和Midjourney开启艺术新纪元

迷图网(kk.zlrxjh.top)是一个融合了顶尖人工智能技术的多功能助手&#xff0c;集成了ChatGPT3.5、GPT4.0、DALLE 3和Midjourney等多种智能系统&#xff0c;为用户提供了丰富的体验。以下是对这些技术的概述&#xff1a; ChatGPT3.5是由OpenAI开发的一个自然语言处理模型&#x…

第17章 反射机制

一 反射(Reflection)的概念 1.1 反射的出现背景 Java程序中&#xff0c;所有的对象都有两种类型&#xff1a;编译时类型和运行时类型&#xff0c;而很多时候对象的编译时类型和运行时类型不一致。 Object obj new String(“hello”); obj.getClass() 例如&#xff1a;某些变…

C++多线程:单例模式与共享数据安全(七)

1、单例设计模式 单例设计模式&#xff0c;使用的频率比较高&#xff0c;整个项目中某个特殊的类对象只能创建一个 并且该类只对外暴露一个public方法用来获得这个对象。 单例设计模式又分懒汉式和饿汉式&#xff0c;同时对于懒汉式在多线程并发的情况下存在线程安全问题 饿汉…

深入理解计算机系统 家庭作业 2.75

/* 书中的公式是w位的公式(mod 就是为了截断成w位),我们现在做的是2w位中的前w位 注意书上这句话:由于模运算符,所有带有权重和的项都丢掉 对应到本题 该项除以后还是超过了2w位所以被丢弃了,因为题目说了只有2w位 这个式子除以就是我们想要的最终结果 函数signed_high_p…

前端学习<四>JavaScript基础——03-常量和变量

常量&#xff08;字面量&#xff09;&#xff1a;数字和字符串 常量也称之为“字面量”&#xff0c;是固定值&#xff0c;不可改变。看见什么&#xff0c;它就是什么。 常量有下面这几种&#xff1a; 数字常量&#xff08;数值常量&#xff09; 字符串常量 布尔常量 自定义…

Vol.34 Good Men Project:一个博客网站,每月90万访问量,通过付费订阅和广告变现

今天给大家分享的案例网站是&#xff1a;Good Men Project&#xff0c;这是一个专门针对男性成长的博客网站&#xff0c;内容包括人际关系、家庭、职业发展等话题。 它的网址是&#xff1a;The Good Men Project - The Conversation No One Else Is Having 流量情况 我们先看…

高分卫星助力台湾省花莲县地震应急救援

4月3日7时58分&#xff0c;在台湾省花莲县海域&#xff08;北纬23.81度&#xff0c;东经121.74度&#xff09;发生7.3级地震&#xff0c;震源深度12公里。接中国地震局地震预测研究所应急需求&#xff0c;国家航天局对地观测与数据中心&#xff08;以下简称“中心”&#xff09…

C#仿OutLook的特色窗体设计

目录 1. 资源图片准备 2. 设计流程&#xff1a; &#xff08;1&#xff09;用MenuStrip控件设计菜单栏 &#xff08;2&#xff09;用ToolStrip控件设计工具栏 &#xff08;3&#xff09;用StatusStrip控件设计状态栏 &#xff08;4&#xff09;ImageList组件装载树节点图…

SQLyog连接数据库8.0版本解析错误问题解决方案

问题描述&#xff1a; 解决方案&#xff1a; alter userrootlocalhostidentified with mysql_native_password by 密码; 再次连接就可以了。

实现顺序表的增删查改

现在让我们探索数据结构这个美妙的世界吧&#xff01; 概念介绍 线性表是具有相同特性的数据元素的有限序列。线性表是一种在实际运用中广泛运用的线性结构&#xff0c;如线性表&#xff0c;栈&#xff0c;队列&#xff0c;字符串等。 顺序表的本质是数组&#xff0c;实现了…

js的事件冒泡、捕获、委托

事件不仅存在js中&#xff0c;也存在在其他语言中&#xff0c;js事件背后的主要思想是能够在特定事件发生时运行代码。 先普及一个概念&#xff0c;什么是事件处理程序&#xff1f; 事件处理程序就像一个特殊的通用遥控器&#xff0c;可以执行某些操作&#xff0c;例如更改电…

java自动化-03-04java基础之数据类型举例

1、需要特殊注意的数据类型举例 1&#xff09;定义float类型&#xff0c;赋值时需要再小数后面带f float num11.2f; System.out.println(num1);2&#xff09;定义double类型&#xff0c;赋值时直接输入小数就可以 3&#xff09;另外需要注意&#xff0c;float类型的精度问题…

鸿蒙开发就业前景到底怎么样?

随着科技的不断进步&#xff0c;鸿蒙操作系统的推出为开发者们带来了新的机遇和挑战。鸿蒙&#xff0c;作为华为自主研发的操作系统&#xff0c;旨在为消费者提供更为流畅、安全的智能设备体验。那么&#xff0c;鸿蒙开发就业前景如何呢&#xff1f; 一、鸿蒙操作系统的优势 …

探索--------------redis缓存三大问题及解决方案

目录 一、redis的三大缓存问题 1、缓存穿透 1.1 问题描述 1.2缓存穿透发生的条件 1.3缓存穿透发生的原因 1.4解决方案 2、缓存雪崩 2.1问题描述 2.2解决缓存雪崩问题的方法有&#xff1a; 3、缓存击穿 &#xff08;热点数据集中失效&#xff09; 3.1问题描述 3.2缓…

SpringBoot快速入门笔记(3)

文章目录 一、MybatisPlus1、ORM2、添加依赖3、全局配置4、Navicat5、UserController6、CRUD操作7、BaseMapper8、两个注解 二、多表查询1、模拟用户订单2、通过用户查相关订单3、UserMapperNew4、查询订单和所属用户5、OrderMapper6、OrderController 三、条件查询四、分页查询…

【Ubuntu】用 VMware 安装 macOS

本教程使用 Ubuntu 20.04.6 LTS&#xff0c;VMware Workstation Pro 17.5.1&#xff0c;macOS Sonoma 14.4。文中所有需要的下载链接均以 Markdown 的形式体现在文字上。 下载 VMware Workstation Pro&#xff0c;目前最新版本是 17.5.1。 使用密钥&#xff0c;进行破解。 VM…

金融中的数学知识

随机偏微分方程相比普通偏微分方程具有额外的随机项&#xff0c;反映了其描述的现象具有随机性质