【程序环境和程序预处理】万字详文,忘记了,看这篇就对了(1)

简介: 1.程序翻译环境和运行环境假设一个test.c文件经过编译器编译运行后生成可执行文件test.exe,这中间存在两个过程:一个是翻译,在这个环境中源代码被转换为可执行的机器指令。一个是运行,它用于实际执行代码。在翻译环境阶段,会进行编译和链接操作。在汇编阶段,是将汇编指令转换成二进制指令。1.1程序翻译中的的编译和链接

1.程序翻译环境和运行环境

假设一个test.c文件经过编译器编译运行后生成可执行文件test.exe,这中间存在两个过程:

一个是翻译,在这个环境中源代码被转换为可执行的机器指令。
一个是运行,它用于实际执行代码

在翻译环境阶段,会进行编译和链接操作。

在汇编阶段,是将汇编指令转换成二进制指令。

1.1程序翻译中的的编译和链接

我们先来看这段代码:

extern Add(int a, int b);
int main()
{
  printf("%d\n", Add(2, 5));
}

这是在test.c文件中的代码,

int Add(int a, int b)
{
  return a + b;
}

这是在Add.c文件中的代码。

编译运行后,我们走到代码源文件所在目录下,


b38be8071c374edeae507ff16eaa6a6e.png

会发现有两个obj文件,这两个obj文件就是通过编译器编译源码生成的目标文件。

而这仅仅是目标文件,想要生成可执行程序,还需要通过链接器链接,调用链接库,才能生成可执行文件。

9800dba807a8459ab0953ada52c4ee51.png

在链接器将目标文件链接成可执行程序期间,会做两件事:

1.合并段表

一个目标文件:可能是一个.o文件,该文件内部有一个许多关于该文件的信息,并且是分区存放的,也


fdda3af9b0a1485f8d2aa2e5ab2899e0.png

以上面的例子为例,既然目标文件test.o是这样的,那么另一个目标文件Add.o的分段也应该是如上图,只是里面的内容存放不同而已。

所以我们可以将test.o和Add.o中的各个段的信息合并,就叫做合并段表


36ec98430fe743c793c3ce2608d4fa24.png

2.符号表的合并和重定位

在上面的两个文件中,我们假设main函数在内存中的地址是0x20000000,Add函数的地址是0x10000000,如下图:

e821e2484c3c485faab022bb12779367.png

执行test.c程序时,首先进入执行extern Add语句,发现这是一个函数声明,意思就是我只知道有Add这个函数,但是具体在哪里不知道,接着进入main函数,发现main函数在内存中的地址是0x20000000,记录下来,执行完test.c文件后,接着进入Add.c文件中,发现Add.c文件中有一个Add.c函数,地址是0x10000000,记录下来。


随后,将两个目标文件通过链接器合并时,会将test.c和Add.c文件中的地址合并,即

efdcb5a79fd2479991268b5861ea2134.png

这就是符号表的合并,那么重定位呢?

重定位就是在符号表合并后,程序只认识新的合成后的符号表,并将该符号表作为运行时的信息,不再以之前的符号表作为信息,这个就是重定位

当然,上述的讲解只是表层的介绍,具体的内容还会更加深入。

2. 预编译详解

2.1 预定义符号

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

这是几个c语言内置的符号,来看他们的用法:

1.__ FILE __


c068ccfae3754d5a8cfbad7587991747.png

我们打印__FILE__时,显示的是该源文件所在的路径。

2.__ LINE__

8d5c94c6f9f64d53b3341a780d918311.png

__LINE __ 显示打印的位置。

3.__DATE __

显示编译代码的日期

fca0a3c8240b48c1822ef3015bb20c11.png

4.__TIME __

显示文件被编译的时间

edb961ffbabd44cc9bc428323a1b79a8.png

5.__ STDC__

而在VS2019环境下,__STDC __没有被定义,如果被定义,其值为1。

