什么是预处理

预处理


 因为计算机只认识二进制,我们需要把我们写的C程序其他语言也是一样我们都需要把他翻译为二进制。我们这就需要来了解计算机的发展历史了,计算机首先被设计用来计算炮弹的飞行路线,主要运用于军事,当时写的程序都是运用纸带来进行编写的,有孔为0,无孔为1。但是,这种写程序的方法需要记着一堆的二进制,通常也不利于理解和检查,当时能写程序的都是科学家,程序的BUG这个名词也是来源于那个时期,在打孔的时候一个虫子(BUG)趴在了纸带上,导致了程序的运行错误,随着计算机的发展,有了键盘的输入,显示器,人们就把一些常用的二进制起了一个别名(add之类的)也就成了汇编语言,汇编语言一旦出现就需要有一个能解释汇编语言的东西了——编译器,负责把汇编语言翻译为二进制文件,但是计算机再次进行了发展,出现了类似我们思考的高级语言C语言(当然也出现了其他的,这里只是举个例子),我们只需要把C语言翻译为汇编在翻译为二进制就行了,会更加方便,也更加利于C语言的开发,

 所以我们写的C程序需要经过:编译(把C语言翻译成汇编语言),汇编(把汇编语言翻译为二进制)

 下面来思考一些我们写C语言的过程,我们是不是会写一些注释,或者是宏(这个老师都会给我们宏会被直接进行替换),还有我们会使用printfscanf等函数,这些我们并没有进行自己实现,但是却可以直接调用,这些是不合理的,那么为什么我们可以进行使用呢?因为有一些编写库的程序员大佬给我们写好了相关的库(.ddl .lab文件),我们要把他们写的库链接到我们写的程序中,减少我们的负担不然写一个printf("hello world");print这个函数却要我们来实现,这显然是有点忘了我们的初心只是要打印一个Hello world了,有了库我们就可以提高我们开发的效率,提高我们程序的健壮性

 所以一个C程序要成为可运行文件.exe需要经过:预处理、编译、汇编、链接

预处理阶段会做的事情:去注释、宏替换、条件编译、头文件展开

了解内容 Linux中的一些命令

预处理 -E: 头文件展开,去注释

编译 -S:将C语言编译成汇编语言

汇编 -c:将汇编语言转换为目标二进制文件 .obj(不是可执行文件)

链接 : 将目标二进制文件与相关链接(.ddl .lab),形成可执行程序 (.exe)

image-20221017195848954

