预处理
文章目录
因为计算机只认识二进制,我们需要把我们写的C程序其他语言也是一样我们都需要把他翻译为二进制。我们这就需要来了解计算机的发展历史了,计算机首先被设计用来计算炮弹的飞行路线,主要运用于军事,当时写的程序都是运用纸带来进行编写的,有孔为0,无孔为1。但是,这种写程序的方法需要记着一堆的二进制,通常也不利于理解和检查,当时能写程序的都是科学家,程序的BUG这个名词也是来源于那个时期,在打孔的时候一个虫子(BUG)趴在了纸带上,导致了程序的运行错误,随着计算机的发展,有了键盘的输入,显示器,人们就把一些常用的二进制起了一个别名(add之类的)也就成了汇编语言,汇编语言一旦出现就需要有一个能解释汇编语言的东西了——编译器,负责把汇编语言翻译为二进制文件,但是计算机再次进行了发展,出现了类似我们思考的高级语言C语言(当然也出现了其他的,这里只是举个例子),我们只需要把C语言翻译为汇编在翻译为二进制就行了,会更加方便,也更加利于C语言的开发,
所以我们写的C程序需要经过:编译(把C语言翻译成汇编语言),汇编(把汇编语言翻译为二进制)
下面来思考一些我们写C语言的过程,我们是不是会写一些注释,或者是宏(这个老师都会给我们宏会被直接进行替换),还有我们会使用printf
、scanf
等函数,这些我们并没有进行自己实现,但是却可以直接调用,这些是不合理的,那么为什么我们可以进行使用呢?因为有一些编写库的程序员大佬给我们写好了相关的库(.ddl .lab文件),我们要把他们写的库链接到我们写的程序中,减少我们的负担不然写一个printf("hello world");
print这个函数却要我们来实现,这显然是有点忘了我们的初心只是要打印一个Hello world了,有了库我们就可以提高我们开发的效率,提高我们程序的健壮性
所以一个C程序要成为可运行文件.exe需要经过:预处理、编译、汇编、链接
预处理阶段会做的事情:去注释、宏替换、条件编译、头文件展开
了解内容 Linux中的一些命令
预处理 -E: 头文件展开,去注释
编译 -S:将C语言编译成汇编语言
汇编 -c:将汇编语言转换为目标二进制文件 .obj(不是可执行文件)
链接 : 将目标二进制文件与相关链接(.ddl .lab),形成可执行程序 (.exe)
这个```lesson_define``,以前写的测试,忘了删除了,结果一样的有Linux环境的条件下可以测试一下
一、宏定义
(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"
(3)宏定义充当注释符号
在大学学校里面,老师在教授C语言的时候都会说的一句话,你们写的注释在预处理的时候会变为空格,而宏会被直接替换到你用宏的位置,那么宏替换和去注释究竟是谁先呢?
我们把宏定义为注释符号,来做一个测试
#inclde<stdio.h>
#define BSD //
int main()
{
BSD printf("hello world\n");
printf("you must see me\n");
return 0;
}
输出了两句话,如果先进行宏替换在进行去注释的话显然只会输出第二句话,而不会输出第一句,如果优先进行宏替换会变成下面的那样
#include<stdio.h>
int main()
{
// printf("hellow world\n");
printf("you must see me");
return 0;
}
可以得出去注释优先于宏替换,我们看一下,预处理后的文件
所以#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;
}
那宏定义是否可以替换多个语句呢?答案是可以的不过有一些注意事项。
如果有多条语句尽量放到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;
}
来看下面这份代码:
#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
进行匹配,引发错误。
下面我们用do{}while(0)
来进行测试
没有进行报错,并且正常输出,我们来看一下他预处理后的代码
和我们预期一致,这样我们可以宏替换语句数量便没有限制了。
(5)undef取消宏定义
1.宏定义的生效范围
宏定义只能定义到程序的开始吗?答案不是的,宏定义也可以定义在函数内部main函数以及我们自己定义的各种函数都可以的,宏定义的作用域是宏定义的开始到遇到undef
或者是到程序的结束,宏的替换是在预处理阶段是在函数调用前的,我们定义的宏在预处理的时候就会把函数体内对应的宏进行了替换.
#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下面
#define放在show里面或者说被替换的上面
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
后面并没有进行替换,所以宏定义的替换范围是宏定义开始到宏定义取消
宏定义替换在调用函数前,调用前就被替换就没有任何问题
二、条件编译
条件编译:可以根据我们的需求对代码进行裁剪,以减少我们对代码的维护量,可以让一份代码可以运行在不同的操作系统,或者说一份代码对于收费版和付费版进行同时维护,版本的更新等等。
下面来看一下宏定义和宏真值的区别
宏定义:看这个宏是否存在。
逻辑判断宏真假:首先这个宏得存在才可以判断他的真假,是根据宏的值来进行判断的
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行了
这就是头文件的展开,把对应文件的内容复制一份,当然也会进行去注释和条件编译
下面我们在做一个测试,为了结果明显就不在进行去除头文件的重复包含了,也不用输入和输出函数了
// text.h
extern void show();
extern int showint();
// text.c
#include"text.h"
int main()
{
return 0;
}
四、 #号的使用
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;
}