注意,上述的五个预定义符号,在书写时均为大写!

2.2 #define 用法

2.2.1#define定义标识符

语法:
#define name stuff

凡是以#开头的,都是预处理指令。 后续还会讲到#pragma,#include,#line等等

举个例子:

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

注意:#define定义标识符在程序运行的时候进行的是替换!是替换!替换!

#define定义的标识符不会参与任何运算。

#define还可以定义各式各样的东西,甚至可以定义代码

#define reg register      //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;)   //用更形象的符号来替换一种实现
#define CASE break;case     //在写case语句的时候自动把 break写上。
#define DEBUG_PRINT printf("file:%s\tline:%d\tdate:%s\ttime:%s\n",__FILE__,__LINE__ ,__DATE__,__TIME__ ) 

这样写也可以实现。

注意这里有个问题:

#define do_forever for(;;)
1.
int main()
{
  do_forever
  return 0;
}
2.
int main()
{
  do_forever;
  return 0;
}

当我们这样定义for循环时,请问运行1和2的结果分别是什么?

答案:第一个运行的结果是,直接程序什么都不做,就结束。

第二个运行的结果是,程序死循环。

因为

#define定义标识符在程序运行的时候进行的是替换!

所以do_forever会替换成for(; ; ) , 当for循环后面不跟大括号时,默认跟一条语句。

对于第一个代码,return 0 是for循环里面的语句,

对于第二个代码,for循环内部的语句是一个分号,

for(; ; )
    ;

也就是这样,所以会死循环。

那么就有一个问题,

在define定义标识符的时候,要不要在最后加上 ; ?

到底需不需要加呢?

举个简单的例子

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

我们知道,MAX会被替换成100;

所以在赋值给a时,是这样的:

int a = 100;;  有两个分号

这样打印出来可能没什么问题,但是当我们把打印a换成打印MAX时,就有问题了

b3155437f44347a295b7653d4a6c1113.png

打印的是

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的一部分。

举个例子:

#define SQUARE( x ) x * x
int main()
{
  printf("%d\n",SQUARE(5));
}

结果会输出什么?

0f2242fd2ae349c1b23ef8e503a7e185.png

5将会被传入x中,x就是5,宏也一样,是被替换的,所以

SQUARE(5) 会被替换成 5*5。

但是,这样的写法会有一些问题:

看下面的例子:

#define SQUARE( x ) x * x
int main()
{
  int a = 5;
  printf("%d\n" ,SQUARE( a + 1) );
}

请问输出结果是什么呢?乍一看,你可能会认为是36,但是,不对。

原因是,还是那句话,

宏一样是被替换的!!!

宏一样是被替换的!!!

宏一样是被替换的!!!


a是5,a+1会被放入SQUARE(a+1), 然后被替换成 a+1a+1,计算的是这个结果,5+15+1,结果就是11.

所以记住这句话:

宏一样是被替换的!!!

所以我们这样改就可以了

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

这样结果就是36了。

这里还有一个宏定义:

#define DOUBLE(x) (x) + (x)
int main()
{
  int a = 5;
  printf("%d\n" ,10 * DOUBLE(a));
}


请问结果是什么?

可能会说,100,但是,结果不正确,记住那句话

宏一样是被替换的!!!

打印结果是10*(5)+ (5),结果是55

那么,我们怎么改呢?

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

这样改正才是正确的,才是完美的。

总结,对宏进行定义时,应该对每个替换后的参数都加上括号,避免操作顺序不当出现错误。

2.2.3 #define 替换规则

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


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

被替换。

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

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

述处理过程。

注意:

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

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

2.2.4 #和##

首先来看这段代码:

void print(int x)
{
  printf("the value of a is %d\n", x);
}
int main()
{
  int a = 10;
  int b = 20;
  print(a);
  print(b);
  return 0;
}


我们想把a和b的值都打印出来,

然而结果不是我们想要的,两次都是打印a,虽然值不同。

