带你了解程序环境和预处理


前言

程序的编译链接,运行环境及预处理相关知识的了解和学习

虽然这些知识点在我们看来可能有些难和不是很重要,但是往往这样的心理会把我们带偏,其实这些知识点对我们对计算机的进一步了解又巨大的帮助,也是对我们很重要的,我们也一定要掌握好!!

一、程序环境

程序环境由翻译环境和运行环境(执行环境)构成
ecb293c44b13468eab097b6ffe402ea3.png
我们的.c源文件可能不止一个源文件需要编译链接,这个时候全部源文件会一个一个独自经过编译器编译再将全部的源文件一起通过链接器让这些源文件和链接库一起经过链接生成可执行文件;

1.1翻译环境

翻译环境是将.c源文件编译成可执行文件.exe;
翻译环境又由编译和链接组成;
而编译又是由预编译(预处理),编译,汇编组成;
预编译的功能是:
1.进行头文件的包含
2.注释的删除
3.#define定义的符号和宏的替换
编译的功能:
1.主要功能是将c语言转换成汇编语言
2.符号汇总(全局变量,函数名的汇总)
3.词性分析,词义分析,语义分析;
汇编的功能:

1.将汇编语言转换成机器语言(二进制语言)
2**.形成符号表(全局变量和函数名以及各自的地址汇总成表)**
编译功能将text.c文件转换成了目标文件text.o,该目标文件是elf格式的,该格式是由一个一个段组成的,值得注意的是可执行文件a.out也是elf格式的;
链接的功能:
1.合并段表;
每个目标文件都是elf格式,由段组成,当进行链接时,需要将多个或者只有一个时一起进行链接与链接库链接形成可执行文件,这个时候就会将多个目标文件的对应段表合并成一个,因为链接后生成一个可执行文件嘛;
2.符号表合并和重定位
比如在带main函数的.c里调用另外一个.c里的函数时,带main函数的.c需要声明外部函数的,这个时候在汇编的时候两个.c都有该函数的符号表,但是声明的这个函数(在main函数.c里的调的)生成的符号表中函数的地址其实是空的(0x00000000)因为只是声明,不是函数本身,但是在另外真正函数实现的.c里的函数符号表的地址是有实际意义的,链接的时候会将有用的符号表留下,没有用的替换,比如声明的那个函数的符号表会被真正实现的.c那里的符号表给合并;
值得注意的是:链接时是要根据符号表中函数的地址去找函数的,判断函数的存在与是否有真正意义,所以有了符号表的合并和重定位,所以我们可以调用外部.c文件中的函数也是因为我们有了合并和重定向后的符号表,可以找到外部函数的位置进行调用!!!

1.2运行环境

程序是一定要载入内存中使用的,要是有操作系统在,一般都是由操作系统完成载入内存这项工作的;
载入内存后,就开始调用main函数了,调用main函数时,内存会为main函数开辟函数栈帧(在栈区开辟的空间)用于储存局部变量和调用函数的地址,返回地址等等;还可能在静态区内开辟空间(是全局变量和static修饰的变量储存的地方,这个空间是直到程序结束才销毁的,使用全局变量是能够再程序不同函数乃至整个程序中用的),开辟空间后就进行程序运转了,程序终将结束,结束可能是正常结束,也可能是异常结束的,具体原因看程序本身

4664ccbd4a534a028e8407f06c352d7e.png

  1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序
    的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
  1. 程序的执行便开始。接着便调用main函数
  2. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回
    地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程
    一直保留他们的值。
  3. 终止程序。正常终止main函数;也有可能是意外终止。

二、预处理详解

1.预定义符号

0afd20ce2b434156aff2a45e5f6f653c.png

FILE //进行编译的源文件
LINE //文件当前的行号
DATE //文件被编译的日期
TIME //文件被编译的时间
—FUNCTION—//该文件内容所在的函数名称
STDC //如果编译器遵循ANSI C,其值为1,否则未定义
上图有例子,这个东西的作用也蛮大的,这个预定义符号是直接用的,作用主要是记录我们写代码中一些比较重要或者有意义的变量或者数据的代码日志,方便代码量多的时候检查一些东西的时候知道这些数据的位置等等;
ba8b4e75df1a4933bf66ef5ff4414077.png

a6fc26d8212f435d9f1cf6b99908e96e.png
记录数据如上;

2.#define的详解

2.1#define定义的标识符

#define MAX 1000
#define reg register //为 register这个关键字,创建一个简短的名字
#define do_forever for( ; ;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf(“file:%s\tline:%d\t
date:%s\ttime:%s\n” ,
FILE,LINE ,
DATE,TIME )
179aa70a8e3548a7844c21bb4976137f.png
用name代替stuff的内容,可以是变量代替数据等等;
9b25c41fafc346ffb05d3b7bfee5d473.png
最好是不加“;”的,因为我们习惯在语句后面加分号的,要是标识符本身加了分号,我们再加就会是语法错误了,不过每个人习惯不同,看自己吧,习惯用都可以的;

2.2#define 定义宏

我们可以将表达式用define重新定义,包括运算表达式,如计算大小的表达式:a>b? a:b也可以被替代的;
34d3621cda6e4e7cb4e31d8effd72b29.png
打印出来就是9嘛,直接替代;

