本章介绍一个test.c文件是如何生成一个test.exe文件。首先了解程序环境和程序预处理的大致流程,本章会分别介绍各个流程,但重点是翻译中的编译中的预编译阶段。
文章目录:
1.程序翻译环境和运行环境
假设一个test.c文件经过编译器编译运行后生成可执行文件test.exe,这中间存在两个过程:
一个是翻译,在这个环境中源代码被转换为可执行的机器指令。
一个是运行,它用于实际执行代码。
在翻译环境阶段,会进行编译和链接操作。
在汇编阶段,是将汇编指令转换成二进制指令。
1.1程序翻译中的的编译和链接
我们先来看这段代码:
externAdd(int a,int b);intmain(){printf("%d\n",Add(2,5));}
这是在test.c文件中的代码,
intAdd(int a,int b){return a + b;}
这是在Add.c文件中的代码。
编译运行后,我们走到代码源文件所在目录下,
会发现有两个obj文件,这两个obj文件就是通过编译器编译源码生成的目标文件。
而这仅仅是目标文件,想要生成可执行程序,还需要通过链接器链接,调用链接库,才能生成可执行文件。
在链接器将目标文件链接成可执行程序期间,会做两件事:
1.合并段表
一个目标文件:可能是一个.o文件,该文件内部有一个许多关于该文件的信息,并且是分区存放的,也就是一段一段的,如下图:
以上面的例子为例,既然目标文件test.o是这样的,那么另一个目标文件Add.o的分段也应该是如上图,只是里面的内容存放不同而已。
所以我们可以将test.o和Add.o中的各个段的信息合并,就叫做合并段表
2.符号表的合并和重定位
在上面的两个文件中,我们假设main函数在内存中的地址是0x20000000,Add函数的地址是0x10000000,如下图:
执行test.c程序时,首先进入执行extern Add语句,发现这是一个函数声明,意思就是我只知道有Add这个函数,但是具体在哪里不知道,接着进入main函数,发现main函数在内存中的地址是0x20000000,记录下来,执行完test.c文件后,接着进入Add.c文件中,发现Add.c文件中有一个Add.c函数,地址是0x10000000,记录下来。
随后,将两个目标文件通过链接器合并时,会将test.c和Add.c文件中的地址合并,即
这就是符号表的合并,那么重定位呢?
重定位就是在符号表合并后,程序只认识新的合成后的符号表,并将该符号表作为运行时的信息,不再以之前的符号表作为信息,这个就是重定位。
当然,上述的讲解只是表层的介绍,具体的内容还会更加深入。
2. 预编译详解
2.1 预定义符号
__FILE__//进行编译的源文件__LINE__//文件当前的行号__DATE__//文件被编译的日期__TIME__//文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
这是几个c语言内置的符号,来看他们的用法:
1.__ FILE __
我们打印__FILE__时,显示的是该源文件所在的路径。
2.__ LINE__
__LINE __ 显示打印的位置。
3.__DATE __
显示编译代码的日期
4.__TIME __
显示文件被编译的时间
5.__ STDC__
而在VS2019环境下,__STDC __没有被定义,如果被定义,其值为1。
注意,上述的五个预定义符号,在书写时均为大写!
2.2 #define 用法
2.2.1#define定义标识符
语法:
#definenamestuff
凡是以#开头的,都是预处理指令。 后续还会讲到#pragma,#include,#line等等
举个例子:
#defineMAX1000intmain(){printf("%d\n",MAX);}
注意:#define定义标识符在程序运行的时候进行的是替换!是替换!替换!
#define定义的标识符不会参与任何运算。
#define还可以定义各式各样的东西,甚至可以定义代码
#defineregregister//为 register这个关键字,创建一个简短的名字#definedo_foreverfor(;;)//用更形象的符号来替换一种实现#defineCASEbreak;case//在写case语句的时候自动把 break写上。#defineDEBUG_PRINTprintf("file:%s\tline:%d\tdate:%s\ttime:%s\n",__FILE__,__LINE__,__DATE__,__TIME__)
这样写也可以实现。
注意这里有个问题:
#definedo_foreverfor(;;)1.intmain(){
do_forever
return0;}2.intmain(){
do_forever;return0;}
当我们这样定义for循环时,请问运行1和2的结果分别是什么?
答案:第一个运行的结果是,直接程序什么都不做,就结束。
第二个运行的结果是,程序死循环。
因为
#define定义标识符在程序运行的时候进行的是替换!
所以do_forever会替换成for(; ; ) , 当for循环后面不跟大括号时,默认跟一条语句。
对于第一个代码,return 0 是for循环里面的语句,
对于第二个代码,for循环内部的语句是一个分号,
for(;;);
也就是这样,所以会死循环。
那么就有一个问题,
在define定义标识符的时候,要不要在最后加上 ; ?
到底需不需要加呢?
举个简单的例子
#defineMAX100;intmain(){int a = MAX;printf("%d\n", a);return0;}
我们知道,MAX会被替换成100;
所以在赋值给a时,是这样的:
int a =100;; 有两个分号
这样打印出来可能没什么问题,但是当我们把打印a换成打印MAX时,就有问题了
打印的是
printf("%d\n",100;);
就会有错误
所以,在使用#define定义标识符的时候最好还是不要加分号
2.2.2 #define 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下面是宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
意思就是:name要和小括号紧紧相连在一起,如果中间有空格,那么(parament-list)就会被当作是stuff的一部分。
举个例子:
#defineSQUARE( x ) x * xintmain(){printf("%d\n",SQUARE(5));}
结果会输出什么?
5将会被传入x中,x就是5,宏也一样,是被替换的,所以
SQUARE(5) 会被替换成 5*5。
但是,这样的写法会有一些问题:
看下面的例子:
#defineSQUARE( x ) x * xintmain(){int a =5;printf("%d\n",SQUARE( a +1));}
请问输出结果是什么呢?乍一看,你可能会认为是36,但是,不对。
原因是,还是那句话,
宏一样是被替换的!!!
宏一样是被替换的!!!
宏一样是被替换的!!!
a是5,a+1会被放入SQUARE(a+1), 然后被替换成 a+1a+1,计算的是这个结果,5+15+1,结果就是11.
所以记住这句话:
宏一样是被替换的!!!
所以我们这样改就可以了
#defineSQUARE(x)(x)*(x)
这样结果就是36了。
这里还有一个宏定义:
#defineDOUBLE(x)(x)+(x)intmain(){int a =5;printf("%d\n",10*DOUBLE(a));}
请问结果是什么?
可能会说,100,但是,结果不正确,记住那句话
宏一样是被替换的!!!
打印结果是10*(5)+ (5),结果是55
那么,我们怎么改呢?
#defineDOUBLE(x)((x)+(x))
这样改正才是正确的,才是完美的。
总结,对宏进行定义时,应该对每个替换后的参数都加上括号,避免操作顺序不当出现错误。
2.2.3 #define 替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先 被替换。
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上 述处理过程。注意:
1.宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
4. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
2.2.4 #和##
首先来看这段代码:
voidprint(int x){printf("the value of a is %d\n", x);}intmain(){int a =10;int b =20;print(a);print(b);return0;}
我们想把a和b的值都打印出来,
然而结果不是我们想要的,两次都是打印a,虽然值不同。
那么我们应该怎么做才能达到我们想要的效果呢?
先看下面,
intmain(){printf("Hello World\n");printf("Hello ""World\n");return0;}
这两个打印的结果一样吗?
一样的,原因是,在同一个printf中,两个双引号引起来的两个字符串会被当成同一个字符串处理。
当然,中间的空格可有可无,可以有很多个,也可以没有,这里放一个空格隔开只是方便看。
了解了这个之后,我们就可以改造上面如何打印a和b的值出来了。
#definePRINT(X)printf("the value of "#X "is %d\n",X)
当我们这样改造时,前面的双引号引起的是the value of(这里有个空格) ,后面的双引号引起的是is %d\n ,两个字符串之间使用一个#来吧X也变成一个字符串,这样三个字符串连在一起,就完成了。
相当于
printf("the value of ""a""is %d\n",a)printf("the value of ""b""is %d\n",b)
所以#的作用是,把宏参数对应的内容变成一个字符串
有些东西是函数无法做到但是宏能够做到的。比如上面这个例子。
(2) ## 的作用
##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。
举个例子:
#defineCAT(X,Y) X##Yintmain(){int student =100;printf("%d\n",CAT(stu,dent));printf("%d\n", student);return0;}
该段代码的输出结果是两个100,##的作用就是,把X,Y宏参数合成一个新的符号。
stu是参数X,dent是参数Y,合成后成为一个新的符号student,打印出来就是100.
注意:
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
2.2.5 带副作用的宏参数
副作用就是后遗症的意思,
x+1; 不带副作用
x++; 带有副作用
举个例子:
#defineMAX(a, b)((a)>(b)?(a):(b))intmain(){
x =2;
y =5;printf("%d\n",MAX(a++, b++));printf("%d\n", a);printf("%d\n", b);}
请问上面的这段代码输出结果是什么?
输出结果是:6,3,7
原因是,宏是被替换的,首先x是2,y是5,在使用宏时,参数是a++和b++,然后进行替换,替换结果是:
printf("%d\n",((a++)>(b++)?(a++):(b++)));
所以在比较时,先使用,后++,2和5比较完,再各自++,此时2小于5,执行b++,此时b已经是6了,先使用后++,所以打印第一个结果是6,对于a,a只++一次,结果是3,
最后再打印b出来时,b已经++完成了,所以b打印出来是7.
总结:当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能
出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
2.2.6 宏和函数对比
宏是经常被用来执行计算量较小的计算,如比较大小,那为什么不使用函数来比较呢?
1.我们知道,函数在调用的时候会有返回的开销,反观宏,则没有类似的问题。
以比较大小为例:
#defineMAX(a,b)((a)>(b)?(a):(b))floatMAX2(float c,float d){return c > d ? c : d;}intmain(){int a =2;int b =5;float c =3.0f;float d =4.0f;float max2 =MAX2(c, d);//函数调用printf("%f\n", max2);printf("%f\n",MAX(c, d));//宏调用return0;}
我们分别使用函数和使用宏来比较大小,
我们调试起来,转到反汇编后,注意看,现在准备进入函数调用,在此之前是准备工作。
当我们调用该函数时,会发现这么一大堆东西,这些都是函数在调用时需要做的工作,以及返回值需要做的工作。
再来看宏的开销:
对比函数和宏调用的开销,会发现,仅仅是比较大小,函数的开销比宏的开销多出了很多。
所以宏比函数在程序的规模和速度方面更胜一筹。
2. 函数的参数必须声明为特定的类型,而宏是类型无关的。
以上面的例子为例:
#defineMAX(a,b)((a)>(b)?(a):(b))floatMAX2(float c,float d){return c > d ? c : d;}intMAX1(int a,int b){return a > b ? a : b;}intmain(){int a =2;int b =5;float c =3.0f;float d =4.0f;float max2 =MAX2(c, d);//函数调用printf("%f\n", max2);int max1 =MAX1(c, d);//函数调用printf("%d\n", max1);printf("%f\n",MAX(c, d));//宏调用printf("%d\n",MAX(a, b));//宏调用return0;}
分别使用函数和宏对整型和浮点型数据进行大小比较,此时两个没有任何问题,但是接下来,
当我们更改图中数据,用浮点型函数比较整型大小时,回出现警告,可能会丢失数据,
结果也不符合,
所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。
记住,宏是类型无关的。
宏的缺点:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序
的长度。
#defineTEST()printf("Test Successfully!\n")intmain(){TEST();TEST();TEST();//等价于printf("Test Successfully!\n");printf("Test Successfully!\n");printf("Test Successfully!\n");}
我们这样复制三份宏,替换后就已经出现代码较冗余的情况, 假如宏定义的代码有五十行,复制三份后就有一百五十行,情况更加严重。
2. 宏是没法调试的
调试起来的时候,按下F11,并没有跳转到宏所在的地方,因为宏在预编译的时候就已经完成了替换。
3. 宏由于类型无关,也就不够严谨。
宏是类型无关的,既是优点,也是缺点。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
5.宏还可以做到传一个类型,然而函数做不到。
比如说,宏的参数可以是一个int,但是没有函数传参传一个int的说法,函数只能传一个int类型的值,但是绝对不会传一个int。
举个例子,好好体会一下。
#defineMALLOC(num,type)(type*)malloc(num*sizeof(type))intmain(){int ret1 =(int*)malloc(10*sizeof(int));int ret =MALLOC(10,int);//等价于printf("%d\n", ret1);printf("%d\n", ret);return0;}
2.2.7命名约定
一般函数的宏的使用语法很相似。
所以语言本身没法帮我们区分二者。
我们平时的一个习惯是:
把宏名全部大写
函数名不要全部大写
2.3#undef的用法
#undef 是用来移除一个宏定义的
举个例子:
#defineMAX100intmain(){printf("%d\n", MAX);#undefMAXprintf("%d\n", MAX);return0;}
可以发现,编译都无法编译成功,说明MAX已经被移除了。
2.4 命令行定义
通俗地讲,命令行定义就是再程序预编译的时候改变一些参数,使他们能够随时地发生变化。
比如说下面:
给一个数组赋值,ARRARY_SIZE代表数组的大小,数组的大小通过命令行定义,在预编译阶段是可以发生改变的。
#include<stdio.h>intmain(){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");return0;}
总结:命令行定义就是再程序预编译的时候改变一些参数,使他们能够随时地发生变化。
2.5 条件编译
条件编译,也就是有选择性地编译,把想要的留下。
比方说:
intmain(){int arr[10]={0};for(int i =0; i <10; i++){#ifdefDEBUG
arr[i]= i;#endifprintf("%d ", arr[i]);}return0;}
请问这段代码输出结果是什么?
结果输出10个0。
因为这里我们使用了条件编译,#ifdef DEBUG,意思就是如果定义有DEBUG,就使用下面的语句,结束编译语句是#endif,在这区间内,如果条件成立,则执行,不成立就不执行。
由于未定义有DEBUG,所以条件不成立,不执行赋值语句,当我们在前面定义DEBUG,就可以了。
在这里可以给DEBUG一个替换对象,也可以仅仅定义DEBUG。
常见的条件编译指令:
1.常量表达式
intmain(){#if1printf("hehe\n");#endifreturn0;}
2.多分支的条件编译
intmain(){#if1==1printf("hehe\n");#elif2==1printf("haha\n");#elseprintf("heihei\n");#endifreturn0;}
3.判断是否被定义过
#defineDEBUG0//即使DEBUG被定义为0,为假,但是它已经被定义过了,就打印heheintmain(){#if!defined(DEBUG)// 只要定义过,不管定义什么,满足条件就参与编译printf("hehe\n");#endifreturn0;}
注意,只要被定义过,不管被定义成什么,都成立。
并且,define后面加了一个字母d,表示defined,定义过的意思。
还有一个是
#ifndefDEBUG //注意这里多了个n,表示noprintf("hehe\n",);#endif
表示如果没有定义DEBUG,就打印hehe。
4,嵌套定义
嵌套定义可以跟嵌套的条件判断类比,也就是 if 中还有 if 。
4.嵌套指令
#ifdefined(OS_UNIX)#ifdefOPTION1unix_version_option1();#endif#ifdefOPTION2unix_version_option2();#endif#elifdefined(OS_MSDOS)#ifdefOPTION2msdos_version_option2();#endif#endif
2.6文件包含
2.6.1 #include <> 和#include " "
我们知道,对于文件来说,假如我们需要打印东西,就需要引一个头文件,引#include<stdio.h>
那假如我用 #include "stdio.h "
这样的写法呢?能否通过?
仍然可以打印出来。
#include"filename"
的查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。
如果找不到就提示编译错误。
用#include " "的方式包含文件,会先在源文件所在目录下查找,
也就是这些文件里查找,如果找不到,才会去标准库函数里面查找。
如上图.
而#include< >,则是直接在目录里面查找了。
所以,当我们有成千上万个源文件时,应该使用#include<>去查找。
2.6.2 嵌套文件包含
如果出现了这样的情况,也就是一个多个文件中都包含了同一个头文件,这会重复调用头文件,造成代码冗余,也会造成文件的速度的减慢。
解决办法:
1.条件编译
每个头文件的开头写:
#ifndef__TEST_H__#define__TEST_H__//头文件的内容#endif//__TEST_H__
意思就是,如果没有定义TEST.H这个头文件,那么就定义它,如果定义了,就不再次定义。
比如说这个,上面的红色框框,是test.h文件的内容,下面红色框框是test.c文件内容,在test.c文件中包含test.h文件,然后进入test.h文件,执行#ifndef,如果自己没有被定义,那就定义,如果定义过了,那就不重复定义了。
直接写下面这句话就可以了。
#pragmaonce
更加推荐第二种写法。
这篇文章到这里就结束了!
如果对于有帮助,不妨点赞关注吧!
版权归原作者 在肯德基吃麻辣烫 所有, 如有侵权,请联系我们删除。