0e71de5249f248649e83dc4fbc87f012.png

那么我们应该怎么做才能达到我们想要的效果呢?

先看下面,

int main()
{
  printf("Hello World\n");
  printf("Hello " "World\n");
  return 0;
}


5138adb7194745de9a66a47effab1c52.png

一样的,原因是,在同一个printf中,两个双引号引起来的两个字符串会被当成同一个字符串处理。

28b5a5b819b5438284a8e484eedcfead.png

当然,中间的空格可有可无,可以有很多个,也可以没有,这里放一个空格隔开只是方便看。

了解了这个之后,我们就可以改造上面如何打印a和b的值出来了。

#define PRINT(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)


fc21cde916304762a2d3c928458f2cc3.png

所以#的作用是,把宏参数对应的内容变成一个字符串

有些东西是函数无法做到但是宏能够做到的。比如上面这个例子。

##

(2) ## 的作用

##可以把位于它两边的符号合成一个符号。

它允许宏定义从分离的文本片段创建标识符。

举个例子:

#define CAT(X,Y) X##Y
int main()
{
  int student = 100;
  printf("%d\n", CAT(stu,dent));
  printf("%d\n", student);
  return 0;
}

该段代码的输出结果是两个100,##的作用就是,把X,Y宏参数合成一个新的符号。

stu是参数X,dent是参数Y,合成后成为一个新的符号student,打印出来就是100.

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

相关文章
|
程序员 编译器 Linux
程序员进阶之路:程序环境和预处理(二)
程序员进阶之路:程序环境和预处理(二)
34 0
|
6月前
|
编译器 C语言 C++
程序翻译过程详解
程序翻译过程详解
|
6月前
|
数据采集 机器学习/深度学习 数据可视化
分享261个Python源码源代码总有一个是你想要的
分享261个Python源码源代码总有一个是你想要的
402 0
|
存储 编译器 程序员
C语言——程序环境和预处理(再也不用担心会忘记预处理的知识)
C语言——程序环境和预处理(再也不用担心会忘记预处理的知识)
|
存储 自然语言处理 程序员
程序员进阶之路:程序环境和预处理(一)
程序员进阶之路:程序环境和预处理(一)
64 0
|
存储 并行计算 测试技术
【CUDA学习笔记】第五篇:内存以及案例解释(附案例代码下载方式)(二)
【CUDA学习笔记】第五篇:内存以及案例解释(附案例代码下载方式)(二)
170 0
【CUDA学习笔记】第五篇:内存以及案例解释(附案例代码下载方式)(二)
【程序环境和程序预处理】万字详文,忘记了,看这篇就对了(2)
1.程序翻译环境和运行环境 假设一个test.c文件经过编译器编译运行后生成可执行文件test.exe,这中间存在两个过程: 一个是翻译,在这个环境中源代码被转换为可执行的机器指令。 一个是运行,它用于实际执行代码。 在翻译环境阶段,会进行编译和链接操作。 在汇编阶段,是将汇编指令转换成二进制指令。
|
存储 自然语言处理 编译器
程序的编译与链接(C语言为例) #代码写好后到运行期间要经过怎样的过程呢?# 粗略版 #
程序的编译与链接(C语言为例) #代码写好后到运行期间要经过怎样的过程呢?# 粗略版 #
|
存储 并行计算 计算机视觉
【CUDA学习笔记】第五篇:内存以及案例解释(附案例代码下载方式)(一)
【CUDA学习笔记】第五篇:内存以及案例解释(附案例代码下载方式)(一)
295 0
|
编译器 Linux C语言
【C语言进阶】—— 程序环境和预处理 ( 坚持总会有收获!!!)(下)
【C语言进阶】—— 程序环境和预处理 ( 坚持总会有收获!!!)(下)
118 0
【C语言进阶】—— 程序环境和预处理 ( 坚持总会有收获!!!)(下)