要注意的是:最好将运算表达式用括号括起来,不然会计算不出来正确值哦!!!
如:
#define SQUARE( x ) x * x
这个宏接收一个参数 x .
如果在上述声明之后,你把
SQUARE( 5 );
置于程序中,预处理器就会用下面这个表达式替换上面的表达式:
5 * 5
警告:
这个宏存在一个问题:
观察下面的代码段:
int a = 5;
printf(“%d\n” ,SQUARE( a + 1) );
乍一看,你可能觉得这段代码将打印36这个值。
事实上,它将打印11.
为什么?
替换文本时,参数x被替换成a + 1,所以这条语句实际上变成了:
printf (“%d\n”,a + 1 * a + 1 );
这样就比较清晰了,由替换产生的表达式并没有按照预想的次序进行求值。
在宏定义上加上两个括号,这个问题便轻松的解决了
还有比如++这个运算符,要是放进去就不是我们想象的那个值了,比如要是放(2++),实际上参数将被替换时只是将2传上去了,++的作用还没有用,但是函数是表达式算完后才传参的,这就有了差距和警告,一定要带上括号使用宏!!!,否则非常非常容易造成运算错误
还有就是因为在.c文件进行预处理时就会进行#define的宏的替换,(现在就把刚刚上面的知识用上了)所以我们调试的时候是调试可执行程序,这个时候早就进行了表达式的替换,所以我们是观察不到被替换的数据的调试的,因为直接在预处理时就被替换了,如上面的宏:SQUARE( x )是直接被替换的,调试的时候是看不到这个的,直接被表达式替换了,观察到的是表达式,故我们说#define定义的宏是不可以被调试的哦!!!切记切记!!!

3.#和##的用法

**首先介绍一下“#”的用法吧;我们打印一句话的时候是不是这样?printf(“hello world\n”),其实吧,我们也可以这样哦!printf("hello "“world\n”),就是分号的区别嘛,分号的各自内容会打印出来,但是这个于”#“有啥关系?看着哦!我这样做,printf(“hello “#world”\n”)打印出来的也是一样的哦,我们没有将world用分号包着,而是用#修饰了,其实吧,作用可想而知咯,就是#后面修饰的等于用分号修饰!!!那么我们的##又该怎么用呢?再来吧
4d6f3aaaf44c4160802ce25a79f52b44.png
##作用就是将两个符号连接起来,组成一个符号,如上面的图是两个printf打印出来的东西都是一样的,说实话,#和##这两个玩意都挺偏的,我们懂这个东西就行了

4.宏的副作用

3c0a03a5f8464955b1d40d48b5a87a9a.png
想一想自己能不能得出答案来?上面我也说过吧?++这个玩意是很麻烦的东西,用不好就很容易得到错误答案;这个我在上文也提过;
4179416872d7499ebb8de161a7ef3fda.png

这里是将没有进行++的x,y进行比较后再进行了++,这个时候,x为6,y为9了,x>y是错误的,所以执行y++,但是是先将y=9赋给z,然后y再++变成10;所以宏的++是麻烦的,易错的!!!

5.宏和函数的比较

我们已经懂一定宏了,它可以替换较复杂的运算和语句,说实话是可以代替函数的,但是都有各自的优点和缺点,我们看看他们的优缺点吧,比较一下
2a636e7c6a794ed69dfb49b142bb2eec.png

注意:该图来自比特就业课!!!
上面图包含了函数和宏的优缺点,还蛮全的哦

7.消除宏的定义

#undef
这个玩意儿就是不能再进行宏的替代了,宏功能给取消了
62e1db99f60b4128b32e6e767b0b704e.png

8.条件编译

  1.  

1.1#if 常量表达式
//…
#endif
如果常量表达式为假,中间语句不执行,为真才执行;

1.2#ifdef 定义数据
//中间语句
#endif
如果#ifdef 后面定义的数据是有#define定义的,就执行中间语句,否则不执行;
//常量表达式由预处理器求值。
如:
#define DEBUG 1
#if DEBUG
//…
#endif

2.多个分支的条件编译
#if 常量表达式
//…
#elif 常量表达式
//…
#else
//…
#endif
跟if差不多的用法,只是有#elif与#endif不同的东西罢了,都是为真就执行,为假就不执行嘛,与if的语法条件一样用的;

3.判断是否被定义
#if defined(symbol)
#ifdef symbol
上面两种都表达一样的意思,其实是一样的,看自己喜欢那个就用那个呗,一样的,如果定义了就执行,没有就不执行;
#if !defined(symbol)
#ifndef symbol
这两个也是一样的,只是和上面两个意思相反,如果定义了就不执行

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
我们可以将这些条件语句嵌套用的,和if嵌套是一样的,这样我们就更好理解与使用了;

9.文件包含

8904c7bfa0f74e50ab9c3b17c48cd59a.png
上面那个想必大家都知道,下面这个需要解释一下了;下面这个头文件是自己定义的嘛,上面那个是库里面的库函数嘛;区别呢?
2e4241edd4ea40339b9f1228bbf739f5.png
值得注意的是:库函数头文件也可以用" ",不过有点啰嗦,因为在自己目录下肯定找不到库函数头文件的,这不是多此一举嘛?查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。

9.1嵌套文件包含

我们使用库函数头文件的时候,编译预处理的时候头文件的库函数的内容是会拷贝一份到代码中的,如果两次引头文件的话,就会拷贝两次库函数内容,会有一定的影响,所以我们需要防止引两次头文件,我们有两种方法;
1.#pragma once
2.#ifndef
#define
#endif
第二个是条件编译嘛,刚刚讲的;两个都可以防止两次引头文件带来影响,即使两次引头文件,也只拷贝一份内容;


总结

好了,预处理的知识就讲完了;
如果觉得开阔以就点个关注呗!!!哈哈哈

猜你喜欢

转载自blog.csdn.net/qq_68844357/article/details/124876595