这个```lesson_define``,以前写的测试,忘了删除了,结果一样的有Linux环境的条件下可以测试一下

image-20221018173333957

image-20221018173457811

一、宏定义

(1)数值宏常量

 我们常常把常量定义为宏,因为这样我们可以见名知意,并且我们不会因为我们的误操作去修改了我们的常量,况且在我们进行修改的时候,只需要维护宏就可以了。

#define PI 3.1415
#define ERROW_POEERROR -1

(2)字符串定义宏常量

我们在定义宏常量的时候,如果被定义的为字符串的时候我们一定要加引号

#define PATH C:\Windows\addins// ×
#define pATH "C:\Windows\addins//语法上没有错误,但是 /w 会被进行转义得不到我们想要得到的结果
#define PATH "C:\\Windows\\addins"//√的
 
// 如果对应字符串较长我们可以用续行符号 \+Enter
#define PATH "C:\\Windows\\addins\
C:\\Windows\\addins"

image-20221017194206488

(3)宏定义充当注释符号

 在大学学校里面,老师在教授C语言的时候都会说的一句话,你们写的注释在预处理的时候会变为空格,而宏会被直接替换到你用宏的位置,那么宏替换和去注释究竟是谁先呢?

我们把宏定义为注释符号,来做一个测试

#inclde<stdio.h>
#define BSD //
int main()
{
    BSD printf("hello world\n");
    printf("you must see me\n");
    return 0;
}

image-20221018174902057

输出了两句话,如果先进行宏替换在进行去注释的话显然只会输出第二句话,而不会输出第一句,如果优先进行宏替换会变成下面的那样

#include<stdio.h>
int main()
{
    // printf("hellow world\n");
    printf("you must see me");
    return 0;
}

可以得出去注释优先于宏替换,我们看一下,预处理后的文件

QQ图片20221018175429

所以#defind BSD //,后面的//优先被当做注释,而BSD空替换,得到我们看到的去注释后的文件。

(4)用宏定义表达式

 我们可以用宏来定义表达式来达到类似函数的作用,但是只是进行替换,而不是真的进行函数调用,如果明显具有字符串的特征"",则不会进行替换,其他类型的均可被宏替换

#include<stdio.h>
#define SUB(x,y)  ((x)-(y))
int main()
{
    printf("%d",SUB(5,3));
    printf("SUB(5,3)");
    return 0;
}

image-20221019072103170

image-20221019072532586

那宏定义是否可以替换多个语句呢?答案是可以的不过有一些注意事项。

如果有多条语句尽量放到do{}while(0),里面,不然可能会出现一些问题

#include<stdio.h>
#define INIT_C(x,y)\
x=0;\
y=0;
int main()
{
    int x=100,y=100;
    INIT_C(x,y);
    printf("%d %d\n",x,y);
    return 0;
}

image-20221019074146947

image-20221019074525916

来看下面这份代码:

#include<stdio.h>
#define INIT_C(x,y)\
x=0;\
y=0;
// \+Enter 表示这一行还没完,续行的作用
int main()
{
    int x=100,y=100;
    INIT_C(x,y);
    int flag=1;
    if(flag) INIT_C(x,y);
    else x=200,y=200;
    printf("%d %d\n",x,y);
    return 0;
}
//我们来想一下预处理后的代码
/*
int main()
{
    int x=100,y=100;
    INIT_C(x,y);
    int flag=1;
    if(flag) x=0;y=0;; //if 不加括号只可以跟一条语句,这样会导致下面else无法进行匹配,导致错误
    else x=200,y=200;
    printf("%d %d\n",x,y);
    return 0;
}*/

// 我们可以通过在 if{}else{} 来解决这个问题但是我们可以这样做但不能保证别人也会这样做
// 所以我们应该怎么进行解决呢

// 给宏替换加一个{}不就可以了但是我们写语句的时候通常会带一个分号导致if与else无法匹配导致错误

// 我们在写一条语句的时候通常会带一个分号,注意观察上面进行替换的时候是不是多了一个分号
// 那我们想什么时候会有{}把我们写的语句在一个代码块内并且还需要一个分号
// do{}while(0)完全可以解决我们的需求,代码只执行一次,拥有代码块并且还需要末尾有一个分号

多了一个分号导致else无法与if进行匹配,引发错误。

image-20221019080019277

image-20221019080108936

下面我们用do{}while(0)来进行测试

image-20221019080833160

没有进行报错,并且正常输出,我们来看一下他预处理后的代码

image-20221019081136689

和我们预期一致,这样我们可以宏替换语句数量便没有限制了。

(5)undef取消宏定义

1.宏定义的生效范围

 宏定义只能定义到程序的开始吗?答案不是的,宏定义也可以定义在函数内部main函数以及我们自己定义的各种函数都可以的,宏定义的作用域是宏定义的开始到遇到undef或者是到程序的结束,宏的替换是在预处理阶段是在函数调用前的,我们定义的宏在预处理的时候就会把函数体内对应的宏进行了替换.

image-20221019100339327

image-20221019100935178

#include<stdio.h>
void show()
{
    printf("show:%d\n",M);
}
int main()
{
    #define M 10
    printf("main:%d",M);
    return 0;
}
//M宏的定义在show函数下面,所以并不会进行宏替换,在编译的时候会显示M未定义而出现错误

//我们如果把宏定义到show的print之前就不会有问题,因为他的替换范围是他下面的所有

/*
void show()
{
	#define M 10
    printf("show:%d\n",M);
}
int main()
{
    printf("main:%d",M);
    return 0;
}*/

#define放在show下面

image-20221019101336557

image-20221019101551807

#define放在show里面或者说被替换的上面

image-20221019102325517

image-20221019102549103

2.undef的使用

我们上面讲了宏定义的作用范围是定义的地点开始,那么我可以手动控制他结束的范围吗?当然可以

#define + 宏定义的名字表示宏的作用范围到此为止

#include<stdio.h>
int main()
{
    #define M 200
    printf("before : M=%d",M);
    printf("before : M=%d",M);
    printf("before : M=%d",M);
    #undef M
    //会因为宏定义M作用范围结束导致下面编译的时候显示M未定义
    printf("after : M=%d",M);
    printf("after : M=%d",M);
    printf("after : M=%d",M);
    return 0;
    
}

可以看到#undef M后面并没有进行替换,所以宏定义的替换范围是宏定义开始到宏定义取消

image-20221019104518379

宏定义替换在调用函数前,调用前就被替换就没有任何问题

image-20221019104222313

image-20221019104403693

二、条件编译

 条件编译:可以根据我们的需求对代码进行裁剪,以减少我们对代码的维护量,可以让一份代码可以运行在不同的操作系统,或者说一份代码对于收费版和付费版进行同时维护,版本的更新等等。

下面来看一下宏定义和宏真值的区别

宏定义:看这个宏是否存在。

逻辑判断宏真假:首先这个宏得存在才可以判断他的真假,是根据宏的值来进行判断的

1.#ifdef判断宏是否定义

/*
 * #ifdef 是if define的缩写用来判断宏是否被定义
 * 如果被定义则保留#ifdef直到#endif代码间的代码(不包括#else中的)
 * #else的理解就和if-else的类似
 * 否则则在预处理的时候进行裁剪
 * #ifdef 也是可以带else的,具体带不带看需求
*/

//#define DEBUG_WIN //可以后面不跟任何替代值但是这个宏还是被定义了

#ifdef DEBUG_WIN
// #else
#endif

/*
 * #ifndef 和#ifdef相反
 * 该宏没有定义则保留 #ifdef直到#endif的代码(不包括else中的)
 * 定义了就进行裁剪留下#else部分
*/
#ifndef DEBUG_WIN

// #else
#endif
防止头文件被重复包含

注意:#ifndef有一种特殊的用法需要我们记住就是防止头文件被重复包含,因为我们在预处理期间头文件会被copy进我们的源文件,如果我们没有用extern关键字来声明我们的变量的话,在编译的期间重复包含会报重复定义的错误,就算我们使用了extern声明变量,多余的头文件对于编译的效率也是有很大的影响的。

// 第一种比较简单的防止头文件被重复包含的方法
// 现在在主流的编辑器都没有问题
#program once
//第二种
#ifndef _TEXT_H_
#define _TEXT_H_


#endif
/* 
 * 如果重未定义这个宏这份代码就会留下
 * 并在第二个语句定义这个宏
 * 如果我们错误再次引入一遍这个头文件
 * 因为这个宏已经被定义导致下一份直接被裁剪掉
*/

2. #if判断宏的真值情况

/*
 * #if 用来判断宏的真值情况可以进行级联
 * 运用特殊的方法也可以达到 #ifdef #ifndef的效果
 * #if 也可以支持嵌套
 * #if 后面可以根 #elif #else 进行多分支判断
 * 只有符合真值条件的才会留下不符合真值条件的会被裁剪
*/

// 需要判断宏的真假情况
#if DT

#elif DL

#else

#endif

//等同于 #ifdef #endif
#if define(DEBUG_WIN)

#endif

//等同于 #ifndef #endif
#if !define(DEBUG_WIN)

#endif

//嵌套和级联 当然逻辑符号在这里依旧可以进行使用
# if define(WINDOW)&&define(LINUX)

#elif define(MAC)

#else

#endif

三、文件展开

#include<stdio.h>
int main()
{
	printf("hellow world");
	return 0;
}

6行变6-700行了

这就是头文件的展开,把对应文件的内容复制一份,当然也会进行去注释和条件编译

image-20221020164242235

下面我们在做一个测试,为了结果明显就不在进行去除头文件的重复包含了,也不用输入和输出函数了

// text.h
extern void show();
extern int showint();
// text.c
#include"text.h"
int main()
{
    return 0;
}

image-20221020165339605

image-20221020165703440

四、 #号的使用

1.一个#的使用

一个#可以把对应的式子转化为字符串

#include<stdio.h>
#define CHANGE(x) #x
int main()
{
    printf("%s",CHANGE(3.14));
    return 0;
}

2.两个#的作用

可以形成新的符号,就是可以替换的时候可以有变量,并且没有括号

#include<stdio.h>
#define MATH(a,b) a##e##b
int main()
{
    printf("%d",MATH(3.14,2));
    //会被替换为 3.14e2=314
    return 0;
}

image-20221020172109669

  • 11
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学c的小李

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值